{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tool-chips",
  "title": "Tool chips",
  "description": "Progressively revealed tool calls with expandable details and file diff summaries.",
  "registryDependencies": [
    "https://beadkit.dev/r/ai-core.json"
  ],
  "files": [
    {
      "path": "components/ai/ToolChips.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useState, type ReactNode } from \"react\";\nimport styles from \"./ai-state.module.css\";\nimport { useReducedMotion } from \"./useReducedMotion\";\n\nexport type ToolIcon = \"think\" | \"write\" | \"run\" | \"read\" | (string & {});\n\nexport interface ToolDetailLine {\n  text: string;\n  tone?: \"add\" | \"remove\" | \"muted\";\n}\n\nexport interface ToolCall {\n  id: string;\n  icon?: ToolIcon;\n  label: string;\n  chip: string;\n  mono?: boolean;\n  detailMono?: boolean;\n  details?: readonly ToolDetailLine[];\n}\n\nexport interface ToolDiff {\n  file: string;\n  additions?: number;\n  deletions?: number;\n}\n\nexport interface ToolChipsProps {\n  rows: readonly ToolCall[];\n  diffs?: readonly ToolDiff[];\n  messageCount?: number;\n  /** Reveal one tool call at this interval. Set to 0 to show all immediately. */\n  revealInterval?: number;\n  defaultExpanded?: boolean;\n  defaultOpenRows?: readonly string[];\n  className?: string;\n}\n\nconst iconPaths: Record<string, ReactNode> = {\n  think: <path d=\"M12 2l2.4 7.2L22 12l-7.6 2.8L12 22l-2.4-7.2L2 12l7.6-2.8z\" />,\n  write: <path d=\"M17 3a2.8 2.8 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5z\" />,\n  run: <><path d=\"M4 17l6-5-6-5M12 19h8\" /></>,\n  read: <><path d=\"M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z\" /><path d=\"M14 2v6h6\" /></>,\n};\n\nfunction ToolIconMark({ name }: { name?: string }) {\n  return (\n    <svg aria-hidden=\"true\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill={name === \"think\" ? \"currentColor\" : \"none\"} stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\n      {iconPaths[name ?? \"\"] ?? <circle cx=\"12\" cy=\"12\" r=\"8\" />}\n    </svg>\n  );\n}\n\nexport function ToolChips({ rows, diffs = [], messageCount, revealInterval = 700, defaultExpanded = true, defaultOpenRows = [], className = \"\" }: ToolChipsProps) {\n  const reducedMotion = useReducedMotion();\n  const [visibleCount, setVisibleCount] = useState(revealInterval > 0 ? 0 : rows.length);\n  const [expanded, setExpanded] = useState(defaultExpanded);\n  const [openRows, setOpenRows] = useState<Set<string>>(() => new Set(defaultOpenRows));\n\n  useEffect(() => {\n    if (reducedMotion) {\n      setVisibleCount(rows.length);\n      return;\n    }\n    if (revealInterval <= 0 || visibleCount >= rows.length) return;\n    const timer = window.setTimeout(() => setVisibleCount((count) => Math.min(rows.length, count + 1)), revealInterval);\n    return () => window.clearTimeout(timer);\n  }, [reducedMotion, revealInterval, rows.length, visibleCount]);\n\n  const shownRows = rows.slice(0, visibleCount);\n  const complete = visibleCount >= rows.length;\n  const toggleRow = (id: string) => setOpenRows((current) => {\n    const next = new Set(current);\n    if (next.has(id)) next.delete(id); else next.add(id);\n    return next;\n  });\n\n  return (\n    <div data-ai-state=\"tool-chips\" className={`w-full max-w-md pb-1 ${className}`}>\n      <span className=\"sr-only\" role=\"status\" aria-live=\"polite\">\n        {complete ? `${rows.length} tool calls ready.` : `${visibleCount} of ${rows.length} tool calls ready.`}\n      </span>\n      <button type=\"button\" aria-expanded={expanded} onClick={() => setExpanded((value) => !value)} className=\"text-muted-foreground hover:bg-muted -mx-1.5 flex min-h-8 w-fit items-center gap-1.5 rounded-md px-1.5 text-xs transition-colors duration-150\">\n        <svg aria-hidden=\"true\" width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2.2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" className=\"transition-transform duration-200 motion-reduce:transition-none\" style={{ transform: expanded ? \"rotate(0deg)\" : \"rotate(-90deg)\" }}><path d=\"M6 9l6 6 6-6\" /></svg>\n        <span className=\"tabular-nums\">{rows.length} tool calls{messageCount === undefined ? \"\" : `, ${messageCount} messages`}</span>\n      </button>\n      <div className={`${styles.motion} grid transition-[grid-template-rows,opacity] duration-300`} style={{ gridTemplateRows: expanded ? \"1fr\" : \"0fr\", opacity: expanded ? 1 : 0 }}>\n        <div className=\"min-h-0 overflow-hidden px-1.5 pb-1\">\n          <div className=\"mt-1.5 flex flex-col gap-1\">\n            {shownRows.map((row) => {\n              const open = openRows.has(row.id);\n              return <div key={row.id} className={styles.fadeUp}>\n                <button type=\"button\" aria-expanded={open} onClick={() => toggleRow(row.id)} className=\"hover:bg-muted group flex min-h-8 w-full min-w-0 items-center gap-2 rounded-md px-1 text-left transition-colors duration-150\">\n                  <span className=\"text-muted-foreground relative flex size-4 shrink-0 items-center justify-center\"><span className={`transition-opacity duration-150 motion-reduce:transition-none ${open ? \"opacity-0\" : \"group-hover:opacity-0\"}`}><ToolIconMark name={row.icon} /></span><svg aria-hidden=\"true\" width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2.2\" strokeLinecap=\"round\" className={`absolute transition-[opacity,transform] duration-150 motion-reduce:transition-none ${open ? \"opacity-100\" : \"opacity-0 group-hover:opacity-100\"}`} style={{ transform: open ? \"rotate(0deg)\" : \"rotate(-90deg)\" }}><path d=\"M6 9l6 6 6-6\" /></svg></span>\n                  <span className=\"shrink-0 text-xs font-medium\">{row.label}</span>\n                  <span className={`bg-muted text-muted-foreground min-w-0 flex-1 truncate rounded-md px-1.5 py-1 text-[11.5px] ${row.mono ? \"font-mono\" : \"\"}`}>{row.chip}</span>\n                </button>\n                <div className={`${styles.motion} grid transition-[grid-template-rows,opacity] duration-300`} style={{ gridTemplateRows: open ? \"1fr\" : \"0fr\", opacity: open ? 1 : 0 }}><div className=\"min-h-0 overflow-hidden\"><div className=\"border-border text-muted-foreground ml-3.5 flex flex-col gap-0.5 border-l py-0.5 pl-3 text-[11.5px] leading-relaxed\">{row.details?.map((line) => <span key={line.text} className={`${row.detailMono ? \"font-mono\" : \"\"} ${line.tone === \"add\" ? \"text-success\" : line.tone === \"remove\" ? \"text-destructive\" : \"\"}`}>{line.text}</span>)}</div></div></div>\n              </div>;\n            })}\n          </div>\n          {complete && diffs.length > 0 ? <div className=\"border-border mt-2.5 flex max-w-full flex-wrap gap-1.5 border-t pt-2.5\">{diffs.map((diff, index) => <span key={diff.file} className={`${styles.fadeIn} bg-card border-border inline-flex min-h-7 max-w-full items-center gap-1.5 rounded-md border px-2 font-mono text-[11.5px] shadow-sm`} style={{ animationDelay: `${index * 80}ms` }}><span className=\"min-w-0 truncate\">{diff.file}</span>{diff.additions !== undefined ? <span className=\"text-success shrink-0 tabular-nums\">+{diff.additions}</span> : null}{diff.deletions ? <span className=\"text-destructive shrink-0 tabular-nums\">−{diff.deletions}</span> : null}</span>)}</div> : null}\n        </div>\n      </div>\n    </div>\n  );\n}\n\nexport default ToolChips;\n",
      "type": "registry:component",
      "target": "@components/ai/ToolChips.tsx"
    }
  ],
  "type": "registry:component"
}
