{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "multi-step",
  "title": "Multi-Step Flow",
  "description": "A Motion-powered multi-step container that slides between steps and animates height changes.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "button",
    "https://ui.ericts.com/r/use-element-height.json"
  ],
  "files": [
    {
      "path": "registry/base/ui/multi-step.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport {\n  AnimatePresence,\n  motion,\n  MotionConfig,\n  useReducedMotion,\n  type HTMLMotionProps,\n} from \"motion/react\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { useElementHeight } from \"@/hooks/use-element-height\";\nimport { cn } from \"@/lib/utils\";\n\ntype MotionTransition = NonNullable<HTMLMotionProps<\"div\">[\"transition\"]>;\ntype StepDirection = -1 | 1;\n\nexport type MultiStepItem = {\n  id: string;\n  content: React.ReactNode;\n};\n\nexport type MultiStepProps = Omit<\n  HTMLMotionProps<\"div\">,\n  | \"animate\"\n  | \"children\"\n  | \"defaultValue\"\n  | \"initial\"\n  | \"onChange\"\n  | \"transition\"\n> & {\n  steps: MultiStepItem[];\n  currentStep?: number;\n  defaultStep?: number;\n  onStepChange?: (step: number) => void;\n  backLabel?: React.ReactNode;\n  continueLabel?: React.ReactNode;\n  completeLabel?: React.ReactNode;\n  disableBack?: boolean;\n  disableContinue?: boolean;\n  footer?: React.ReactNode;\n  contentClassName?: string;\n  innerClassName?: string;\n  actionsClassName?: string;\n  transition?: MotionTransition;\n};\n\nexport function MultiStep({\n  steps,\n  currentStep,\n  defaultStep = 0,\n  onStepChange,\n  backLabel = \"Back\",\n  continueLabel = \"Continue\",\n  completeLabel = \"Done\",\n  disableBack,\n  disableContinue,\n  footer,\n  className,\n  contentClassName,\n  innerClassName,\n  actionsClassName,\n  transition,\n  ...props\n}: MultiStepProps) {\n  const [uncontrolledStep, setUncontrolledStep] = React.useState(defaultStep);\n  const [direction, setDirection] = React.useState<StepDirection>(1);\n  const [innerRef, height] = useElementHeight<HTMLDivElement>();\n  const shouldReduceMotion = useReducedMotion();\n\n  const selectedStep = clampStep(currentStep ?? uncontrolledStep, steps.length);\n  const isControlled = currentStep !== undefined;\n  const isFirstStep = selectedStep === 0;\n  const isLastStep = selectedStep === steps.length - 1;\n  const activeStep = steps[selectedStep];\n\n  const setStep = React.useCallback(\n    (nextStep: number, nextDirection: StepDirection) => {\n      const resolvedStep = clampStep(nextStep, steps.length);\n\n      setDirection(nextDirection);\n\n      if (!isControlled) {\n        setUncontrolledStep(resolvedStep);\n      }\n\n      onStepChange?.(resolvedStep);\n    },\n    [isControlled, onStepChange, steps.length],\n  );\n\n  const resolvedTransition: MotionTransition = shouldReduceMotion\n    ? { duration: 0 }\n    : (transition ?? { type: \"spring\", duration: 0.5, bounce: 0 });\n  const contentVariants = shouldReduceMotion\n    ? reducedMotionStepVariants\n    : stepVariants;\n\n  if (!activeStep) {\n    return null;\n  }\n\n  return (\n    <MotionConfig reducedMotion=\"user\" transition={resolvedTransition}>\n      {/*\n        `contain: layout` scopes the per-frame reflow of the height animation to this\n        element, so the layout cost stays bounded no matter how heavy a step's content\n        is. This keeps the transition smooth on lower-powered / mobile devices; removing\n        it can cause frame drops there. It does not affect the measured `auto` height.\n      */}\n      <motion.div\n        {...props}\n        data-slot=\"multi-step\"\n        initial={false}\n        animate={\n          shouldReduceMotion ? { height: \"auto\" } : { height: height ?? \"auto\" }\n        }\n        className={cn(\n          \"overflow-hidden rounded-lg border bg-background contain-[layout]\",\n          className,\n        )}\n      >\n        <div\n          ref={innerRef}\n          data-slot=\"multi-step-inner\"\n          className={cn(\"flex flex-col\", innerClassName)}\n        >\n          <div\n            data-slot=\"multi-step-viewport\"\n            className=\"relative overflow-hidden\"\n          >\n            <AnimatePresence\n              mode={shouldReduceMotion ? \"sync\" : \"popLayout\"}\n              initial={false}\n              custom={direction}\n            >\n              <motion.div\n                key={activeStep.id}\n                data-slot=\"multi-step-content\"\n                custom={direction}\n                variants={contentVariants}\n                initial=\"initial\"\n                animate=\"active\"\n                exit=\"exit\"\n                className={cn(\"w-full p-4\", contentClassName)}\n              >\n                {activeStep.content}\n              </motion.div>\n            </AnimatePresence>\n          </div>\n          {footer ?? (\n            <motion.div\n              layout={!shouldReduceMotion}\n              data-slot=\"multi-step-actions\"\n              className={cn(\n                \"flex items-center justify-between gap-3 border-t p-4\",\n                actionsClassName,\n              )}\n            >\n              <Button\n                type=\"button\"\n                variant=\"outline\"\n                disabled={disableBack || isFirstStep}\n                onClick={() => setStep(selectedStep - 1, -1)}\n              >\n                {backLabel}\n              </Button>\n              <Button\n                type=\"button\"\n                disabled={disableContinue || isLastStep}\n                onClick={() => setStep(selectedStep + 1, 1)}\n              >\n                {isLastStep ? completeLabel : continueLabel}\n              </Button>\n            </motion.div>\n          )}\n        </div>\n      </motion.div>\n    </MotionConfig>\n  );\n}\n\nconst stepVariants = {\n  initial: (direction: StepDirection) => ({\n    x: `${110 * direction}%`,\n    opacity: 0,\n  }),\n  active: { x: \"0%\", opacity: 1 },\n  exit: (direction: StepDirection) => ({\n    x: `${-110 * direction}%`,\n    opacity: 0,\n  }),\n};\n\nconst reducedMotionStepVariants = {\n  initial: { opacity: 0 },\n  active: { opacity: 1 },\n  exit: { opacity: 0 },\n};\n\nfunction clampStep(step: number, stepCount: number) {\n  if (stepCount <= 0) return 0;\n\n  return Math.min(Math.max(step, 0), stepCount - 1);\n}\n",
      "type": "registry:ui",
      "target": "components/ui/multi-step.tsx"
    }
  ],
  "meta": {
    "tags": [
      "wizard",
      "stepper",
      "onboarding",
      "form-flow"
    ],
    "effects": [
      "slide",
      "height-animation"
    ]
  },
  "categories": [
    "flow"
  ],
  "type": "registry:ui"
}