{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "plan",
  "type": "registry:block",
  "title": "Plan",
  "description": "Display step-by-step task workflows in AI interfaces.",
  "dependencies": [
    "lucide-react",
    "zod"
  ],
  "registryDependencies": [
    "accordion",
    "card",
    "collapsible"
  ],
  "files": [
    {
      "path": "components/tool-ui/plan/_adapter.tsx",
      "type": "registry:component",
      "target": "components/tool-ui/plan/_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 *   Accordion   → shadcn/ui Accordion\n *   Card        → shadcn/ui Card\n *   Collapsible → shadcn/ui Collapsible\n */\n\nexport { cn } from \"@/lib/utils\";\nexport {\n  Accordion,\n  AccordionItem,\n  AccordionTrigger,\n  AccordionContent,\n} from \"@/components/ui/accordion\";\nexport {\n  Card,\n  CardHeader,\n  CardTitle,\n  CardDescription,\n  CardContent,\n  CardFooter,\n} from \"@/components/ui/card\";\nexport {\n  Collapsible,\n  CollapsibleTrigger,\n  CollapsibleContent,\n} from \"@/components/ui/collapsible\";\n"
    },
    {
      "path": "components/tool-ui/plan/index.tsx",
      "type": "registry:component",
      "target": "components/tool-ui/plan/index.tsx",
      "content": "export { Plan, PlanCompact } from \"./plan\";\nexport type {\n  PlanProps,\n  PlanTodo,\n  PlanTodoStatus,\n  SerializablePlan,\n} from \"./schema\";\n"
    },
    {
      "path": "components/tool-ui/plan/plan.tsx",
      "type": "registry:component",
      "target": "components/tool-ui/plan/plan.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { useMemo, useState, useEffect, useRef, memo } from \"react\";\nimport { Loader2, Check, X, MoreHorizontal, ChevronRight } from \"lucide-react\";\nimport type { PlanProps, PlanTodo, PlanTodoStatus } from \"./schema\";\nimport {\n  cn,\n  Card,\n  CardHeader,\n  CardTitle,\n  CardDescription,\n  CardContent,\n  Accordion,\n  AccordionItem,\n  AccordionTrigger,\n  AccordionContent,\n  Collapsible,\n  CollapsibleTrigger,\n  CollapsibleContent,\n} from \"./_adapter\";\nimport { calculatePlanProgress, shouldCelebrateProgress } from \"./progress\";\n\nconst INITIAL_VISIBLE_TODO_COUNT = 4;\n\nconst TodoIcon = memo(function TodoIcon({\n  status,\n}: {\n  status: PlanTodoStatus;\n}) {\n  if (status === \"pending\") {\n    return (\n      <span\n        className=\"border-border bg-card flex size-6 shrink-0 items-center justify-center rounded-full border motion-safe:transition-all motion-safe:duration-200\"\n        aria-hidden=\"true\"\n      />\n    );\n  }\n\n  if (status === \"in_progress\") {\n    return (\n      <span\n        className=\"border-border bg-card flex size-6 shrink-0 items-center justify-center rounded-full border shadow-[0_0_0_4px_hsl(var(--primary)/0.1)] motion-safe:transition-all motion-safe:duration-300\"\n        aria-hidden=\"true\"\n      >\n        <Loader2 className=\"text-primary size-5 motion-safe:animate-spin\" />\n      </span>\n    );\n  }\n\n  if (status === \"completed\") {\n    return (\n      <span\n        className=\"border-primary bg-primary flex size-6 shrink-0 items-center justify-center rounded-full border shadow-sm motion-safe:animate-in motion-safe:fade-in motion-safe:zoom-in-75 motion-safe:duration-300 motion-safe:ease-out\"\n        aria-hidden=\"true\"\n      >\n        <Check\n          className=\"text-primary-foreground size-4 motion-safe:animate-in motion-safe:fade-in motion-safe:zoom-in-75 motion-safe:delay-75 motion-safe:duration-200 motion-safe:fill-mode-both\"\n          strokeWidth={3}\n        />\n      </span>\n    );\n  }\n\n  if (status === \"cancelled\") {\n    return (\n      <span\n        className=\"border-destructive bg-destructive flex size-6 shrink-0 items-center justify-center rounded-full border shadow-sm motion-safe:animate-in motion-safe:fade-in motion-safe:zoom-in-75 motion-safe:duration-300 motion-safe:ease-out dark:border-red-600 dark:bg-red-600\"\n        aria-hidden=\"true\"\n      >\n        <X\n          className=\"size-4 text-white motion-safe:animate-in motion-safe:fade-in motion-safe:zoom-in-75 motion-safe:delay-75 motion-safe:duration-200 motion-safe:fill-mode-both\"\n          strokeWidth={3}\n        />\n      </span>\n    );\n  }\n\n  return null;\n});\n\ninterface PlanTodoItemProps {\n  todo: PlanTodo;\n  className?: string;\n  style?: React.CSSProperties;\n  showConnector?: boolean;\n}\n\nfunction areTodoPropsEqual(\n  prev: PlanTodoItemProps,\n  next: PlanTodoItemProps,\n): boolean {\n  if (prev.todo.id !== next.todo.id) return false;\n  if (prev.todo.label !== next.todo.label) return false;\n  if (prev.todo.status !== next.todo.status) return false;\n  if (prev.todo.description !== next.todo.description) return false;\n  if (prev.showConnector !== next.showConnector) return false;\n  if (prev.className !== next.className) return false;\n  const prevStyle = prev.style;\n  const nextStyle = next.style;\n  if (prevStyle === nextStyle) return true;\n  if (!prevStyle || !nextStyle) return false;\n  return (\n    prevStyle.animationDelay === nextStyle.animationDelay &&\n    prevStyle.animationFillMode === nextStyle.animationFillMode\n  );\n}\n\nconst PlanTodoItem = memo(function PlanTodoItem({\n  todo,\n  className,\n  style,\n  showConnector,\n}: PlanTodoItemProps) {\n  const [isOpen, setIsOpen] = React.useState(false);\n\n  const labelElement = (\n    <span\n      className={cn(\n        \"text-sm leading-6 font-medium break-words\",\n        todo.status === \"pending\" && \"text-muted-foreground\",\n        todo.status === \"in_progress\" &&\n          \"motion-safe:shimmer shimmer-invert text-foreground\",\n        (todo.status === \"completed\" || todo.status === \"cancelled\") &&\n          \"text-muted-foreground\",\n      )}\n    >\n      {todo.label}\n    </span>\n  );\n\n  if (!todo.description) {\n    return (\n      <li\n        className={cn(\n          \"relative -mx-2 flex cursor-default items-start gap-3 rounded-md px-2 py-1.5\",\n          className,\n        )}\n        style={style}\n      >\n        {showConnector && (\n          <div\n            className=\"bg-border absolute top-6 left-5 w-px\"\n            style={{\n              height: \"calc(100% + 0.25rem)\",\n            }}\n            aria-hidden=\"true\"\n          />\n        )}\n        <div className=\"relative z-10\">\n          <TodoIcon status={todo.status} />\n        </div>\n        <div className=\"min-w-0 flex-1\">{labelElement}</div>\n      </li>\n    );\n  }\n\n  return (\n    <li\n      className={cn(\n        \"relative -mx-2 min-w-0 cursor-default rounded-md\",\n        className,\n      )}\n      style={style}\n    >\n      {showConnector && (\n        <div\n          className=\"bg-border absolute top-6 left-5 w-px\"\n          style={{\n            height: \"calc(100% + 0.25rem)\",\n          }}\n          aria-hidden=\"true\"\n        />\n      )}\n      <Collapsible asChild open={isOpen} onOpenChange={setIsOpen}>\n        <div\n          className=\"data-[state=open]:bg-primary/5 min-w-0 rounded-md motion-safe:transition-all motion-safe:duration-200\"\n          style={{\n            backdropFilter: isOpen ? \"blur(2px)\" : undefined,\n          }}\n        >\n          <CollapsibleTrigger className=\"group/todo flex w-full cursor-default items-start gap-3 px-2 py-1.5 text-left\">\n            <div className=\"relative z-10\">\n              <TodoIcon status={todo.status} />\n            </div>\n            <span className=\"min-w-0 flex-1\">{labelElement}</span>\n            <ChevronRight className=\"text-muted-foreground/50 group-hover/todo:text-muted-foreground mt-0.5 size-4 shrink-0 rotate-90 group-data-[state=open]/todo:[transform:rotateY(180deg)] motion-safe:transition-transform motion-safe:duration-300 motion-safe:ease-[cubic-bezier(0.34,1.56,0.64,1)]\" />\n          </CollapsibleTrigger>\n          <CollapsibleContent\n            className=\"group/content\"\n            data-slot=\"collapsible-content\"\n          >\n            <div className=\"min-w-0 motion-safe:group-data-[state=closed]/content:animate-out motion-safe:group-data-[state=closed]/content:fade-out motion-safe:group-data-[state=closed]/content:slide-out-to-top-1 motion-safe:group-data-[state=closed]/content:duration-150 motion-safe:group-data-[state=open]/content:animate-in motion-safe:group-data-[state=open]/content:fade-in motion-safe:group-data-[state=open]/content:slide-in-from-top-1 motion-safe:group-data-[state=open]/content:delay-75 motion-safe:group-data-[state=open]/content:duration-150 motion-safe:group-data-[state=open]/content:fill-mode-both\">\n              <p className=\"text-muted-foreground min-w-0 pr-2 pb-1.5 pl-11 text-sm text-pretty break-words\">\n                {todo.description}\n              </p>\n            </div>\n          </CollapsibleContent>\n        </div>\n      </Collapsible>\n    </li>\n  );\n}, areTodoPropsEqual);\n\ninterface TodoListProps {\n  todos: PlanTodo[];\n  newTodoIds: Set<string>;\n}\n\nfunction TodoList({ todos, newTodoIds }: TodoListProps) {\n  return (\n    <>\n      {todos.map((todo, index) => {\n        const isNew = newTodoIds.has(todo.id);\n        const staggerDelay = isNew ? index * 50 : 0;\n\n        return (\n          <PlanTodoItem\n            key={todo.id}\n            todo={todo}\n            showConnector={index < todos.length - 1}\n            className={cn(\n              isNew &&\n                \"motion-safe:animate-in motion-safe:fade-in motion-safe:slide-in-from-bottom-1 motion-safe:duration-300 motion-safe:ease-out\",\n            )}\n            style={\n              isNew\n                ? {\n                    animationDelay: `${staggerDelay}ms`,\n                    animationFillMode: \"backwards\",\n                  }\n                : undefined\n            }\n          />\n        );\n      })}\n    </>\n  );\n}\n\ninterface ProgressBarProps {\n  progress: number;\n  isCelebrating: boolean;\n}\n\nconst ProgressBar = memo(function ProgressBar({\n  progress,\n  isCelebrating,\n}: ProgressBarProps) {\n  return (\n    <div\n      className=\"bg-muted relative mb-3 h-1.5 overflow-hidden rounded-full\"\n      role=\"progressbar\"\n      aria-valuemin={0}\n      aria-valuemax={100}\n      aria-valuenow={progress}\n    >\n      <div\n        className={cn(\n          \"h-full rounded-full transition-all duration-500\",\n          progress === 100\n            ? \"bg-gradient-to-r from-emerald-600 via-emerald-500 to-emerald-400 motion-safe:animate-in motion-safe:fade-in motion-safe:duration-500 motion-safe:ease-out\"\n            : \"bg-primary\",\n        )}\n        style={{\n          width: `${progress}%`,\n          boxShadow:\n            \"inset 0 1px 0 rgba(255,255,255,0.3), 0 1px 2px rgba(0,0,0,0.2)\",\n        }}\n      />\n      {isCelebrating && (\n        <div\n          className=\"pointer-events-none absolute inset-0 rounded-full motion-safe:animate-pulse\"\n          style={{\n            boxShadow: \"0 0 20px rgba(16, 185, 129, 0.6)\",\n          }}\n        />\n      )}\n    </div>\n  );\n});\n\nfunction PlanRoot({\n  id,\n  title,\n  description,\n  todos,\n  maxVisibleTodos = INITIAL_VISIBLE_TODO_COUNT,\n  className,\n  compact = false,\n}: PlanProps & { compact?: boolean }) {\n  const seenTodoIds = useRef(new Set<string>());\n  const [newTodoIds, setNewTodoIds] = useState<Set<string>>(new Set());\n  const [isCelebrating, setIsCelebrating] = useState(false);\n  const prevProgressRef = useRef(0);\n\n  const { visibleTodos, hiddenTodos, completedCount, allComplete, progress } =\n    useMemo(() => {\n      const completed = todos.filter((t) => t.status === \"completed\").length;\n      return {\n        visibleTodos: todos.slice(0, maxVisibleTodos),\n        hiddenTodos: todos.slice(maxVisibleTodos),\n        completedCount: completed,\n        allComplete: completed === todos.length,\n        progress: calculatePlanProgress({\n          completedCount: completed,\n          totalCount: todos.length,\n        }),\n      };\n    }, [todos, maxVisibleTodos]);\n\n  useEffect(() => {\n    const newIds = new Set<string>();\n\n    todos.forEach((todo) => {\n      if (!seenTodoIds.current.has(todo.id)) {\n        newIds.add(todo.id);\n        seenTodoIds.current.add(todo.id);\n      }\n    });\n\n    if (newIds.size > 0) {\n      setNewTodoIds(newIds);\n\n      // Clear animation class after entrance completes\n      const timer = setTimeout(() => {\n        setNewTodoIds(new Set());\n      }, 500);\n\n      return () => clearTimeout(timer);\n    }\n  }, [todos]);\n\n  useEffect(() => {\n    const shouldCelebrate = shouldCelebrateProgress({\n      previous: prevProgressRef.current,\n      next: progress,\n    });\n    prevProgressRef.current = progress;\n\n    if (shouldCelebrate) {\n      setIsCelebrating(true);\n      const timer = setTimeout(() => setIsCelebrating(false), 1000);\n      return () => clearTimeout(timer);\n    }\n  }, [progress]);\n\n  const todoList = (\n    <ul className={cn(\"min-w-0 space-y-1\", compact ? \"mt-0\" : \"mt-4\")}>\n      <TodoList todos={visibleTodos} newTodoIds={newTodoIds} />\n\n      {hiddenTodos.length > 0 && (\n        <li className=\"mt-1\">\n          <Accordion type=\"single\" collapsible>\n            <AccordionItem value=\"more\" className=\"border-0\">\n              <AccordionTrigger className=\"text-muted-foreground hover:text-primary flex cursor-default items-start justify-start gap-2 py-1 text-sm font-normal [&>svg:last-child]:hidden\">\n                <MoreHorizontal className=\"text-muted-foreground/70 mt-0.5 size-4 shrink-0\" />\n                <span>{hiddenTodos.length} more</span>\n              </AccordionTrigger>\n              <AccordionContent className=\"pt-2 pb-0\">\n                <ul className=\"-mx-2 space-y-2 px-2\">\n                  <TodoList todos={hiddenTodos} newTodoIds={newTodoIds} />\n                </ul>\n              </AccordionContent>\n            </AccordionItem>\n          </Accordion>\n        </li>\n      )}\n    </ul>\n  );\n\n  return (\n    <Card\n      className={cn(\"isolate w-full max-w-xl min-w-80 gap-4 py-4\", className)}\n      data-tool-ui-id={id}\n      data-slot=\"plan\"\n    >\n      {!compact && (\n        <CardHeader className=\"flex flex-row items-start justify-between gap-4\">\n          <div className=\"space-y-1.5\">\n            <CardTitle className=\"leading-5 font-medium text-pretty\">\n              {title}\n            </CardTitle>\n            {description && <CardDescription>{description}</CardDescription>}\n          </div>\n          {allComplete && (\n            <Check className=\"mt-0.5 size-5 shrink-0 text-emerald-500\" />\n          )}\n        </CardHeader>\n      )}\n\n      <CardContent className=\"min-w-0 px-4\">\n        <div\n          className={cn(\n            \"min-w-0\",\n            !compact && \"bg-muted/70 rounded-lg px-6 py-4\",\n          )}\n        >\n          {!compact && (\n            <>\n              <div className=\"text-muted-foreground mb-2 text-sm\">\n                {completedCount} of {todos.length} complete\n              </div>\n              <ProgressBar progress={progress} isCelebrating={isCelebrating} />\n            </>\n          )}\n          {todoList}\n        </div>\n      </CardContent>\n    </Card>\n  );\n}\n\nfunction PlanComponent(props: PlanProps) {\n  return <PlanRoot key={props.id} {...props} />;\n}\n\nexport function PlanCompact(props: PlanProps) {\n  return <PlanRoot key={props.id} {...props} compact />;\n}\n\ntype PlanComponentType = typeof PlanComponent & {\n  Compact: typeof PlanCompact;\n};\n\nexport const Plan = Object.assign(PlanComponent, {\n  Compact: PlanCompact,\n}) as PlanComponentType;\n"
    },
    {
      "path": "components/tool-ui/plan/progress.ts",
      "type": "registry:lib",
      "target": "components/tool-ui/plan/progress.ts",
      "content": "type ProgressInput = {\n  completedCount: number;\n  totalCount: number;\n};\n\ntype CelebrateProgressInput = {\n  previous: number;\n  next: number;\n};\n\nfunction clampProgress(value: number): number {\n  if (!Number.isFinite(value)) return 0;\n  return Math.max(0, Math.min(100, value));\n}\n\nexport function calculatePlanProgress({\n  completedCount,\n  totalCount,\n}: ProgressInput): number {\n  if (totalCount <= 0) return 0;\n  return clampProgress((completedCount / totalCount) * 100);\n}\n\nexport function shouldCelebrateProgress({\n  previous,\n  next,\n}: CelebrateProgressInput): boolean {\n  return previous < 100 && next === 100;\n}\n"
    },
    {
      "path": "components/tool-ui/plan/README.md",
      "type": "registry:file",
      "target": "components/tool-ui/plan/README.md",
      "content": "# Plan\n\nImplementation for the \"plan\" Tool UI surface.\n\n## Files\n\n- public exports: components/tool-ui/plan/index.tsx\n- serializable schema + parse helpers: components/tool-ui/plan/schema.ts\n\n## Companion assets\n\n- Docs page: app/docs/plan/content.mdx\n- Preset payload: lib/presets/plan.ts\n\n## Quick check\n\nRun this after edits:\n\npnpm test\n"
    },
    {
      "path": "components/tool-ui/plan/schema.ts",
      "type": "registry:lib",
      "target": "components/tool-ui/plan/schema.ts",
      "content": "import { z } from \"zod\";\nimport {\n  ToolUIIdSchema,\n  ToolUIReceiptSchema,\n  ToolUIRoleSchema,\n} from \"../shared/schema\";\nimport { defineToolUiContract } from \"../shared/contract\";\n\nexport const PlanTodoStatusSchema = z.enum([\n  \"pending\",\n  \"in_progress\",\n  \"completed\",\n  \"cancelled\",\n]);\n\nexport const PlanTodoSchema = z.object({\n  id: z.string().min(1),\n  label: z.string().min(1),\n  status: PlanTodoStatusSchema,\n  description: z.string().optional(),\n});\n\nexport type PlanTodoStatus = z.infer<typeof PlanTodoStatusSchema>;\nexport type PlanTodo = z.infer<typeof PlanTodoSchema>;\n\nexport const PlanPropsSchema = z\n  .object({\n    id: ToolUIIdSchema,\n    role: ToolUIRoleSchema.optional(),\n    receipt: ToolUIReceiptSchema.optional(),\n    title: z.string().min(1),\n    description: z.string().optional(),\n    todos: z.array(PlanTodoSchema).min(1),\n    maxVisibleTodos: z.number().finite().int().min(1).optional(),\n  })\n  .superRefine((value, ctx) => {\n    const seenTodoIds = new Set<string>();\n    value.todos.forEach((todo, index) => {\n      if (seenTodoIds.has(todo.id)) {\n        ctx.addIssue({\n          code: \"custom\",\n          path: [\"todos\", index, \"id\"],\n          message: `Duplicate todo id \"${todo.id}\".`,\n        });\n        return;\n      }\n      seenTodoIds.add(todo.id);\n    });\n  });\n\nexport type PlanProps = z.infer<typeof PlanPropsSchema> & {\n  className?: string;\n};\n\nexport const SerializablePlanSchema = PlanPropsSchema;\n\nexport type SerializablePlan = z.infer<typeof SerializablePlanSchema>;\n\nconst SerializablePlanSchemaContract = defineToolUiContract(\n  \"Plan\",\n  SerializablePlanSchema,\n);\n\nexport const parseSerializablePlan: (input: unknown) => SerializablePlan =\n  SerializablePlanSchemaContract.parse;\n\nexport const safeParseSerializablePlan: (\n  input: unknown,\n) => SerializablePlan | null = SerializablePlanSchemaContract.safeParse;\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"
    }
  ]
}
