{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"stepper","type":"registry:ui","title":"Stepper","description":"Progress steps, horizontal or vertical, with number, icon, dot or bar markers; connectors fill with a spring, numbers morph into drawn checks and the current step breathes.","author":"GetUrUI <hello@geturui.dev>","dependencies":["motion","lucide-react"],"registryDependencies":[],"files":[{"path":"registry/geturui/ui/stepper.tsx","type":"registry:ui","target":"components/ui/stepper.tsx","content":"\"use client\";\n\nimport * as React from \"react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { X } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\nexport type StepState = \"upcoming\" | \"current\" | \"complete\" | \"error\";\n\nexport interface StepItem {\n  title: string;\n  description?: string;\n  /** Glyph for the \"icon\" marker. */\n  icon?: React.ReactNode;\n  /** Mark this step as failed (overrides the computed state). */\n  error?: boolean;\n}\n\nexport interface StepperProps extends Omit<React.HTMLAttributes<HTMLDivElement>, \"onChange\"> {\n  steps: StepItem[];\n  /** Index of the current step (0-based). Pass steps.length when everything is done. */\n  current: number;\n  orientation?: \"horizontal\" | \"vertical\";\n  /** \"number\" | \"icon\" | \"dot\" markers, or \"bar\" for segmented story-style bars (horizontal). */\n  marker?: \"number\" | \"icon\" | \"dot\" | \"bar\";\n  size?: \"sm\" | \"md\";\n  /** Makes completed steps buttons that jump back. */\n  onStepClick?: (index: number) => void;\n  /**\n   * Compact \"Step 2 of 4\" + progress bar. \"mobile\" swaps to it below the sm breakpoint.\n   * Defaults to \"mobile\" for horizontal steppers and \"never\" for vertical ones.\n   */\n  compact?: \"always\" | \"mobile\" | \"never\";\n  /** Accessible name for the step list. */\n  label?: string;\n}\n\nconst stateWord: Record<StepState, string> = { complete: \"completed\", current: \"current step\", upcoming: \"not started\", error: \"needs attention\" };\n\nfunction stateOf(step: StepItem, i: number, current: number): StepState {\n  if (step.error) return \"error\";\n  if (i < current) return \"complete\";\n  if (i === current) return \"current\";\n  return \"upcoming\";\n}\n\nconst markerSize = { sm: { box: \"size-7 text-xs\", px: 28, glyph: \"size-3.5\" }, md: { box: \"size-9 text-sm\", px: 36, glyph: \"size-4\" } };\nconst DOT_PX = 12;\n\n/** Check that draws itself when a step completes. */\nfunction DrawnCheck({ className, reduce }: { className: string; reduce: boolean }) {\n  return (\n    <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth={3} strokeLinecap=\"round\" strokeLinejoin=\"round\" className={className} aria-hidden>\n      <motion.path d=\"m5 12.5 4.5 4.5L19 7.5\" initial={{ pathLength: reduce ? 1 : 0 }} animate={{ pathLength: 1 }} transition={{ type: \"spring\", stiffness: 180, damping: 22 }} />\n    </svg>\n  );\n}\n\nfunction Marker({ state, index, step, marker, size, reduce }: { state: StepState; index: number; step: StepItem; marker: \"number\" | \"icon\" | \"dot\"; size: \"sm\" | \"md\"; reduce: boolean }) {\n  const m = markerSize[size];\n  if (marker === \"dot\") {\n    return (\n      <span aria-hidden className=\"relative flex items-center justify-center\" style={{ width: DOT_PX, height: DOT_PX }}>\n        {state === \"current\" && !reduce && (\n          <motion.span className=\"absolute inset-0 rounded-full bg-volt\" animate={{ scale: [1, 2.4], opacity: [0.4, 0] }} transition={{ duration: 1.8, repeat: Infinity, ease: \"easeOut\" }} />\n        )}\n        <span\n          className={cn(\n            \"relative size-full rounded-full transition-colors duration-300\",\n            state === \"complete\" && \"bg-volt\",\n            state === \"current\" && \"bg-volt shadow-[0_0_0_3px_var(--color-volt-soft)]\",\n            state === \"upcoming\" && \"border-2 border-border-strong bg-surface\",\n            state === \"error\" && \"bg-danger\",\n          )}\n        />\n      </span>\n    );\n  }\n\n  const face =\n    state === \"complete\" ? (\n      <DrawnCheck className={m.glyph} reduce={reduce} />\n    ) : state === \"error\" ? (\n      <X className={m.glyph} strokeWidth={2.75} />\n    ) : marker === \"icon\" && step.icon ? (\n      <span className={cn(\"inline-flex [&_svg]:size-full\", m.glyph)}>{step.icon}</span>\n    ) : (\n      <span className=\"font-mono font-semibold tabular-nums\">{index + 1}</span>\n    );\n\n  return (\n    <span aria-hidden className={cn(\"relative flex shrink-0 items-center justify-center rounded-full\", m.box)}>\n      {state === \"current\" && !reduce && (\n        <motion.span\n          className=\"absolute inset-0 rounded-full border-2 border-volt-ink\"\n          animate={{ scale: [1, 1.35], opacity: [0.5, 0] }}\n          transition={{ duration: 2, repeat: Infinity, ease: \"easeOut\" }}\n        />\n      )}\n      <span\n        className={cn(\n          \"relative flex size-full items-center justify-center overflow-hidden rounded-full transition-[background-color,color,box-shadow] duration-300\",\n          state === \"complete\" && \"bg-volt text-on-volt\",\n          state === \"current\" && \"bg-volt-soft text-volt-ink shadow-[inset_0_0_0_2px_var(--color-volt-ink)]\",\n          state === \"upcoming\" && \"bg-surface text-ink-subtle shadow-[inset_0_0_0_1.5px_var(--color-border-strong)]\",\n          state === \"error\" && \"bg-danger-soft text-danger shadow-[inset_0_0_0_2px_var(--color-danger)]\",\n        )}\n      >\n        <AnimatePresence mode=\"popLayout\" initial={false}>\n          <motion.span\n            key={state === \"complete\" ? \"done\" : state === \"error\" ? \"err\" : \"idle\"}\n            className=\"flex items-center justify-center\"\n            initial={reduce ? { opacity: 0 } : { opacity: 0, scale: 0.5, filter: \"blur(4px)\" }}\n            animate={{ opacity: 1, scale: 1, filter: \"blur(0px)\" }}\n            exit={reduce ? { opacity: 0 } : { opacity: 0, scale: 0.5, filter: \"blur(4px)\" }}\n            transition={{ type: \"spring\", stiffness: 420, damping: 28 }}\n          >\n            {face}\n          </motion.span>\n        </AnimatePresence>\n      </span>\n    </span>\n  );\n}\n\n/** Connector track with a volt fill that springs along it when the step before completes. */\nfunction Connector({ filled, vertical, reduce, className, style }: { filled: boolean; vertical?: boolean; reduce: boolean; className?: string; style?: React.CSSProperties }) {\n  return (\n    <span aria-hidden className={cn(\"absolute overflow-hidden rounded-full bg-border\", className)} style={style}>\n      <motion.span\n        className={cn(\"absolute inset-0 rounded-full bg-volt\", vertical ? \"origin-top\" : \"origin-left\")}\n        initial={false}\n        animate={vertical ? { scaleY: filled ? 1 : 0 } : { scaleX: filled ? 1 : 0 }}\n        transition={reduce ? { duration: 0 } : { type: \"spring\", stiffness: 120, damping: 22 }}\n      />\n    </span>\n  );\n}\n\n/** Compact \"Step 2 of 4\" header with a springing progress bar — the mobile form of the stepper. */\nexport function StepperCompact({ steps, current, className }: { steps: StepItem[]; current: number; className?: string }) {\n  const reduce = !!useReducedMotion();\n  const total = steps.length;\n  const at = Math.min(current, total - 1);\n  const done = current >= total;\n  const step = steps[at];\n  const pct = total ? Math.min(1, (current + (done ? 0 : 1)) / total) : 0;\n  return (\n    <div className={cn(\"w-full\", className)}>\n      <div className=\"flex items-baseline justify-between gap-3\">\n        <p className=\"text-[13px] font-medium text-ink-muted\">\n          {done ? \"All steps complete\" : <>Step <span className=\"font-mono text-ink\">{at + 1}</span> of <span className=\"font-mono\">{total}</span></>}\n        </p>\n        {step?.error && <p className=\"text-[13px] font-medium text-danger\">Needs attention</p>}\n      </div>\n      {!done && step && (\n        <div className=\"relative mt-1 min-h-6 overflow-hidden\">\n          <AnimatePresence mode=\"popLayout\" initial={false}>\n            <motion.p\n              key={at}\n              className=\"text-base font-semibold text-ink\"\n              initial={reduce ? { opacity: 0 } : { opacity: 0, y: 8, filter: \"blur(6px)\" }}\n              animate={{ opacity: 1, y: 0, filter: \"blur(0px)\" }}\n              exit={reduce ? { opacity: 0 } : { opacity: 0, y: -8, filter: \"blur(6px)\" }}\n              transition={{ type: \"spring\", stiffness: 380, damping: 32 }}\n            >\n              {step.title}\n            </motion.p>\n          </AnimatePresence>\n        </div>\n      )}\n      <div\n        role=\"progressbar\"\n        aria-label=\"Progress\"\n        aria-valuemin={0}\n        aria-valuemax={total}\n        aria-valuenow={Math.min(current + (done ? 0 : 1), total)}\n        aria-valuetext={done ? \"All steps complete\" : `Step ${at + 1} of ${total}: ${step?.title ?? \"\"}`}\n        className=\"mt-2.5 h-1.5 overflow-hidden rounded-full bg-surface-sunken\"\n      >\n        <motion.div\n          className={cn(\"h-full origin-left rounded-full\", step?.error ? \"bg-danger\" : \"bg-volt\")}\n          initial={false}\n          animate={{ scaleX: pct }}\n          transition={reduce ? { duration: 0 } : { type: \"spring\", stiffness: 140, damping: 22 }}\n        />\n      </div>\n    </div>\n  );\n}\n\n/** Progress steps, horizontal or vertical, with number/icon/dot/bar markers, spring-filled connectors and clickable completed steps. */\nexport function Stepper({\n  steps,\n  current,\n  orientation = \"horizontal\",\n  marker = \"number\",\n  size = \"md\",\n  onStepClick,\n  compact,\n  label = \"Progress\",\n  className,\n  ...props\n}: StepperProps) {\n  const reduce = !!useReducedMotion();\n  const vertical = orientation === \"vertical\";\n  const mode = compact ?? (vertical ? \"never\" : \"mobile\");\n  const bars = marker === \"bar\" && !vertical;\n  const markerKind = marker === \"bar\" ? \"dot\" : marker;\n  const m = markerKind === \"dot\" ? DOT_PX : markerSize[size].px;\n\n  const full = (\n    <ol\n      aria-label={label}\n      className={cn(vertical ? \"flex flex-col\" : \"grid\", mode === \"mobile\" && \"hidden sm:grid\", mode === \"mobile\" && vertical && \"sm:flex\")}\n      style={vertical ? undefined : { gridTemplateColumns: `repeat(${steps.length}, minmax(0, 1fr))`, gap: bars ? 12 : undefined }}\n    >\n      {steps.map((step, i) => {\n        const state = stateOf(step, i, current);\n        const clickable = !!onStepClick && state === \"complete\";\n        const isLast = i === steps.length - 1;\n        const text = (\n          <>\n            <span\n              className={cn(\n                \"block font-semibold leading-snug transition-colors duration-300\",\n                size === \"sm\" ? \"text-[13px]\" : \"text-sm\",\n                state === \"upcoming\" ? \"text-ink-muted\" : state === \"error\" ? \"text-danger\" : \"text-ink\",\n                clickable && \"group-hover/step:text-volt-ink\",\n              )}\n            >\n              {step.title}\n            </span>\n            {step.description && (\n              <span className={cn(\"mt-0.5 block leading-snug text-ink-subtle\", size === \"sm\" ? \"text-xs\" : \"text-[13px]\", state === \"upcoming\" && \"opacity-80\")}>\n                {step.description}\n              </span>\n            )}\n            <span className=\"sr-only\">, {stateWord[state]}</span>\n          </>\n        );\n\n        const body = bars ? (\n          <>\n            <span aria-hidden className=\"relative block h-1.5 overflow-hidden rounded-full bg-border\">\n              <motion.span\n                className={cn(\"absolute inset-0 origin-left rounded-full\", state === \"error\" ? \"bg-danger\" : \"bg-volt\", state === \"current\" && \"opacity-60\")}\n                initial={false}\n                animate={{ scaleX: state === \"upcoming\" ? 0 : 1 }}\n                transition={reduce ? { duration: 0 } : { type: \"spring\", stiffness: 120, damping: 22 }}\n              />\n            </span>\n            <span className=\"mt-3 block text-left\">{text}</span>\n          </>\n        ) : vertical ? (\n          <span className=\"flex gap-3.5 text-left\">\n            <span className=\"flex shrink-0 justify-center\" style={{ width: Math.max(m, 28) }}>\n              <Marker state={state} index={i} step={step} marker={markerKind} size={size} reduce={reduce} />\n            </span>\n            <span className={cn(\"min-w-0\", isLast ? \"pb-0\" : \"pb-7\")} style={{ paddingTop: markerKind === \"dot\" ? 0 : (m - 20) / 2 }}>\n              {text}\n            </span>\n          </span>\n        ) : (\n          <span className=\"flex flex-col items-center text-center\">\n            <span className=\"flex items-center justify-center\" style={{ height: Math.max(m, 12) }}>\n              <Marker state={state} index={i} step={step} marker={markerKind} size={size} reduce={reduce} />\n            </span>\n            <span className=\"mt-2.5 block px-1\">{text}</span>\n          </span>\n        );\n\n        const connector = isLast || bars ? null : vertical ? (\n          <Connector\n            vertical\n            filled={i < current && !step.error}\n            reduce={reduce}\n            className=\"w-0.5\"\n            style={{\n              left: Math.max(m, 28) / 2 - 1,\n              top: (markerKind === \"dot\" ? DOT_PX : m) + 6,\n              bottom: 6,\n            }}\n          />\n        ) : (\n          <Connector\n            filled={i < current && !step.error}\n            reduce={reduce}\n            className=\"h-0.5\"\n            style={{\n              top: Math.max(m, 12) / 2 - 1,\n              left: `calc(50% + ${m / 2 + 8}px)`,\n              right: `calc(-50% + ${m / 2 + 8}px)`,\n            }}\n          />\n        );\n\n        return (\n          <li key={i} aria-current={state === \"current\" ? \"step\" : undefined} className=\"relative min-w-0\">\n            {connector}\n            {clickable ? (\n              <button\n                type=\"button\"\n                onClick={() => onStepClick?.(i)}\n                className={cn(\"group/step block w-full rounded-sm outline-none focus-visible:shadow-[var(--focus-ring)]\", vertical ? \"\" : \"pb-1\")}\n              >\n                <span className=\"sr-only\">Go back to step {i + 1}: </span>\n                {body}\n              </button>\n            ) : (\n              <div className={cn(vertical ? \"\" : \"pb-1\")}>{body}</div>\n            )}\n          </li>\n        );\n      })}\n    </ol>\n  );\n\n  return (\n    <div className={cn(\"w-full\", className)} {...props}>\n      {mode !== \"always\" && full}\n      {mode !== \"never\" && <StepperCompact steps={steps} current={current} className={mode === \"mobile\" ? \"sm:hidden\" : undefined} />}\n    </div>\n  );\n}\n"},{"path":"LICENSE-geturui.md","type":"registry:file","target":"LICENSE-geturui.md","content":"MIT License\n\nCopyright (c) 2026 geturui.com\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"}],"categories":["components","stepper","steps","wizard","progress","onboarding","checkout"],"meta":{"tier":"free","license":"MIT","docs":"https://geturui.com/docs/stepper"}}