{
  "schemaVersion": 1,
  "target": "react",
  "name": "tool-call-card",
  "version": "0.2.0",
  "category": "agent",
  "title": "Tool Call Card",
  "description": "Show the status and result of an agent tool call.",
  "tags": [
    "agent",
    "tool",
    "status",
    "ai"
  ],
  "requires": {
    "projectTypes": [
      "next",
      "vite-react"
    ],
    "styling": "tailwind-v4"
  },
  "npmDependencies": {},
  "registryDependencies": [
    {
      "name": "badge",
      "version": "0.2.0"
    }
  ],
  "docsPath": "/components/tool-call-card",
  "props": [
    {
      "name": "name",
      "type": "string",
      "description": "Tool name, shown in monospace."
    },
    {
      "name": "status",
      "type": "\"queued\" | \"running\" | \"success\" | \"error\"",
      "description": "Current state of the call."
    },
    {
      "name": "summary",
      "type": "ReactNode",
      "description": "One line describing what the call does."
    },
    {
      "name": "input",
      "type": "unknown",
      "description": "Arguments passed to the tool, rendered as formatted JSON."
    },
    {
      "name": "output",
      "type": "ReactNode",
      "description": "Result shown on success. Strings render in a soft-wrapping well."
    },
    {
      "name": "error",
      "type": "ReactNode",
      "description": "Message shown on failure in a red tinted box, always visible."
    },
    {
      "name": "durationMs",
      "type": "number",
      "description": "Elapsed time, shown once the call finishes."
    },
    {
      "name": "defaultOpen",
      "type": "boolean",
      "default": "false",
      "description": "Opens the input and output disclosure initially."
    }
  ],
  "accessibility": [
    "The status label is a live region, so screen readers announce Running, Succeeded, or Failed as the status changes.",
    "Every state has a text label and its own stroke glyph (dashed circle, spinning arc, check, x); color is never the only signal.",
    "aria-busy is set while running.",
    "Input and output use native <details>/<summary>, which work with the keyboard out of the box. Height animates only where ::details-content is supported.",
    "With reduced motion the spinner holds still and the glyph swap, check draw, and disclosure are instant; the label still changes."
  ],
  "states": [
    "queued",
    "running",
    "success",
    "error"
  ],
  "files": [
    {
      "path": "agent/tool-call-card.tsx",
      "content": "import type { ComponentProps, ReactNode } from \"react\";\nimport { Badge, type BadgeTone } from \"../ui/badge\";\n\nexport type ToolCallStatus = \"queued\" | \"running\" | \"success\" | \"error\";\n\nconst statuses: Record<ToolCallStatus, { label: string; tone: BadgeTone; border: string }> = {\n  queued: { label: \"Queued\", tone: \"neutral\", border: \"border-zinc-950/10 dark:border-white/10\" },\n  running: { label: \"Running\", tone: \"info\", border: \"border-sky-500/35\" },\n  success: { label: \"Succeeded\", tone: \"success\", border: \"border-zinc-950/10 dark:border-white/10\" },\n  error: { label: \"Failed\", tone: \"danger\", border: \"border-red-500/35\" },\n};\n\nconst ease = \"ease-[cubic-bezier(0.23,1,0.32,1)]\";\n\nexport interface ToolCallCardProps extends Omit<ComponentProps<\"article\">, \"children\"> {\n  /** Tool name, for example `search_docs`. */\n  name: string;\n  status: ToolCallStatus;\n  /** One line describing what the call does. */\n  summary?: ReactNode;\n  /** Arguments passed to the tool. Rendered as formatted JSON. */\n  input?: unknown;\n  /** Result shown when the call succeeds. Strings render as preformatted text. */\n  output?: ReactNode;\n  /** Message shown when the call fails. */\n  error?: ReactNode;\n  /** Elapsed time, shown once the call finishes. */\n  durationMs?: number;\n  /** Opens the input and output details initially. */\n  defaultOpen?: boolean;\n}\n\n/** One agent tool call: its status, what it does, how long it took, and its input and output. */\nexport function ToolCallCard({\n  name,\n  status,\n  summary,\n  input,\n  output,\n  error,\n  durationMs,\n  defaultOpen = false,\n  className,\n  ...props\n}: ToolCallCardProps) {\n  const meta = statuses[status];\n  const finished = status === \"success\" || status === \"error\";\n  const showOutput = status === \"success\" && output != null;\n  const hasInput = input !== undefined;\n  const detailsLabel = hasInput && showOutput ? \"Input and output\" : hasInput ? \"Input\" : \"Output\";\n\n  return (\n    <article\n      data-status={status}\n      aria-busy={status === \"running\" || undefined}\n      className={[\n        \"overflow-hidden rounded-xl border bg-white text-left text-zinc-950 shadow-[0_1px_2px_rgb(0_0_0/0.04)]\",\n        \"transition-[border-color] duration-200 motion-reduce:transition-none\",\n        \"dark:bg-black dark:text-white dark:shadow-[inset_0_1px_0_rgb(255_255_255/0.06)]\",\n        meta.border,\n        className,\n      ]\n        .filter(Boolean)\n        .join(\" \")}\n      {...props}\n    >\n      <div className=\"flex items-start gap-3 p-4\">\n        <StatusIcon key={status} status={status} />\n        <div className=\"min-w-0 flex-1\">\n          <div className=\"flex items-center justify-between gap-3\">\n            <h3\n              className={[\n                \"min-w-0 truncate font-mono text-[13px] font-medium transition-colors duration-200 motion-reduce:transition-none\",\n                status === \"queued\" ? \"text-zinc-600 dark:text-zinc-400\" : \"\",\n              ].join(\" \")}\n            >\n              {name}\n            </h3>\n            <div className=\"flex shrink-0 items-center gap-2.5\">\n              {finished && durationMs != null ? (\n                <span className=\"font-mono text-xs text-zinc-500 tabular-nums\">{formatDuration(durationMs)}</span>\n              ) : null}\n              <span role=\"status\">\n                <Badge tone={meta.tone}>{meta.label}</Badge>\n              </span>\n            </div>\n          </div>\n          {summary ? <p className=\"mt-1 text-sm text-zinc-600 dark:text-zinc-400\">{summary}</p> : null}\n          {status === \"error\" && error != null ? (\n            <p\n              key=\"error\"\n              className={`mt-3 rounded-lg border border-red-500/35 bg-red-500/[0.08] px-3 py-2 text-sm text-red-700 transition-[opacity,translate] duration-200 ${ease} starting:-translate-y-1 starting:opacity-0 motion-reduce:transition-none dark:text-red-300`}\n            >\n              {error}\n            </p>\n          ) : null}\n        </div>\n      </div>\n\n      {hasInput || showOutput ? (\n        <details\n          open={defaultOpen}\n          className={[\n            \"group border-t border-zinc-950/[0.07] [interpolate-size:allow-keywords] dark:border-white/[0.08]\",\n            // Height animates where ::details-content and interpolate-size are supported; elsewhere it simply opens.\n            \"[&::details-content]:h-0 [&::details-content]:overflow-clip [&::details-content]:transition-[height,content-visibility] [&::details-content]:duration-[220ms] [&::details-content]:ease-[cubic-bezier(0.23,1,0.32,1)] [&::details-content]:[transition-behavior:allow-discrete]\",\n            \"open:[&::details-content]:h-auto motion-reduce:[&::details-content]:transition-none\",\n          ].join(\" \")}\n        >\n          <summary\n            className={[\n              \"flex cursor-pointer list-none items-center justify-between gap-3 px-4 py-2.5 text-xs font-medium select-none [&::-webkit-details-marker]:hidden\",\n              \"text-zinc-600 transition-colors duration-150 hover:bg-zinc-950/[0.03] hover:text-zinc-950 motion-reduce:transition-none\",\n              \"dark:text-zinc-400 dark:hover:bg-white/[0.04] dark:hover:text-white\",\n              \"focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-sky-500\",\n            ].join(\" \")}\n          >\n            {detailsLabel}\n            <svg\n              aria-hidden=\"true\"\n              viewBox=\"0 0 16 16\"\n              className={`size-4 text-zinc-500 transition-transform duration-200 ${ease} group-open:rotate-180 motion-reduce:transition-none`}\n            >\n              <path d=\"m4.5 6.25 3.5 3.5 3.5-3.5\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n            </svg>\n          </summary>\n          <div\n            className={`grid grid-cols-[minmax(0,1fr)] gap-3 px-4 pt-1 pb-4 opacity-0 transition-opacity duration-200 ${ease} group-open:opacity-100 motion-reduce:transition-none`}\n          >\n            {/* The summary already names a lone section, so labels only appear when there are two. */}\n            {hasInput ? <Section label={showOutput ? \"Input\" : undefined}>{JSON.stringify(input, null, 2)}</Section> : null}\n            {showOutput ? (\n              <Section label={hasInput ? \"Output\" : undefined} wrap>\n                {output}\n              </Section>\n            ) : null}\n          </div>\n        </details>\n      ) : null}\n    </article>\n  );\n}\n\n/** A labeled well. `wrap` soft-wraps long lines (tool output); JSON keeps its layout and scrolls. */\nfunction Section({ label, wrap = false, children }: { label?: string; wrap?: boolean; children: ReactNode }) {\n  return (\n    <div>\n      {label ? <p className=\"mb-1.5 text-xs font-medium text-zinc-500\">{label}</p> : null}\n      {typeof children === \"string\" ? (\n        <pre className={`${wrap ? \"break-words whitespace-pre-wrap\" : \"overflow-x-auto\"} rounded-lg border border-zinc-950/[0.07] bg-zinc-950/[0.02] px-3 py-2.5 font-mono text-xs leading-relaxed text-zinc-800 dark:border-white/[0.08] dark:bg-transparent dark:text-zinc-300`}>\n          {children}\n        </pre>\n      ) : (\n        <div className=\"text-sm\">{children}</div>\n      )}\n    </div>\n  );\n}\n\n/**\n * The shared status glyphs at 20px. Remounts on each status change (via `key`), so the\n * new shape scales in and the success check draws; both are instant with reduced motion.\n */\nfunction StatusIcon({ status }: { status: ToolCallStatus }) {\n  const base = `size-5 shrink-0 transition-[opacity,scale] duration-200 ${ease} starting:scale-90 starting:opacity-0 motion-reduce:transition-none`;\n  // A 16 unit box drawn at 20px: a 1.2 stroke renders at 1.5px.\n  const stroke = { fill: \"none\", stroke: \"currentColor\", strokeWidth: 1.2, strokeLinecap: \"round\", strokeLinejoin: \"round\" } as const;\n\n  if (status === \"running\") {\n    return (\n      <svg aria-hidden=\"true\" viewBox=\"0 0 16 16\" {...stroke} className={`${base} text-sky-600 dark:text-sky-400`}>\n        <circle cx=\"8\" cy=\"8\" r=\"6.25\" strokeOpacity=\"0.2\" />\n        <path d=\"M8 1.75A6.25 6.25 0 1 1 1.75 8\" className=\"origin-center motion-safe:animate-spin\" />\n      </svg>\n    );\n  }\n  if (status === \"success\") {\n    return (\n      <svg aria-hidden=\"true\" viewBox=\"0 0 16 16\" {...stroke} className={`${base} text-emerald-600 dark:text-emerald-400`}>\n        <circle cx=\"8\" cy=\"8\" r=\"6.25\" />\n        <path\n          d=\"m5.35 8.25 1.75 1.75 3.55-3.75\"\n          pathLength={1}\n          strokeDasharray=\"1\"\n          className={`[stroke-dashoffset:0] transition-[stroke-dashoffset] delay-75 duration-300 ${ease} starting:[stroke-dashoffset:1] motion-reduce:transition-none`}\n        />\n      </svg>\n    );\n  }\n  if (status === \"error\") {\n    return (\n      <svg aria-hidden=\"true\" viewBox=\"0 0 16 16\" {...stroke} className={`${base} text-red-600 dark:text-red-400`}>\n        <circle cx=\"8\" cy=\"8\" r=\"6.25\" />\n        <path d=\"m6.1 6.1 3.8 3.8m0-3.8-3.8 3.8\" />\n      </svg>\n    );\n  }\n  return (\n    <svg aria-hidden=\"true\" viewBox=\"0 0 16 16\" {...stroke} className={`${base} text-zinc-500`}>\n      <circle cx=\"8\" cy=\"8\" r=\"6.25\" strokeDasharray=\"2.15 2.75\" />\n    </svg>\n  );\n}\n\nfunction formatDuration(ms: number): string {\n  return ms < 1000 ? `${Math.round(ms)} ms` : `${(ms / 1000).toFixed(1)} s`;\n}\n"
    }
  ],
  "example": {
    "content": "import { ToolCallCard } from \"@/components/seamless/agent/tool-call-card\";\n\nexport default function ToolCallCardExample() {\n  return (\n    <div className=\"grid w-full max-w-md gap-3\">\n      <ToolCallCard\n        name=\"search_docs\"\n        status=\"success\"\n        summary=\"Search the docs for “rate limits”\"\n        input={{ query: \"rate limits\", limit: 5 }}\n        output=\"Found 3 pages.\"\n        durationMs={820}\n      />\n      <ToolCallCard\n        name=\"fetch_page\"\n        status=\"error\"\n        summary=\"Read /docs/api/rate-limits\"\n        error=\"The docs server returned 504 Gateway Timeout.\"\n        durationMs={1340}\n      />\n    </div>\n  );\n}\n"
  }
}
