{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "task-rows",
  "type": "registry:component",
  "title": "Task rows",
  "description": "Live agent task status — running, failed, completed.",
  "categories": [
    "ai-interface"
  ],
  "registryDependencies": [
    "https://irisui.dev/r/foundation.json",
    "https://irisui.dev/r/utils.json"
  ],
  "files": [
    {
      "path": "src/registry/components/task-rows/task-rows.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { cn } from \"../lib/utils\";\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,\n    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 width={size} height={size} className={cn(\"absolute inset-0\", active && \"animate-task-rows-ring\")}>\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}\n            cy={size / 2}\n            r={r}\n            fill=\"none\"\n            stroke=\"var(--ink-3)\"\n            strokeWidth={stroke}\n            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={cn(\n        \"flex size-5.5 shrink-0 animate-task-rows-badge-in items-center justify-center rounded-full text-white\",\n        tone === \"red\" ? \"bg-red\" : \"bg-green\",\n      )}\n    >\n      {children}\n    </span>\n  );\n}\n\nconst XIcon = (\n  <svg\n    width=\"12\"\n    height=\"12\"\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n    stroke=\"currentColor\"\n    strokeWidth=\"3.5\"\n    strokeLinecap=\"round\"\n  >\n    <path d=\"M18 6L6 18M6 6l12 12\" />\n  </svg>\n);\nconst CheckIcon = (\n  <svg\n    width=\"13\"\n    height=\"13\"\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n    stroke=\"currentColor\"\n    strokeWidth=\"3.5\"\n    strokeLinecap=\"round\"\n    strokeLinejoin=\"round\"\n  >\n    <path d=\"M20 6L9 17l-5-5\" />\n  </svg>\n);\nconst RetryIcon = (\n  <svg\n    width=\"12\"\n    height=\"12\"\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n    stroke=\"currentColor\"\n    strokeWidth=\"3\"\n    strokeLinecap=\"round\"\n    strokeLinejoin=\"round\"\n  >\n    <path d=\"M21 12a9 9 0 1 1-2.64-6.36M21 3v6h-6\" />\n  </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\n/* a CapyDB agent working through project upkeep */\nconst TASK_ROWS: TaskRow[] = [\n  {\n    key: \"verify\",\n    label: \"Verified nightly backups\",\n    amount: \"3 projects\",\n    status: \"done\",\n    details: [\n      { label: \"Checked restore points\", meta: \"3/3\" },\n      { label: \"Flagged failed backups\", meta: \"0\" },\n    ],\n  },\n  {\n    key: \"index\",\n    label: \"Create a preview database\",\n    amount: \"pr-142\",\n    status: \"running\",\n    step: 2,\n    details: [\n      { label: \"Cloning shop\", meta: \"1.0 GiB\" },\n      { label: \"Applying migration 0042\", meta: \"68%\" },\n    ],\n  },\n  {\n    key: \"draft\",\n    label: \"Rotate credentials\",\n    amount: \"2 projects\",\n    status: \"sequence\",\n    step: 3,\n    details: [\n      { label: \"api connection strings\", meta: \"queued\" },\n      { label: \"shop connection strings\", meta: \"queued\" },\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 animate-task-rows-pill-in items-center gap-1.5 rounded-full bg-red-tint px-2 text-[11.5px] font-medium text-red\">\n        {copy.failed} <span className=\"flex animate-task-rows-retry\">{RetryIcon}</span>\n      </span>\n    ) : row2 === \"done\" ? (\n      <span className=\"inline-flex h-5.5 animate-task-rows-pill-in items-center gap-1.5 rounded-full bg-green-tint px-2 text-[11.5px] font-medium text-green\">\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-surface 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={cn(\n              \"animate-task-rows-row-in self-stretch overflow-hidden transition-[border-radius,background-color] duration-300 hover:bg-inset\",\n              list ? \"border-b border-line last:border-0\" : \"bg-surface shadow-card\",\n            )}\n            style={{\n              borderRadius: list ? 0 : open ? 14 : 22,\n              animationDelay: `${i * 80}ms`,\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\">{badgeFor(row)}</span>\n              <span className=\"min-w-0 flex-1 truncate text-[13px] font-medium text-ink\">{row.label}</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\"\n                  height=\"15\"\n                  viewBox=\"0 0 24 24\"\n                  fill=\"none\"\n                  stroke=\"currentColor\"\n                  strokeWidth=\"2.2\"\n                  strokeLinecap=\"round\"\n                  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={cn(\"flex items-center justify-between\", open && \"animate-task-rows-detail-in\")}\n                        style={open ? { animationDelay: `${120 + j * 100}ms` } : undefined}\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\">{d.meta}</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/iris/task-rows/task-rows.tsx"
    }
  ],
  "docs": "Docs and live preview: https://irisui.dev/components/task-rows",
  "meta": {
    "kind": "component",
    "group": "AI interface",
    "export": "TaskRows",
    "docs": "https://irisui.dev/components/task-rows"
  }
}
