{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "validated-checkout",
  "type": "registry:component",
  "title": "Validated Checkout",
  "description": "A checkout where each step has a Zod schema; validate() inside a beforeStepChange guard blocks invalid input before the review step.",
  "author": "Stepperize",
  "dependencies": [
    "@stepperize/react",
    "lucide-react",
    "zod"
  ],
  "registryDependencies": [
    "button",
    "input",
    "label"
  ],
  "categories": [
    "commerce"
  ],
  "meta": {
    "capabilities": [
      "validation"
    ],
    "level": "advanced",
    "tags": [
      "checkout",
      "zod",
      "validation",
      "guard",
      "payment"
    ]
  },
  "files": [
    {
      "path": "components/stepperize/validated-checkout.tsx",
      "type": "registry:component",
      "target": "components/stepperize/validated-checkout.tsx",
      "content": "\"use client\";\n\nimport { defineStepper } from \"@stepperize/react\";\nimport { Check, CreditCard, MapPin, Pencil } from \"lucide-react\";\nimport { useState } from \"react\";\nimport { z } from \"zod\";\nimport { Button, buttonVariants } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\n\n// Per-step schemas. `validate()` runs the stored data through these, and the\n// `beforeStepChange` guard turns a failed result into blocked navigation.\nconst shippingSchema = z.object({\n\tname: z.string().min(1, \"Name is required\"),\n\taddress: z.string().min(1, \"Address is required\"),\n\tzip: z.string().regex(/^\\d{5}$/, \"Enter a 5-digit ZIP\"),\n});\n\nconst paymentSchema = z.object({\n\tcard: z.string().regex(/^\\d{16}$/, \"Enter a 16-digit card number\"),\n\tcvc: z.string().regex(/^\\d{3}$/, \"3-digit CVC\"),\n});\n\nconst checkout = defineStepper(\n\t[\n\t\t{ id: \"shipping\", title: \"Shipping\", schema: shippingSchema },\n\t\t{ id: \"payment\", title: \"Payment\", schema: paymentSchema },\n\t\t{ id: \"review\", title: \"Review\" },\n\t\t{ id: \"done\", title: \"Done\" },\n\t] as const,\n\t// Seed empty drafts so validate() reports per-field issues from the start\n\t// (an undefined value would fail at the object root instead of each field).\n\t{\n\t\tdefaultData: {\n\t\t\tshipping: { name: \"\", address: \"\", zip: \"\" },\n\t\t\tpayment: { card: \"\", cvc: \"\" },\n\t\t},\n\t},\n);\n\ntype Errors = Record<string, string>;\n\n/** Read the first issue per field from a failed `validate()` result. */\nfunction toErrors(\n\tissues: ReadonlyArray<{ message: string; path?: ReadonlyArray<unknown> }>,\n): Errors {\n\tconst out: Errors = {};\n\tfor (const issue of issues) {\n\t\tconst seg = issue.path?.[0];\n\t\tconst key =\n\t\t\ttypeof seg === \"object\" && seg !== null\n\t\t\t\t? String((seg as { key: PropertyKey }).key)\n\t\t\t\t: String(seg ?? \"_\");\n\t\tout[key] ??= issue.message;\n\t}\n\treturn out;\n}\n\n/**\n * Validation + guard rejection: each step carries a Zod schema. Pressing\n * Continue runs `ctx.validate()` inside `beforeStepChange`; if it fails, the\n * guard returns `false`, the move is cancelled, and the issues are shown inline.\n * The review step reads every step's data back with `data.all()`.\n */\nexport function ValidatedCheckoutBlock() {\n\tconst [errors, setErrors] = useState<Errors>({});\n\n\treturn (\n\t\t<checkout.Stepper.Root\n\t\t\tlinear\n\t\t\tclassName=\"w-full max-w-md rounded-xl border bg-background p-6 shadow-sm\"\n\t\t\tbeforeStepChange={async ({ direction, validate }) => {\n\t\t\t\t// Only gate forward moves (next()); Back/Edit should never be blocked.\n\t\t\t\tif (direction !== \"next\") {\n\t\t\t\t\tsetErrors({});\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t\t// Validate the step we're leaving against the transition data snapshot.\n\t\t\t\tconst result = await validate();\n\t\t\t\tif (!result.success) {\n\t\t\t\t\tsetErrors(toErrors(result.issues));\n\t\t\t\t\treturn false; // cancel the transition\n\t\t\t\t}\n\t\t\t\tsetErrors({});\n\t\t\t\treturn true;\n\t\t\t}}\n\t\t>\n\t\t\t{({ stepper }) => {\n\t\t\t\treturn (\n\t\t\t\t\t<>\n\t\t\t\t\t\t<Stepperline stepper={stepper} />\n\n\t\t\t\t\t\t<div className=\"mt-6 min-h-44\">\n\t\t\t\t\t\t\t<ShippingStep stepper={stepper} errors={errors} />\n\t\t\t\t\t\t\t<PaymentStep stepper={stepper} errors={errors} />\n\t\t\t\t\t\t\t<ReviewStep stepper={stepper} />\n\t\t\t\t\t\t\t<DoneStep />\n\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t{!stepper.is(\"done\") && (\n\t\t\t\t\t\t\t<checkout.Stepper.Actions className=\"mt-6 flex justify-between\">\n\t\t\t\t\t\t\t\t<checkout.Stepper.Prev className={buttonVariants({ variant: \"outline\" })}>\n\t\t\t\t\t\t\t\t\tBack\n\t\t\t\t\t\t\t\t</checkout.Stepper.Prev>\n\t\t\t\t\t\t\t\t<checkout.Stepper.Next className={buttonVariants()}>\n\t\t\t\t\t\t\t\t\t{stepper.is(\"review\") ? \"Place order\" : \"Continue\"}\n\t\t\t\t\t\t\t\t</checkout.Stepper.Next>\n\t\t\t\t\t\t\t</checkout.Stepper.Actions>\n\t\t\t\t\t\t)}\n\t\t\t\t\t</>\n\t\t\t\t);\n\t\t\t}}\n\t\t</checkout.Stepper.Root>\n\t);\n}\n\ntype Stepper = ReturnType<typeof checkout.useStepper>;\n\nfunction Stepperline({ stepper }: { stepper: Stepper }) {\n\treturn (\n\t\t<checkout.Stepper.List className=\"flex items-center gap-2\">\n\t\t\t<checkout.Stepper.Items>\n\t\t\t\t{(step, index) => (\n\t\t\t\t\t<checkout.Stepper.Item\n\t\t\t\t\t\tkey={step.id}\n\t\t\t\t\t\tstep={step.id}\n\t\t\t\t\t\tclassName=\"flex flex-1 items-center gap-2 last:flex-none\"\n\t\t\t\t\t>\n\t\t\t\t\t\t<checkout.Stepper.Indicator className=\"group grid size-7 shrink-0 place-items-center rounded-full border text-xs font-semibold transition-colors data-[status=active]:border-primary data-[status=active]:bg-primary data-[status=active]:text-primary-foreground data-[status=previous]:border-primary data-[status=previous]:bg-primary/10 data-[status=previous]:text-primary data-[status=upcoming]:border-border data-[status=upcoming]:text-muted-foreground\">\n\t\t\t\t\t\t\t<Check className=\"hidden size-3.5 group-data-[status=previous]:block\" />\n\t\t\t\t\t\t\t<span className=\"group-data-[status=previous]:hidden\">\n\t\t\t\t\t\t\t\t{index + 1}\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</checkout.Stepper.Indicator>\n\t\t\t\t\t\t{index < stepper.count - 1 && (\n\t\t\t\t\t\t\t<span className=\"h-px flex-1 bg-border\" />\n\t\t\t\t\t\t)}\n\t\t\t\t\t</checkout.Stepper.Item>\n\t\t\t\t)}\n\t\t\t</checkout.Stepper.Items>\n\t\t</checkout.Stepper.List>\n\t);\n}\n\nfunction Field({\n\tlabel,\n\tvalue,\n\tonChange,\n\terror,\n\tplaceholder,\n}: {\n\tlabel: string;\n\tvalue: string;\n\tonChange: (v: string) => void;\n\terror?: string;\n\tplaceholder?: string;\n}) {\n\treturn (\n\t\t<div className=\"space-y-1.5\">\n\t\t\t<Label className=\"text-xs text-muted-foreground\">{label}</Label>\n\t\t\t<Input\n\t\t\t\tvalue={value}\n\t\t\t\tplaceholder={placeholder}\n\t\t\t\tonChange={(e) => onChange(e.target.value)}\n\t\t\t\taria-invalid={error ? true : undefined}\n\t\t\t/>\n\t\t\t{error && (\n\t\t\t\t<p className=\"text-xs font-medium text-destructive\">{error}</p>\n\t\t\t)}\n\t\t</div>\n\t);\n}\n\nfunction ShippingStep({\n\tstepper,\n\terrors,\n}: {\n\tstepper: Stepper;\n\terrors: Errors;\n}) {\n\tconst value = stepper.data.get(\"shipping\") ?? {\n\t\tname: \"\",\n\t\taddress: \"\",\n\t\tzip: \"\",\n\t};\n\tconst set = (patch: Partial<typeof value>) =>\n\t\tstepper.data.set(\"shipping\", { ...value, ...patch });\n\n\treturn (\n\t\t<checkout.Stepper.Content step=\"shipping\" className=\"space-y-3\">\n\t\t\t<div className=\"flex items-center gap-2 text-sm font-semibold\">\n\t\t\t\t<MapPin className=\"size-4 text-primary\" /> Where should we ship?\n\t\t\t</div>\n\t\t\t<Field\n\t\t\t\tlabel=\"Full name\"\n\t\t\t\tvalue={value.name}\n\t\t\t\tonChange={(name) => set({ name })}\n\t\t\t\terror={errors.name}\n\t\t\t\tplaceholder=\"Ada Lovelace\"\n\t\t\t/>\n\t\t\t<Field\n\t\t\t\tlabel=\"Address\"\n\t\t\t\tvalue={value.address}\n\t\t\t\tonChange={(address) => set({ address })}\n\t\t\t\terror={errors.address}\n\t\t\t\tplaceholder=\"12 Analytical Ave\"\n\t\t\t/>\n\t\t\t<Field\n\t\t\t\tlabel=\"ZIP code\"\n\t\t\t\tvalue={value.zip}\n\t\t\t\tonChange={(zip) => set({ zip })}\n\t\t\t\terror={errors.zip}\n\t\t\t\tplaceholder=\"90210\"\n\t\t\t/>\n\t\t</checkout.Stepper.Content>\n\t);\n}\n\nfunction PaymentStep({\n\tstepper,\n\terrors,\n}: {\n\tstepper: Stepper;\n\terrors: Errors;\n}) {\n\tconst value = stepper.data.get(\"payment\") ?? { card: \"\", cvc: \"\" };\n\tconst set = (patch: Partial<typeof value>) =>\n\t\tstepper.data.set(\"payment\", { ...value, ...patch });\n\n\treturn (\n\t\t<checkout.Stepper.Content step=\"payment\" className=\"space-y-3\">\n\t\t\t<div className=\"flex items-center gap-2 text-sm font-semibold\">\n\t\t\t\t<CreditCard className=\"size-4 text-primary\" /> Payment details\n\t\t\t</div>\n\t\t\t<Field\n\t\t\t\tlabel=\"Card number\"\n\t\t\t\tvalue={value.card}\n\t\t\t\tonChange={(card) => set({ card })}\n\t\t\t\terror={errors.card}\n\t\t\t\tplaceholder=\"4242424242424242\"\n\t\t\t/>\n\t\t\t<Field\n\t\t\t\tlabel=\"CVC\"\n\t\t\t\tvalue={value.cvc}\n\t\t\t\tonChange={(cvc) => set({ cvc })}\n\t\t\t\terror={errors.cvc}\n\t\t\t\tplaceholder=\"123\"\n\t\t\t/>\n\t\t\t<p className=\"text-xs text-muted-foreground\">\n\t\t\t\tTry “1234” to see the guard block the step.\n\t\t\t</p>\n\t\t</checkout.Stepper.Content>\n\t);\n}\n\nfunction ReviewStep({ stepper }: { stepper: Stepper }) {\n\tconst all = stepper.data.all();\n\treturn (\n\t\t<checkout.Stepper.Content step=\"review\" className=\"space-y-3\">\n\t\t\t<p className=\"text-sm font-semibold\">Review your order</p>\n\t\t\t<Summary title=\"Shipping\" onEdit={() => stepper.goTo(\"shipping\")}>\n\t\t\t\t<p>{all.shipping?.name}</p>\n\t\t\t\t<p className=\"text-muted-foreground\">\n\t\t\t\t\t{all.shipping?.address}, {all.shipping?.zip}\n\t\t\t\t</p>\n\t\t\t</Summary>\n\t\t\t<Summary title=\"Payment\" onEdit={() => stepper.goTo(\"payment\")}>\n\t\t\t\t<p>•••• •••• •••• {all.payment?.card?.slice(-4)}</p>\n\t\t\t</Summary>\n\t\t</checkout.Stepper.Content>\n\t);\n}\n\nfunction Summary({\n\ttitle,\n\tonEdit,\n\tchildren,\n}: {\n\ttitle: string;\n\tonEdit: () => void;\n\tchildren: React.ReactNode;\n}) {\n\treturn (\n\t\t<div className=\"rounded-lg border bg-muted/30 p-3 text-sm\">\n\t\t\t<div className=\"mb-1 flex items-center justify-between\">\n\t\t\t\t<span className=\"text-xs font-semibold uppercase tracking-wide text-muted-foreground\">\n\t\t\t\t\t{title}\n\t\t\t\t</span>\n\t\t\t\t<Button\n\t\t\t\t\tvariant=\"link\"\n\t\t\t\t\tsize=\"xs\"\n\t\t\t\t\tonClick={onEdit}\n\t\t\t\t\tclassName=\"h-auto p-0\"\n\t\t\t\t>\n\t\t\t\t\t<Pencil /> Edit\n\t\t\t\t</Button>\n\t\t\t</div>\n\t\t\t{children}\n\t\t</div>\n\t);\n}\n\nfunction DoneStep() {\n\treturn (\n\t\t<checkout.Stepper.Content\n\t\t\tstep=\"done\"\n\t\t\tclassName=\"grid place-items-center gap-2 py-6 text-center\"\n\t\t>\n\t\t\t<span className=\"grid size-12 place-items-center rounded-full bg-chart-2/15 text-chart-2\">\n\t\t\t\t<Check className=\"size-6\" />\n\t\t\t</span>\n\t\t\t<p className=\"text-sm font-medium\">Order placed</p>\n\t\t\t<p className=\"text-xs text-muted-foreground\">\n\t\t\t\tEvery step passed validation before we got here.\n\t\t\t</p>\n\t\t</checkout.Stepper.Content>\n\t);\n}\n\nexport default ValidatedCheckoutBlock;\n"
    }
  ]
}
