{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "save-resume",
  "type": "registry:component",
  "title": "Save & Resume",
  "description": "A controlled wizard whose active step is synced to localStorage and the URL hash.",
  "author": "Stepperize",
  "dependencies": [
    "@stepperize/react",
    "lucide-react"
  ],
  "registryDependencies": [
    "alert",
    "button",
    "input"
  ],
  "categories": [
    "flow-control"
  ],
  "meta": {
    "capabilities": [
      "persistence"
    ],
    "level": "advanced",
    "tags": [
      "persistence",
      "controlled",
      "localstorage",
      "url",
      "resume"
    ]
  },
  "files": [
    {
      "path": "components/stepperize/save-resume.tsx",
      "type": "registry:component",
      "target": "components/stepperize/save-resume.tsx",
      "content": "\"use client\";\n\nimport { defineStepper } from \"@stepperize/react\";\nimport { Check, RotateCcw } from \"lucide-react\";\nimport { useEffect, useState } from \"react\";\nimport { Alert, AlertDescription } from \"@/components/ui/alert\";\nimport { Button, buttonVariants } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\n\nconst wizard = defineStepper([\n\t{ id: \"workspace\", title: \"Workspace\" },\n\t{ id: \"details\", title: \"Details\" },\n\t{ id: \"review\", title: \"Review\" },\n\t{ id: \"done\", title: \"Done\" },\n] as const);\n\nconst { Stepper, useStepper } = wizard;\n\ntype StepId = (typeof wizard.steps)[number][\"id\"];\nconst STEP_IDS = wizard.steps.map((s) => s.id) as StepId[];\n\nconst KEY = \"stepperize:save-resume\";\n\ntype Saved = { step: StepId; name: string };\n\n// localStorage + URL are the source of truth for *where the user left off*.\n// The stepper runs in controlled mode, so this external state drives it.\nfunction load(): Saved | null {\n\tif (typeof window === \"undefined\") return null;\n\tconst fromHash = new URLSearchParams(window.location.hash.slice(1)).get(\n\t\t\"sr\",\n\t) as StepId | null;\n\ttry {\n\t\tconst raw = window.localStorage.getItem(KEY);\n\t\tconst saved = raw ? (JSON.parse(raw) as Saved) : null;\n\t\tconst step =\n\t\t\tfromHash && STEP_IDS.includes(fromHash) ? fromHash : saved?.step;\n\t\tif (!step) return null;\n\t\treturn { step, name: saved?.name ?? \"\" };\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nfunction save(data: Saved) {\n\tif (typeof window === \"undefined\") return;\n\ttry {\n\t\twindow.localStorage.setItem(KEY, JSON.stringify(data));\n\t\twindow.history.replaceState(null, \"\", `#sr=${data.step}`);\n\t} catch {\n\t\t/* storage unavailable */\n\t}\n}\n\nfunction clear() {\n\tif (typeof window === \"undefined\") return;\n\ttry {\n\t\twindow.localStorage.removeItem(KEY);\n\t\twindow.history.replaceState(\n\t\t\tnull,\n\t\t\t\"\",\n\t\t\twindow.location.pathname + window.location.search,\n\t\t);\n\t} catch {\n\t\t/* storage unavailable */\n\t}\n}\n\n/**\n * Controlled mode + persistence: the active step lives in React state that is\n * synced to localStorage and the URL hash. `step` drives the stepper and\n * `onStepChange` writes every move back out, so a refresh resumes from the\n * persisted step.\n */\nexport function SaveResumeBlock() {\n\tconst [step, setStep] = useState<StepId>(\"workspace\");\n\tconst [name, setName] = useState(\"\");\n\tconst [resumed, setResumed] = useState(false);\n\n\t// Hydrate from storage after mount (avoids SSR/client mismatch).\n\tuseEffect(() => {\n\t\tconst saved = load();\n\t\tif (saved) {\n\t\t\tsetStep(saved.step);\n\t\t\tsetName(saved.name);\n\t\t\tif (saved.step !== \"workspace\" || saved.name) setResumed(true);\n\t\t}\n\t}, []);\n\n\tconst persist = (next: Partial<Saved>) => {\n\t\tconst data: Saved = { step, name, ...next };\n\t\tsave(data);\n\t};\n\n\tconst reset = () => {\n\t\tclear();\n\t\tsetStep(\"workspace\");\n\t\tsetName(\"\");\n\t\tsetResumed(false);\n\t};\n\n\treturn (\n\t\t<Stepper.Root\n\t\t\tstep={step}\n\t\t\tonStepChange={(id) => {\n\t\t\t\tsetStep(id);\n\t\t\t\tpersist({ step: id });\n\t\t\t}}\n\t\t\tclassName=\"w-full max-w-md rounded-xl border bg-background p-6 shadow-sm\"\n\t\t>\n\t\t\t{() => (\n\t\t\t\t<>\n\t\t\t\t\t<div className=\"mb-4 flex items-center justify-between\">\n\t\t\t\t\t\t<Crumbs current={step} />\n\t\t\t\t\t\t<Button variant=\"outline\" size=\"sm\" onClick={reset}>\n\t\t\t\t\t\t\t<RotateCcw /> Reset\n\t\t\t\t\t\t</Button>\n\t\t\t\t\t</div>\n\n\t\t\t\t\t{resumed && (\n\t\t\t\t\t\t<Alert className=\"mb-3\">\n\t\t\t\t\t\t\t<AlertDescription>\n\t\t\t\t\t\t\t\tResumed from where you left off — try refreshing the page.\n\t\t\t\t\t\t\t</AlertDescription>\n\t\t\t\t\t\t</Alert>\n\t\t\t\t\t)}\n\n\t\t\t\t\t<div className=\"min-h-32\">\n\t\t\t\t\t\t<Stepper.Content step=\"workspace\" className=\"space-y-3\">\n\t\t\t\t\t\t\t<p className=\"text-sm font-semibold\">Name your workspace</p>\n\t\t\t\t\t\t\t<Input\n\t\t\t\t\t\t\t\tvalue={name}\n\t\t\t\t\t\t\t\tonChange={(e) => {\n\t\t\t\t\t\t\t\t\tsetName(e.target.value);\n\t\t\t\t\t\t\t\t\tpersist({ name: e.target.value });\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\tplaceholder=\"Acme Inc.\"\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t<p className=\"text-xs text-muted-foreground\">\n\t\t\t\t\t\t\t\tSaved as you type — reload and it's still here.\n\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t</Stepper.Content>\n\n\t\t\t\t\t\t<Stepper.Content step=\"details\" className=\"space-y-3\">\n\t\t\t\t\t\t\t<p className=\"text-sm font-semibold\">A few details</p>\n\t\t\t\t\t\t\t<Input placeholder=\"Industry\" />\n\t\t\t\t\t\t\t<Input placeholder=\"Team size\" />\n\t\t\t\t\t\t</Stepper.Content>\n\n\t\t\t\t\t\t<Stepper.Content step=\"review\" className=\"space-y-2 text-sm\">\n\t\t\t\t\t\t\t<p className=\"font-semibold\">Review</p>\n\t\t\t\t\t\t\t<div className=\"rounded-lg border bg-muted/30 p-3\">\n\t\t\t\t\t\t\t\tWorkspace: <span className=\"font-medium\">{name || \"—\"}</span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</Stepper.Content>\n\n\t\t\t\t\t\t<Stepper.Content\n\t\t\t\t\t\t\tstep=\"done\"\n\t\t\t\t\t\t\tclassName=\"grid place-items-center gap-2 py-6 text-center\"\n\t\t\t\t\t\t>\n\t\t\t\t\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\t\t\t\t<Check className=\"size-6\" />\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t<p className=\"text-sm font-medium\">Workspace created</p>\n\t\t\t\t\t\t</Stepper.Content>\n\t\t\t\t\t</div>\n\n\t\t\t\t\t<Footer />\n\t\t\t\t</>\n\t\t\t)}\n\t\t</Stepper.Root>\n\t);\n}\n\nfunction Crumbs({ current }: { current: StepId }) {\n\tconst index = STEP_IDS.indexOf(current);\n\treturn (\n\t\t<div className=\"flex items-center gap-1.5\">\n\t\t\t{STEP_IDS.map((id, i) => (\n\t\t\t\t<span\n\t\t\t\t\tkey={id}\n\t\t\t\t\tclassName={`size-1.5 rounded-full transition-colors ${i <= index ? \"bg-primary\" : \"bg-muted\"}`}\n\t\t\t\t/>\n\t\t\t))}\n\t\t\t<span className=\"ml-1 text-xs font-medium text-muted-foreground\">\n\t\t\t\t#sr={current}\n\t\t\t</span>\n\t\t</div>\n\t);\n}\n\nfunction Footer() {\n\tconst stepper = useStepper();\n\tif (stepper.is(\"done\")) return null;\n\treturn (\n\t\t<Stepper.Actions className=\"mt-5 flex justify-between\">\n\t\t\t<Stepper.Prev className={buttonVariants({ variant: \"outline\" })}>\n\t\t\t\tBack\n\t\t\t</Stepper.Prev>\n\t\t\t<Stepper.Next className={buttonVariants()}>\n\t\t\t\t{stepper.is(\"review\") ? \"Create\" : \"Continue\"}\n\t\t\t</Stepper.Next>\n\t\t</Stepper.Actions>\n\t);\n}\n\nexport default SaveResumeBlock;\n"
    }
  ]
}
