{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "spawn-tree",
  "type": "registry:component",
  "title": "Spawn Tree",
  "description": "See which agent started which, with each one's time and cost ticking on its card and every branch adding up what it spent.",
  "categories": [
    "component",
    "Mission Control"
  ],
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "utils",
    "https://coss.com/ui/r/button.json"
  ],
  "files": [
    {
      "path": "registry/devl/spawn-tree.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport { ChevronRightIcon, RotateCcwIcon } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion, useSpring, useTransform } from \"motion/react\";\nimport { useEffect, useId, useMemo, useState } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface SpawnNode {\n  id: string;\n  name: string;\n  model?: string;\n  task?: string;\n  start: number;\n  duration: number;\n  costPerSecond: number;\n  outcome?: \"done\" | \"failed\";\n  children?: SpawnNode[];\n}\n\nexport interface SpawnTreeProps {\n  root: SpawnNode;\n  speed?: number;\n  autoplay?: boolean;\n  title?: string;\n  className?: string;\n}\n\ntype Status = \"queued\" | \"running\" | \"done\" | \"failed\";\n\ninterface Live {\n  status: Status;\n  elapsed: number;\n  cost: number;\n  branchCost: number;\n  counts: Record<Status, number>;\n}\n\nfunction endOf(node: SpawnNode): number {\n  return Math.max(node.start + node.duration, ...(node.children ?? []).map(endOf));\n}\n\nfunction compute(node: SpawnNode, t: number, out: Map<string, Live>): Live {\n  const elapsed = Math.min(Math.max(t - node.start, 0), node.duration);\n  const status: Status =\n    t < node.start ? \"queued\" : t < node.start + node.duration ? \"running\" : (node.outcome ?? \"done\");\n  const cost = elapsed * node.costPerSecond;\n  const counts: Record<Status, number> = { queued: 0, running: 0, done: 0, failed: 0 };\n  let branchCost = cost;\n  for (const child of node.children ?? []) {\n    const c = compute(child, t, out);\n    branchCost += c.branchCost;\n    counts[c.status]++;\n    for (const k of Object.keys(counts) as Status[]) counts[k] += c.counts[k];\n  }\n  const live = { status, elapsed, cost, branchCost, counts };\n  out.set(node.id, live);\n  return live;\n}\n\nfunction clock(s: number) {\n  const whole = Math.floor(s);\n  return `${Math.floor(whole / 60)}:${String(whole % 60).padStart(2, \"0\")}`;\n}\n\nexport function SpawnTree({ root, speed = 1.2, autoplay = true, title = \"Agent run\", className }: SpawnTreeProps) {\n  const reduce = useReducedMotion();\n  const total = useMemo(() => endOf(root), [root]);\n  const [t, setT] = useState(autoplay ? 0 : total);\n  const [folded, setFolded] = useState<Set<string>>(new Set());\n  const finished = t >= total;\n\n  useEffect(() => {\n    if (finished) return;\n    const id = setInterval(() => setT((v) => Math.min(v + 0.2 * speed, total)), 200);\n    return () => clearInterval(id);\n  }, [finished, speed, total]);\n\n  const live = useMemo(() => {\n    const map = new Map<string, Live>();\n    compute(root, t, map);\n    return map;\n  }, [root, t]);\n\n  const rootLive = live.get(root.id)!;\n  const running = rootLive.counts.running + (rootLive.status === \"running\" ? 1 : 0);\n\n  const toggle = (id: string) =>\n    setFolded((f) => {\n      const next = new Set(f);\n      if (next.has(id)) next.delete(id);\n      else next.add(id);\n      return next;\n    });\n\n  return (\n    <div className={cn(\"w-full max-w-[480px] rounded-2xl border bg-card text-card-foreground shadow-xs/5\", className)}>\n      <div className=\"flex items-center justify-between gap-3 border-b px-4 py-3\">\n        <div className=\"min-w-0\">\n          <h3 className=\"font-medium text-sm\">{title}</h3>\n          <p className=\"text-muted-foreground text-xs tabular-nums\">\n            {finished ? \"Finished\" : `${running} working`} · {clock(t)} ·{\" \"}\n            <Money className=\"text-foreground\" value={rootLive.branchCost} /> total\n          </p>\n        </div>\n        <AnimatePresence initial={false}>\n          {finished && (\n            <motion.span\n              animate={{ opacity: 1, scale: 1, filter: \"blur(0px)\" }}\n              className=\"origin-right\"\n              exit={{ opacity: 0, scale: reduce ? 1 : 0.95, filter: \"blur(2px)\" }}\n              initial={{ opacity: 0, scale: reduce ? 1 : 0.95, filter: \"blur(2px)\" }}\n              transition={{ duration: 0.18, ease: [0.23, 1, 0.32, 1] }}\n            >\n              <Button\n                className=\"transition-[scale,box-shadow,background-color] duration-150 ease-out active:scale-[0.97]\"\n                onClick={() => setT(0)}\n                size=\"xs\"\n                variant=\"outline\"\n              >\n                <RotateCcwIcon />\n                Replay\n              </Button>\n            </motion.span>\n          )}\n        </AnimatePresence>\n      </div>\n      <ul className=\"p-2\" role=\"tree\">\n        <TreeNode folded={folded} isLast live={live} node={root} onToggle={toggle} root />\n      </ul>\n    </div>\n  );\n}\n\nfunction TreeNode({\n  node,\n  live,\n  folded,\n  onToggle,\n  isLast,\n  root,\n}: {\n  node: SpawnNode;\n  live: Map<string, Live>;\n  folded: Set<string>;\n  onToggle: (id: string) => void;\n  isLast: boolean;\n  root?: boolean;\n}) {\n  const reduce = useReducedMotion();\n  const groupId = useId();\n  const l = live.get(node.id)!;\n  const kids = node.children ?? [];\n  const isFolded = folded.has(node.id);\n  const spring = reduce ? { duration: 0 } : { type: \"spring\" as const, stiffness: 380, damping: 34 };\n  const descendants = l.counts.queued + l.counts.running + l.counts.done + l.counts.failed;\n\n  return (\n    <li\n      aria-expanded={kids.length ? !isFolded : undefined}\n      className={cn(\"relative\", !root && \"pt-1.5 pl-5\")}\n      role=\"treeitem\"\n    >\n      {!root && (\n        <>\n          <span\n            aria-hidden\n            className={cn(\"absolute top-0 left-2 w-px bg-border\", isLast ? \"h-7\" : \"bottom-0\")}\n          />\n          <span aria-hidden className=\"absolute top-7 left-2 h-px w-3 bg-border\" />\n        </>\n      )}\n      <motion.div\n        className={cn(\n          \"relative rounded-lg border bg-background px-3 py-2 transition-[border-color,opacity] duration-300 ease-out\",\n          l.status === \"running\" && \"border-info/32\",\n          l.status === \"queued\" && \"border-dashed\",\n        )}\n        layout=\"position\"\n        transition={spring}\n      >\n        <div className=\"flex items-center gap-2\">\n          {kids.length > 0 ? (\n            <button\n              aria-controls={groupId}\n              aria-label={isFolded ? `Expand ${node.name}` : `Fold ${node.name}`}\n              className=\"relative -ml-1 flex size-5 shrink-0 cursor-pointer items-center justify-center rounded-md text-muted-foreground outline-none transition-colors duration-150 after:absolute after:-inset-1.5 hover:bg-accent hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring\"\n              onClick={() => onToggle(node.id)}\n              type=\"button\"\n            >\n              <ChevronRightIcon\n                className={cn(\n                  \"size-3.5 transition-transform duration-200 ease-out motion-reduce:transition-none\",\n                  !isFolded && \"rotate-90\",\n                )}\n              />\n            </button>\n          ) : null}\n          <StatusDot status={l.status} />\n          <span className={cn(\"truncate font-medium text-sm\", l.status === \"queued\" && \"text-muted-foreground\")}>{node.name}</span>\n          {node.model && (\n            <span className=\"shrink-0 rounded-full bg-muted px-1.5 py-px font-mono text-[10px] text-muted-foreground\">\n              {node.model}\n            </span>\n          )}\n          <span className=\"ml-auto flex shrink-0 items-center gap-2.5 font-mono text-xs tabular-nums\">\n            <span className={l.status === \"running\" ? \"text-foreground\" : \"text-muted-foreground\"}>\n              {clock(l.elapsed)}\n            </span>\n            <Money className=\"w-12 text-right text-muted-foreground\" value={l.cost} />\n          </span>\n        </div>\n        {node.task && (\n          <p className={cn(\"mt-0.5 truncate text-muted-foreground text-xs\", kids.length > 0 ? \"pl-10\" : \"pl-4\")}>\n            {node.task}\n          </p>\n        )}\n        {kids.length > 0 && (\n          <div className=\"mt-1.5 flex items-center justify-between gap-2 border-t border-dashed pt-1.5 text-xs\">\n            <span className=\"truncate text-muted-foreground\">\n              {descendants} sub-agent{descendants === 1 ? \"\" : \"s\"}\n              {l.counts.running > 0 && <span className=\"text-info-foreground\"> · {l.counts.running} running</span>}\n              {l.counts.failed > 0 && <span className=\"text-destructive-foreground\"> · {l.counts.failed} failed</span>}\n            </span>\n            <span className=\"shrink-0 text-muted-foreground\">\n              branch <Money className=\"font-medium font-mono text-foreground\" value={l.branchCost} />\n            </span>\n          </div>\n        )}\n      </motion.div>\n\n      <AnimatePresence initial={false}>\n        {kids.length > 0 && !isFolded && (\n          <motion.ul\n            animate={{ height: \"auto\", opacity: 1 }}\n            className=\"overflow-hidden\"\n            exit={{ height: 0, opacity: 0 }}\n            id={groupId}\n            initial={{ height: 0, opacity: 0 }}\n            role=\"group\"\n            transition={spring}\n          >\n            {kids.map((child, i) => (\n              <TreeNode\n                folded={folded}\n                isLast={i === kids.length - 1}\n                key={child.id}\n                live={live}\n                node={child}\n                onToggle={onToggle}\n              />\n            ))}\n          </motion.ul>\n        )}\n      </AnimatePresence>\n    </li>\n  );\n}\n\nfunction StatusDot({ status }: { status: Status }) {\n  return (\n    <span className=\"relative flex size-2 shrink-0\">\n      <span\n        className={cn(\n          \"size-2 rounded-full transition-colors duration-300 ease-out\",\n          status === \"queued\" && \"bg-muted-foreground/40\",\n          status === \"running\" && \"bg-info\",\n          status === \"done\" && \"bg-success\",\n          status === \"failed\" && \"bg-destructive\",\n        )}\n      />\n      <span className=\"sr-only\">{status}</span>\n    </span>\n  );\n}\n\nfunction Money({ value, className }: { value: number; className?: string }) {\n  const reduce = useReducedMotion();\n  const mv = useSpring(value, reduce ? { duration: 0 } : { stiffness: 140, damping: 22 });\n  const text = useTransform(mv, (v) => `$${Math.max(v, 0).toFixed(2)}`);\n  useEffect(() => {\n    mv.set(value);\n  }, [mv, value]);\n  return <motion.span className={cn(\"inline-block tabular-nums\", className)}>{text}</motion.span>;\n}\n"
    }
  ]
}