{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "code-block",
  "dependencies": [
    "@radix-ui/react-collapsible",
    "@radix-ui/react-slot",
    "class-variance-authority",
    "lucide-react",
    "shiki",
    "zod"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/components/watermelon-ui/code-block.tsx",
      "content": "\"use client\";\n\nimport { useMemo, useState, useCallback, useEffect } from \"react\";\nimport { createHighlighter, type Highlighter } from \"shiki\";\nimport { Copy, Check, ChevronDown, ChevronUp } from \"lucide-react\";\n\nconst COPY_ID = \"codeblock-code\";\n\nlet highlighterPromise: Promise<Highlighter> | null = null;\n\nfunction getHighlighter(): Promise<Highlighter> {\n  if (!highlighterPromise) {\n    highlighterPromise = createHighlighter({\n      themes: [\"github-dark\", \"github-light\"],\n      langs: [],\n    });\n  }\n  return highlighterPromise;\n}\n\nconst htmlCache = new Map<string, string>();\n\nfunction getCacheKey(\n  code: string,\n  language: string,\n  theme: string,\n  showLineNumbers: boolean,\n  highlightLines?: number[],\n): string {\n  return `${code}::${language}::${theme}::${showLineNumbers}::${highlightLines?.join(\",\") ?? \"\"}`;\n}\n\nconst LANGUAGE_DISPLAY_NAMES: Record<string, string> = {\n  typescript: \"TypeScript\",\n  javascript: \"JavaScript\",\n  python: \"Python\",\n  tsx: \"TSX\",\n  jsx: \"JSX\",\n  json: \"JSON\",\n  bash: \"Bash\",\n  shell: \"Shell\",\n  css: \"CSS\",\n  html: \"HTML\",\n  markdown: \"Markdown\",\n  sql: \"SQL\",\n  yaml: \"YAML\",\n  go: \"Go\",\n  rust: \"Rust\",\n  text: \"Plain Text\",\n};\n\nfunction getLanguageDisplayName(lang: string): string {\n  return LANGUAGE_DISPLAY_NAMES[lang.toLowerCase()] || lang.toUpperCase();\n}\n\nfunction getSystemTheme(): \"light\" | \"dark\" {\n  if (typeof window === \"undefined\") return \"light\";\n  return window.matchMedia?.(\"(prefers-color-scheme: dark)\").matches\n    ? \"dark\"\n    : \"light\";\n}\n\nfunction getDocumentTheme(): \"light\" | \"dark\" | null {\n  if (typeof document === \"undefined\") return null;\n  const root = document.documentElement;\n  if (root.classList.contains(\"dark\")) return \"dark\";\n  if (root.classList.contains(\"light\")) return \"light\";\n  return null;\n}\n\nfunction useResolvedTheme(): \"light\" | \"dark\" {\n  const [theme, setTheme] = useState<\"light\" | \"dark\">(() => {\n    return getDocumentTheme() ?? getSystemTheme();\n  });\n\n  useEffect(() => {\n    if (typeof window === \"undefined\" || typeof document === \"undefined\") {\n      return;\n    }\n\n    const update = () => setTheme(getDocumentTheme() ?? getSystemTheme());\n\n    const mql = window.matchMedia?.(\"(prefers-color-scheme: dark)\");\n    mql?.addEventListener(\"change\", update);\n\n    const observer = new MutationObserver(update);\n    observer.observe(document.documentElement, {\n      attributes: true,\n      attributeFilter: [\"class\"],\n    });\n\n    return () => {\n      mql?.removeEventListener(\"change\", update);\n      observer.disconnect();\n    };\n  }, []);\n\n  return theme;\n}\n\nexport function CodeBlock({\n  id,\n  code,\n  language = \"text\",\n  filename,\n  showLineNumbers = true,\n  highlightLines,\n  maxCollapsedLines,\n  responseActions,\n  onResponseAction,\n  onBeforeResponseAction,\n  isLoading,\n  className,\n}: CodeBlockProps) {\n  const resolvedTheme = useResolvedTheme();\n  const [isExpanded, setIsExpanded] = useState(false);\n  const { copiedId, copy } = useCopyToClipboard();\n  const isCopied = copiedId === COPY_ID;\n\n  const theme = resolvedTheme === \"dark\" ? \"github-dark\" : \"github-light\";\n  const cacheKey = getCacheKey(\n    code,\n    language,\n    theme,\n    showLineNumbers,\n    highlightLines,\n  );\n\n  const [highlightedHtml, setHighlightedHtml] = useState<string | null>(\n    () => htmlCache.get(cacheKey) ?? null,\n  );\n\n  useEffect(() => {\n    const cached = htmlCache.get(cacheKey);\n    if (cached) {\n\n      setHighlightedHtml(cached);\n      return;\n    }\n\n    let cancelled = false;\n\n    async function highlight() {\n      if (!code) {\n        if (!cancelled) setHighlightedHtml(\"\");\n        return;\n      }\n\n      try {\n        const highlighter = await getHighlighter();\n        const loadedLangs = highlighter.getLoadedLanguages();\n\n        if (!loadedLangs.includes(language)) {\n          await highlighter.loadLanguage(\n            language as Parameters<Highlighter[\"loadLanguage\"]>[0],\n          );\n        }\n\n        const lineCount = code.split(\"\\n\").length;\n        const lineNumberWidth = `${String(lineCount).length + 0.5}ch`;\n\n        const html = highlighter.codeToHtml(code, {\n          lang: language,\n          theme,\n          transformers: [\n            {\n              // eslint-disable-next-line @typescript-eslint/no-explicit-any\n              line(node: any, line: number) {\n                node.properties[\"data-line\"] = line;\n                if (highlightLines?.includes(line)) {\n                  const highlightBg =\n                    resolvedTheme === \"dark\"\n                      ? \"rgba(255,255,255,0.1)\"\n                      : \"rgba(0,0,0,0.05)\";\n                  node.properties[\"style\"] = `background:${highlightBg};`;\n                }\n                if (showLineNumbers) {\n                  node.children.unshift({\n                    type: \"element\",\n                    tagName: \"span\",\n                    properties: {\n                      style: `display:inline-block;width:${lineNumberWidth};text-align:right;margin-right:1.5em;user-select:none;opacity:0.5;`,\n                      \"aria-hidden\": \"true\",\n                    },\n                    children: [{ type: \"text\", value: String(line) }],\n                  });\n                }\n              },\n            },\n          ],\n        });\n        if (!cancelled) {\n          htmlCache.set(cacheKey, html);\n          setHighlightedHtml(html);\n        }\n      } catch {\n        const escaped = code\n          .replace(/&/g, \"&amp;\")\n          .replace(/</g, \"&lt;\")\n          .replace(/>/g, \"&gt;\");\n        if (!cancelled)\n          setHighlightedHtml(`<pre><code>${escaped}</code></pre>`);\n      }\n    }\n    void highlight();\n    return () => {\n      cancelled = true;\n    };\n  }, [\n    cacheKey,\n    code,\n    language,\n    theme,\n    highlightLines,\n    showLineNumbers,\n    resolvedTheme,\n  ]);\n\n  const normalizedFooterActions = useMemo(\n    () => normalizeActionsConfig(responseActions),\n    [responseActions],\n  );\n\n  const lineCount = code.split(\"\\n\").length;\n  const shouldCollapse = maxCollapsedLines && lineCount > maxCollapsedLines;\n  const isCollapsed = shouldCollapse && !isExpanded;\n\n  const handleCopy = useCallback(() => {\n    copy(code, COPY_ID);\n  }, [code, copy]);\n\n  if (isLoading) {\n    return (\n      <div\n        className={cn(\n          \"@container flex w-full min-w-80 flex-col gap-3\",\n          className,\n        )}\n        data-tool-ui-id={id}\n        aria-busy=\"true\"\n      >\n        <div className=\"border-border bg-card overflow-hidden rounded-lg border shadow-xs\">\n          <CodeBlockProgress />\n        </div>\n      </div>\n    );\n  }\n\n  return (\n    <div\n      className={cn(\n        \"@container flex w-full min-w-80 flex-col gap-3\",\n        className,\n      )}\n      data-tool-ui-id={id}\n      data-slot=\"code-block\"\n    >\n      <div className=\"border-border bg-card overflow-hidden rounded-lg border shadow-xs\">\n        <div className=\"bg-card flex items-center justify-between border-b px-4 py-2\">\n          <div className=\"flex items-center gap-1\">\n            <span className=\"text-muted-foreground text-sm\">\n              {getLanguageDisplayName(language)}\n            </span>\n            {filename && (\n              <>\n                <span className=\"text-muted-foreground/50\">•</span>\n                <span className=\"text-foreground text-sm font-medium\">\n                  {filename}\n                </span>\n              </>\n            )}\n          </div>\n          <Button\n            variant=\"ghost\"\n            size=\"sm\"\n            onClick={handleCopy}\n            className=\"h-7 w-7 p-0\"\n            aria-label={isCopied ? \"Copied\" : \"Copy code\"}\n          >\n            {isCopied ? (\n              <Check className=\"h-4 w-4 text-green-700 dark:text-green-400\" />\n            ) : (\n              <Copy className=\"text-muted-foreground h-4 w-4\" />\n            )}\n          </Button>\n        </div>\n\n        <Collapsible open={!isCollapsed}>\n          <div\n            className={cn(\n              \"overflow-x-auto overflow-y-clip text-sm [&_pre]:bg-transparent [&_pre]:py-4\",\n              isCollapsed && \"max-h-[200px]\",\n            )}\n          >\n            {highlightedHtml && (\n              <div dangerouslySetInnerHTML={{ __html: highlightedHtml }} />\n            )}\n          </div>\n\n          {shouldCollapse && (\n            <CollapsibleTrigger asChild>\n              <Button\n                variant=\"ghost\"\n                onClick={() => setIsExpanded(!isExpanded)}\n                className=\"text-muted-foreground w-full rounded-none border-t font-normal\"\n              >\n                {isCollapsed ? (\n                  <>\n                    <ChevronDown className=\"mr-1 size-4\" />\n                    Show all {lineCount} lines\n                  </>\n                ) : (\n                  <>\n                    <ChevronUp className=\"mr-2 h-4 w-4\" />\n                    Collapse\n                  </>\n                )}\n              </Button>\n            </CollapsibleTrigger>\n          )}\n        </Collapsible>\n      </div>\n\n      {normalizedFooterActions && (\n        <div className=\"@container/actions\">\n          <ActionButtons\n            actions={normalizedFooterActions.items}\n            align={normalizedFooterActions.align}\n            confirmTimeout={normalizedFooterActions.confirmTimeout}\n            onAction={(id) => onResponseAction?.(id)}\n            onBeforeAction={onBeforeResponseAction}\n          />\n        </div>\n      )}\n    </div>\n  );\n}\n\nexport function CodeBlockProgress({ className }: { className?: string }) {\n  return (\n    <div\n      className={cn(\n        \"flex w-full flex-col gap-2 motion-safe:animate-pulse\",\n        className,\n      )}\n    >\n      <div className=\"flex items-center justify-between px-4 py-2\">\n        <div className=\"bg-muted h-4 w-20 rounded\" />\n        <div className=\"bg-muted h-6 w-6 rounded\" />\n      </div>\n      <div className=\"flex flex-col gap-1.5 px-4 py-3\">\n        <div className=\"bg-muted h-4 w-3/4 rounded\" />\n        <div className=\"bg-muted h-4 w-1/2 rounded\" />\n        <div className=\"bg-muted h-4 w-5/6 rounded\" />\n        <div className=\"bg-muted h-4 w-2/3 rounded\" />\n        <div className=\"bg-muted h-4 w-4/5 rounded\" />\n      </div>\n    </div>\n  );\n}\n\nimport * as CollapsiblePrimitive from \"@radix-ui/react-collapsible\"\n\nfunction Collapsible({\n  ...props\n}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {\n  return <CollapsiblePrimitive.Root data-slot=\"collapsible\" {...props} />\n}\n\nfunction CollapsibleTrigger({\n  ...props\n}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {\n  return (\n    <CollapsiblePrimitive.CollapsibleTrigger\n      data-slot=\"collapsible-trigger\"\n      {...props}\n    />\n  )\n}\n\nfunction CollapsibleContent({\n  className,\n  ...props\n}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {\n  return (\n    <CollapsiblePrimitive.CollapsibleContent\n      data-slot=\"collapsible-content\"\n      className={cn(\n        \"overflow-hidden data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nexport { Collapsible, CollapsibleTrigger, CollapsibleContent }\n\nimport * as React from \"react\";\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nconst buttonVariants = cva(\n  \"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-primary text-primary-foreground hover:bg-primary/90\",\n        destructive:\n          \"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60\",\n        outline:\n          \"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50\",\n        secondary:\n          \"bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n        ghost:\n          \"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50\",\n        link: \"text-primary underline-offset-4 hover:underline\",\n      },\n      size: {\n        default: \"h-9 px-4 py-2 has-[>svg]:px-3\",\n        sm: \"h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5\",\n        lg: \"h-10 rounded-md px-6 has-[>svg]:px-4\",\n        icon: \"size-9\",\n        \"icon-sm\": \"size-8\",\n        \"icon-lg\": \"size-10\",\n        homeCTA: \"rounded-full px-6 py-3 text-lg w-fit text-start\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\",\n    },\n  },\n);\n\nfunction Button({\n  className,\n  variant = \"default\",\n  size = \"default\",\n  asChild = false,\n  ...props\n}: React.ComponentProps<\"button\"> &\n  VariantProps<typeof buttonVariants> & {\n    asChild?: boolean;\n  }) {\n  const Comp = asChild ? Slot : \"button\";\n\n  return (\n    <Comp\n      data-slot=\"button\"\n      data-variant={variant}\n      data-size={size}\n      className={cn(buttonVariants({ variant, size, className }))}\n      {...props}\n    />\n  );\n}\n\nexport { Button, buttonVariants };\n\nexport interface ActionButtonsProps {\n  actions: Action[];\n  onAction: (actionId: string) => void | Promise<void>;\n  onBeforeAction?: (actionId: string) => boolean | Promise<boolean>;\n  confirmTimeout?: number;\n  align?: \"left\" | \"center\" | \"right\";\n  className?: string;\n}\n\nexport function ActionButtons({\n  actions,\n  onAction,\n  onBeforeAction,\n  confirmTimeout = 3000,\n  align = \"right\",\n  className,\n}: ActionButtonsProps) {\n  const { actions: resolvedActions, runAction } = useActionButtons({\n    actions,\n    onAction,\n    onBeforeAction,\n    confirmTimeout,\n  });\n\n  return (\n    <div\n      className={cn(\n        \"flex flex-col gap-3\",\n        \"@sm/actions:flex-row @sm/actions:flex-wrap @sm/actions:items-center @sm/actions:gap-2\",\n        align === \"left\" && \"@sm/actions:justify-start\",\n        align === \"center\" && \"@sm/actions:justify-center\",\n        align === \"right\" && \"@sm/actions:justify-end\",\n        className,\n      )}\n    >\n      {resolvedActions.map((action) => {\n        const label = action.currentLabel;\n        const variant = action.variant || \"default\";\n\n        return (\n          <Button\n            key={action.id}\n            variant={variant}\n            onClick={() => runAction(action.id)}\n            disabled={action.isDisabled}\n            className={cn(\n              \"rounded-full px-4!\",\n              \"justify-center\",\n              \"min-h-11 w-full text-base\",\n              \"@sm/actions:min-h-0 @sm/actions:w-auto @sm/actions:px-3 @sm/actions:py-2 @sm/actions:text-sm\",\n              action.isConfirming &&\n              \"ring-destructive ring-2 ring-offset-2 motion-safe:animate-pulse\",\n            )}\n            aria-label={\n              action.shortcut ? `${label} (${action.shortcut})` : label\n            }\n          >\n            {action.isLoading && (\n              <svg\n                className=\"mr-2 h-4 w-4 motion-safe:animate-spin\"\n                xmlns=\"http://www.w3.org/2000/svg\"\n                fill=\"none\"\n                viewBox=\"0 0 24 24\"\n              >\n                <circle\n                  className=\"opacity-25\"\n                  cx=\"12\"\n                  cy=\"12\"\n                  r=\"10\"\n                  stroke=\"currentColor\"\n                  strokeWidth=\"4\"\n                />\n                <path\n                  className=\"opacity-75\"\n                  fill=\"currentColor\"\n                  d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"\n                />\n              </svg>\n            )}\n            {action.icon && !action.isLoading && (\n              <span className=\"mr-2\">{action.icon}</span>\n            )}\n            {label}\n            {action.shortcut && !action.isLoading && (\n              <kbd className=\"border-border bg-muted ml-2.5 hidden rounded-lg border px-2 py-0.5 font-mono text-xs font-medium sm:inline-block\">\n                {action.shortcut}\n              </kbd>\n            )}\n          </Button>\n        );\n      })}\n    </div>\n  );\n}\n\nimport { z } from \"zod\";\nimport type { ReactNode } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\n/**\n * Tool UI conventions:\n * - Serializable schemas are JSON-safe (no callbacks/ReactNode/`className`).\n * - Schema: `SerializableXSchema`\n * - Parser: `parseSerializableX(input: unknown)`\n * - Actions: `responseActions`, `onResponseAction`, `onBeforeResponseAction`\n * - Root attrs: `data-tool-ui-id` + `data-slot`\n */\n\n/**\n * Schema for tool UI identity.\n *\n * Every tool UI should have a unique identifier that:\n * - Is stable across re-renders\n * - Is meaningful (not auto-generated)\n * - Is unique within the conversation\n *\n * Format recommendation: `{component-type}-{semantic-identifier}`\n * Examples: \"data-table-expenses-q3\", \"option-list-deploy-target\"\n */\nexport const ToolUIIdSchema = z.string().min(1);\n\nexport type ToolUIId = z.infer<typeof ToolUIIdSchema>;\n\n/**\n * Primary role of a Tool UI surface in a chat context.\n */\nexport const ToolUIRoleSchema = z.enum([\n  \"information\",\n  \"decision\",\n  \"control\",\n  \"state\",\n  \"composite\",\n]);\n\nexport type ToolUIRole = z.infer<typeof ToolUIRoleSchema>;\n\nexport const ToolUIReceiptOutcomeSchema = z.enum([\n  \"success\",\n  \"partial\",\n  \"failed\",\n  \"cancelled\",\n]);\n\nexport type ToolUIReceiptOutcome = z.infer<typeof ToolUIReceiptOutcomeSchema>;\n\n/**\n * Optional receipt metadata: a durable summary of an outcome.\n */\nexport const ToolUIReceiptSchema = z.object({\n  outcome: ToolUIReceiptOutcomeSchema,\n  summary: z.string().min(1),\n  identifiers: z.record(z.string(), z.string()).optional(),\n  at: z.string().datetime(),\n});\n\nexport type ToolUIReceipt = z.infer<typeof ToolUIReceiptSchema>;\n\n/**\n * Base schema for Tool UI payloads (id + optional role/receipt).\n */\nexport const ToolUISurfaceSchema = z.object({\n  id: ToolUIIdSchema,\n  role: ToolUIRoleSchema.optional(),\n  receipt: ToolUIReceiptSchema.optional(),\n});\n\nexport type ToolUISurface = z.infer<typeof ToolUISurfaceSchema>;\n\nexport const ActionSchema = z.object({\n  id: z.string().min(1),\n  label: z.string().min(1),\n  /**\n   * Canonical narration the assistant can use after this action is taken.\n   *\n   * Example: \"I exported the table as CSV.\" / \"I opened the link in a new tab.\"\n   */\n  sentence: z.string().optional(),\n  confirmLabel: z.string().optional(),\n  variant: z\n    .enum([\"default\", \"destructive\", \"secondary\", \"ghost\", \"outline\"])\n    .optional(),\n  icon: z.custom<ReactNode>().optional(),\n  loading: z.boolean().optional(),\n  disabled: z.boolean().optional(),\n  shortcut: z.string().optional(),\n});\n\nexport type Action = z.infer<typeof ActionSchema>;\n\nexport const ActionButtonsPropsSchema = z.object({\n  actions: z.array(ActionSchema).min(1),\n  align: z.enum([\"left\", \"center\", \"right\"]).optional(),\n  confirmTimeout: z.number().positive().optional(),\n  className: z.string().optional(),\n});\n\nexport const SerializableActionSchema = ActionSchema.omit({ icon: true });\nexport const SerializableActionsSchema = ActionButtonsPropsSchema.extend({\n  actions: z.array(SerializableActionSchema),\n}).omit({ className: true });\n\nexport interface ActionsConfig {\n  items: Action[];\n  align?: \"left\" | \"center\" | \"right\";\n  confirmTimeout?: number;\n}\n\nexport const SerializableActionsConfigSchema = z.object({\n  items: z.array(SerializableActionSchema).min(1),\n  align: z.enum([\"left\", \"center\", \"right\"]).optional(),\n  confirmTimeout: z.number().positive().optional(),\n});\n\nexport type SerializableActionsConfig = z.infer<\n  typeof SerializableActionsConfigSchema\n>;\n\nexport type SerializableAction = z.infer<typeof SerializableActionSchema>;\n\nexport type UseActionButtonsOptions = {\n  actions: Action[];\n  onAction: (actionId: string) => void | Promise<void>;\n  onBeforeAction?: (actionId: string) => boolean | Promise<boolean>;\n  confirmTimeout?: number;\n};\n\nexport type UseActionButtonsResult = {\n  actions: Array<\n    Action & {\n      currentLabel: string;\n      isConfirming: boolean;\n      isExecuting: boolean;\n      isDisabled: boolean;\n      isLoading: boolean;\n    }\n  >;\n  runAction: (actionId: string) => Promise<void>;\n  confirmingActionId: string | null;\n  executingActionId: string | null;\n};\n\nexport function useActionButtons(\n  options: UseActionButtonsOptions,\n): UseActionButtonsResult {\n  const {\n    actions,\n    onAction,\n    onBeforeAction,\n    confirmTimeout = 3000,\n  } = options;\n\n  const [confirmingActionId, setConfirmingActionId] = useState<string | null>(\n    null,\n  );\n  const [executingActionId, setExecutingActionId] = useState<string | null>(\n    null,\n  );\n\n  useEffect(() => {\n    if (!confirmingActionId) return;\n    const id = setTimeout(() => setConfirmingActionId(null), confirmTimeout);\n    return () => clearTimeout(id);\n  }, [confirmingActionId, confirmTimeout]);\n\n  useEffect(() => {\n    if (!confirmingActionId) return;\n    const handleKeyDown = (e: KeyboardEvent) => {\n      if (e.key === \"Escape\") {\n        setConfirmingActionId(null);\n      }\n    };\n\n    window.addEventListener(\"keydown\", handleKeyDown);\n    return () => window.removeEventListener(\"keydown\", handleKeyDown);\n  }, [confirmingActionId]);\n\n  const runAction = useCallback(\n    async (actionId: string) => {\n      const action = actions.find((a) => a.id === actionId);\n      if (!action) return;\n\n      const isAnyActionExecuting = executingActionId !== null;\n      if (action.disabled || action.loading || isAnyActionExecuting) {\n        return;\n      }\n\n      if (action.confirmLabel && confirmingActionId !== action.id) {\n        setConfirmingActionId(action.id);\n        return;\n      }\n\n      if (onBeforeAction) {\n        const shouldProceed = await onBeforeAction(action.id);\n        if (!shouldProceed) {\n          setConfirmingActionId(null);\n          return;\n        }\n      }\n\n      try {\n        setExecutingActionId(action.id);\n        await onAction(action.id);\n      } finally {\n        setExecutingActionId(null);\n        setConfirmingActionId(null);\n      }\n    },\n    [actions, confirmingActionId, executingActionId, onAction, onBeforeAction],\n  );\n\n  const resolvedActions = useMemo(\n    () =>\n      actions.map((action) => {\n        const isConfirming = confirmingActionId === action.id;\n        const isThisActionExecuting = executingActionId === action.id;\n        const isLoading = action.loading || isThisActionExecuting;\n        const isDisabled =\n          action.disabled || (executingActionId !== null && !isThisActionExecuting);\n        const currentLabel =\n          isConfirming && action.confirmLabel\n            ? action.confirmLabel\n            : action.label;\n\n        return {\n          ...action,\n          currentLabel,\n          isConfirming,\n          isExecuting: isThisActionExecuting,\n          isDisabled,\n          isLoading,\n        };\n      }),\n    [actions, confirmingActionId, executingActionId],\n  );\n\n  return {\n    actions: resolvedActions,\n    runAction,\n    confirmingActionId,\n    executingActionId,\n  };\n}\n\nexport type ActionsProp = ActionsConfig | Action[];\n\nconst NEGATORY_ACTION_IDS = new Set([\n  \"cancel\",\n  \"dismiss\",\n  \"skip\",\n  \"no\",\n  \"reset\",\n  \"close\",\n  \"decline\",\n  \"reject\",\n  \"back\",\n  \"later\",\n  \"not-now\",\n  \"maybe-later\",\n]);\n\nfunction inferVariant(action: Action): Action {\n  if (action.variant) return action;\n  if (NEGATORY_ACTION_IDS.has(action.id)) {\n    return { ...action, variant: \"ghost\" };\n  }\n  return action;\n}\n\nexport function normalizeActionsConfig(\n  actions?: ActionsProp,\n): ActionsConfig | null {\n  if (!actions) return null;\n\n  const rawItems = Array.isArray(actions) ? actions : (actions.items ?? []);\n\n  if (rawItems.length === 0) {\n    return null;\n  }\n\n  const items = rawItems.map(inferVariant);\n\n  return Array.isArray(actions)\n    ? { items }\n    : {\n      items,\n      align: actions.align,\n      confirmTimeout: actions.confirmTimeout,\n    };\n}\n\nfunction fallbackCopyToClipboard(text: string): boolean {\n  try {\n    const textArea = document.createElement(\"textarea\");\n    textArea.value = text;\n    textArea.setAttribute(\"readonly\", \"\");\n    textArea.style.position = \"fixed\";\n    textArea.style.top = \"-9999px\";\n    textArea.style.left = \"-9999px\";\n    document.body.appendChild(textArea);\n    textArea.select();\n    const ok = document.execCommand(\"copy\");\n    document.body.removeChild(textArea);\n    return ok;\n  } catch {\n    return false;\n  }\n}\n\nexport function useCopyToClipboard(options?: {\n  resetAfterMs?: number;\n}): {\n  copiedId: string | null;\n  copy: (text: string, id?: string) => Promise<boolean>;\n} {\n  const resetAfterMs = options?.resetAfterMs ?? 2000;\n  const [copiedId, setCopiedId] = useState<string | null>(null);\n\n  const copy = useCallback(\n    async (text: string, id: string = \"default\") => {\n      let ok = false;\n      try {\n        if (navigator.clipboard?.writeText) {\n          await navigator.clipboard.writeText(text);\n          ok = true;\n        } else {\n          ok = fallbackCopyToClipboard(text);\n        }\n      } catch {\n        ok = fallbackCopyToClipboard(text);\n      }\n\n      if (ok) {\n        setCopiedId(id);\n      }\n\n      return ok;\n    },\n    [],\n  );\n\n  useEffect(() => {\n    if (!copiedId) return;\n    const timeout = setTimeout(() => setCopiedId(null), resetAfterMs);\n    return () => clearTimeout(timeout);\n  }, [copiedId, resetAfterMs]);\n\n  return { copiedId, copy };\n}\n\nexport const CodeBlockPropsSchema = z.object({\n  id: ToolUIIdSchema,\n  role: ToolUIRoleSchema.optional(),\n  receipt: ToolUIReceiptSchema.optional(),\n  code: z.string(),\n  language: z.string().default(\"text\"),\n  filename: z.string().optional(),\n  showLineNumbers: z.boolean().default(true),\n  highlightLines: z.array(z.number()).optional(),\n  maxCollapsedLines: z.number().min(1).optional(),\n  responseActions: z\n    .union([z.array(SerializableActionSchema), SerializableActionsConfigSchema])\n    .optional(),\n  className: z.string().optional(),\n});\n\nexport type CodeBlockProps = z.infer<typeof CodeBlockPropsSchema> & {\n  isLoading?: boolean;\n  onResponseAction?: (actionId: string) => void | Promise<void>;\n  onBeforeResponseAction?: (actionId: string) => boolean | Promise<boolean>;\n};\n\nexport const SerializableCodeBlockSchema = z.object({\n  id: ToolUIIdSchema,\n  role: ToolUIRoleSchema.optional(),\n  receipt: ToolUIReceiptSchema.optional(),\n  code: z.string(),\n  language: z.string().default(\"text\"),\n  filename: z.string().optional(),\n  showLineNumbers: z.boolean().default(true),\n  highlightLines: z.array(z.number()).optional(),\n  maxCollapsedLines: z.number().min(1).optional(),\n  responseActions: z\n    .union([z.array(SerializableActionSchema), SerializableActionsConfigSchema])\n    .optional(),\n});\n\nexport type SerializableCodeBlock = z.infer<typeof SerializableCodeBlockSchema>;\n\nfunction formatZodPath(path: Array<string | number | symbol>): string {\n  if (path.length === 0) return \"root\";\n  return path\n    .map((segment) =>\n      typeof segment === \"number\" ? `[${segment}]` : String(segment),\n    )\n    .join(\".\");\n}\n\n/**\n * Format Zod errors into a compact `path: message` string.\n */\nexport function formatZodError(error: z.ZodError): string {\n  const parts = error.issues.map((issue) => {\n    const path = formatZodPath(issue.path);\n    return `${path}: ${issue.message}`;\n  });\n\n  return Array.from(new Set(parts)).join(\"; \");\n}\n\n/**\n * Parse unknown input and throw a readable error.\n */\nexport function parseWithSchema<T>(\n  schema: z.ZodType<T>,\n  input: unknown,\n  name: string,\n): T {\n  const res = schema.safeParse(input);\n  if (!res.success) {\n    throw new Error(`Invalid ${name} payload: ${formatZodError(res.error)}`);\n  }\n  return res.data;\n}\n",
      "type": "registry:ui",
      "target": "components/ui/code-block.tsx"
    }
  ],
  "type": "registry:ui"
}
