{
  "name": "interactive-stepper",
  "type": "registry:ui",
  "title": "Interactive Stepper",
  "description": "Fluid directional animated stepper input with spring vertical sliding numerals and boundary protections.",
  "dependencies": [
    "framer-motion",
    "lucide-react"
  ],
  "files": [
    {
      "path": "registry/forms/interactive-stepper/interactive-stepper.tsx",
      "type": "registry:ui",
      "target": "components/ui/interactive-stepper.tsx",
      "content": "import React, { useState } from \"react\";\nimport { motion, AnimatePresence } from \"framer-motion\";\nimport { Minus, Plus } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface InteractiveStepperProps {\n  /** Initial value */\n  defaultValue?: number;\n  /** Minimum value */\n  min?: number;\n  /** Maximum value */\n  max?: number;\n  /** Step increment */\n  step?: number;\n  /** onChange callback */\n  onChange?: (value: number) => void;\n  /** Size variant */\n  size?: \"sm\" | \"md\" | \"lg\";\n  /** Label for accessibility */\n  label?: string;\n  /** Additional CSS classes */\n  className?: string;\n}\n\nexport function InteractiveStepper({\n  defaultValue = 1,\n  min = 0,\n  max = 99,\n  step = 1,\n  onChange,\n  size = \"md\",\n  label = \"Quantity Stepper\",\n  className,\n}: InteractiveStepperProps) {\n  const [value, setValue] = useState(defaultValue);\n  const [direction, setDirection] = useState<\"up\" | \"down\">(\"up\");\n\n  const sizes = {\n    sm: {\n      container: \"h-9 gap-1.5\",\n      button: \"h-9 w-9 text-xs\",\n      display: \"w-12 h-9 text-sm\",\n    },\n    md: {\n      container: \"h-11 gap-2\",\n      button: \"h-11 w-11 text-sm\",\n      display: \"w-16 h-11 text-base\",\n    },\n    lg: {\n      container: \"h-13 gap-2.5\",\n      button: \"h-13 w-13 text-base\",\n      display: \"w-20 h-13 text-lg\",\n    },\n  };\n\n  const iconSizes = {\n    sm: \"h-3.5 w-3.5\",\n    md: \"h-4 w-4\",\n    lg: \"h-5 w-5\",\n  };\n\n  const s = sizes[size];\n\n  const handleDecrement = () => {\n    if (value - step < min) return;\n    setDirection(\"down\");\n    const next = Math.max(min, value - step);\n    setValue(next);\n    onChange?.(next);\n  };\n\n  const handleIncrement = () => {\n    if (value + step > max) return;\n    setDirection(\"up\");\n    const next = Math.min(max, value + step);\n    setValue(next);\n    onChange?.(next);\n  };\n\n  const isAtMin = value <= min;\n  const isAtMax = value >= max;\n\n  const variants = {\n    enter: (dir: \"up\" | \"down\") => ({\n      y: dir === \"up\" ? 18 : -18,\n      opacity: 0,\n    }),\n    center: { y: 0, opacity: 1 },\n    exit: (dir: \"up\" | \"down\") => ({\n      y: dir === \"up\" ? -18 : 18,\n      opacity: 0,\n    }),\n  };\n\n  return (\n    <div\n      className={cn(\"inline-flex items-center select-none\", s.container, className)}\n      aria-label={label}\n    >\n      {/* Decrement Button */}\n      <motion.button\n        type=\"button\"\n        onClick={handleDecrement}\n        disabled={isAtMin}\n        aria-label=\"Decrement\"\n        className={cn(\n          \"relative flex items-center justify-center rounded-xl border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 shadow-sm transition-colors text-zinc-700 dark:text-zinc-200 cursor-pointer\",\n          \"hover:bg-zinc-100 dark:hover:bg-zinc-800 hover:text-zinc-950 dark:hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500\",\n          \"disabled:opacity-30 disabled:cursor-not-allowed\",\n          s.button\n        )}\n        whileTap={{ scale: isAtMin ? 1 : 0.9 }}\n        whileHover={{ scale: isAtMin ? 1 : 1.05 }}\n        transition={{ type: \"spring\", stiffness: 400, damping: 20 }}\n      >\n        <Minus className={iconSizes[size]} />\n      </motion.button>\n\n      {/* Number Display */}\n      <div\n        className={cn(\n          \"relative flex items-center justify-center overflow-hidden rounded-xl border border-zinc-200 dark:border-zinc-800 bg-zinc-50/70 dark:bg-zinc-900/50 font-mono font-bold tabular-nums text-zinc-900 dark:text-zinc-50 shadow-inner\",\n          s.display\n        )}\n      >\n        <AnimatePresence mode=\"popLayout\" custom={direction} initial={false}>\n          <motion.span\n            key={value}\n            custom={direction}\n            variants={variants}\n            initial=\"enter\"\n            animate=\"center\"\n            exit=\"exit\"\n            transition={{\n              y: { type: \"spring\", stiffness: 450, damping: 30 },\n              opacity: { duration: 0.12 },\n            }}\n            className=\"absolute\"\n          >\n            {value}\n          </motion.span>\n        </AnimatePresence>\n      </div>\n\n      {/* Increment Button */}\n      <motion.button\n        type=\"button\"\n        onClick={handleIncrement}\n        disabled={isAtMax}\n        aria-label=\"Increment\"\n        className={cn(\n          \"relative flex items-center justify-center rounded-xl border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 shadow-sm transition-colors text-zinc-700 dark:text-zinc-200 cursor-pointer\",\n          \"hover:bg-zinc-100 dark:hover:bg-zinc-800 hover:text-zinc-950 dark:hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500\",\n          \"disabled:opacity-30 disabled:cursor-not-allowed\",\n          s.button\n        )}\n        whileTap={{ scale: isAtMax ? 1 : 0.9 }}\n        whileHover={{ scale: isAtMax ? 1 : 1.05 }}\n        transition={{ type: \"spring\", stiffness: 400, damping: 20 }}\n      >\n        <Plus className={iconSizes[size]} />\n      </motion.button>\n    </div>\n  );\n}\n\nexport default InteractiveStepper;\n"
    }
  ],
  "$schema": "https://www.cooki-ui.design/schema/registry-item.json"
}