{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"Animated stepper/wizard component with step transitions","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { type ReactNode, useCallback, useId, useState } from \"react\";\n\nexport interface StepItem {\n  content?: ReactNode;\n  description?: string;\n  icon?: ReactNode;\n  label: string;\n}\n\nexport interface AnimatedStepperProps {\n  allowClickNavigation?: boolean;\n  className?: string;\n  currentStep?: number;\n  defaultStep?: number;\n  onStepChange?: (step: number) => void;\n  steps: StepItem[];\n  variant?: \"horizontal\" | \"vertical\";\n}\n\n/* ─────────────────────────────────────────────────────────\n * ANIMATION STORYBOARD\n *\n *    0ms   stepper enters viewport\n *  100ms   step circles stagger in (50ms each)\n *  click   active ring pulse + circle scale bounce\n *  step    progress line fills with spring\n *  done    checkmark draws with pathLength animation\n *  slide   content slides directionally with crossfade\n * ───────────────────────────────────────────────────────── */\n\nconst SPRING = {\n  bounce: 0.1,\n  duration: 0.25,\n  type: \"spring\" as const,\n};\n\nconst SPRING_BOUNCY = {\n  bounce: 0.2,\n  duration: 0.3,\n  type: \"spring\" as const,\n};\n\nfunction CheckIcon() {\n  return (\n    <svg\n      aria-hidden=\"true\"\n      className=\"h-5 w-5\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth={2.5}\n      viewBox=\"0 0 24 24\"\n    >\n      <path d=\"M5 13l4 4L19 7\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n    </svg>\n  );\n}\n\nexport default function AnimatedStepper({\n  steps,\n  currentStep: controlledStep,\n  defaultStep = 0,\n  onStepChange,\n  variant = \"horizontal\",\n  allowClickNavigation = false,\n  className,\n}: AnimatedStepperProps) {\n  const shouldReduceMotion = useReducedMotion();\n  const id = useId();\n\n  const [internalStep, setInternalStep] = useState(defaultStep);\n  const [direction, setDirection] = useState(1);\n\n  const isControlled = controlledStep !== undefined;\n  const activeStep = isControlled ? controlledStep : internalStep;\n\n  const handleStepChange = useCallback(\n    (step: number) => {\n      if (step < 0 || step >= steps.length) {\n        return;\n      }\n      setDirection(step > activeStep ? 1 : -1);\n      if (!isControlled) {\n        setInternalStep(step);\n      }\n      onStepChange?.(step);\n    },\n    [isControlled, onStepChange, activeStep, steps.length]\n  );\n\n  const handleKeyDown = useCallback(\n    (event: React.KeyboardEvent) => {\n      if (!allowClickNavigation) {\n        return;\n      }\n      const isHoriz = variant === \"horizontal\";\n      const nextKey = isHoriz ? \"ArrowRight\" : \"ArrowDown\";\n      const prevKey = isHoriz ? \"ArrowLeft\" : \"ArrowUp\";\n\n      if (event.key === nextKey) {\n        event.preventDefault();\n        handleStepChange(Math.min(activeStep + 1, steps.length - 1));\n      } else if (event.key === prevKey) {\n        event.preventDefault();\n        handleStepChange(Math.max(activeStep - 1, 0));\n      }\n    },\n    [allowClickNavigation, variant, activeStep, steps.length, handleStepChange]\n  );\n\n  const progress = steps.length > 1 ? activeStep / (steps.length - 1) : 0;\n  const isHorizontal = variant === \"horizontal\";\n\n  return (\n    <div\n      className={cn(\n        \"flex w-full gap-6\",\n        isHorizontal ? \"flex-col\" : \"flex-row\",\n        className\n      )}\n    >\n      <div\n        aria-label=\"Progress steps\"\n        className={cn(\n          \"relative flex\",\n          isHorizontal\n            ? \"flex-row items-center justify-between\"\n            : \"flex-col items-start gap-2\"\n        )}\n        role=\"group\"\n      >\n        {steps.map((step, index) => {\n          const isActive = index === activeStep;\n          const isCompleted = index < activeStep;\n\n          return (\n            <div\n              className={cn(\n                \"relative z-10 flex items-center\",\n                isHorizontal ? \"flex-1\" : \"gap-3\",\n                index < steps.length - 1 && isHorizontal && \"flex-1\"\n              )}\n              key={`${id}-step-${step.label}`}\n            >\n              <motion.button\n                animate={shouldReduceMotion ? undefined : { scale: 1 }}\n                aria-label={`Step ${index + 1}: ${step.label}${isCompleted ? \", completed\" : \"\"}${isActive ? \", current\" : \"\"}`}\n                aria-selected={isActive}\n                className={cn(\n                  \"relative flex h-10 w-10 shrink-0 items-center justify-center rounded-full border-2 font-medium text-sm\",\n                  \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\",\n                  isActive &&\n                    \"border-primary bg-primary text-primary-foreground\",\n                  isCompleted &&\n                    \"border-primary bg-primary text-primary-foreground\",\n                  !(isActive || isCompleted) &&\n                    \"border-muted-foreground/30 bg-background text-muted-foreground\",\n                  allowClickNavigation ? \"cursor-pointer\" : \"cursor-default\"\n                )}\n                disabled={!allowClickNavigation}\n                id={`${id}-step-${index}`}\n                onClick={() => allowClickNavigation && handleStepChange(index)}\n                onKeyDown={handleKeyDown}\n                role=\"tab\"\n                tabIndex={isActive ? 0 : -1}\n                transition={shouldReduceMotion ? { duration: 0 } : SPRING}\n                type=\"button\"\n                whileTap={\n                  allowClickNavigation && !shouldReduceMotion\n                    ? { scale: 0.9 }\n                    : undefined\n                }\n              >\n                {/* Active ring pulse */}\n                {isActive && !shouldReduceMotion && (\n                  <motion.span\n                    animate={{ opacity: 0, scale: 1.6 }}\n                    className=\"absolute inset-0 rounded-full border-2 border-primary\"\n                    initial={{ opacity: 0.5, scale: 1 }}\n                    transition={{ duration: 0.6, ease: [0.23, 1, 0.32, 1] }}\n                  />\n                )}\n\n                <AnimatePresence initial={false} mode=\"wait\">\n                  {isCompleted ? (\n                    <motion.span\n                      animate={\n                        shouldReduceMotion\n                          ? { opacity: 1 }\n                          : { opacity: 1, scale: 1 }\n                      }\n                      exit={\n                        shouldReduceMotion\n                          ? { opacity: 0 }\n                          : { opacity: 0, scale: 0.5 }\n                      }\n                      initial={\n                        shouldReduceMotion\n                          ? { opacity: 0 }\n                          : { opacity: 0, scale: 0.5 }\n                      }\n                      key=\"check\"\n                      transition={\n                        shouldReduceMotion ? { duration: 0 } : SPRING_BOUNCY\n                      }\n                    >\n                      <CheckIcon />\n                    </motion.span>\n                  ) : step.icon ? (\n                    <motion.span\n                      animate={\n                        shouldReduceMotion\n                          ? { opacity: 1 }\n                          : { opacity: 1, scale: 1 }\n                      }\n                      exit={\n                        shouldReduceMotion\n                          ? { opacity: 0 }\n                          : { opacity: 0, scale: 0.8 }\n                      }\n                      initial={\n                        shouldReduceMotion\n                          ? { opacity: 0 }\n                          : { opacity: 0, scale: 0.8 }\n                      }\n                      key=\"icon\"\n                      transition={shouldReduceMotion ? { duration: 0 } : SPRING}\n                    >\n                      {step.icon}\n                    </motion.span>\n                  ) : (\n                    <motion.span\n                      animate={\n                        shouldReduceMotion\n                          ? { opacity: 1 }\n                          : { opacity: 1, scale: 1 }\n                      }\n                      exit={\n                        shouldReduceMotion\n                          ? { opacity: 0 }\n                          : { opacity: 0, scale: 0.8 }\n                      }\n                      initial={\n                        shouldReduceMotion\n                          ? { opacity: 0 }\n                          : { opacity: 0, scale: 0.8 }\n                      }\n                      key=\"number\"\n                      transition={shouldReduceMotion ? { duration: 0 } : SPRING}\n                    >\n                      {index + 1}\n                    </motion.span>\n                  )}\n                </AnimatePresence>\n              </motion.button>\n\n              {isHorizontal && (\n                <div className=\"ml-2 hidden sm:block\">\n                  <p\n                    className={cn(\n                      \"font-medium text-sm transition-colors duration-200\",\n                      isActive ? \"text-foreground\" : \"text-muted-foreground\"\n                    )}\n                  >\n                    {step.label}\n                  </p>\n                  {step.description ? (\n                    <p className=\"text-muted-foreground text-xs\">\n                      {step.description}\n                    </p>\n                  ) : null}\n                </div>\n              )}\n\n              {!isHorizontal && (\n                <div>\n                  <p\n                    className={cn(\n                      \"font-medium text-sm transition-colors duration-200\",\n                      isActive ? \"text-foreground\" : \"text-muted-foreground\"\n                    )}\n                  >\n                    {step.label}\n                  </p>\n                  {step.description ? (\n                    <p className=\"text-muted-foreground text-xs\">\n                      {step.description}\n                    </p>\n                  ) : null}\n                </div>\n              )}\n\n              {isHorizontal && index < steps.length - 1 && (\n                <div className=\"mx-2 h-0.5 flex-1 overflow-hidden rounded-full bg-muted\">\n                  <motion.div\n                    animate={{ width: index < activeStep ? \"100%\" : \"0%\" }}\n                    className=\"h-full bg-primary\"\n                    transition={shouldReduceMotion ? { duration: 0 } : SPRING}\n                  />\n                </div>\n              )}\n            </div>\n          );\n        })}\n\n        {!isHorizontal && (\n          <div className=\"absolute top-5 left-5 h-[calc(100%-2.5rem)] w-0.5 -translate-x-1/2 overflow-hidden bg-muted\">\n            <motion.div\n              animate={{ height: `${progress * 100}%` }}\n              className=\"w-full bg-primary\"\n              transition={shouldReduceMotion ? { duration: 0 } : SPRING}\n            />\n          </div>\n        )}\n      </div>\n\n      <div aria-label={`Step ${activeStep + 1} content`} role=\"tabpanel\">\n        <AnimatePresence initial={false} mode=\"wait\">\n          <motion.div\n            animate={shouldReduceMotion ? { opacity: 1 } : { opacity: 1, x: 0 }}\n            exit={\n              shouldReduceMotion\n                ? { opacity: 0, transition: { duration: 0 } }\n                : { opacity: 0, x: -direction * 20 }\n            }\n            initial={\n              shouldReduceMotion\n                ? { opacity: 0 }\n                : { opacity: 0, x: direction * 20 }\n            }\n            key={activeStep}\n            transition={shouldReduceMotion ? { duration: 0 } : SPRING}\n          >\n            {steps[activeStep]?.content}\n          </motion.div>\n        </AnimatePresence>\n      </div>\n    </div>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/animated-stepper/index.tsx","type":"registry:ui"}],"name":"animated-stepper","registryDependencies":[],"title":"Animated Stepper","type":"registry:ui"}