{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "chat-tool",
  "title": "Chat Tool",
  "description": "A tool call with its state, opening onto its input and output and resizing as the output arrives.",
  "registryDependencies": [
    "https://ui.ndekocode.dev/r/theme.json",
    "https://ui.ndekocode.dev/r/utils.json",
    "https://ui.ndekocode.dev/r/icons.json",
    "https://ui.ndekocode.dev/r/badge.json",
    "https://ui.ndekocode.dev/r/chat-motion.json"
  ],
  "files": [
    {
      "path": "registry/ndeko/ui/chat-tool.tsx",
      "content": "// Adapted from prompt-kit (MIT, Copyright (c) 2025 Julien Thibeaut).\n'use client';\n\nimport { useId, useState } from 'react';\n\nimport {\n  ChevronDownIcon,\n  CircleCheckIcon,\n  CircleDashedIcon,\n  CircleXIcon,\n  LoaderIcon,\n} from '@/registry/ndeko/lib/icons';\nimport { cn } from '@/registry/ndeko/lib/utils';\nimport { Badge } from '@/registry/ndeko/ui/badge';\nimport { Fold, Swap } from '@/registry/ndeko/ui/chat-motion';\n\ntype ToolState =\n  'input-streaming' | 'input-available' | 'output-available' | 'output-error';\n\ntype ToolPart = {\n  type: string;\n  state: ToolState;\n  input?: Record<string, unknown>;\n  output?: Record<string, unknown>;\n  toolCallId?: string;\n  errorText?: string;\n};\n\ntype BadgeVariant = React.ComponentProps<typeof Badge>['variant'];\n\ntype StateLook = {\n  label: string;\n  badge: BadgeVariant;\n  icon: React.ReactNode;\n};\n\nconst STATE_LOOK: Record<ToolState, StateLook> = {\n  'input-streaming': {\n    label: 'Preparing',\n    badge: 'secondary',\n    icon: <CircleDashedIcon className='text-muted-foreground size-4' />,\n  },\n  'input-available': {\n    label: 'Running',\n    badge: 'info',\n    icon: <LoaderIcon className='text-info-foreground size-4 animate-spin' />,\n  },\n  'output-available': {\n    label: 'Done',\n    badge: 'success',\n    icon: <CircleCheckIcon className='text-success-foreground size-4' />,\n  },\n  'output-error': {\n    label: 'Failed',\n    badge: 'destructive',\n    icon: <CircleXIcon className='text-destructive-ink size-4' />,\n  },\n};\n\nfunction formatValue(value: unknown) {\n  if (value === null) {\n    return 'null';\n  }\n  if (value === undefined) {\n    return 'undefined';\n  }\n  if (typeof value === 'string') {\n    return value;\n  }\n  if (typeof value === 'object') {\n    return JSON.stringify(value, null, 2);\n  }\n  return String(value);\n}\n\nfunction filledRecordOf(record: Record<string, unknown> | undefined) {\n  if (!record) {\n    return null;\n  }\n  const isEmpty = Object.keys(record).length === 0;\n  if (isEmpty) {\n    return null;\n  }\n  return record;\n}\n\nfunction visibleErrorOf(state: ToolState, errorText: string | undefined) {\n  const isFailed = state === 'output-error';\n  if (!isFailed) {\n    return null;\n  }\n  return errorText ?? null;\n}\n\nfunction chevronTurnOf(isOpen: boolean) {\n  if (isOpen) {\n    return 'rotate-180';\n  }\n  return 'rotate-0';\n}\n\nconst SECTION_LABEL = 'text-muted-foreground mb-1.5 text-xs font-medium';\n\nconst VALUE_BOX =\n  'bg-foreground/[0.03] dark:bg-foreground/[0.05] rounded-lg p-2.5 font-mono text-xs leading-relaxed';\n\nfunction ToolInput({ input }: { input: Record<string, unknown> }) {\n  const entries = Object.entries(input);\n\n  return (\n    <div>\n      <div className={SECTION_LABEL}>Input</div>\n      <dl className={cn(VALUE_BOX, 'space-y-1')}>\n        {entries.map(([key, value]) => (\n          <div key={key} className='flex gap-1.5'>\n            <dt className='text-muted-foreground shrink-0'>{key}:</dt>\n            <dd className='min-w-0 break-words whitespace-pre-wrap'>\n              {formatValue(value)}\n            </dd>\n          </div>\n        ))}\n      </dl>\n    </div>\n  );\n}\n\nfunction ToolOutput({ output }: { output: Record<string, unknown> }) {\n  return (\n    <div>\n      <div className={SECTION_LABEL}>Output</div>\n      <pre\n        className={cn(VALUE_BOX, 'max-h-60 overflow-auto whitespace-pre-wrap')}\n      >\n        {formatValue(output)}\n      </pre>\n    </div>\n  );\n}\n\nfunction ToolError({ errorText }: { errorText: string }) {\n  return (\n    <div>\n      <div className={SECTION_LABEL}>Error</div>\n      <div className='bg-destructive/10 text-destructive-ink rounded-lg p-2.5 text-xs leading-relaxed'>\n        {errorText}\n      </div>\n    </div>\n  );\n}\n\ntype ToolProps = {\n  toolPart: ToolPart;\n  defaultOpen?: boolean;\n  open?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  className?: string;\n};\n\nfunction Tool({\n  toolPart,\n  defaultOpen = false,\n  open,\n  onOpenChange,\n  className,\n}: ToolProps) {\n  const [internalOpen, setInternalOpen] = useState(defaultOpen);\n  const contentId = useId();\n  const isControlled = open !== undefined;\n  const isOpen = open ?? internalOpen;\n  const { state, input, output, toolCallId, errorText } = toolPart;\n  const look = STATE_LOOK[state];\n  const visibleInput = filledRecordOf(input);\n  const visibleError = visibleErrorOf(state, errorText);\n  const isPreparing = state === 'input-streaming';\n  const isRunning = state === 'input-available';\n\n  function toggle() {\n    const nextOpen = !isOpen;\n    if (!isControlled) {\n      setInternalOpen(nextOpen);\n    }\n    onOpenChange?.(nextOpen);\n  }\n\n  return (\n    <div\n      data-slot='tool'\n      data-state={state}\n      className={cn(\n        'bg-card hairline-ring ring-border overflow-hidden rounded-xl shadow-[0_1px_2px_rgb(0_0_0/0.04)]',\n        className\n      )}\n    >\n      <button\n        type='button'\n        aria-expanded={isOpen}\n        aria-controls={contentId}\n        onClick={toggle}\n        className='hover:bg-foreground/[0.025] focus-visible:ring-ring/25 flex h-10 w-full cursor-pointer items-center gap-2 rounded-xl px-3 text-start text-[13px] transition-colors outline-none focus-visible:ring-[3px] focus-visible:ring-inset'\n      >\n        <Swap swapKey={state} className='size-4 shrink-0'>\n          {look.icon}\n        </Swap>\n        <span className='min-w-0 truncate font-mono text-xs font-medium'>\n          {toolPart.type}\n        </span>\n        <Swap swapKey={state}>\n          <Badge variant={look.badge}>{look.label}</Badge>\n        </Swap>\n        <ChevronDownIcon\n          className={cn(\n            'text-muted-foreground ms-auto size-4 shrink-0 transition-transform duration-200 ease-(--ease-out-strong)',\n            chevronTurnOf(isOpen)\n          )}\n        />\n      </button>\n      <Fold open={isOpen} id={contentId}>\n        <div className='space-y-3 px-3 pt-1 pb-3'>\n          {visibleInput && <ToolInput input={visibleInput} />}\n          {output && <ToolOutput output={output} />}\n          {visibleError && <ToolError errorText={visibleError} />}\n          {isPreparing && (\n            <p className='text-muted-foreground text-xs'>Preparing the call</p>\n          )}\n          {isRunning && (\n            <p className='text-muted-foreground text-xs'>\n              Waiting for the result\n            </p>\n          )}\n          {toolCallId && (\n            <p className='text-muted-foreground font-mono text-xs'>\n              Call {toolCallId}\n            </p>\n          )}\n        </div>\n      </Fold>\n    </div>\n  );\n}\n\nexport { Tool, type ToolPart, type ToolProps, type ToolState };\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "primitives",
    "chat"
  ],
  "type": "registry:ui"
}