{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tool-call",
  "type": "registry:ui",
  "title": "Tool Call",
  "description": "One tool invocation: name and icon, queued/running/done/failed/awaiting-approval status, live elapsed time and collapsible JSON args and result.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "https://kuro.abstergo.fyi/r/kura-theme.json",
    "https://kuro.abstergo.fyi/r/code-block.json",
    "https://kuro.abstergo.fyi/r/shimmer-text.json",
    "https://kuro.abstergo.fyi/r/use-controllable-state.json",
    "utils"
  ],
  "files": [
    {
      "path": "registry/kura/tool-call/index.ts",
      "type": "registry:ui",
      "target": "components/kura/tool-call/index.ts",
      "content": "export { formatDuration, ToolCall, type ToolCallProps, type ToolCallStatus } from \"./tool-call\";\n"
    },
    {
      "path": "registry/kura/tool-call/tool-call.tsx",
      "type": "registry:ui",
      "target": "components/kura/tool-call/tool-call.tsx",
      "content": "\"use client\";\n\nimport { Check, ChevronRight, Hand, Wrench, X } from \"lucide-react\";\nimport * as React from \"react\";\nimport { useControllableState } from \"@/hooks/kura/use-controllable-state\";\nimport { cn } from \"@/lib/utils\";\nimport { CodeBlock } from \"@/components/kura/code-block\";\nimport { ShimmerText } from \"@/components/kura/shimmer-text\";\n\nexport type ToolCallStatus = \"pending\" | \"running\" | \"success\" | \"error\" | \"awaiting-approval\";\n\nexport interface ToolCallProps extends Omit<React.ComponentProps<\"div\">, \"title\"> {\n  /** The tool's identifier, e.g. \"search_web\". Rendered in mono. */\n  name: string;\n  /** Human label, e.g. \"Searching the web\". Shown instead of `name` when given. */\n  title?: React.ReactNode;\n  /** Icon for the tool, e.g. `<Globe />`. Default a wrench. */\n  icon?: React.ReactNode;\n  /** Lifecycle state. Default \"running\". */\n  status?: ToolCallStatus;\n  /** Arguments the model passed. Objects are pretty-printed as JSON; strings are shown as-is. */\n  args?: unknown;\n  /** The tool's output. Objects are pretty-printed as JSON; strings are shown as-is. */\n  result?: unknown;\n  /** Error message for status=\"error\". */\n  error?: React.ReactNode;\n  /** One-line summary next to the title. Default: the first primitive value in `args`. Pass null to hide. */\n  summary?: React.ReactNode;\n  /** When the call started (for the live timer). Default: when status became \"running\". */\n  startedAt?: Date | number;\n  /** Final duration in ms. Default: measured from running → done. */\n  duration?: number;\n  /** `card` = a bordered card (default); `inline` = a compact row inside message text. */\n  variant?: \"card\" | \"inline\";\n  /** Details (args/result) expanded (controlled). */\n  open?: boolean;\n  /** Initial expanded state when uncontrolled. Default: true for \"error\" and \"awaiting-approval\". */\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  /** Always-visible footer, e.g. Approve / Deny buttons while awaiting approval. */\n  actions?: React.ReactNode;\n}\n\nexport function formatDuration(ms: number): string {\n  if (ms < 1000) return `${Math.max(0, Math.round(ms / 10) * 10)}ms`;\n  if (ms < 60_000) return `${(ms / 1000).toFixed(1)}s`;\n  const s = Math.round(ms / 1000);\n  return `${Math.floor(s / 60)}m ${s % 60}s`;\n}\n\nconst statusText: Record<ToolCallStatus, string> = {\n  pending: \"Queued\",\n  running: \"Running\",\n  success: \"Completed\",\n  error: \"Failed\",\n  \"awaiting-approval\": \"Waiting for approval\",\n};\n\nfunction toText(value: unknown): string {\n  if (typeof value === \"string\") return value;\n  try {\n    return JSON.stringify(value, null, 2) ?? String(value);\n  } catch {\n    return String(value);\n  }\n}\n\nfunction deriveSummary(args: unknown): string | null {\n  if (args == null) return null;\n  if (typeof args !== \"object\") return String(args);\n  for (const v of Object.values(args as Record<string, unknown>)) {\n    if (typeof v === \"string\" || typeof v === \"number\" || typeof v === \"boolean\") return String(v);\n  }\n  return null;\n}\n\nfunction StatusGlyph({ status, small }: { status: ToolCallStatus; small?: boolean }) {\n  return (\n    <span\n      aria-hidden\n      data-slot=\"tool-call-status\"\n      className={cn(\n        \"relative grid shrink-0 place-items-center rounded-full transition-colors duration-(--kura-duration-base) ease-kura-out\",\n        small ? \"size-4 [&_svg]:size-2.5\" : \"size-5 [&_svg]:size-3\",\n        status === \"pending\" &&\n          \"text-kura-fg-subtle shadow-[inset_0_0_0_1px_var(--kura-border-strong)]\",\n        status === \"running\" && \"bg-kura-accent-subtle text-kura-accent-text\",\n        status === \"success\" && \"bg-kura-hover text-kura-success-text\",\n        status === \"error\" && \"bg-kura-danger-subtle text-kura-danger-text\",\n        status === \"awaiting-approval\" && \"bg-kura-accent-subtle text-kura-accent-text\",\n      )}\n    >\n      {status === \"running\" && (\n        <span className=\"size-1.5 animate-kura-pulse rounded-full bg-current motion-reduce:animate-none\" />\n      )}\n      {status === \"success\" && <Check strokeWidth={2.5} className=\"animate-kura-pop-in\" />}\n      {status === \"error\" && <X strokeWidth={2.5} className=\"animate-kura-pop-in\" />}\n      {status === \"awaiting-approval\" && <Hand className=\"animate-kura-pop-in\" />}\n    </span>\n  );\n}\n\nfunction useElapsed(status: ToolCallStatus, startedAt?: Date | number, duration?: number) {\n  const start = React.useRef<number | null>(startedAt != null ? +new Date(startedAt) : null);\n  const [now, setNow] = React.useState<number | null>(null);\n  const [final, setFinal] = React.useState<number | null>(null);\n  React.useEffect(() => {\n    if (status === \"running\") {\n      start.current = startedAt != null ? +new Date(startedAt) : Date.now();\n      setFinal(null);\n      setNow(Date.now());\n      const id = setInterval(() => setNow(Date.now()), 100);\n      return () => clearInterval(id);\n    }\n    const began = start.current;\n    if ((status === \"success\" || status === \"error\") && began != null) {\n      setFinal((f) => f ?? Date.now() - began);\n    }\n  }, [status, startedAt]);\n  if (duration != null) return duration;\n  if (status === \"running\" && start.current != null && now != null) return now - start.current;\n  return final;\n}\n\n/**\n * One tool invocation: name and icon, a status glyph (queued, running, done, failed, awaiting\n * approval), a live elapsed timer, and collapsible pretty-printed arguments and result.\n */\nexport function ToolCall({\n  name,\n  title,\n  icon,\n  status = \"running\",\n  args,\n  result,\n  error,\n  summary,\n  startedAt,\n  duration,\n  variant = \"card\",\n  open: openProp,\n  defaultOpen,\n  onOpenChange,\n  actions,\n  className,\n  ...props\n}: ToolCallProps) {\n  const [open, setOpen] = useControllableState({\n    value: openProp,\n    defaultValue: defaultOpen ?? (status === \"error\" || status === \"awaiting-approval\"),\n    onChange: onOpenChange,\n  });\n  const elapsed = useElapsed(status, startedAt, duration);\n  const id = React.useId();\n  const hasDetails = args !== undefined || result !== undefined || error != null;\n  const line = summary === undefined ? deriveSummary(args) : summary;\n  const inline = variant === \"inline\";\n  const running = status === \"running\";\n\n  const label = title ?? <span className=\"font-kura-mono text-[0.93em]\">{name}</span>;\n\n  const headerContent = (\n    <>\n      <StatusGlyph status={status} small={inline} />\n      <span\n        aria-hidden\n        className=\"shrink-0 text-kura-fg-subtle [&_svg]:size-3.5\"\n        data-slot=\"tool-call-icon\"\n      >\n        {icon ?? <Wrench />}\n      </span>\n      <span className=\"flex min-w-0 flex-1 items-baseline gap-2\">\n        <span data-slot=\"tool-call-title\" className=\"shrink-0 font-medium text-kura-fg\">\n          {running ? <ShimmerText>{label}</ShimmerText> : label}\n        </span>\n        {line != null && line !== \"\" && (\n          <span data-slot=\"tool-call-summary\" className=\"truncate text-kura-fg-subtle\">\n            {line}\n          </span>\n        )}\n      </span>\n      <span className=\"sr-only\">, {statusText[status]}</span>\n      {elapsed != null && (\n        <span\n          data-slot=\"tool-call-elapsed\"\n          className=\"shrink-0 font-kura-mono text-kura-fg-subtle text-xs tabular-nums\"\n        >\n          {formatDuration(elapsed)}\n        </span>\n      )}\n      {hasDetails && (\n        <ChevronRight\n          aria-hidden\n          className={cn(\n            \"size-3.5 shrink-0 text-kura-fg-subtle transition-transform duration-(--kura-duration-base) ease-kura-out motion-reduce:transition-none\",\n            open && \"rotate-90\",\n          )}\n        />\n      )}\n    </>\n  );\n\n  const headerClass = cn(\n    \"flex w-full min-w-0 items-center gap-2 text-left outline-none\",\n    inline ? \"h-7 rounded-kura-sm px-1.5 text-[13px]\" : \"h-11 px-3 text-sm\",\n  );\n\n  return (\n    <div\n      data-slot=\"tool-call\"\n      data-status={status}\n      data-variant={variant}\n      data-state={hasDetails ? (open ? \"open\" : \"closed\") : undefined}\n      className={cn(\n        \"flex min-w-0 flex-col font-kura-sans\",\n        inline\n          ? \"-mx-1.5 w-fit max-w-full\"\n          : \"overflow-hidden rounded-kura-lg bg-kura-surface shadow-kura-sm\",\n        className,\n      )}\n      {...props}\n    >\n      {hasDetails ? (\n        <button\n          type=\"button\"\n          data-slot=\"tool-call-trigger\"\n          aria-expanded={open}\n          aria-controls={`${id}-details`}\n          onClick={() => setOpen(!open)}\n          className={cn(\n            headerClass,\n            \"cursor-pointer transition-[background-color,box-shadow] duration-(--kura-duration-fast) ease-kura-out hover:bg-kura-hover focus-visible:shadow-[inset_0_0_0_2px_var(--kura-ring)]\",\n            inline && \"focus-visible:shadow-kura-focus\",\n          )}\n        >\n          {headerContent}\n        </button>\n      ) : (\n        <div data-slot=\"tool-call-trigger\" className={headerClass}>\n          {headerContent}\n        </div>\n      )}\n      <span className=\"sr-only\" role=\"status\">\n        {name} {statusText[status]}\n      </span>\n\n      {hasDetails && (\n        <div\n          id={`${id}-details`}\n          data-slot=\"tool-call-details\"\n          inert={!open || undefined}\n          className={cn(\n            \"grid transition-[grid-template-rows,opacity] duration-(--kura-duration-slow) ease-kura-out motion-reduce:transition-opacity\",\n            open ? \"grid-rows-[1fr] opacity-100\" : \"grid-rows-[0fr] opacity-0\",\n          )}\n        >\n          <div className=\"min-h-0 overflow-hidden\">\n            <div\n              className={cn(\n                \"flex flex-col gap-3\",\n                inline\n                  ? \"mt-1 mb-2 ml-[18px] border-kura-border border-l pl-4\"\n                  : \"px-3 pt-1 pb-3 shadow-[inset_0_1px_0_var(--kura-border-subtle)] [&>:first-child]:mt-2\",\n              )}\n            >\n              {args !== undefined && <Section label=\"Arguments\" value={args} />}\n              {error != null && (\n                <div\n                  data-slot=\"tool-call-error\"\n                  className=\"rounded-kura-md bg-kura-danger-subtle px-3 py-2 font-kura-mono text-kura-danger-text text-xs leading-relaxed\"\n                >\n                  {error}\n                </div>\n              )}\n              {result !== undefined && <Section label=\"Result\" value={result} />}\n            </div>\n          </div>\n        </div>\n      )}\n\n      {actions != null && (\n        <div\n          data-slot=\"tool-call-actions\"\n          className={cn(\n            \"flex items-center justify-end gap-2\",\n            inline ? \"py-1.5\" : \"px-3 py-2.5 shadow-[inset_0_1px_0_var(--kura-border-subtle)]\",\n          )}\n        >\n          {actions}\n        </div>\n      )}\n    </div>\n  );\n}\n\nfunction Section({ label, value }: { label: string; value: unknown }) {\n  const text = toText(value);\n  const json = typeof value !== \"string\";\n  return (\n    <div data-slot=\"tool-call-section\" className=\"flex flex-col gap-1.5\">\n      <span className=\"font-medium text-[11px] text-kura-fg-subtle uppercase tracking-wider\">\n        {label}\n      </span>\n      <CodeBlock\n        code={text}\n        filename={label}\n        language={json ? \"json\" : \"text\"}\n        header={false}\n        wrap\n        maxHeight={220}\n        className=\"rounded-kura-md text-xs shadow-[0_0_0_1px_var(--kura-border-subtle)] [&_[data-slot=code-block-line]]:px-3 [&_section]:py-2\"\n      />\n    </div>\n  );\n}\n"
    }
  ],
  "categories": [
    "agent",
    "tool",
    "function-calling",
    "agent",
    "mcp"
  ],
  "docs": "Docs: https://kuro.abstergo.fyi/components/tool-call · Markdown for agents: https://kuro.abstergo.fyi/components/tool-call.md",
  "meta": {
    "kura": {
      "status": "beta",
      "whenToUse": [
        "Show a function/tool call an LLM made (AI SDK tool parts, MCP calls, OpenAI/Anthropic tool_use blocks) inside an assistant message.",
        "Show a tool's progress with a live timer, then its result or error.",
        "A tool that needs the user's go-ahead: status=\"awaiting-approval\" with Approve/Deny buttons in `actions`.",
        "Compact inline mentions of tool use within answer text (variant=\"inline\")."
      ],
      "whenNotToUse": [
        "A multi-step plan with many steps and progress — use agent-timeline (put ToolCalls inside steps if needed).",
        "A dedicated, prominent permission request with risk level and keyboard shortcuts — use approval-prompt.",
        "Plain code output — use code-block.",
        "Model thinking — use reasoning."
      ]
    }
  }
}
