{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"step-flow","type":"registry:ui","title":"Step Flow","description":"Multi-step flow with a filling progress track, pulsing active step, sliding content that resizes smoothly and a completion state.","dependencies":["motion","react-use-measure"],"registryDependencies":["utils"],"files":[{"path":"registry/ui/step-flow.tsx","type":"registry:ui","content":"\"use client\";\n\nimport { Children, useState, type CSSProperties, type ReactElement, type ReactNode } from \"react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport useMeasure from \"react-use-measure\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface StepProps {\n  /** Label under the step's dot. */\n  title?: string;\n  children?: ReactNode;\n}\n\n/** One step of a `Stepper`. Renders its children when active. */\nexport function Step({ children }: StepProps) {\n  return <>{children}</>;\n}\n\nexport interface StepFlowProps {\n  /** `Step` elements, one per step. */\n  children: ReactNode;\n  /** 1-based step to start on. */\n  initialStep?: number;\n  onStepChange?: (step: number) => void;\n  onComplete?: () => void;\n  backText?: string;\n  nextText?: string;\n  completeText?: string;\n  /** Shown after the last step is completed. */\n  completedContent?: ReactNode;\n  /** Let users jump to a step by clicking its dot. */\n  clickableSteps?: boolean;\n  /** Active and completed color. */\n  accentColor?: string;\n  /** Text/icon color on top of the accent. */\n  accentForeground?: string;\n  className?: string;\n}\n\nconst spring = { type: \"spring\", stiffness: 380, damping: 34 } as const;\n\nexport function StepFlow({\n  children,\n  initialStep = 1,\n  onStepChange,\n  onComplete,\n  backText = \"Back\",\n  nextText = \"Continue\",\n  completeText = \"Complete\",\n  completedContent,\n  clickableSteps = true,\n  accentColor = \"#c6ff3d\",\n  accentForeground = \"#0a0a0a\",\n  className,\n}: StepFlowProps) {\n  const steps = Children.toArray(children) as ReactElement<StepProps>[];\n  const total = steps.length;\n  const [{ step, dir, done }, setNav] = useState(() => ({ step: Math.min(Math.max(1, initialStep), total), dir: 1, done: false }));\n  const reduce = useReducedMotion();\n  const [measure, bounds] = useMeasure();\n\n  const go = (next: number) => {\n    if (next === step || next < 1 || next > total) return;\n    setNav({ step: next, dir: next > step ? 1 : -1, done: false });\n    onStepChange?.(next);\n  };\n  const finish = () => {\n    setNav({ step, dir: 1, done: true });\n    onComplete?.();\n  };\n  const restart = () => {\n    setNav({ step: 1, dir: -1, done: false });\n    onStepChange?.(1);\n  };\n\n  const slide = reduce ? 0 : 48;\n  const variants = {\n    enter: (d: number) => ({ x: d * slide, opacity: 0, filter: reduce ? \"none\" : \"blur(4px)\" }),\n    center: { x: 0, opacity: 1, filter: \"blur(0px)\" },\n    exit: (d: number) => ({ x: -d * slide, opacity: 0, filter: reduce ? \"none\" : \"blur(4px)\" }),\n  };\n  const vars = { \"--st-accent\": accentColor, \"--st-fg\": accentForeground } as CSSProperties;\n  const current = done ? total + 1 : step;\n\n  return (\n    <div className={cn(\"w-full max-w-md rounded-3xl border border-border bg-card p-6 text-card-foreground shadow-2xl sm:p-8\", className)} style={vars}>\n      {/* Indicator row */}\n      <ol className={cn(\"flex items-center\", steps.some((x) => x.props.title) && \"sm:pb-7\")} aria-label=\"Progress\">\n        {steps.map((s, i) => {\n          const n = i + 1;\n          const state = n < current ? \"complete\" : n === current ? \"active\" : \"upcoming\";\n          const canClick = clickableSteps && !done && n !== step;\n          return (\n            <li key={n} className={cn(\"flex items-center\", n < total && \"flex-1\")}>\n              <button\n                type=\"button\"\n                disabled={!canClick}\n                onClick={() => go(n)}\n                aria-current={state === \"active\" ? \"step\" : undefined}\n                aria-label={`Step ${n}${s.props.title ? `: ${s.props.title}` : \"\"}, ${state}`}\n                className=\"group relative rounded-full outline-none enabled:cursor-pointer disabled:cursor-default\"\n              >\n                <motion.span\n                  className=\"relative grid size-9 place-items-center rounded-full text-sm font-semibold group-focus-visible:ring-2 group-focus-visible:ring-ring group-focus-visible:ring-offset-2 group-focus-visible:ring-offset-card\"\n                  initial={false}\n                  animate={{\n                    backgroundColor: state === \"upcoming\" ? \"var(--muted)\" : \"var(--st-accent)\",\n                    color: state === \"upcoming\" ? \"var(--muted-foreground)\" : \"var(--st-fg)\",\n                    scale: state === \"active\" ? 1.08 : 1,\n                  }}\n                  transition={spring}\n                >\n                  {state === \"active\" && !reduce && (\n                    <motion.span\n                      aria-hidden\n                      className=\"absolute inset-0 rounded-full\"\n                      style={{ boxShadow: \"0 0 0 0 var(--st-accent)\" }}\n                      animate={{ boxShadow: [\"0 0 0 0px color-mix(in oklab, var(--st-accent) 55%, transparent)\", \"0 0 0 10px color-mix(in oklab, var(--st-accent) 0%, transparent)\"] }}\n                      transition={{ duration: 1.6, repeat: Infinity, ease: \"easeOut\" }}\n                    />\n                  )}\n                  {state === \"complete\" ? (\n                    <svg viewBox=\"0 0 24 24\" className=\"size-4\" fill=\"none\" stroke=\"currentColor\" strokeWidth={3} strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden>\n                      <motion.path\n                        d=\"M5 12.5l4.5 4.5L19 7.5\"\n                        initial={{ pathLength: reduce ? 1 : 0 }}\n                        animate={{ pathLength: 1 }}\n                        transition={{ duration: 0.35, ease: \"easeOut\", delay: 0.1 }}\n                      />\n                    </svg>\n                  ) : state === \"active\" ? (\n                    <motion.span\n                      key=\"dot\"\n                      className=\"size-2.5 rounded-full bg-current\"\n                      initial={{ scale: 0 }}\n                      animate={{ scale: 1 }}\n                      transition={spring}\n                    />\n                  ) : (\n                    n\n                  )}\n                </motion.span>\n                {s.props.title && (\n                  <span\n                    className={cn(\n                      \"absolute left-1/2 top-full mt-2 hidden -translate-x-1/2 whitespace-nowrap text-xs font-medium transition-colors sm:block\",\n                      state === \"upcoming\" ? \"text-muted-foreground\" : \"text-foreground\",\n                    )}\n                  >\n                    {s.props.title}\n                  </span>\n                )}\n              </button>\n              {n < total && (\n                <span aria-hidden className=\"relative mx-2 h-0.5 flex-1 overflow-hidden rounded-full bg-muted\">\n                  <motion.span\n                    className=\"absolute inset-0 origin-left rounded-full bg-[var(--st-accent)]\"\n                    initial={false}\n                    animate={{ scaleX: n < current ? 1 : 0 }}\n                    transition={{ duration: reduce ? 0 : 0.45, ease: [0.65, 0, 0.35, 1] }}\n                  />\n                </span>\n              )}\n            </li>\n          );\n        })}\n      </ol>\n\n      {/* Sliding content, with the height following whatever is inside. */}\n      <motion.div\n        className=\"relative mt-8 overflow-hidden\"\n        animate={{ height: bounds.height || \"auto\" }}\n        transition={reduce ? { duration: 0 } : spring}\n      >\n        <div ref={measure}>\n          <AnimatePresence mode=\"popLayout\" initial={false} custom={dir}>\n            <motion.div\n              key={done ? \"done\" : step}\n              custom={dir}\n              variants={variants}\n              initial=\"enter\"\n              animate=\"center\"\n              exit=\"exit\"\n              transition={{ ...spring, opacity: { duration: 0.2 } }}\n              aria-live=\"polite\"\n            >\n              {done\n                ? (completedContent ?? (\n                    <div className=\"flex flex-col items-center py-4 text-center\">\n                      <span className=\"grid size-14 place-items-center rounded-full bg-[var(--st-accent)] text-[var(--st-fg)]\">\n                        <svg viewBox=\"0 0 24 24\" className=\"size-7\" fill=\"none\" stroke=\"currentColor\" strokeWidth={2.5} strokeLinecap=\"round\" aria-hidden>\n                          <motion.path d=\"M5 12.5l4.5 4.5L19 7.5\" initial={{ pathLength: 0 }} animate={{ pathLength: 1 }} transition={{ duration: 0.5, delay: 0.2 }} />\n                        </svg>\n                      </span>\n                      <p className=\"mt-4 text-lg font-semibold\">All done</p>\n                      <p className=\"mt-1 text-sm text-muted-foreground\">Every step is complete.</p>\n                    </div>\n                  ))\n                : steps[step - 1]}\n            </motion.div>\n          </AnimatePresence>\n        </div>\n      </motion.div>\n\n      {/* Controls */}\n      <div className=\"mt-8 flex items-center justify-between gap-3\">\n        {done ? (\n          <button\n            type=\"button\"\n            onClick={restart}\n            className=\"ml-auto rounded-full px-4 py-2 text-sm font-medium text-muted-foreground outline-none transition-colors hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring\"\n          >\n            Start over\n          </button>\n        ) : (\n          <>\n            <button\n              type=\"button\"\n              onClick={() => go(step - 1)}\n              disabled={step === 1}\n              className=\"rounded-full px-4 py-2 text-sm font-medium text-muted-foreground outline-none transition-[color,opacity] hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-0\"\n            >\n              {backText}\n            </button>\n            <motion.button\n              type=\"button\"\n              onClick={() => (step === total ? finish() : go(step + 1))}\n              whileTap={{ scale: 0.96 }}\n              className=\"rounded-full bg-[var(--st-accent)] px-6 py-2.5 text-sm font-semibold text-[var(--st-fg)] outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-card\"\n            >\n              {step === total ? completeText : nextText}\n            </motion.button>\n          </>\n        )}\n      </div>\n    </div>\n  );\n}\n"}]}