{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tool-call-card",
  "title": "Tool Call Card",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "Shows one agent tool call: the tool name, a pending, running, success or error status with icon and text, duration, and collapsible pretty-printed input and result.",
  "dependencies": [
    "lucide-react",
    "radix-ui"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/i18n.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/tool-call-card.tsx",
      "content": "// Ballmac UI: Tool Call Card. https://ui.ballmac.com/components/tool-call-card\n\"use client\"\n\nimport * as React from \"react\"\nimport { ChevronRight, CircleCheck, CircleDashed, CircleX, LoaderCircle, Wrench } from \"lucide-react\"\nimport { Collapsible as CollapsiblePrimitive } from \"radix-ui\"\n\nimport { cn } from \"@/lib/utils\"\nimport { defineMessage, useMessages, type Message } from \"@/lib/ballmac/i18n\"\n\ntype ToolCallStatus = \"pending\" | \"running\" | \"success\" | \"error\"\n\nconst statusConfig: Record<ToolCallStatus, { label: Message; icon: React.ElementType; className: string }> = {\n  pending: { label: defineMessage(\"tool-call-card.statusConfig.pending\", \"Pending\"), icon: CircleDashed, className: \"text-muted-foreground\" },\n  running: { label: defineMessage(\"tool-call-card.statusConfig.running\", \"Running\"), icon: LoaderCircle, className: \"text-foreground\" },\n  success: { label: defineMessage(\"tool-call-card.statusConfig.success\", \"Success\"), icon: CircleCheck, className: \"text-chart-2\" }, // icon only; chart-2 is too light for small text\n  error: { label: defineMessage(\"tool-call-card.statusConfig.error\", \"Error\"), icon: CircleX, className: \"text-destructive\" },\n}\n\n/** 340 -> \"340ms\", 1240 -> \"1.2s\", 72000 -> \"1m 12s\". Locale-independent so SSR and the browser agree. */\nfunction formatToolDuration(ms: number) {\n  if (ms < 1000) return `${Math.max(0, Math.round(ms))}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\nfunction formatPayload(value: unknown) {\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\ntype ToolCallStatusBadgeProps = React.ComponentProps<\"span\"> & {\n  /** The tool call's lifecycle state. Rendered as an icon plus text, never color alone. */\n  status: ToolCallStatus\n}\n\nfunction ToolCallStatusBadge({ status, className, ...props }: ToolCallStatusBadgeProps) {\n  const msg = useMessages()\n  const { label, icon: Icon, className: tone } = statusConfig[status]\n  return (\n    <span\n      data-slot=\"tool-call-status\"\n      data-status={status}\n      className={cn(\"inline-flex shrink-0 items-center gap-1 text-xs font-medium\", status === \"pending\" ? \"text-muted-foreground\" : \"text-foreground\", className)}\n      {...props}\n    >\n      <Icon\n        aria-hidden=\"true\"\n        className={cn(\"size-3.5\", tone, status === \"running\" && \"animate-spin motion-reduce:animate-none\")}\n      />\n      {msg.of(label)}\n    </span>\n  )\n}\n\nfunction PayloadBlock({ label, value, tone }: { label: string; value: unknown; tone?: \"error\" }) {\n  return (\n    <div data-slot=\"tool-call-section\" className=\"grid gap-1.5\">\n      <div className=\"font-mono text-[11px] tracking-wide text-muted-foreground uppercase\">{label}</div>\n      <pre dir=\"ltr\"\n        tabIndex={0}\n        aria-label={label}\n        className={cn(\n          \"max-h-64 overflow-auto rounded-md border bg-muted/50 px-3 py-2.5 font-mono text-xs leading-5 outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50\",\n          tone === \"error\" && \"border-destructive/30 text-destructive\"\n        )}\n      >\n        {formatPayload(value)}\n      </pre>\n    </div>\n  )\n}\n\ntype ToolCallCardProps = Omit<React.ComponentProps<\"div\">, \"dir\"> & {\n  /** Tool name, shown in monospace, for example \"search_docs\". */\n  name: string\n  /** Lifecycle state of the call. */\n  status?: ToolCallStatus\n  /** How long the call took, in milliseconds. */\n  duration?: number\n  /** Arguments the model passed. Objects are pretty-printed as JSON; strings are shown as-is. */\n  input?: unknown\n  /** What the tool returned, or the error message when status is \"error\". */\n  result?: unknown\n  /** Short human description shown under the name. */\n  description?: React.ReactNode\n  /** Controlled open state of the details panel. */\n  open?: boolean\n  /** Initial open state when uncontrolled. */\n  defaultOpen?: boolean\n  /** Called when the details panel opens or closes. */\n  onOpenChange?: (open: boolean) => void\n}\n\nfunction ToolCallCard({\n  name,\n  status = \"pending\",\n  duration,\n  input,\n  result,\n  description,\n  open,\n  defaultOpen = false,\n  onOpenChange,\n  className,\n  children,\n  ...props\n}: ToolCallCardProps) {\n  const hasInput = input !== undefined\n  const hasResult = result !== undefined\n  const hasDetails = hasInput || hasResult || children != null\n\n  return (\n    <CollapsiblePrimitive.Root\n      data-slot=\"tool-call-card\"\n      data-status={status}\n      open={open}\n      defaultOpen={defaultOpen}\n      onOpenChange={onOpenChange}\n      disabled={!hasDetails}\n      className={cn(\"group/tool w-full overflow-hidden rounded-xl border bg-card text-card-foreground\", className)}\n      {...props}\n    >\n      <div className=\"flex min-w-0 items-center gap-2 px-3 py-2\">\n        <CollapsiblePrimitive.Trigger\n          data-slot=\"tool-call-trigger\"\n          className=\"-mx-1 flex min-w-0 flex-1 items-center gap-2 rounded-md px-1 py-1 text-start outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-default\"\n        >\n          <ChevronRight\n            aria-hidden=\"true\"\n            className={cn(\n              \"size-3.5 shrink-0 text-muted-foreground transition-transform duration-150 group-data-[state=open]/tool:rotate-90 rtl:group-data-[state=closed]/tool:rotate-180 motion-reduce:transition-none\",\n              !hasDetails && \"invisible\"\n            )}\n          />\n          <Wrench aria-hidden=\"true\" className=\"size-3.5 shrink-0 text-muted-foreground\" />\n          <span className=\"min-w-0\">\n            <span className=\"block truncate font-mono text-[13px] font-medium\">{name}</span>\n            {description ? <span className=\"block truncate text-xs text-muted-foreground\">{description}</span> : null}\n          </span>\n        </CollapsiblePrimitive.Trigger>\n        <span aria-live=\"polite\" className=\"flex shrink-0 items-center gap-2.5\">\n          <ToolCallStatusBadge status={status} />\n          {duration !== undefined ? (\n            <span className=\"font-mono text-[11px] text-muted-foreground tabular-nums\">{formatToolDuration(duration)}</span>\n          ) : null}\n        </span>\n      </div>\n      {hasDetails ? (\n        <CollapsiblePrimitive.Content data-slot=\"tool-call-details\" className=\"grid gap-3 border-t px-3 py-3\">\n          {hasInput ? <PayloadBlock label=\"Input\" value={input} /> : null}\n          {hasResult ? (\n            <PayloadBlock\n              label={status === \"error\" ? \"Error\" : \"Result\"}\n              value={result}\n              tone={status === \"error\" ? \"error\" : undefined}\n            />\n          ) : null}\n          {children}\n        </CollapsiblePrimitive.Content>\n      ) : null}\n    </CollapsiblePrimitive.Root>\n  )\n}\n\nexport {\n  ToolCallCard,\n  ToolCallStatusBadge,\n  formatToolDuration,\n  type ToolCallCardProps,\n  type ToolCallStatusBadgeProps,\n  type ToolCallStatus,\n}\n",
      "type": "registry:ui",
      "target": "@components/ballmac/tool-call-card.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "ai",
      "agent",
      "tool",
      "function-calling",
      "json",
      "status",
      "llm"
    ],
    "version": "1.0.0",
    "updated": "2026-09-28",
    "ai": {
      "summary": "Render each tool invocation of an agent: <ToolCallCard name status duration input result />. Objects are shown as indented JSON, strings as-is; the details panel is a disclosure (collapsed by default).",
      "whenToUse": [
        "Showing function/tool calls inside an assistant message",
        "Agent run logs where each step calls a tool and returns data",
        "Debug views for MCP tool calls"
      ],
      "whenNotToUse": [
        "Generic key/value data (use a table or description list)",
        "Long source files (use code-block)"
      ],
      "composesWith": [
        "ai-message",
        "ai-chat",
        "reasoning-disclosure"
      ],
      "a11y": [
        {
          "keys": "Enter / Space",
          "action": "On the tool name, opens or closes the input and result (aria-expanded)"
        },
        {
          "keys": "Tab",
          "action": "Focuses the scrollable input and result blocks so long JSON can be scrolled with the keyboard"
        },
        {
          "keys": "—",
          "action": "Status changes are announced politely; status is always icon plus text"
        }
      ],
      "customization": [
        "status: pending | running | success (chart-2 token) | error (destructive token)",
        "duration in milliseconds, formatted as 340ms / 1.2s / 1m 12s",
        "open/defaultOpen/onOpenChange control the details panel",
        "children render inside the details panel after input and result (e.g. a preview of the data)",
        "AI SDK: map a tool part's state ('input-streaming' | 'input-available' → running, 'output-available' → success, 'output-error' → error) and pass part.input / part.output"
      ]
    },
    "examples": [
      "tool-call-card-demo",
      "tool-call-card-states"
    ],
    "url": "https://ui.ballmac.com/components/tool-call-card"
  },
  "categories": [
    "ai"
  ],
  "type": "registry:ui"
}