{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "message-draft",
  "type": "registry:block",
  "title": "Message Draft",
  "description": "Review and confirm drafted messages before sending.",
  "dependencies": [
    "lucide-react",
    "zod"
  ],
  "registryDependencies": [
    "button"
  ],
  "files": [
    {
      "path": "components/tool-ui/message-draft/_adapter.tsx",
      "type": "registry:component",
      "target": "components/tool-ui/message-draft/_adapter.tsx",
      "content": "/**\n * Adapter: UI and utility re-exports for copy-standalone portability.\n *\n * When copying this component to another project, update these imports\n * to match your project's paths:\n *\n *   cn        → Your Tailwind merge utility (e.g., \"@/lib/utils\", \"~/lib/cn\")\n *   Button    → shadcn/ui Button\n */\n\nexport { cn } from \"@/lib/utils\";\nexport { Button } from \"@/components/ui/button\";\n"
    },
    {
      "path": "components/tool-ui/message-draft/index.tsx",
      "type": "registry:component",
      "target": "components/tool-ui/message-draft/index.tsx",
      "content": "export { MessageDraft } from \"./message-draft\";\nexport {\n  type SerializableMessageDraft,\n  type SerializableEmailDraft,\n  type SerializableSlackDraft,\n  type MessageDraftChannel,\n  type MessageDraftOutcome,\n  type SlackTarget,\n  type MessageDraftProps,\n} from \"./schema\";\n"
    },
    {
      "path": "components/tool-ui/message-draft/message-draft.tsx",
      "type": "registry:component",
      "target": "components/tool-ui/message-draft/message-draft.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn, Button } from \"./_adapter\";\nimport type {\n  MessageDraftProps,\n  SerializableEmailDraft,\n  SerializableSlackDraft,\n} from \"./schema\";\nimport { ActionButtons } from \"../shared/action-buttons\";\nimport type { Action } from \"../shared/schema\";\nimport { Check, ChevronDown } from \"lucide-react\";\n\ntype DraftState = \"review\" | \"sending\" | \"sent\" | \"cancelled\";\ntype DraftOutcome = MessageDraftProps[\"outcome\"];\n\nconst DEFAULT_GRACE_PERIOD = 5000;\nconst COLLAPSED_BODY_HEIGHT = 280;\n\ninterface RecipientRowProps {\n  label: string;\n  recipients: string[];\n  maxVisible?: number;\n  muted?: boolean;\n}\n\nfunction RecipientRow({\n  label,\n  recipients,\n  maxVisible = 3,\n  muted = false,\n}: RecipientRowProps) {\n  const visibleRecipients = recipients.slice(0, maxVisible);\n  const overflowCount = recipients.length - maxVisible;\n\n  return (\n    <tr className=\"text-sm\">\n      <td className=\"text-muted-foreground w-0 pr-4 pb-1 text-right align-top font-medium whitespace-nowrap\">\n        {label}\n      </td>\n      <td className={cn(\"pb-1 align-top\", muted && \"text-muted-foreground\")}>\n        {visibleRecipients.join(\", \")}\n        {overflowCount > 0 && (\n          <span className=\"text-muted-foreground\"> +{overflowCount} more</span>\n        )}\n      </td>\n    </tr>\n  );\n}\n\ninterface SingleFieldRowProps {\n  label: string;\n  value: string;\n}\n\nfunction SingleFieldRow({ label, value }: SingleFieldRowProps) {\n  return (\n    <tr className=\"text-sm\">\n      <td className=\"text-muted-foreground w-0 pr-4 pb-1 text-right align-top font-medium whitespace-nowrap\">\n        {label}\n      </td>\n      <td className=\"pb-1 align-top\">{value}</td>\n    </tr>\n  );\n}\n\ninterface ExpandableBodyProps {\n  body: string;\n  isExpanded: boolean;\n  onNeedsExpansionChange?: (needsExpansion: boolean) => void;\n}\n\nfunction ExpandableBody({\n  body,\n  isExpanded,\n  onNeedsExpansionChange,\n}: ExpandableBodyProps) {\n  const [needsExpansion, setNeedsExpansion] = React.useState<boolean | null>(\n    null,\n  );\n  const contentRef = React.useRef<HTMLDivElement>(null);\n\n  React.useLayoutEffect(() => {\n    if (contentRef.current) {\n      const needs = contentRef.current.scrollHeight > COLLAPSED_BODY_HEIGHT;\n      setNeedsExpansion(needs);\n      onNeedsExpansionChange?.(needs);\n    }\n  }, [body, onNeedsExpansionChange]);\n\n  return (\n    <div className=\"relative\">\n      <div\n        ref={contentRef}\n        className={cn(\n          \"overflow-hidden text-sm leading-relaxed\",\n          needsExpansion !== null &&\n            \"transition-[max-height] duration-300 ease-in-out\",\n        )}\n        style={{\n          maxHeight:\n            needsExpansion === null\n              ? `${COLLAPSED_BODY_HEIGHT}px`\n              : isExpanded || !needsExpansion\n                ? `${contentRef.current?.scrollHeight ?? 1000}px`\n                : `${COLLAPSED_BODY_HEIGHT}px`,\n        }}\n      >\n        <p className=\"pt-1 whitespace-pre-wrap\">{body}</p>\n      </div>\n      {needsExpansion && (\n        <div\n          className={cn(\n            \"from-card pointer-events-none absolute inset-x-0 bottom-0 bg-gradient-to-t to-transparent transition-[height] duration-300 ease-in-out\",\n            isExpanded ? \"h-0\" : \"h-12\",\n          )}\n        />\n      )}\n    </div>\n  );\n}\n\ninterface EmailDraftContentProps {\n  draft: SerializableEmailDraft;\n  titleId: string;\n  isExpanded: boolean;\n  onNeedsExpansionChange?: (needsExpansion: boolean) => void;\n}\n\nfunction EmailDraftContent({\n  draft,\n  titleId,\n  isExpanded,\n  onNeedsExpansionChange,\n}: EmailDraftContentProps) {\n  return (\n    <>\n      <h2 id={titleId} className=\"pt-2 text-base leading-tight font-semibold\">\n        {draft.subject}\n      </h2>\n\n      <table className=\"w-full\">\n        <tbody>\n          {draft.from && <SingleFieldRow label=\"From\" value={draft.from} />}\n          <RecipientRow label=\"To\" recipients={draft.to} />\n          {draft.cc && draft.cc.length > 0 && (\n            <RecipientRow label=\"Cc\" recipients={draft.cc} />\n          )}\n          {draft.bcc && draft.bcc.length > 0 && (\n            <RecipientRow label=\"Bcc\" recipients={draft.bcc} muted />\n          )}\n        </tbody>\n      </table>\n\n      <div className=\"bg-border -mx-5 h-px\" role=\"separator\" />\n\n      <ExpandableBody\n        body={draft.body}\n        isExpanded={isExpanded}\n        onNeedsExpansionChange={onNeedsExpansionChange}\n      />\n    </>\n  );\n}\n\ninterface SlackDraftContentProps {\n  draft: SerializableSlackDraft;\n  titleId: string;\n  isExpanded: boolean;\n  onNeedsExpansionChange?: (needsExpansion: boolean) => void;\n}\n\nfunction SlackLogo({ className }: { className?: string }) {\n  return (\n    <svg className={className} viewBox=\"0 0 24 24\" aria-hidden=\"true\">\n      <path\n        fill=\"#E01E5A\"\n        d=\"M5.042 15.165a2.528 2.528 0 0 1-2.52 2.523A2.528 2.528 0 0 1 0 15.165a2.527 2.527 0 0 1 2.522-2.52h2.52v2.52zm1.271 0a2.527 2.527 0 0 1 2.521-2.52 2.527 2.527 0 0 1 2.521 2.52v6.313A2.528 2.528 0 0 1 8.834 24a2.528 2.528 0 0 1-2.521-2.522v-6.313z\"\n      />\n      <path\n        fill=\"#36C5F0\"\n        d=\"M8.834 5.042a2.528 2.528 0 0 1-2.521-2.52A2.528 2.528 0 0 1 8.834 0a2.528 2.528 0 0 1 2.521 2.522v2.52H8.834zm0 1.271a2.528 2.528 0 0 1 2.521 2.521 2.528 2.528 0 0 1-2.521 2.521H2.522A2.528 2.528 0 0 1 0 8.834a2.528 2.528 0 0 1 2.522-2.521h6.312z\"\n      />\n      <path\n        fill=\"#2EB67D\"\n        d=\"M18.958 8.834a2.528 2.528 0 0 1 2.522-2.521A2.528 2.528 0 0 1 24 8.834a2.528 2.528 0 0 1-2.52 2.521h-2.522V8.834zm-1.271 0a2.528 2.528 0 0 1-2.521 2.521 2.528 2.528 0 0 1-2.521-2.521V2.522A2.528 2.528 0 0 1 15.165 0a2.528 2.528 0 0 1 2.522 2.522v6.312z\"\n      />\n      <path\n        fill=\"#ECB22E\"\n        d=\"M15.165 18.958a2.528 2.528 0 0 1 2.522 2.522A2.528 2.528 0 0 1 15.165 24a2.527 2.527 0 0 1-2.521-2.52v-2.522h2.521zm0-1.271a2.527 2.527 0 0 1-2.521-2.521 2.526 2.526 0 0 1 2.521-2.521h6.313A2.527 2.527 0 0 1 24 15.165a2.528 2.528 0 0 1-2.522 2.521h-6.313z\"\n      />\n    </svg>\n  );\n}\n\nfunction SlackDraftContent({\n  draft,\n  titleId,\n  isExpanded,\n  onNeedsExpansionChange,\n}: SlackDraftContentProps) {\n  const { target } = draft;\n  const isChannel = target.type === \"channel\";\n  const targetDisplay = isChannel\n    ? `#${target.name}`\n    : `Message to @${target.name}`;\n  const memberCount = isChannel ? target.memberCount : undefined;\n\n  return (\n    <>\n      <div\n        id={titleId}\n        className=\"flex items-center gap-1.5 text-sm font-medium\"\n      >\n        <SlackLogo className=\"size-4\" />\n        <span>{targetDisplay}</span>\n        {memberCount !== undefined && (\n          <span className=\"text-muted-foreground ml-auto text-sm font-normal\">\n            {memberCount.toLocaleString()} members\n          </span>\n        )}\n      </div>\n\n      <div className=\"bg-border -mx-5 h-px\" role=\"separator\" />\n\n      <ExpandableBody\n        body={draft.body}\n        isExpanded={isExpanded}\n        onNeedsExpansionChange={onNeedsExpansionChange}\n      />\n    </>\n  );\n}\n\nfunction formatSentTime(date: Date): string {\n  return date.toLocaleTimeString(undefined, {\n    hour: \"numeric\",\n    minute: \"2-digit\",\n  });\n}\n\nexport function resolveStateFromOutcome(outcome: DraftOutcome): DraftState {\n  if (outcome === \"sent\") return \"sent\";\n  if (outcome === \"cancelled\") return \"cancelled\";\n  return \"review\";\n}\n\nexport function resolveOutcomeTransition(\n  previousOutcome: DraftOutcome,\n  nextOutcome: DraftOutcome,\n): DraftState | null {\n  if (previousOutcome === nextOutcome) {\n    return null;\n  }\n\n  return resolveStateFromOutcome(nextOutcome);\n}\n\ninterface SentConfirmationProps {\n  sentAt: Date;\n}\n\nfunction SentConfirmation({ sentAt }: SentConfirmationProps) {\n  return (\n    <div\n      className=\"flex items-center justify-end gap-2 text-sm\"\n      role=\"status\"\n      aria-label=\"Message sent\"\n    >\n      <span className=\"text-muted-foreground\">\n        Sent at {formatSentTime(sentAt)}\n      </span>\n      <span className=\"bg-primary/10 text-primary flex size-6 shrink-0 items-center justify-center rounded-full\">\n        <Check className=\"size-3.5\" />\n      </span>\n    </div>\n  );\n}\n\nexport function MessageDraft(props: MessageDraftProps) {\n  const {\n    id,\n    className,\n    outcome,\n    undoGracePeriod = DEFAULT_GRACE_PERIOD,\n    onSend,\n    onUndo,\n    onCancel,\n  } = props;\n\n  const [state, setState] = React.useState<DraftState>(() =>\n    resolveStateFromOutcome(outcome),\n  );\n  const [countdown, setCountdown] = React.useState(\n    Math.ceil(undoGracePeriod / 1000),\n  );\n  const [sentAt, setSentAt] = React.useState<Date | null>(() =>\n    outcome === \"sent\" ? new Date() : null,\n  );\n  const [isExpanded, setIsExpanded] = React.useState(false);\n  const [needsExpansion, setNeedsExpansion] = React.useState(false);\n  const undoButtonRef = React.useRef<HTMLButtonElement>(null);\n  const timerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);\n  const countdownRef = React.useRef<ReturnType<typeof setInterval> | null>(\n    null,\n  );\n  const previousOutcomeRef = React.useRef<DraftOutcome>(outcome);\n\n  const clearTimers = React.useCallback(() => {\n    if (timerRef.current) {\n      clearTimeout(timerRef.current);\n      timerRef.current = null;\n    }\n    if (countdownRef.current) {\n      clearInterval(countdownRef.current);\n      countdownRef.current = null;\n    }\n  }, []);\n\n  React.useEffect(() => {\n    return clearTimers;\n  }, [clearTimers]);\n\n  React.useEffect(() => {\n    const nextState = resolveOutcomeTransition(\n      previousOutcomeRef.current,\n      outcome,\n    );\n\n    previousOutcomeRef.current = outcome;\n\n    if (nextState === null) {\n      return;\n    }\n\n    clearTimers();\n    setState(nextState);\n    setCountdown(Math.ceil(undoGracePeriod / 1000));\n    setSentAt(nextState === \"sent\" ? new Date() : null);\n  }, [outcome, undoGracePeriod, clearTimers]);\n\n  React.useEffect(() => {\n    if (state === \"sending\") {\n      undoButtonRef.current?.focus();\n\n      setCountdown(Math.ceil(undoGracePeriod / 1000));\n\n      countdownRef.current = setInterval(() => {\n        setCountdown((prev) => {\n          if (prev <= 1) {\n            if (countdownRef.current) {\n              clearInterval(countdownRef.current);\n              countdownRef.current = null;\n            }\n            return 0;\n          }\n          return prev - 1;\n        });\n      }, 1000);\n\n      timerRef.current = setTimeout(async () => {\n        clearTimers();\n        await onSend?.();\n        setSentAt(new Date());\n        setState(\"sent\");\n      }, undoGracePeriod);\n    }\n  }, [state, undoGracePeriod, onSend, clearTimers]);\n\n  const handleSend = React.useCallback(() => {\n    setState(\"sending\");\n  }, []);\n\n  const handleUndo = React.useCallback(() => {\n    clearTimers();\n    setState(\"review\");\n    onUndo?.();\n  }, [clearTimers, onUndo]);\n\n  const handleCancel = React.useCallback(() => {\n    clearTimers();\n    setState(\"cancelled\");\n    onCancel?.();\n  }, [clearTimers, onCancel]);\n\n  const handleKeyDown = React.useCallback(\n    (event: React.KeyboardEvent) => {\n      if (event.key === \"Escape\" && state === \"review\") {\n        event.preventDefault();\n        handleCancel();\n      }\n    },\n    [state, handleCancel],\n  );\n\n  const handleNeedsExpansionChange = React.useCallback((needs: boolean) => {\n    setNeedsExpansion(needs);\n  }, []);\n\n  const handleToggleExpand = React.useCallback(() => {\n    setIsExpanded((prev) => !prev);\n  }, []);\n\n  const handleAction = React.useCallback(\n    async (actionId: string) => {\n      if (actionId === \"send\") {\n        handleSend();\n      } else if (actionId === \"cancel\") {\n        handleCancel();\n      }\n    },\n    [handleSend, handleCancel],\n  );\n\n  const actions: Action[] = [\n    {\n      id: \"cancel\",\n      label: \"Cancel\",\n      variant: \"ghost\",\n    },\n    {\n      id: \"send\",\n      label: \"Send\",\n      variant: \"default\",\n    },\n  ];\n\n  const expandButton = needsExpansion ? (\n    <Button\n      variant=\"ghost\"\n      size=\"sm\"\n      onClick={handleToggleExpand}\n      className=\"h-7 gap-1 px-2 text-sm\"\n    >\n      {isExpanded ? \"Show less\" : \"Read more\"}\n      <ChevronDown className={cn(\"size-3\", isExpanded && \"rotate-180\")} />\n    </Button>\n  ) : null;\n\n  const renderActions = () => {\n    switch (state) {\n      case \"sending\":\n        return (\n          <div\n            className=\"flex items-center justify-end gap-3\"\n            aria-live=\"polite\"\n          >\n            <span className=\"text-muted-foreground text-sm\">\n              Sending in {countdown}s\n            </span>\n            <Button\n              ref={undoButtonRef}\n              variant=\"outline\"\n              size=\"sm\"\n              onClick={handleUndo}\n              className=\"rounded-full\"\n            >\n              Undo\n            </Button>\n          </div>\n        );\n      case \"sent\":\n        return <SentConfirmation sentAt={sentAt ?? new Date()} />;\n      case \"cancelled\":\n        return null;\n      default:\n        return <ActionButtons actions={actions} onAction={handleAction} />;\n    }\n  };\n\n  if (state === \"cancelled\") {\n    return null;\n  }\n\n  return (\n    <article\n      className={cn(\n        \"flex w-full max-w-lg min-w-64 flex-col gap-3\",\n        \"text-foreground\",\n        className,\n      )}\n      data-slot=\"message-draft\"\n      data-tool-ui-id={id}\n      data-state={state}\n      aria-labelledby={`${id}-title`}\n      onKeyDown={handleKeyDown}\n    >\n      <div className=\"bg-card flex w-full flex-col gap-3 rounded-2xl border px-5 pt-3 pb-5 shadow-xs transition-none\">\n        {props.channel === \"email\" ? (\n          <EmailDraftContent\n            draft={props}\n            titleId={`${id}-title`}\n            isExpanded={isExpanded}\n            onNeedsExpansionChange={handleNeedsExpansionChange}\n          />\n        ) : (\n          <SlackDraftContent\n            draft={props}\n            titleId={`${id}-title`}\n            isExpanded={isExpanded}\n            onNeedsExpansionChange={handleNeedsExpansionChange}\n          />\n        )}\n\n        {expandButton}\n      </div>\n\n      <div className=\"@container/actions\">{renderActions()}</div>\n    </article>\n  );\n}\n"
    },
    {
      "path": "components/tool-ui/message-draft/README.md",
      "type": "registry:file",
      "target": "components/tool-ui/message-draft/README.md",
      "content": "# Message Draft\n\nImplementation for the \"message-draft\" Tool UI surface.\n\n## Files\n\n- public exports: components/tool-ui/message-draft/index.tsx\n- serializable schema + parse helpers: components/tool-ui/message-draft/schema.ts\n\n## Companion assets\n\n- Docs page: app/docs/message-draft/content.mdx\n- Preset payload: lib/presets/message-draft.ts\n\n## Quick check\n\nRun this after edits:\n\npnpm test\n"
    },
    {
      "path": "components/tool-ui/message-draft/schema.ts",
      "type": "registry:lib",
      "target": "components/tool-ui/message-draft/schema.ts",
      "content": "import { z } from \"zod\";\nimport { ToolUIIdSchema, ToolUIRoleSchema } from \"../shared/schema\";\nimport { defineToolUiContract } from \"../shared/contract\";\n\nexport const MessageDraftChannelSchema = z.enum([\"email\", \"slack\"]);\n\nexport type MessageDraftChannel = z.infer<typeof MessageDraftChannelSchema>;\n\nexport const MessageDraftOutcomeSchema = z.enum([\"sent\", \"cancelled\"]);\n\nexport type MessageDraftOutcome = z.infer<typeof MessageDraftOutcomeSchema>;\n\nconst SlackTargetSchema = z.discriminatedUnion(\"type\", [\n  z.object({\n    type: z.literal(\"channel\"),\n    name: z.string().min(1),\n    memberCount: z.number().optional(),\n  }),\n  z.object({ type: z.literal(\"dm\"), name: z.string().min(1) }),\n]);\n\nexport type SlackTarget = z.infer<typeof SlackTargetSchema>;\n\nexport const SerializableEmailDraftSchema = z.object({\n  id: ToolUIIdSchema,\n  role: ToolUIRoleSchema.optional(),\n  body: z.string().min(1),\n  outcome: MessageDraftOutcomeSchema.optional(),\n  channel: z.literal(\"email\"),\n  subject: z.string().min(1),\n  from: z.string().optional(),\n  to: z.array(z.string()).min(1),\n  cc: z.array(z.string()).optional(),\n  bcc: z.array(z.string()).optional(),\n});\n\nexport const SerializableSlackDraftSchema = z.object({\n  id: ToolUIIdSchema,\n  role: ToolUIRoleSchema.optional(),\n  body: z.string().min(1),\n  outcome: MessageDraftOutcomeSchema.optional(),\n  channel: z.literal(\"slack\"),\n  target: SlackTargetSchema,\n});\n\nexport const SerializableMessageDraftSchema = z.discriminatedUnion(\"channel\", [\n  SerializableEmailDraftSchema,\n  SerializableSlackDraftSchema,\n]);\n\nexport type SerializableMessageDraft = z.infer<\n  typeof SerializableMessageDraftSchema\n>;\n\nexport type SerializableEmailDraft = z.infer<\n  typeof SerializableEmailDraftSchema\n>;\n\nexport type SerializableSlackDraft = z.infer<\n  typeof SerializableSlackDraftSchema\n>;\n\nconst SerializableMessageDraftSchemaContract = defineToolUiContract(\n  \"MessageDraft\",\n  SerializableMessageDraftSchema,\n);\n\nexport const parseSerializableMessageDraft: (\n  input: unknown,\n) => SerializableMessageDraft = SerializableMessageDraftSchemaContract.parse;\n\nexport const safeParseSerializableMessageDraft: (\n  input: unknown,\n) => SerializableMessageDraft | null =\n  SerializableMessageDraftSchemaContract.safeParse;\n\nexport type MessageDraftProps = SerializableMessageDraft & {\n  className?: string;\n  undoGracePeriod?: number;\n  onSend?: () => void | Promise<void>;\n  onUndo?: () => void;\n  onCancel?: () => void;\n};\n"
    },
    {
      "path": "components/tool-ui/shared/_adapter.tsx",
      "type": "registry:component",
      "target": "components/tool-ui/shared/_adapter.tsx",
      "content": "/**\n * Adapter: UI and utility re-exports for copy-standalone portability.\n *\n * When copying this component to another project, update these imports\n * to match your project's paths:\n *\n *   cn     → Your Tailwind merge utility (e.g., \"@/lib/utils\", \"~/lib/cn\")\n *   Button → shadcn/ui Button\n */\n\nexport { cn } from \"@/lib/utils\";\nexport { Button } from \"@/components/ui/button\";\n"
    },
    {
      "path": "components/tool-ui/shared/action-buttons.tsx",
      "type": "registry:component",
      "target": "components/tool-ui/shared/action-buttons.tsx",
      "content": "\"use client\";\n\nimport type { Action } from \"./schema\";\nimport { cn, Button } from \"./_adapter\";\nimport { useActionButtons } from \"./use-action-buttons\";\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"
    },
    {
      "path": "components/tool-ui/shared/contract.ts",
      "type": "registry:lib",
      "target": "components/tool-ui/shared/contract.ts",
      "content": "import { z } from \"zod\";\nimport { parseWithSchema, safeParseWithSchema } from \"./parse\";\n\nexport interface ToolUiContract<T> {\n  schema: z.ZodType<T>;\n  parse: (input: unknown) => T;\n  safeParse: (input: unknown) => T | null;\n}\n\nexport function defineToolUiContract<T>(\n  componentName: string,\n  schema: z.ZodType<T>,\n): ToolUiContract<T> {\n  return {\n    schema,\n    parse: (input: unknown) => parseWithSchema(schema, input, componentName),\n    safeParse: (input: unknown) => safeParseWithSchema(schema, input),\n  };\n}\n"
    },
    {
      "path": "components/tool-ui/shared/parse.ts",
      "type": "registry:lib",
      "target": "components/tool-ui/shared/parse.ts",
      "content": "import { z } from \"zod\";\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\n/**\n * Parse unknown input, returning `null` instead of throwing on failure.\n *\n * Use this in assistant-ui `render` functions where `args` stream in\n * incrementally and may be incomplete until the tool call finishes.\n */\nexport function safeParseWithSchema<T>(\n  schema: z.ZodType<T>,\n  input: unknown,\n): T | null {\n  const res = schema.safeParse(input);\n  return res.success ? res.data : null;\n}\n"
    },
    {
      "path": "components/tool-ui/shared/schema.ts",
      "type": "registry:lib",
      "target": "components/tool-ui/shared/schema.ts",
      "content": "import { z } from \"zod\";\nimport type { ReactNode } from \"react\";\n\n/**\n * Tool UI conventions:\n * - Serializable schemas are JSON-safe (no callbacks/ReactNode/`className`).\n * - Schema: `SerializableXSchema`\n * - Parser: `parseSerializableX(input: unknown)` (throws on invalid)\n * - Safe parser: `safeParseSerializableX(input: unknown)` (returns `null` on invalid)\n * - Actions: `LocalActions` for non-receipt actions and `DecisionActions` for consequential actions\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>;\nexport type LocalAction = Action;\nexport type DecisionAction = Action;\n\nexport const DecisionResultSchema = z.object({\n  kind: z.literal(\"decision\"),\n  version: z.literal(1),\n  decisionId: z.string().min(1),\n  actionId: z.string().min(1),\n  actionLabel: z.string().min(1),\n  at: z.string().datetime(),\n  payload: z.record(z.string(), z.unknown()).optional(),\n});\n\nexport type DecisionResult<\n  TPayload extends Record<string, unknown> = Record<string, unknown>,\n> = Omit<z.infer<typeof DecisionResultSchema>, \"payload\"> & {\n  payload?: TPayload;\n};\n\nexport function createDecisionResult<\n  TPayload extends Record<string, unknown> = Record<string, unknown>,\n>(args: {\n  decisionId: string;\n  action: { id: string; label: string };\n  payload?: TPayload;\n}): DecisionResult<TPayload> {\n  return {\n    kind: \"decision\",\n    version: 1,\n    decisionId: args.decisionId,\n    actionId: args.action.id,\n    actionLabel: args.action.label,\n    at: new Date().toISOString(),\n    payload: args.payload,\n  };\n}\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"
    },
    {
      "path": "components/tool-ui/shared/use-action-buttons.tsx",
      "type": "registry:component",
      "target": "components/tool-ui/shared/use-action-buttons.tsx",
      "content": "\"use client\";\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport type { Action } from \"./schema\";\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\ntype ActionExecutionLock = {\n  tryAcquire: () => boolean;\n  release: () => void;\n};\n\nexport function createActionExecutionLock(): ActionExecutionLock {\n  let locked = false;\n\n  return {\n    tryAcquire: () => {\n      if (locked) return false;\n      locked = true;\n      return true;\n    },\n    release: () => {\n      locked = false;\n    },\n  };\n}\n\nexport function useActionButtons(\n  options: UseActionButtonsOptions,\n): UseActionButtonsResult {\n  const { actions, onAction, onBeforeAction, confirmTimeout = 3000 } = options;\n\n  const [confirmingActionId, setConfirmingActionId] = useState<string | null>(\n    null,\n  );\n  const [executingActionId, setExecutingActionId] = useState<string | null>(\n    null,\n  );\n  const executionLockRef = useRef<ActionExecutionLock>(\n    createActionExecutionLock(),\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 (!executionLockRef.current.tryAcquire()) {\n        return;\n      }\n\n      if (onBeforeAction) {\n        const shouldProceed = await onBeforeAction(action.id);\n        if (!shouldProceed) {\n          setConfirmingActionId(null);\n          executionLockRef.current.release();\n          return;\n        }\n      }\n\n      try {\n        setExecutingActionId(action.id);\n        await onAction(action.id);\n      } finally {\n        executionLockRef.current.release();\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 ||\n          (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"
    }
  ]
}
