{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tool-chips",
  "type": "registry:component",
  "title": "Tool chips",
  "description": "Code edits and tool calls as compact chips.",
  "categories": [
    "ai-interface"
  ],
  "registryDependencies": [
    "https://irisui.dev/r/foundation.json"
  ],
  "files": [
    {
      "path": "src/registry/components/tool-chips/tool-chips.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { createPortal } from \"react-dom\";\n\n/* ─────────────────────────────────────────────────────────\n * TOOL CHIPS\n * An agent run as compact rows: tool calls with inline\n * chips, then file-diff chips summarizing the edits.\n * Hover a row to reveal its chevron; every row expands\n * to show what the tool actually did.\n * ───────────────────────────────────────────────────────── */\n\nconst STEP_MS = 700;\n\nconst Icons: Record<string, React.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: (\n    <g fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\n      <path d=\"M17 3a2.8 2.8 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5z\" />\n    </g>\n  ),\n  run: (\n    <g fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\n      <path d=\"M4 17l6-5-6-5M12 19h8\" />\n    </g>\n  ),\n  read: (\n    <g fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\n      <path d=\"M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z\" />\n      <path d=\"M14 2v6h6\" />\n    </g>\n  ),\n};\n\nexport type ToolDetailLine = { text: string; tone?: \"add\" };\n\nexport type ToolStep = {\n  icon: string;\n  label: string;\n  chip: string;\n  mono: boolean;\n  detailMono: boolean;\n  detail: ToolDetailLine[];\n};\n\nexport type ToolDiff = { file: string; add: number; del: number };\n\nexport type ToolDiffLine = { text: string; tone: \"add\" | \"del\" | \"ctx\" };\n\nexport type ToolChipsLabels = {\n  header: string;\n  more: string;\n};\n\nconst DEFAULT_LABELS: ToolChipsLabels = {\n  header: \"4 tool calls, 2 messages\",\n  more: \"+2 more\",\n};\n\n/* docs-bot@agent working in a notdocs project */\nconst ROWS: ToolStep[] = [\n  {\n    icon: \"think\",\n    label: \"Thinking\",\n    chip: \"Planning the search panel…\",\n    mono: false,\n    detailMono: false,\n    detail: [\n      { text: \"The Search spec is in review, so the panel follows it.\" },\n      { text: \"Only specs and decisions need to show in results.\" },\n    ],\n  },\n  {\n    icon: \"write\",\n    label: \"Write 204 lines\",\n    chip: \"SearchPanel.tsx\",\n    mono: true,\n    detailMono: true,\n    detail: [\n      { text: '+ const results = docs.filter((doc) => doc.type === \"spec\")', tone: \"add\" },\n      { text: '+ return group(results, { by: \"status\" })', tone: \"add\" },\n    ],\n  },\n  {\n    icon: \"run\",\n    label: \"Rebuild and verify\",\n    chip: \"npm run check\",\n    mono: true,\n    detailMono: true,\n    detail: [{ text: \"✓ built in 1.2s\" }, { text: \"✓ 34 checks passed\" }],\n  },\n  {\n    icon: \"read\",\n    label: \"Read image\",\n    chip: \"search-wireframe.png\",\n    mono: true,\n    detailMono: false,\n    detail: [{ text: \"1280 × 720 · wireframe, three panels.\" }, { text: \"The query bar sits above the results list.\" }],\n  },\n];\n\nconst DIFFS: ToolDiff[] = [\n  { file: \"search.css\", add: 13, del: 0 },\n  { file: \"SearchPanel.tsx\", add: 74, del: 41 },\n  { file: \"search.md\", add: 8, del: 2 },\n];\n\n/* hovering a file chip opens its diff — green added, red removed */\nconst DIFF_LINES: Record<string, ToolDiffLine[]> = {\n  \"search.css\": [\n    { text: \".search-panel {\", tone: \"ctx\" },\n    { text: \"  gap: 14px;\", tone: \"del\" },\n    { text: \"  gap: 12px;\", tone: \"add\" },\n    { text: \"  container-type: inline-size;\", tone: \"add\" },\n    { text: \"}\", tone: \"ctx\" },\n  ],\n  \"SearchPanel.tsx\": [\n    { text: \"const docs = await listDocuments(project);\", tone: \"ctx\" },\n    { text: \"const results = docs;\", tone: \"del\" },\n    { text: \"const results = docs.filter(\", tone: \"add\" },\n    { text: '  (doc) => doc.type === \"spec\",', tone: \"add\" },\n    { text: \");\", tone: \"add\" },\n  ],\n  \"search.md\": [\n    { text: \"status: draft\", tone: \"del\" },\n    { text: \"status: review\", tone: \"add\" },\n  ],\n};\n\nexport default function ToolChips({\n  steps = ROWS,\n  diffs = DIFFS,\n  diffLines = DIFF_LINES,\n  labels,\n  className,\n  onOpenChange,\n  onToggleRow,\n}: {\n  /** Accepted for gallery/registry parity; ToolChips has no visual variants. */\n  variant?: string;\n  steps?: ToolStep[];\n  diffs?: ToolDiff[];\n  diffLines?: Record<string, ToolDiffLine[]>;\n  labels?: Partial<ToolChipsLabels>;\n  className?: string;\n  onOpenChange?: (open: boolean) => void;\n  onToggleRow?: (label: string, open: boolean) => void;\n} = {}) {\n  const copy = { ...DEFAULT_LABELS, ...labels };\n  const [step, setStep] = useState(0);\n  const [open, setOpen] = useState(true);\n  const [openRows, setOpenRows] = useState<Set<string>>(new Set());\n  /* Rendered in a body portal so animated/translated reply wrappers cannot\n   * redefine the fixed-position coordinate system. */\n  const [preview, setPreview] = useState<{\n    file: string;\n    x: number;\n    top?: number;\n    bottom?: number;\n  } | null>(null);\n  const openPreview = (file: string) => (event: React.SyntheticEvent) => {\n    const rect = (event.currentTarget as Element).closest(\"[data-diffchip]\")!.getBoundingClientRect();\n    const previewHeight = 38 + (diffLines[file]?.length ?? 0) * 19;\n    const fitsBelow = rect.bottom + 6 + previewHeight <= window.innerHeight - 12;\n    setPreview({\n      file,\n      x: Math.max(12, Math.min(rect.left, window.innerWidth - 300)),\n      ...(fitsBelow ? { top: rect.bottom + 6 } : { bottom: window.innerHeight - rect.top + 6 }),\n    });\n  };\n  const closePreview = (file: string) => () => setPreview((current) => (current?.file === file ? null : current));\n  const total = steps.length + 1; // rows, then diff chips\n\n  useEffect(() => {\n    if (step >= total) return;\n    const t = setTimeout(() => setStep((s) => s + 1), STEP_MS);\n    return () => clearTimeout(t);\n  }, [step, total]);\n\n  const toggleRow = (label: string) =>\n    setOpenRows((current) => {\n      const next = new Set(current);\n      if (next.has(label)) next.delete(label);\n      else next.add(label);\n      onToggleRow?.(label, next.has(label));\n      return next;\n    });\n\n  return (\n    <div className={`min-h-[220px] w-full max-w-80 pb-1${className ? ` ${className}` : \"\"}`}>\n      {/* collapsed run header */}\n      <button\n        type=\"button\"\n        aria-expanded={open}\n        onClick={() =>\n          setOpen((current) => {\n            onOpenChange?.(!current);\n            return !current;\n          })\n        }\n        className=\"-mx-1.5 flex w-fit items-center gap-1.5 rounded-button px-1.5 py-1 text-[12.5px] text-ink-2 transition-colors duration-100 hover:bg-hover-2\"\n      >\n        <svg\n          width=\"12\"\n          height=\"12\"\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-200\"\n          style={{ transform: open ? \"rotate(0deg)\" : \"rotate(-90deg)\" }}\n        >\n          <path d=\"M6 9l6 6 6-6\" />\n        </svg>\n        <span className=\"tabular-nums\">{copy.header}</span>\n      </button>\n\n      {/* tool call rows */}\n      <div\n        className=\"grid transition-[grid-template-rows,opacity] duration-300\"\n        style={{ gridTemplateRows: open ? \"1fr\" : \"0fr\", opacity: open ? 1 : 0 }}\n      >\n        {/* -mx-1 + px-1.5 keeps content at the same x while giving the\n            row hover pills room inside this overflow-hidden clip box */}\n        <div className=\"-mx-1 overflow-hidden px-1.5 pb-1\">\n          <div className=\"mt-1.5 flex flex-col gap-1\">\n            {steps.slice(0, step).map((row) => {\n              const rowOpen = openRows.has(row.label);\n              return (\n                <div key={row.label} className=\"animate-tool-chips-row-in\">\n                  <button\n                    type=\"button\"\n                    aria-expanded={rowOpen}\n                    onClick={() => toggleRow(row.label)}\n                    className=\"group/row -mx-[3px] flex h-7 w-[calc(100%+6px)] min-w-0 items-center gap-2 rounded-control px-[3px] text-left transition-colors duration-100 hover:bg-hover-2\"\n                  >\n                    <span className=\"relative flex size-4 shrink-0 items-center justify-center text-ink-3\">\n                      <svg\n                        width=\"13\"\n                        height=\"13\"\n                        viewBox=\"0 0 24 24\"\n                        fill={row.icon === \"think\" ? \"currentColor\" : \"none\"}\n                        stroke=\"currentColor\"\n                        className={`transition-opacity duration-100 group-hover/row:opacity-0 ${rowOpen ? \"opacity-0\" : \"\"}`}\n                      >\n                        {Icons[row.icon]}\n                      </svg>\n                      <svg\n                        width=\"12\"\n                        height=\"12\"\n                        viewBox=\"0 0 24 24\"\n                        fill=\"none\"\n                        stroke=\"currentColor\"\n                        strokeWidth=\"2.2\"\n                        strokeLinecap=\"round\"\n                        strokeLinejoin=\"round\"\n                        className={`absolute transition-[opacity,transform] duration-150 group-hover/row:opacity-100 ${rowOpen ? \"opacity-100\" : \"opacity-0\"}`}\n                        style={{ transform: rowOpen ? \"rotate(0deg)\" : \"rotate(-90deg)\" }}\n                      >\n                        <path d=\"M6 9l6 6 6-6\" />\n                      </svg>\n                    </span>\n                    <span className=\"shrink-0 text-[12.5px] font-medium text-ink\">{row.label}</span>\n                    <span\n                      className={`inline-flex h-5.5 min-w-0 flex-1 cursor-pointer items-center truncate rounded-chip bg-field px-1.5\n                    text-[11.5px] text-ink-2 shadow-hairline transition-colors duration-100 hover:bg-hover-2\n                    ${row.mono ? \"font-mono\" : \"\"}`}\n                    >\n                      {row.chip}\n                    </span>\n                  </button>\n\n                  {/* expanded detail */}\n                  <div\n                    className=\"grid transition-[grid-template-rows,opacity] duration-300\"\n                    style={{\n                      gridTemplateRows: rowOpen ? \"1fr\" : \"0fr\",\n                      opacity: rowOpen ? 1 : 0,\n                      transitionTimingFunction: \"cubic-bezier(0.23, 1, 0.32, 1)\",\n                    }}\n                  >\n                    <div className=\"min-h-0 overflow-hidden\">\n                      <div className=\"mt-0.5 mb-1 ml-2 flex flex-col gap-0.5 border-l border-line py-0.5 pl-3.5\">\n                        {row.detail.map((line) => (\n                          <span\n                            key={line.text}\n                            className={`truncate text-[11.5px] leading-[1.6] ${row.detailMono ? \"font-mono\" : \"\"} ${line.tone === \"add\" ? \"text-green\" : \"text-ink-2\"}`}\n                          >\n                            {line.text}\n                          </span>\n                        ))}\n                      </div>\n                    </div>\n                  </div>\n                </div>\n              );\n            })}\n          </div>\n\n          {/* file-diff chips */}\n          {step >= total && (\n            <div className=\"mt-2.5 flex max-w-full flex-wrap gap-1.5 border-t border-line pt-2.5\">\n              {diffs.map((d, i) => (\n                <span\n                  key={d.file}\n                  data-diffchip\n                  className=\"relative\"\n                  onMouseEnter={openPreview(d.file)}\n                  onMouseLeave={closePreview(d.file)}\n                >\n                  <button\n                    type=\"button\"\n                    aria-expanded={preview?.file === d.file}\n                    aria-label={`Show diff for ${d.file}`}\n                    onFocus={openPreview(d.file)}\n                    onBlur={closePreview(d.file)}\n                    className=\"inline-flex h-7 max-w-full animate-tool-chips-chip-in items-center gap-2 rounded-button\n                  bg-surface px-2.5 font-mono text-[11.5px] text-ink shadow-btn\n                  transition-colors duration-100 hover:bg-hover\"\n                    style={{ animationDelay: `${i * 80}ms` }}\n                  >\n                    <span className=\"min-w-0 truncate\">{d.file}</span>\n                    <span className=\"shrink-0 text-green tabular-nums\">+{d.add}</span>\n                    {d.del > 0 && <span className=\"shrink-0 text-red tabular-nums\">−{d.del}</span>}\n                  </button>\n                </span>\n              ))}\n              <button\n                type=\"button\"\n                className=\"inline-flex h-7 animate-tool-chips-more-in items-center rounded-button px-1.5 font-mono text-[11.5px] text-ink-3\n              underline decoration-transparent underline-offset-2 transition-colors duration-100\n              hover:text-ink-2 hover:decoration-current\"\n                style={{ animationDelay: `${diffs.length * 80}ms` }}\n              >\n                {copy.more}\n              </button>\n            </div>\n          )}\n        </div>\n      </div>\n      {preview &&\n        typeof document !== \"undefined\" &&\n        createPortal(\n          <div\n            className=\"fixed z-50 w-72 animate-tool-chips-preview-in overflow-hidden rounded-[10px] bg-surface shadow-overlay\"\n            style={{\n              left: preview.x,\n              top: preview.top,\n              bottom: preview.bottom,\n              transformOrigin: preview.top === undefined ? \"bottom left\" : \"top left\",\n            }}\n          >\n            <div className=\"flex items-center justify-between border-b border-line px-2.5 py-1.5 font-mono text-[11px]\">\n              <span className=\"min-w-0 truncate text-ink-2\">{preview.file}</span>\n              <span className=\"shrink-0 tabular-nums\">\n                <span className=\"text-green\">+{diffs.find((diff) => diff.file === preview.file)?.add}</span>\n                {(diffs.find((diff) => diff.file === preview.file)?.del ?? 0) > 0 && (\n                  <span className=\"text-red\"> −{diffs.find((diff) => diff.file === preview.file)?.del}</span>\n                )}\n              </span>\n            </div>\n            <div className=\"py-1 font-mono text-[11px] leading-[1.8]\">\n              {(diffLines[preview.file] ?? []).map((line, index) => (\n                <div\n                  key={index}\n                  className={`flex gap-2 px-2.5 whitespace-pre ${\n                    line.tone === \"add\"\n                      ? \"bg-green-tint text-green\"\n                      : line.tone === \"del\"\n                        ? \"bg-red-tint text-red\"\n                        : \"text-ink-2\"\n                  }`}\n                >\n                  <span className=\"w-3 shrink-0 select-none\">\n                    {line.tone === \"add\" ? \"+\" : line.tone === \"del\" ? \"−\" : \" \"}\n                  </span>\n                  <span className=\"min-w-0 truncate\">{line.text}</span>\n                </div>\n              ))}\n            </div>\n          </div>,\n          document.body,\n        )}\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/iris/tool-chips/tool-chips.tsx"
    }
  ],
  "docs": "Docs and live preview: https://irisui.dev/components/tool-chips",
  "meta": {
    "kind": "component",
    "group": "AI interface",
    "export": "ToolChips",
    "docs": "https://irisui.dev/components/tool-chips"
  }
}
