{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"tool-chips","title":"Tool Chips","description":"Code edits and tool calls as compact chips.","dependencies":[],"registryDependencies":["https://ward.so/r/foundation.json"],"files":[{"path":"registry/ward/ToolChips.tsx","content":"\"use client\";\n/*\n * MIT License\n *\n * Copyright (c) 2026 Shane Levine\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\nimport \"./styles/foundation.css\";\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: <g fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\"><path d=\"M17 3a2.8 2.8 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5z\" /></g>,\n  run: <g fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\"><path d=\"M4 17l6-5-6-5M12 19h8\" /></g>,\n  read: <g fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\"><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\" /></g>,\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\nconst ROWS: ToolStep[] = [\n  {\n    icon: \"think\", label: \"Thinking\", chip: \"Planning the plan schedule…\", mono: false, detailMono: false,\n    detail: [\n      { text: \"Weekend demand carries pistachio, so it plans first.\" },\n      { text: \"Batch capacity leaves two evening freezer windows.\" },\n    ],\n  },\n  {\n    icon: \"write\", label: \"Write 204 lines\", chip: \"PlanSchedule.tsx\", mono: true, detailMono: true,\n    detail: [\n      { text: \"+ const windows = slots.filter((s) => s.temp <= -12)\", tone: \"add\" },\n      { text: \"+ return schedule(windows, { hero: \\\"pistachio\\\" })\", tone: \"add\" },\n    ],\n  },\n  {\n    icon: \"run\", label: \"Rebuild and verify\", chip: \"npm run freeze\", mono: true, detailMono: true,\n    detail: [\n      { text: \"✓ built in 1.2s\" },\n      { text: \"✓ 34 checks passed\" },\n    ],\n  },\n  {\n    icon: \"read\", label: \"Read image\", chip: \"project-chart.png\", mono: true, detailMono: false,\n    detail: [\n      { text: \"1280 × 720 · line chart, three summers.\" },\n      { text: \"Mint chip trends up 12% through July.\" },\n    ],\n  },\n];\n\nconst DIFFS: ToolDiff[] = [\n  { file: \"projects.css\", add: 13, del: 0 },\n  { file: \"PlanSchedule.tsx\", add: 74, del: 41 },\n  { file: \"menu.ts\", 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  \"projects.css\": [\n    { text: \".work-card {\", 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  \"PlanSchedule.tsx\": [\n    { text: \"const slots = coldSlots(week);\", tone: \"ctx\" },\n    { text: \"const windows = slots;\", tone: \"del\" },\n    { text: \"const windows = slots.filter(\", tone: \"add\" },\n    { text: \"  (s) => s.temp <= -12,\", tone: \"add\" },\n    { text: \");\", tone: \"add\" },\n  ],\n  \"menu.ts\": [\n    { text: \"export const hero = \\\"mint-chip\\\";\", tone: \"del\" },\n    { text: \"export const hero = \\\"pistachio\\\";\", 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\n        ? { top: rect.bottom + 6 }\n        : { bottom: window.innerHeight - rect.top + 6 }),\n    });\n  };\n  const closePreview = (file: string) => () =>\n    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-control px-1.5 py-1 text-[12.5px] text-ink-2 transition-colors duration-100 hover:bg-hover-2\"\n      >\n        <svg 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\" style={{ transform: open ? \"rotate(0deg)\" : \"rotate(-90deg)\" }}>\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 className=\"grid transition-[grid-template-rows,opacity] duration-300\" style={{ gridTemplateRows: open ? \"1fr\" : \"0fr\", opacity: open ? 1 : 0 }}>\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} style={{ animation: \"fade-up 300ms cubic-bezier(0.23,1,0.32,1) both\" }}>\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\" height=\"13\" viewBox=\"0 0 24 24\" fill={row.icon === \"think\" ? \"currentColor\" : \"none\"} 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\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2.2\" strokeLinecap=\"round\" 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={{ gridTemplateRows: rowOpen ? \"1fr\" : \"0fr\", opacity: rowOpen ? 1 : 0, transitionTimingFunction: \"cubic-bezier(0.23, 1, 0.32, 1)\" }}\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 items-center gap-2 rounded-chip\n                  bg-card px-2 font-mono text-[11.5px] text-ink shadow-btn\n                  transition-colors duration-100 hover:bg-hover\"\n                style={{ animation: `pop-in 250ms cubic-bezier(0.23,1,0.32,1) ${i * 80}ms both` }}\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\n            </span>\n          ))}\n          <button\n            type=\"button\"\n            className=\"inline-flex h-7 items-center rounded-chip 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={{ animation: `fade-in 300ms ease-out ${diffs.length * 80}ms both` }}\n          >\n            {copy.more}\n          </button>\n        </div>\n      )}\n        </div>\n      </div>\n      {preview && typeof document !== \"undefined\" && createPortal(\n        <div\n          className=\"fixed z-50 w-72 overflow-hidden rounded-[10px] bg-card shadow-overlay\"\n          style={{\n            left: preview.x,\n            top: preview.top,\n            bottom: preview.bottom,\n            animation: \"pop-in 160ms cubic-bezier(0.23,1,0.32,1) both\",\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\">{line.tone === \"add\" ? \"+\" : line.tone === \"del\" ? \"−\" : \" \"}</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/ward/ToolChips.tsx"}],"meta":{"variants":["Default"],"version":"1.1.0","source":"https://github.com/slev12397/beautiful-ui/blob/44a274e598395ab61e7c96c26fda2758780253b7/components/primitives/ToolChips.tsx","access":"free"},"categories":["ai"],"type":"registry:component"}