{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tool-fallback",
  "type": "registry:component",
  "title": "Tool Fallback",
  "description": "Default renderer for tool calls that have no dedicated UI.",
  "dependencies": [
    "@assistant-ui/react",
    "lucide-react",
    "tw-shimmer"
  ],
  "registryDependencies": [
    "button",
    "collapsible"
  ],
  "css": {
    "@import \"tw-shimmer\"": {},
    "@custom-variant data-open (&:where([data-state=\"open\"], [data-open]:not([data-open=\"false\"])))": {},
    "@custom-variant data-closed (&:where([data-state=\"closed\"], [data-closed]:not([data-closed=\"false\"])))": {},
    "@keyframes collapsible-down": {
      "from": {
        "height": "0"
      },
      "to": {
        "height": "var(--radix-collapsible-content-height, var(--collapsible-panel-height, auto))"
      }
    },
    "@keyframes collapsible-up": {
      "from": {
        "height": "var(--radix-collapsible-content-height, var(--collapsible-panel-height, auto))"
      },
      "to": {
        "height": "0"
      }
    }
  },
  "files": [
    {
      "type": "registry:component",
      "path": "components/assistant-ui/tool-fallback.tsx",
      "content": "\"use client\";\n\nimport { memo, useCallback, useRef, useState } from \"react\";\nimport {\n  AlertCircleIcon,\n  CheckIcon,\n  ChevronDownIcon,\n  LoaderIcon,\n  XCircleIcon,\n} from \"lucide-react\";\nimport {\n  useScrollLock,\n  useToolCallElapsed,\n  type ToolApprovalOption,\n  type ToolCallMessagePart,\n  type ToolCallMessagePartProps,\n  type ToolCallMessagePartStatus,\n  type ToolCallMessagePartComponent,\n} from \"@assistant-ui/react\";\nimport {\n  Collapsible,\n  CollapsibleContent,\n  CollapsibleTrigger,\n} from \"@/components/ui/collapsible\";\nimport { cn } from \"@/lib/utils\";\nimport { Button } from \"@/components/ui/button\";\n\nconst ANIMATION_DURATION = 200;\n\nconst pressable = \"active:scale-[0.98]\";\n\nexport type ToolFallbackRootProps = Omit<\n  React.ComponentProps<typeof Collapsible>,\n  \"open\" | \"onOpenChange\"\n> & {\n  open?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  defaultOpen?: boolean;\n};\n\nfunction ToolFallbackRoot({\n  className,\n  open: controlledOpen,\n  onOpenChange: controlledOnOpenChange,\n  defaultOpen = false,\n  children,\n  ...props\n}: ToolFallbackRootProps) {\n  const collapsibleRef = useRef<HTMLDivElement>(null);\n  const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);\n  const lockScroll = useScrollLock(collapsibleRef, ANIMATION_DURATION);\n\n  const isControlled = controlledOpen !== undefined;\n  const isOpen = isControlled ? controlledOpen : uncontrolledOpen;\n\n  const handleOpenChange = useCallback(\n    (open: boolean) => {\n      lockScroll();\n      if (!isControlled) {\n        setUncontrolledOpen(open);\n      }\n      controlledOnOpenChange?.(open);\n    },\n    [lockScroll, isControlled, controlledOnOpenChange],\n  );\n\n  return (\n    <Collapsible\n      ref={collapsibleRef}\n      data-slot=\"tool-fallback-root\"\n      open={isOpen}\n      onOpenChange={handleOpenChange}\n      className={cn(\n        \"aui-tool-fallback-root group/tool-fallback-root w-full\",\n        className,\n      )}\n      style={\n        {\n          \"--animation-duration\": `${ANIMATION_DURATION}ms`,\n        } as React.CSSProperties\n      }\n      {...props}\n    >\n      {children}\n    </Collapsible>\n  );\n}\n\ntype ToolStatus = ToolCallMessagePartStatus[\"type\"];\n\nconst statusIconMap: Record<ToolStatus, React.ElementType> = {\n  running: LoaderIcon,\n  complete: CheckIcon,\n  incomplete: XCircleIcon,\n  \"requires-action\": AlertCircleIcon,\n};\n\nconst formatToolDuration = (ms: number) => {\n  if (ms < 1000) return \"<1s\";\n  const seconds = ms / 1000;\n  if (seconds < 10) return `${(Math.floor(seconds * 10) / 10).toFixed(1)}s`;\n  if (seconds < 60) return `${Math.floor(seconds)}s`;\n  return `${Math.floor(seconds / 60)}m ${Math.floor(seconds % 60)}s`;\n};\n\nfunction ToolFallbackDuration({\n  className,\n  ...props\n}: React.ComponentProps<\"span\">) {\n  const elapsedMs = useToolCallElapsed();\n  if (elapsedMs === undefined) return null;\n\n  return (\n    <span\n      data-slot=\"tool-fallback-duration\"\n      className={cn(\n        \"aui-tool-fallback-duration text-muted-foreground text-xs tabular-nums\",\n        className,\n      )}\n      {...props}\n    >\n      {formatToolDuration(elapsedMs)}\n    </span>\n  );\n}\n\nfunction ToolFallbackTrigger({\n  toolName,\n  status,\n  className,\n  ...props\n}: React.ComponentProps<typeof CollapsibleTrigger> & {\n  toolName: string;\n  status?: ToolCallMessagePartStatus;\n}) {\n  const statusType = status?.type ?? \"complete\";\n  const isRunning = statusType === \"running\";\n  const isCancelled =\n    status?.type === \"incomplete\" && status.reason === \"cancelled\";\n\n  const Icon = statusIconMap[statusType];\n  const label = isCancelled ? \"Cancelled tool\" : \"Used tool\";\n\n  return (\n    <CollapsibleTrigger\n      data-slot=\"tool-fallback-trigger\"\n      className={cn(\n        \"aui-tool-fallback-trigger group/trigger text-muted-foreground hover:text-foreground flex w-fit origin-left items-center gap-2 py-1.5 text-sm transition-[color,scale] active:scale-[0.98]\",\n        className,\n      )}\n      {...props}\n    >\n      <Icon\n        data-slot=\"tool-fallback-trigger-icon\"\n        className={cn(\n          \"aui-tool-fallback-trigger-icon size-4 shrink-0\",\n          isCancelled && \"text-muted-foreground\",\n          isRunning && \"animate-spin [animation-duration:0.6s]\",\n        )}\n      />\n      <span\n        data-slot=\"tool-fallback-trigger-label\"\n        className={cn(\n          \"aui-tool-fallback-trigger-label-wrapper relative inline-block text-start leading-none\",\n          isCancelled && \"text-muted-foreground line-through\",\n        )}\n      >\n        <span>\n          {label}: <b>{toolName}</b>\n        </span>\n        {isRunning && (\n          <span\n            aria-hidden\n            data-slot=\"tool-fallback-trigger-shimmer\"\n            className=\"aui-tool-fallback-trigger-shimmer shimmer pointer-events-none absolute inset-0 motion-reduce:animate-none\"\n          >\n            {label}: <b>{toolName}</b>\n          </span>\n        )}\n      </span>\n      <ToolFallbackDuration />\n      <ChevronDownIcon\n        data-slot=\"tool-fallback-trigger-chevron\"\n        className={cn(\n          \"aui-tool-fallback-trigger-chevron size-4 shrink-0\",\n          \"transition-transform duration-(--animation-duration) ease-[cubic-bezier(0.32,0.72,0,1)] motion-reduce:transition-none\",\n          \"-rotate-90\",\n          \"group-data-open/trigger:rotate-0\",\n          \"group-data-panel-open/trigger:rotate-0\",\n        )}\n      />\n    </CollapsibleTrigger>\n  );\n}\n\nfunction ToolFallbackContent({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<typeof CollapsibleContent>) {\n  return (\n    <CollapsibleContent\n      data-slot=\"tool-fallback-content\"\n      className={cn(\n        \"aui-tool-fallback-content relative overflow-hidden text-sm outline-none\",\n        \"group/collapsible-content ease-[cubic-bezier(0.32,0.72,0,1)] motion-reduce:animate-none\",\n        \"data-closed:animate-collapsible-up\",\n        \"data-open:animate-collapsible-down\",\n        \"data-closed:fill-mode-forwards\",\n        \"data-closed:pointer-events-none\",\n        \"data-open:duration-(--animation-duration)\",\n        \"data-closed:duration-(--animation-duration)\",\n        className,\n      )}\n      {...props}\n    >\n      <div\n        className={cn(\n          \"flex flex-col gap-2 ps-6 pt-1 pb-2 ease-[cubic-bezier(0.32,0.72,0,1)] motion-reduce:animate-none\",\n          \"group-data-open/collapsible-content:animate-in group-data-open/collapsible-content:fade-in-0 group-data-open/collapsible-content:blur-in-[2px] group-data-open/collapsible-content:slide-in-from-top-1\",\n          \"group-data-closed/collapsible-content:animate-out group-data-closed/collapsible-content:fade-out-0 group-data-closed/collapsible-content:blur-out-[2px] group-data-closed/collapsible-content:slide-out-to-top-1\",\n          \"group-data-closed/collapsible-content:duration-(--animation-duration) group-data-open/collapsible-content:duration-(--animation-duration)\",\n        )}\n      >\n        {children}\n      </div>\n    </CollapsibleContent>\n  );\n}\n\nfunction ToolFallbackArgs({\n  argsText,\n  className,\n  ...props\n}: React.ComponentProps<\"div\"> & {\n  argsText?: string;\n}) {\n  if (!argsText) return null;\n\n  return (\n    <div\n      data-slot=\"tool-fallback-args\"\n      className={cn(\"aui-tool-fallback-args\", className)}\n      {...props}\n    >\n      <pre className=\"aui-tool-fallback-args-value bg-muted/50 text-foreground/90 rounded-md p-2.5 text-xs whitespace-pre-wrap\">\n        {argsText}\n      </pre>\n    </div>\n  );\n}\n\nfunction ToolFallbackResult({\n  result,\n  className,\n  ...props\n}: React.ComponentProps<\"div\"> & {\n  result?: unknown;\n}) {\n  if (result === undefined) return null;\n\n  return (\n    <div\n      data-slot=\"tool-fallback-result\"\n      className={cn(\"aui-tool-fallback-result\", className)}\n      {...props}\n    >\n      <p className=\"aui-tool-fallback-result-header text-muted-foreground text-xs font-medium\">\n        Result:\n      </p>\n      <pre className=\"aui-tool-fallback-result-content bg-muted/50 text-foreground/90 mt-1 rounded-md p-2.5 text-xs whitespace-pre-wrap\">\n        {typeof result === \"string\" ? result : JSON.stringify(result, null, 2)}\n      </pre>\n    </div>\n  );\n}\n\nfunction ToolFallbackError({\n  status,\n  className,\n  ...props\n}: React.ComponentProps<\"div\"> & {\n  status?: ToolCallMessagePartStatus;\n}) {\n  if (status?.type !== \"incomplete\") return null;\n\n  const error = status.error;\n  const errorText = error\n    ? typeof error === \"string\"\n      ? error\n      : JSON.stringify(error)\n    : null;\n\n  if (!errorText) return null;\n\n  const isCancelled = status.reason === \"cancelled\";\n  const headerText = isCancelled ? \"Cancelled reason:\" : \"Error:\";\n\n  return (\n    <div\n      data-slot=\"tool-fallback-error\"\n      className={cn(\"aui-tool-fallback-error\", className)}\n      {...props}\n    >\n      <p className=\"aui-tool-fallback-error-header text-muted-foreground font-semibold\">\n        {headerText}\n      </p>\n      <p className=\"aui-tool-fallback-error-reason text-muted-foreground\">\n        {errorText}\n      </p>\n    </div>\n  );\n}\n\nconst APPROVED_RESULT = \"Approved by user\";\nconst DENIED_RESULT = \"User denied tool execution\";\n\nconst APPROVAL_OPTION_DEFAULT_LABELS: Record<string, string> = {\n  \"allow-once\": \"Allow\",\n  \"allow-always\": \"Always allow\",\n  \"reject-once\": \"Deny\",\n  \"reject-always\": \"Always deny\",\n};\n\nconst isAllowKind = (kind: string) =>\n  kind === \"allow-once\" || kind === \"allow-always\";\n\nconst approvalOptionLabel = (option: ToolApprovalOption) =>\n  option.label ??\n  (Object.hasOwn(APPROVAL_OPTION_DEFAULT_LABELS, option.kind)\n    ? APPROVAL_OPTION_DEFAULT_LABELS[option.kind]\n    : undefined) ??\n  option.id;\n\nfunction ToolFallbackApproval({\n  className,\n  addResult,\n  resume,\n  interrupt,\n  approval,\n  respondToApproval,\n  ...props\n}: React.ComponentProps<\"div\"> &\n  Partial<\n    Pick<ToolCallMessagePartProps, \"addResult\" | \"resume\" | \"respondToApproval\">\n  > & {\n    interrupt?: ToolCallMessagePart[\"interrupt\"];\n    approval?: ToolCallMessagePart[\"approval\"];\n  }) {\n  const [submitted, setSubmitted] = useState(false);\n  const [confirmingId, setConfirmingId] = useState<string | null>(null);\n\n  if (\n    approval != null &&\n    (approval.approved !== undefined || approval.resolution !== undefined)\n  )\n    return null;\n\n  // Custom (`_`-prefixed) kinds cannot be resolved to a boolean by the kit;\n  // hosts using custom kinds render their own bar. A declared option list is\n  // a host constraint: the kit never adds an approval path beyond it, but\n  // always preserves a refusal path.\n  const declaredOptions = respondToApproval ? approval?.options : undefined;\n  const options = declaredOptions?.filter((o) =>\n    Object.hasOwn(APPROVAL_OPTION_DEFAULT_LABELS, o.kind),\n  );\n\n  const respond = (approved: boolean) => {\n    if (submitted) return;\n    if (\n      approval != null &&\n      approval.approved === undefined &&\n      respondToApproval\n    ) {\n      respondToApproval({ approved });\n    } else if (interrupt) {\n      resume?.({ approved });\n    } else {\n      addResult?.(approved ? APPROVED_RESULT : DENIED_RESULT);\n    }\n    setSubmitted(true);\n  };\n\n  const respondWithOption = (option: ToolApprovalOption) => {\n    if (submitted) return;\n    respondToApproval?.({ optionId: option.id });\n    setSubmitted(true);\n    setConfirmingId(null);\n  };\n\n  const handleOption = (option: ToolApprovalOption) => {\n    if (option.confirm) {\n      setConfirmingId(option.id);\n    } else {\n      respondWithOption(option);\n    }\n  };\n\n  const confirming =\n    confirmingId != null\n      ? options?.find((o) => o.id === confirmingId)\n      : undefined;\n\n  if (confirming) {\n    const confirmMeta =\n      typeof confirming.confirm === \"object\" ? confirming.confirm : undefined;\n    const confirmDescription =\n      confirmMeta?.description ?? confirming.description;\n    return (\n      <div\n        data-slot=\"tool-fallback-approval-confirm\"\n        className={cn(\n          \"aui-tool-fallback-approval-confirm flex flex-col gap-2 pt-1\",\n          className,\n        )}\n        {...props}\n      >\n        <p className=\"aui-tool-fallback-approval-confirm-title font-semibold\">\n          {confirmMeta?.title ?? `${approvalOptionLabel(confirming)}?`}\n        </p>\n        {confirmDescription && (\n          <p className=\"aui-tool-fallback-approval-confirm-description text-muted-foreground\">\n            {confirmDescription}\n          </p>\n        )}\n        {confirming.grants && confirming.grants.length > 0 && (\n          <ul className=\"aui-tool-fallback-approval-confirm-grants flex flex-col gap-1\">\n            {confirming.grants.map((grant) => (\n              <li key={grant}>\n                <code className=\"aui-tool-fallback-approval-confirm-grant bg-muted rounded px-1.5 py-0.5 text-xs\">\n                  {grant}\n                </code>\n              </li>\n            ))}\n          </ul>\n        )}\n        <div className=\"flex items-center gap-2\">\n          <Button\n            size=\"sm\"\n            className={pressable}\n            onClick={() => respondWithOption(confirming)}\n            disabled={submitted}\n          >\n            Confirm\n          </Button>\n          <Button\n            size=\"sm\"\n            variant=\"outline\"\n            className={pressable}\n            onClick={() => setConfirmingId(null)}\n            disabled={submitted}\n          >\n            Back\n          </Button>\n        </div>\n      </div>\n    );\n  }\n\n  if (declaredOptions && declaredOptions.length > 0) {\n    const allowOptions = options?.filter((o) => isAllowKind(o.kind)) ?? [];\n    const rejectOptions = options?.filter((o) => !isAllowKind(o.kind)) ?? [];\n    return (\n      <div\n        data-slot=\"tool-fallback-approval\"\n        className={cn(\n          \"aui-tool-fallback-approval flex flex-wrap items-center gap-2 pt-1\",\n          className,\n        )}\n        {...props}\n      >\n        {[...allowOptions, ...rejectOptions].map((option) => (\n          <Button\n            key={option.id}\n            size=\"sm\"\n            variant={option === allowOptions[0] ? \"default\" : \"outline\"}\n            className={pressable}\n            onClick={() => handleOption(option)}\n            disabled={submitted}\n          >\n            {approvalOptionLabel(option)}\n          </Button>\n        ))}\n        {rejectOptions.length === 0 && (\n          <Button\n            size=\"sm\"\n            variant=\"outline\"\n            className={pressable}\n            onClick={() => respond(false)}\n            disabled={submitted}\n          >\n            Deny\n          </Button>\n        )}\n      </div>\n    );\n  }\n\n  return (\n    <div\n      data-slot=\"tool-fallback-approval\"\n      className={cn(\n        \"aui-tool-fallback-approval flex items-center gap-2 pt-1\",\n        className,\n      )}\n      {...props}\n    >\n      <Button\n        size=\"sm\"\n        className={pressable}\n        onClick={() => respond(true)}\n        disabled={submitted}\n      >\n        Allow\n      </Button>\n      <Button\n        size=\"sm\"\n        variant=\"outline\"\n        className={pressable}\n        onClick={() => respond(false)}\n        disabled={submitted}\n      >\n        Deny\n      </Button>\n    </div>\n  );\n}\n\nconst ToolFallbackImpl: ToolCallMessagePartComponent = ({\n  toolName,\n  argsText,\n  result,\n  status,\n  addResult,\n  resume,\n  interrupt,\n  approval,\n  respondToApproval,\n}) => {\n  const isCancelled =\n    status?.type === \"incomplete\" && status.reason === \"cancelled\";\n  const isRequiresAction = status?.type === \"requires-action\";\n\n  const [open, setOpen] = useState(isRequiresAction);\n  const [prevRequiresAction, setPrevRequiresAction] =\n    useState(isRequiresAction);\n  if (isRequiresAction !== prevRequiresAction) {\n    setPrevRequiresAction(isRequiresAction);\n    if (isRequiresAction) setOpen(true);\n  }\n\n  return (\n    <ToolFallbackRoot open={open} onOpenChange={setOpen}>\n      <ToolFallbackTrigger toolName={toolName} status={status} />\n      <ToolFallbackContent>\n        <ToolFallbackError status={status} />\n        <ToolFallbackArgs\n          argsText={argsText}\n          className={cn(isCancelled && \"opacity-60\")}\n        />\n        {isRequiresAction && (\n          <ToolFallbackApproval\n            addResult={addResult}\n            resume={resume}\n            interrupt={interrupt}\n            approval={approval}\n            respondToApproval={respondToApproval}\n          />\n        )}\n        {!isCancelled && <ToolFallbackResult result={result} />}\n      </ToolFallbackContent>\n    </ToolFallbackRoot>\n  );\n};\n\nconst ToolFallback = memo(\n  ToolFallbackImpl,\n) as unknown as ToolCallMessagePartComponent & {\n  Root: typeof ToolFallbackRoot;\n  Trigger: typeof ToolFallbackTrigger;\n  Content: typeof ToolFallbackContent;\n  Args: typeof ToolFallbackArgs;\n  Result: typeof ToolFallbackResult;\n  Error: typeof ToolFallbackError;\n  Approval: typeof ToolFallbackApproval;\n};\n\nToolFallback.displayName = \"ToolFallback\";\nToolFallback.Root = ToolFallbackRoot;\nToolFallback.Trigger = ToolFallbackTrigger;\nToolFallback.Content = ToolFallbackContent;\nToolFallback.Args = ToolFallbackArgs;\nToolFallback.Result = ToolFallbackResult;\nToolFallback.Error = ToolFallbackError;\nToolFallback.Approval = ToolFallbackApproval;\n\nexport {\n  ToolFallback,\n  ToolFallbackRoot,\n  ToolFallbackTrigger,\n  ToolFallbackContent,\n  ToolFallbackArgs,\n  ToolFallbackResult,\n  ToolFallbackError,\n  ToolFallbackApproval,\n};\n"
    }
  ]
}