{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "order-summary",
  "type": "registry:block",
  "title": "Order Summary",
  "description": "Itemized purchase confirmation with pricing.",
  "dependencies": [
    "lucide-react",
    "zod"
  ],
  "registryDependencies": [
    "button",
    "separator",
    "skeleton"
  ],
  "files": [
    {
      "path": "components/tool-ui/order-summary/_adapter.tsx",
      "type": "registry:component",
      "target": "components/tool-ui/order-summary/_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 *   Separator → shadcn/ui Separator\n *   Skeleton  → shadcn/ui Skeleton\n */\n\nexport { cn } from \"@/lib/utils\";\nexport { Button } from \"@/components/ui/button\";\nexport { Separator } from \"@/components/ui/separator\";\nexport { Skeleton } from \"@/components/ui/skeleton\";\n"
    },
    {
      "path": "components/tool-ui/order-summary/index.tsx",
      "type": "registry:component",
      "target": "components/tool-ui/order-summary/index.tsx",
      "content": "export { OrderSummary } from \"./order-summary\";\nexport type {\n  OrderSummaryDisplayProps,\n  OrderSummaryReceiptProps,\n  OrderSummaryCompoundComponent,\n} from \"./order-summary\";\nexport {\n  type SerializableOrderSummary,\n  type OrderSummaryProps,\n  type OrderSummaryVariant,\n  type OrderItem,\n  type Pricing,\n  type OrderDecision,\n} from \"./schema\";\n"
    },
    {
      "path": "components/tool-ui/order-summary/order-summary.tsx",
      "type": "registry:component",
      "target": "components/tool-ui/order-summary/order-summary.tsx",
      "content": "import { CheckCircle, Package } from \"lucide-react\";\nimport type { ReactElement } from \"react\";\nimport { cn, Separator } from \"./_adapter\";\nimport type {\n  OrderSummaryProps,\n  OrderItem,\n  Pricing,\n  OrderDecision,\n  OrderSummaryVariant,\n} from \"./schema\";\n\nfunction formatCurrency(amount: number, currency: string): string {\n  try {\n    return new Intl.NumberFormat(undefined, {\n      style: \"currency\",\n      currency,\n    }).format(amount);\n  } catch {\n    return `${currency} ${amount.toFixed(2)}`;\n  }\n}\n\nfunction formatQuantity(quantity: number): string {\n  return quantity === 1 ? \"\" : `Qty: ${quantity}`;\n}\n\nfunction ItemImage({ src, alt }: { src?: string; alt: string }) {\n  if (!src) {\n    return (\n      <div className=\"bg-muted flex h-12 w-12 shrink-0 items-center justify-center rounded-md\">\n        <Package\n          aria-hidden=\"true\"\n          focusable=\"false\"\n          className=\"text-muted-foreground h-5 w-5\"\n        />\n      </div>\n    );\n  }\n\n  return (\n    <img\n      src={src}\n      alt={alt}\n      width={48}\n      height={48}\n      className=\"h-12 w-12 shrink-0 rounded-md object-cover\"\n    />\n  );\n}\n\nfunction OrderItemRow({\n  item,\n  currency,\n}: {\n  item: OrderItem;\n  currency: string;\n}) {\n  const quantity = item.quantity ?? 1;\n  const quantityText = formatQuantity(quantity);\n  const hasDescription = item.description || quantityText;\n  const lineTotal = item.unitPrice * quantity;\n\n  return (\n    <div className=\"flex gap-3\">\n      <ItemImage src={item.imageUrl} alt={item.name} />\n      <div className=\"flex min-w-0 flex-1 items-center justify-between\">\n        <div className=\"flex min-w-0 flex-1 flex-col gap-0.5\">\n          <div className=\"flex items-center justify-between\">\n            <span className=\"truncate text-sm font-medium\">{item.name}</span>\n            <span className=\"truncate text-sm tabular-nums\">\n              {formatCurrency(lineTotal, currency)}\n            </span>\n          </div>\n          {hasDescription && (\n            <div className=\"text-muted-foreground truncate text-sm\">\n              {[item.description, quantityText].filter(Boolean).join(\" · \")}\n            </div>\n          )}\n        </div>\n      </div>\n    </div>\n  );\n}\n\nfunction PricingBreakdown({\n  pricing,\n  className,\n}: {\n  pricing: Pricing;\n  className?: string;\n}) {\n  const currency = pricing.currency ?? \"USD\";\n\n  return (\n    <dl className={cn(\"flex flex-col gap-2 text-sm\", className)}>\n      <div className=\"flex justify-between gap-4\">\n        <dt className=\"text-muted-foreground\">Subtotal</dt>\n        <dd className=\"tabular-nums\">\n          {formatCurrency(pricing.subtotal, currency)}\n        </dd>\n      </div>\n\n      {pricing.discount !== undefined && pricing.discount > 0 && (\n        <div className=\"flex justify-between gap-4 text-green-600 dark:text-green-500\">\n          <dt>{pricing.discountLabel || \"Discount\"}</dt>\n          <dd className=\"tabular-nums\">\n            -{formatCurrency(pricing.discount, currency)}\n          </dd>\n        </div>\n      )}\n\n      {pricing.shipping !== undefined && (\n        <div className=\"flex justify-between gap-4\">\n          <dt className=\"text-muted-foreground\">Shipping</dt>\n          <dd className=\"tabular-nums\">\n            {pricing.shipping === 0\n              ? \"Free\"\n              : formatCurrency(pricing.shipping, currency)}\n          </dd>\n        </div>\n      )}\n\n      {pricing.tax !== undefined && (\n        <div className=\"flex justify-between gap-4\">\n          <dt className=\"text-muted-foreground\">{pricing.taxLabel || \"Tax\"}</dt>\n          <dd className=\"tabular-nums\">\n            {formatCurrency(pricing.tax, currency)}\n          </dd>\n        </div>\n      )}\n\n      <div className=\"flex justify-between gap-4\">\n        <dt className=\"font-medium\">Total</dt>\n        <dd className=\"font-semibold tabular-nums\">\n          {formatCurrency(pricing.total, currency)}\n        </dd>\n      </div>\n    </dl>\n  );\n}\n\nfunction formatDate(isoString: string): string | undefined {\n  try {\n    const date = new Date(isoString);\n    if (isNaN(date.getTime())) return undefined;\n    return date.toLocaleDateString(undefined, {\n      month: \"short\",\n      day: \"numeric\",\n      year: \"numeric\",\n    });\n  } catch {\n    return undefined;\n  }\n}\n\nfunction ReceiptBadge({\n  orderId,\n  confirmedAt,\n}: {\n  orderId?: string;\n  confirmedAt?: string;\n}) {\n  const formattedDate = confirmedAt ? formatDate(confirmedAt) : undefined;\n\n  const parts = [orderId && `#${orderId}`, formattedDate].filter(Boolean);\n  if (parts.length === 0) return null;\n\n  return (\n    <p className=\"text-muted-foreground mt-1 text-sm\">{parts.join(\" · \")}</p>\n  );\n}\n\nfunction OrderSummaryRoot({\n  id,\n  title = \"Order Summary\",\n  variant,\n  items,\n  pricing,\n  choice,\n  className,\n}: OrderSummaryProps) {\n  const titleId = `${id}-title`;\n  const resolvedVariant: OrderSummaryVariant =\n    variant ?? (choice === undefined ? \"summary\" : \"receipt\");\n  const isReceipt = resolvedVariant === \"receipt\";\n  const isMalformedPayload =\n    !Array.isArray(items) ||\n    items.length === 0 ||\n    pricing == null ||\n    (isReceipt && choice === undefined);\n\n  if (isMalformedPayload) {\n    return (\n      <article\n        data-slot=\"order-summary\"\n        data-tool-ui-id={id}\n        aria-labelledby={titleId}\n        className={cn(\"flex max-w-md min-w-80 flex-col gap-3\", className)}\n      >\n        <div className=\"text-card-foreground rounded-lg border bg-card p-4 shadow-sm\">\n          <h2 id={titleId} className=\"text-base font-semibold\">\n            {title}\n          </h2>\n          <p className=\"text-muted-foreground mt-2 text-sm\">\n            Unable to render order summary\n          </p>\n        </div>\n      </article>\n    );\n  }\n\n  return (\n    <article\n      data-slot=\"order-summary\"\n      data-tool-ui-id={id}\n      aria-labelledby={titleId}\n      className={cn(\"flex max-w-md min-w-80 flex-col gap-3\", className)}\n    >\n      <div\n        className={cn(\n          \"text-card-foreground rounded-lg border shadow-sm\",\n          isReceipt ? \"bg-card/60\" : \"bg-card\",\n        )}\n      >\n        <div className={cn(\"space-y-4 p-4\", isReceipt && \"opacity-95\")}>\n          <div>\n            <h2\n              id={titleId}\n              className=\"flex items-center gap-2 text-base font-semibold\"\n            >\n              {isReceipt && (\n                <CheckCircle\n                  aria-hidden=\"true\"\n                  focusable=\"false\"\n                  className=\"h-5 w-5 text-green-600 dark:text-green-500\"\n                />\n              )}\n              {title}\n            </h2>\n            {isReceipt && choice && (\n              <ReceiptBadge\n                orderId={choice.orderId}\n                confirmedAt={choice.confirmedAt}\n              />\n            )}\n          </div>\n\n          <div className=\"space-y-3\">\n            {items.map((item) => (\n              <OrderItemRow\n                key={item.id}\n                item={item}\n                currency={pricing.currency ?? \"USD\"}\n              />\n            ))}\n          </div>\n\n          <Separator />\n\n          <PricingBreakdown pricing={pricing} />\n        </div>\n      </div>\n    </article>\n  );\n}\n\nexport type OrderSummaryDisplayProps = OrderSummaryProps;\n\nfunction OrderSummaryDisplay(props: OrderSummaryDisplayProps) {\n  return <OrderSummaryRoot {...props} variant=\"summary\" />;\n}\n\nexport interface OrderSummaryReceiptProps extends Omit<\n  OrderSummaryProps,\n  \"choice\"\n> {\n  choice: OrderDecision;\n}\n\nfunction OrderSummaryReceipt(props: OrderSummaryReceiptProps) {\n  return <OrderSummaryRoot {...props} variant=\"receipt\" />;\n}\n\nexport interface OrderSummaryCompoundComponent {\n  (props: OrderSummaryProps): ReactElement;\n  Display: (props: OrderSummaryDisplayProps) => ReactElement;\n  Receipt: (props: OrderSummaryReceiptProps) => ReactElement;\n}\n\nexport const OrderSummary: OrderSummaryCompoundComponent = Object.assign(\n  OrderSummaryRoot,\n  {\n    Display: OrderSummaryDisplay,\n    Receipt: OrderSummaryReceipt,\n  },\n);\n"
    },
    {
      "path": "components/tool-ui/order-summary/README.md",
      "type": "registry:file",
      "target": "components/tool-ui/order-summary/README.md",
      "content": "# Order Summary\n\nImplementation for the \"order-summary\" Tool UI surface.\n\n## Files\n\n- public exports: components/tool-ui/order-summary/index.tsx\n- serializable schema + parse helpers: components/tool-ui/order-summary/schema.ts\n\n## Companion assets\n\n- Docs page: app/docs/order-summary/content.mdx\n- Preset payload: lib/presets/order-summary.ts\n\n## Quick check\n\nRun this after edits:\n\npnpm test\n"
    },
    {
      "path": "components/tool-ui/order-summary/schema.ts",
      "type": "registry:lib",
      "target": "components/tool-ui/order-summary/schema.ts",
      "content": "import { z } from \"zod\";\nimport { defineToolUiContract } from \"../shared/contract\";\nimport { ToolUIIdSchema, ToolUIRoleSchema } from \"../shared/schema\";\n\nexport const OrderItemSchema = z.object({\n  id: z.string(),\n  name: z.string(),\n  description: z.string().optional(),\n  imageUrl: z.string().url().optional(),\n  quantity: z.number().int().positive().optional(),\n  unitPrice: z.number(),\n});\n\nexport type OrderItem = z.infer<typeof OrderItemSchema>;\n\nconst OrderItemsSchema = z\n  .array(OrderItemSchema)\n  .min(1)\n  .superRefine((items, ctx) => {\n    const seenIds = new Set<string>();\n\n    for (const [index, item] of items.entries()) {\n      if (seenIds.has(item.id)) {\n        ctx.addIssue({\n          code: z.ZodIssueCode.custom,\n          message: `Duplicate item id: \"${item.id}\"`,\n          path: [index, \"id\"],\n        });\n      }\n\n      seenIds.add(item.id);\n    }\n  });\n\nexport const PricingSchema = z.object({\n  subtotal: z.number(),\n  tax: z.number().optional(),\n  taxLabel: z.string().optional(),\n  shipping: z.number().optional(),\n  discount: z.number().nonnegative().optional(),\n  discountLabel: z.string().optional(),\n  total: z.number(),\n  currency: z.string().optional(),\n});\n\nexport type Pricing = z.infer<typeof PricingSchema>;\n\nexport const OrderSummaryVariantSchema = z.enum([\"summary\", \"receipt\"]);\nexport type OrderSummaryVariant = z.infer<typeof OrderSummaryVariantSchema>;\n\nexport const OrderDecisionSchema = z.object({\n  action: z.literal(\"confirm\"),\n  orderId: z.string().optional(),\n  confirmedAt: z.string().datetime().optional(),\n});\n\nexport type OrderDecision = z.infer<typeof OrderDecisionSchema>;\n\nexport const SerializableOrderSummarySchema = z\n  .object({\n    id: ToolUIIdSchema,\n    role: ToolUIRoleSchema.optional(),\n    title: z.string().optional(),\n    variant: OrderSummaryVariantSchema.optional(),\n    items: OrderItemsSchema,\n    pricing: PricingSchema,\n    choice: OrderDecisionSchema.optional(),\n  })\n  .strict()\n  .superRefine((value, ctx) => {\n    if (value.variant === \"receipt\" && value.choice === undefined) {\n      ctx.addIssue({\n        code: z.ZodIssueCode.custom,\n        message: 'Receipt variant requires \"choice\".',\n        path: [\"choice\"],\n      });\n    }\n\n    if (value.variant === \"summary\" && value.choice !== undefined) {\n      ctx.addIssue({\n        code: z.ZodIssueCode.custom,\n        message: 'Summary variant cannot include \"choice\".',\n        path: [\"choice\"],\n      });\n    }\n  });\n\nexport type SerializableOrderSummary = z.infer<\n  typeof SerializableOrderSummarySchema\n>;\n\nconst SerializableOrderSummarySchemaContract = defineToolUiContract(\n  \"OrderSummary\",\n  SerializableOrderSummarySchema,\n);\n\nexport const parseSerializableOrderSummary: (\n  input: unknown,\n) => SerializableOrderSummary = SerializableOrderSummarySchemaContract.parse;\n\nexport const safeParseSerializableOrderSummary: (\n  input: unknown,\n) => SerializableOrderSummary | null =\n  SerializableOrderSummarySchemaContract.safeParse;\n\nexport interface OrderSummaryProps extends SerializableOrderSummary {\n  className?: string;\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"
    }
  ]
}
