{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "user-onboarding",
  "type": "registry:component",
  "title": "User Onboarding",
  "description": "A three-step account onboarding flow with per-step validation.",
  "author": "Stepperize",
  "dependencies": [
    "@stepperize/react",
    "lucide-react",
    "zod"
  ],
  "registryDependencies": [
    "button",
    "input",
    "label",
    "switch"
  ],
  "categories": [
    "onboarding"
  ],
  "meta": {
    "capabilities": [
      "validation"
    ],
    "level": "beginner",
    "tags": [
      "onboarding",
      "signup",
      "account",
      "validation"
    ]
  },
  "files": [
    {
      "path": "components/stepperize/user-onboarding.tsx",
      "type": "registry:component",
      "target": "components/stepperize/user-onboarding.tsx",
      "content": "\"use client\";\n\nimport { defineStepper } from \"@stepperize/react\";\nimport { Check } from \"lucide-react\";\nimport { useState } from \"react\";\nimport { z } from \"zod\";\nimport { buttonVariants } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { Switch } from \"@/components/ui/switch\";\n\n// The account step owns data, so it carries a schema. `validate()` runs the\n// stored values through it and the `beforeStepChange` guard blocks the move when\n// it fails.\nconst accountSchema = z.object({\n\tname: z.string().min(1, \"Name is required\"),\n\temail: z.string().email(\"Enter a valid email\"),\n});\n\nconst onboarding = defineStepper(\n\t[\n\t\t{\n\t\t\tid: \"account\",\n\t\t\ttitle: \"Account\",\n\t\t\tdescription: \"Your details\",\n\t\t\tschema: accountSchema,\n\t\t},\n\t\t{ id: \"preferences\", title: \"Preferences\", description: \"Make it yours\" },\n\t\t{ id: \"confirm\", title: \"Confirm\", description: \"Review & finish\" },\n\t] as const,\n\t{ defaultData: { account: { name: \"\", email: \"\" } } },\n);\n\nconst { Stepper } = onboarding;\n\ntype Errors = Record<string, string>;\n\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\nexport function UserOnboardingBlock() {\n\tconst [errors, setErrors] = useState<Errors>({});\n\tconst [created, setCreated] = useState(false);\n\n\treturn (\n\t\t<Stepper.Root\n\t\t\tclassName=\"w-full max-w-lg rounded-xl border bg-background p-6 shadow-sm\"\n\t\t\tlinear\n\t\t\tbeforeStepChange={async ({ direction, validate }) => {\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\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;\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\t<>\n\t\t\t\t\t<Stepper.List className=\"flex w-full\">\n\t\t\t\t\t\t<Stepper.Items>\n\t\t\t\t\t\t\t{(step, index) => (\n\t\t\t\t\t\t\t\t<Stepper.Item\n\t\t\t\t\t\t\t\t\tkey={step.id}\n\t\t\t\t\t\t\t\t\tstep={step.id}\n\t\t\t\t\t\t\t\t\tclassName=\"relative flex flex-1 justify-center\"\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t{index < stepper.count - 1 && (\n\t\t\t\t\t\t\t\t\t\t<Stepper.Separator className=\"absolute left-[calc(50%+1.125rem)] right-[calc(-50%+1.125rem)] top-[1.125rem] h-px bg-border\" />\n\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t<Stepper.Trigger className=\"relative z-10 flex max-w-32 flex-col items-center gap-2 text-center disabled:cursor-not-allowed\">\n\t\t\t\t\t\t\t\t\t\t<Stepper.Indicator className=\"group grid size-9 shrink-0 place-items-center rounded-full border bg-background text-sm 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 data-[status=previous]:text-primary-foreground data-[status=upcoming]:border-border data-[status=upcoming]:text-muted-foreground\">\n\t\t\t\t\t\t\t\t\t\t\t<span className=\"group-data-[status=previous]:hidden\">\n\t\t\t\t\t\t\t\t\t\t\t\t{index + 1}\n\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t\t<Check className=\"hidden size-4 group-data-[status=previous]:block\" />\n\t\t\t\t\t\t\t\t\t\t</Stepper.Indicator>\n\t\t\t\t\t\t\t\t\t\t<span className=\"hidden min-w-0 sm:block\">\n\t\t\t\t\t\t\t\t\t\t\t<Stepper.Title className=\"block truncate text-sm font-medium leading-none\" />\n\t\t\t\t\t\t\t\t\t\t\t<Stepper.Description className=\"mt-1 block truncate text-xs text-muted-foreground\" />\n\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t</Stepper.Trigger>\n\t\t\t\t\t\t\t\t</Stepper.Item>\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t</Stepper.Items>\n\t\t\t\t\t</Stepper.List>\n\n\t\t\t\t\t<div className=\"mt-6 min-h-36\">\n\t\t\t\t\t\t<Stepper.Content step=\"account\" className=\"space-y-3\">\n\t\t\t\t\t\t\t<AccountFields errors={errors} />\n\t\t\t\t\t\t</Stepper.Content>\n\n\t\t\t\t\t\t<Stepper.Content step=\"preferences\" className=\"space-y-3\">\n\t\t\t\t\t\t\t<Toggle label=\"Product updates\" defaultChecked />\n\t\t\t\t\t\t\t<Toggle label=\"Weekly digest\" />\n\t\t\t\t\t\t\t<Toggle label=\"Beta features\" defaultChecked />\n\t\t\t\t\t\t</Stepper.Content>\n\n\t\t\t\t\t\t<Stepper.Content step=\"confirm\">\n\t\t\t\t\t\t\t<div className=\"rounded-lg border bg-muted/40 p-4 text-sm\">\n\t\t\t\t\t\t\t\t<p className=\"font-medium\">\n\t\t\t\t\t\t\t\t\t{created ? \"Account created\" : \"You're all set 🎉\"}\n\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t<p className=\"mt-1 text-muted-foreground\">\n\t\t\t\t\t\t\t\t\t{created\n\t\t\t\t\t\t\t\t\t\t? \"Your account is ready and preferences were saved.\"\n\t\t\t\t\t\t\t\t\t\t: \"Review your details and create your account.\"}\n\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</Stepper.Content>\n\t\t\t\t\t</div>\n\n\t\t\t\t\t<Stepper.Actions className=\"mt-6 flex justify-between\">\n\t\t\t\t\t\t<Stepper.Prev className={buttonVariants({ variant: \"outline\" })}>\n\t\t\t\t\t\t\tBack\n\t\t\t\t\t\t</Stepper.Prev>\n\t\t\t\t\t\t{created ? (\n\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\tonClick={() => {\n\t\t\t\t\t\t\t\t\tsetCreated(false);\n\t\t\t\t\t\t\t\t\tsetErrors({});\n\t\t\t\t\t\t\t\t\tstepper.data.reset();\n\t\t\t\t\t\t\t\t\tstepper.reset();\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\tclassName={buttonVariants()}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\tStart over\n\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t) : stepper.isLast ? (\n\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\tonClick={() => setCreated(true)}\n\t\t\t\t\t\t\t\tclassName={buttonVariants()}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\tCreate account\n\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t<Stepper.Next className={buttonVariants()}>Continue</Stepper.Next>\n\t\t\t\t\t\t)}\n\t\t\t\t\t</Stepper.Actions>\n\t\t\t\t</>\n\t\t\t)}\n\t\t</Stepper.Root>\n\t);\n}\n\n// Controlled fields write to `stepper.data` so `validate()` can read them.\nfunction AccountFields({ errors }: { errors: Errors }) {\n\tconst stepper = onboarding.useStepper();\n\tconst account = stepper.data.get(\"account\") ?? { name: \"\", email: \"\" };\n\tconst set = (patch: Partial<typeof account>) =>\n\t\tstepper.data.set(\"account\", { ...account, ...patch });\n\n\treturn (\n\t\t<>\n\t\t\t<Field\n\t\t\t\tlabel=\"Full name\"\n\t\t\t\tplaceholder=\"Ada Lovelace\"\n\t\t\t\tvalue={account.name}\n\t\t\t\terror={errors.name}\n\t\t\t\tonChange={(event) => set({ name: event.target.value })}\n\t\t\t/>\n\t\t\t<Field\n\t\t\t\tlabel=\"Email\"\n\t\t\t\ttype=\"email\"\n\t\t\t\tplaceholder=\"ada@example.com\"\n\t\t\t\tvalue={account.email}\n\t\t\t\terror={errors.email}\n\t\t\t\tonChange={(event) => set({ email: event.target.value })}\n\t\t\t/>\n\t\t</>\n\t);\n}\n\nfunction Field({\n\tlabel,\n\terror,\n\t...props\n}: { label: string; error?: string } & React.ComponentProps<typeof Input>) {\n\treturn (\n\t\t<div className=\"space-y-1.5\">\n\t\t\t<Label>{label}</Label>\n\t\t\t<Input aria-invalid={error ? true : undefined} {...props} />\n\t\t\t{error && <p className=\"text-xs text-destructive\">{error}</p>}\n\t\t</div>\n\t);\n}\n\nfunction Toggle({\n\tlabel,\n\tdefaultChecked,\n}: {\n\tlabel: string;\n\tdefaultChecked?: boolean;\n}) {\n\treturn (\n\t\t<Label className=\"flex items-center justify-between rounded-lg border px-3 py-2.5 text-sm font-normal\">\n\t\t\t<span className=\"font-medium\">{label}</span>\n\t\t\t<Switch defaultChecked={defaultChecked} />\n\t\t</Label>\n\t);\n}\n\nexport default UserOnboardingBlock;\n"
    }
  ]
}
