{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"task-rows","title":"Task Rows","description":"Live agent task status — running, failed, completed.","dependencies":[],"registryDependencies":["https://ward.so/r/foundation.json"],"files":[{"path":"registry/ward/TaskRows.tsx","content":"\"use client\";\n/*\n * MIT License\n *\n * Copyright (c) 2026 Shane Levine\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\nimport \"./styles/foundation.css\";\n\nimport { useEffect, useState } from \"react\";\n\n/* ─────────────────────────────────────────────────────────\n * TASK ROWS\n *\n *     0ms   rows enter staggered (80ms apart)\n *   600ms   row 1 ring sweeps 0 → 66%\n *  1500ms   row 1 expands — detail steps drop down\n *  3900ms   row 1 collapses; row 2 flips to Failed + retry\n *  5300ms   row 2 resolves to Completed\n * The status run completes once; task details stay clickable.\n * ───────────────────────────────────────────────────────── */\n\nconst TICKS = [600, 900, 2400, 1400, 2400, 600];\n\nfunction useTick(intervals: number[]) {\n  const [tick, setTick] = useState(0);\n  useEffect(() => {\n    if (tick >= intervals.length - 1) return;\n    const t = setTimeout(() => setTick((x) => x + 1), intervals[tick]);\n    return () => clearTimeout(t);\n  }, [tick, intervals]);\n  return tick;\n}\n\nfunction SpinnerRing({ active, children }: { active?: boolean; children?: React.ReactNode }) {\n  const size = 24, stroke = 2;\n  const r = (size - stroke) / 2;\n  const c = 2 * Math.PI * r;\n  return (\n    <span className=\"relative inline-flex shrink-0 items-center justify-center\" style={{ width: size, height: size }}>\n      <svg\n        width={size} height={size} className=\"absolute inset-0\"\n        style={active ? { animation: \"spin 1.1s linear infinite\" } : undefined}\n      >\n        <circle cx={size / 2} cy={size / 2} r={r} fill=\"none\" stroke=\"var(--line)\" strokeWidth={stroke} />\n        {active && (\n          <circle\n            cx={size / 2} cy={size / 2} r={r} fill=\"none\"\n            stroke=\"var(--ink-3)\" strokeWidth={stroke} strokeLinecap=\"round\"\n            strokeDasharray={`${c * 0.28} ${c * 0.72}`}\n          />\n        )}\n      </svg>\n      <span className=\"relative text-[10.5px] font-semibold tabular-nums text-ink\">{children}</span>\n    </span>\n  );\n}\n\nfunction Badge({ tone, children }: { tone: \"red\" | \"green\"; children: React.ReactNode }) {\n  return (\n    <span\n      className={`flex size-5.5 shrink-0 items-center justify-center rounded-full text-white\n        ${tone === \"red\" ? \"bg-red\" : \"bg-green\"}`}\n      style={{ animation: \"pop-in 300ms cubic-bezier(0.23,1,0.32,1) both\" }}\n    >\n      {children}\n    </span>\n  );\n}\n\nconst XIcon = (\n  <svg width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"3.5\" strokeLinecap=\"round\"><path d=\"M18 6L6 18M6 6l12 12\" /></svg>\n);\nconst CheckIcon = (\n  <svg width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"3.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\"><path d=\"M20 6L9 17l-5-5\" /></svg>\n);\nconst RetryIcon = (\n  <svg width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"3\" strokeLinecap=\"round\" strokeLinejoin=\"round\"><path d=\"M21 12a9 9 0 1 1-2.64-6.36M21 3v6h-6\" /></svg>\n);\n\n/* One detail line shown when a task row is expanded. */\nexport type TaskDetail = { label: string; meta: string };\n\n/* A single task row.\n *  - \"done\"     → green check badge + completed pill (static)\n *  - \"running\"  → active spinner showing `step`, no pill (static)\n *  - \"sequence\" → animation-driven: pending spinner → failed → completed\n */\nexport type TaskRow = {\n  key: string;\n  label: string;\n  amount: string;\n  status: \"done\" | \"running\" | \"sequence\";\n  step?: number;\n  details: TaskDetail[];\n};\n\nexport type TaskRowsLabels = {\n  completed: string;\n  failed: string;\n};\n\nconst DEFAULT_LABELS: TaskRowsLabels = {\n  completed: \"Completed\",\n  failed: \"Failed\",\n};\n\nconst TASK_ROWS: TaskRow[] = [\n  {\n    key: \"verify\",\n    label: \"Verified vendor records\",\n    amount: \"12 suppliers\",\n    status: \"done\",\n    details: [\n      { label: \"Matched tax and contact IDs\", meta: \"12/12\" },\n      { label: \"Flagged stale records\", meta: \"0\" },\n    ],\n  },\n  {\n    key: \"index\",\n    label: \"Build reorder task list\",\n    amount: \"7 SKUs\",\n    status: \"running\",\n    step: 2,\n    details: [\n      { label: \"Reading POS export\", meta: \"3 files\" },\n      { label: \"Scoring stockout risk\", meta: \"68%\" },\n    ],\n  },\n  {\n    key: \"draft\",\n    label: \"Draft supplier emails\",\n    amount: \"2 messages\",\n    status: \"sequence\",\n    step: 3,\n    details: [\n      { label: \"Asset supplier follow-up\", meta: \"draft\" },\n      { label: \"Pistachio reorder note\", meta: \"draft\" },\n    ],\n  },\n];\n\nexport default function TaskRows({\n  variant = \"Capsules\",\n  rows = TASK_ROWS,\n  labels,\n  className,\n  onToggleRow,\n}: {\n  variant?: string;\n  rows?: TaskRow[];\n  labels?: Partial<TaskRowsLabels>;\n  className?: string;\n  onToggleRow?: (key: string, open: boolean) => void;\n}) {\n  const tick = useTick(TICKS);\n  const [manualOpen, setManualOpen] = useState<Record<string, boolean>>({});\n  const row2: \"pending\" | \"failed\" | \"done\" = tick < 3 ? \"pending\" : tick === 3 ? \"failed\" : \"done\";\n  const copy = { ...DEFAULT_LABELS, ...labels };\n\n  const badgeFor = (row: TaskRow) => {\n    if (row.status === \"done\") return <Badge tone=\"green\">{CheckIcon}</Badge>;\n    if (row.status === \"running\") return <SpinnerRing active>{row.step}</SpinnerRing>;\n    return row2 === \"pending\" ? (\n      <SpinnerRing>{row.step}</SpinnerRing>\n    ) : row2 === \"failed\" ? (\n      <Badge tone=\"red\">{XIcon}</Badge>\n    ) : (\n      <Badge tone=\"green\">{CheckIcon}</Badge>\n    );\n  };\n\n  const pillFor = (row: TaskRow) => {\n    if (row.status === \"done\")\n      return (\n        <span className=\"inline-flex h-5.5 items-center rounded-full bg-green-tint px-2 text-[11.5px] font-medium text-green\">\n          {copy.completed}\n        </span>\n      );\n    if (row.status === \"running\") return null;\n    return row2 === \"failed\" ? (\n      <span className=\"inline-flex h-5.5 items-center gap-1.5 rounded-full bg-red-tint px-2 text-[11.5px] font-medium text-red\" style={{ animation: \"fade-in 200ms ease-out both\" }}>\n        {copy.failed} <span style={{ animation: \"spin 1.2s linear infinite\" }} className=\"flex\">{RetryIcon}</span>\n      </span>\n    ) : row2 === \"done\" ? (\n      <span className=\"inline-flex h-5.5 items-center gap-1.5 rounded-full bg-green-tint px-2 text-[11.5px] font-medium text-green\" style={{ animation: \"fade-in 200ms ease-out both\" }}>\n        {copy.completed}\n      </span>\n    ) : null;\n  };\n\n  const list = variant === \"List\";\n  return (\n    <div\n      className={`flex w-full max-w-110 flex-col ${\n        list ? \"gap-0 self-start overflow-hidden rounded-card bg-card shadow-card\" : \"min-h-[196px] gap-2\"\n      }${className ? ` ${className}` : \"\"}`}\n    >\n      {rows.map((row, i) => {\n        const open = manualOpen[row.key] ?? (row.key === \"index\" && tick === 2);\n        return (\n          <div\n            key={row.key}\n            className={`self-stretch overflow-hidden transition-[border-radius,background-color] duration-300 hover:bg-inset ${\n              list ? \"border-b border-line last:border-0\" : \"bg-card shadow-card\"\n            }`}\n            style={{\n              borderRadius: list ? 0 : open ? 14 : 22,\n              animation: `fade-up 450ms cubic-bezier(0.23,1,0.32,1) ${i * 80}ms both`,\n            }}\n          >\n            <button\n              type=\"button\"\n              aria-expanded={open}\n              onClick={() => {\n                setManualOpen((current) => ({ ...current, [row.key]: !open }));\n                onToggleRow?.(row.key, !open);\n              }}\n              className=\"flex h-11 w-full items-center gap-2.5 px-2.5 text-left\"\n            >\n              <span className=\"flex size-6 shrink-0 items-center justify-center\">\n                {badgeFor(row)}\n              </span>\n              <span className=\"min-w-0 flex-1 truncate text-[13px] font-medium text-ink\">\n                {row.label}\n              </span>\n              <span className=\"text-[12.5px] text-ink-2 tabular-nums\">{row.amount}</span>\n              {pillFor(row)}\n              <span\n                aria-hidden=\"true\"\n                className=\"-ml-2 flex size-7 shrink-0 items-center justify-center rounded-full text-ink-3\"\n              >\n                <svg\n                  width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2.2\" strokeLinecap=\"round\" strokeLinejoin=\"round\"\n                  className=\"transition-transform duration-300\"\n                  style={{ transform: open ? \"rotate(180deg)\" : \"rotate(0)\" }}\n                >\n                  <path d=\"M6 9l6 6 6-6\" />\n                </svg>\n              </span>\n            </button>\n\n            {/* dropdown detail — same expandable grammar as Chain of Thought */}\n            <div\n              className=\"grid transition-[grid-template-rows,opacity] duration-300\"\n                style={{\n                  gridTemplateRows: open ? \"1fr\" : \"0fr\",\n                  opacity: open ? 1 : 0,\n                  transitionTimingFunction: \"cubic-bezier(0.23, 1, 0.32, 1)\",\n                }}\n              >\n                <div className=\"overflow-hidden\">\n                  <div className=\"mb-2.5 grid grid-cols-[24px_1fr] gap-2.5 px-2.5\">\n                    <span aria-hidden className=\"mx-auto h-full w-px bg-line\" />\n                    <div className=\"flex flex-col gap-1.5\">\n                      {row.details.map((d, j) => (\n                        <div\n                          key={d.label}\n                          className=\"flex items-center justify-between\"\n                          style={\n                            open\n                              ? { animation: `fade-up 300ms cubic-bezier(0.23,1,0.32,1) ${120 + j * 100}ms both` }\n                              : undefined\n                          }\n                        >\n                          <span className=\"text-[12px] text-ink-2\">{d.label}</span>\n                          <span className=\"font-mono text-[11.5px] text-ink-3 tabular-nums\">\n                            {d.meta}\n                          </span>\n                        </div>\n                      ))}\n                    </div>\n                  </div>\n                </div>\n              </div>\n          </div>\n        );\n      })}\n    </div>\n  );\n}\n","type":"registry:component","target":"components/ward/TaskRows.tsx"}],"meta":{"variants":["Capsules","List"],"version":"1.1.0","source":"https://github.com/slev12397/beautiful-ui/blob/44a274e598395ab61e7c96c26fda2758780253b7/components/primitives/TaskRows.tsx","access":"free"},"categories":["ai"],"type":"registry:component"}