{ "$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.", "registryDependencies": [ "https://www.beautifului.dev/r/foundation.json" ], "files": [ { "path": "components/primitives/TaskRows.tsx", "content": "\"use client\";\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 \n \n \n {active && (\n \n )}\n \n {children}\n \n );\n}\n\nfunction Badge({ tone, children }: { tone: \"red\" | \"green\"; children: React.ReactNode }) {\n return (\n \n {children}\n \n );\n}\n\nconst XIcon = (\n \n);\nconst CheckIcon = (\n \n);\nconst RetryIcon = (\n \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: \"Cone 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;\n className?: string;\n onToggleRow?: (key: string, open: boolean) => void;\n}) {\n const tick = useTick(TICKS);\n const [manualOpen, setManualOpen] = useState>({});\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 {CheckIcon};\n if (row.status === \"running\") return {row.step};\n return row2 === \"pending\" ? (\n {row.step}\n ) : row2 === \"failed\" ? (\n {XIcon}\n ) : (\n {CheckIcon}\n );\n };\n\n const pillFor = (row: TaskRow) => {\n if (row.status === \"done\")\n return (\n \n {copy.completed}\n \n );\n if (row.status === \"running\") return null;\n return row2 === \"failed\" ? (\n \n {copy.failed} {RetryIcon}\n \n ) : row2 === \"done\" ? (\n \n {copy.completed}\n \n ) : null;\n };\n\n const list = variant === \"List\";\n return (\n \n {rows.map((row, i) => {\n const open = manualOpen[row.key] ?? (row.key === \"index\" && tick === 2);\n return (\n \n {\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 \n {badgeFor(row)}\n \n \n {row.label}\n \n {row.amount}\n {pillFor(row)}\n \n \n \n \n \n \n\n {/* dropdown detail — same expandable grammar as Chain of Thought */}\n \n \n \n \n \n {row.details.map((d, j) => (\n \n {d.label}\n \n {d.meta}\n \n \n ))}\n \n \n \n \n \n );\n })}\n \n );\n}\n", "type": "registry:component", "target": "components/primitives/TaskRows.tsx" } ] }