{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion","lucide-react","input-otp"],"description":"A AnimatedOTPInput component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { OTPInput, OTPInputContext } from \"input-otp\";\nimport { MinusIcon } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { type ReactNode, useContext, useEffect, useState } from \"react\";\n\n// Animation constants\nconst EASE_OUT_QUINT_X1 = 0.22;\nconst EASE_OUT_QUINT_Y1 = 1;\nconst EASE_OUT_QUINT_X2 = 0.36;\nconst EASE_OUT_QUINT_Y2 = 1;\nconst EASE_OUT_QUINT = [\n  EASE_OUT_QUINT_X1,\n  EASE_OUT_QUINT_Y1,\n  EASE_OUT_QUINT_X2,\n  EASE_OUT_QUINT_Y2,\n] as const;\nconst ANIMATION_DURATION_SHORT = 0.1;\nconst ANIMATION_DURATION_MEDIUM = 0.15;\nconst ANIMATION_DURATION_STANDARD = 0.2;\nconst ANIMATION_DURATION_LONG = 0.3;\nconst STAGGER_DELAY = 0.05;\nconst SCALE_FILLED = 1.05;\nconst SCALE_HOVER = 1.02;\nconst SCALE_TAP = 0.98;\nconst INITIAL_SCALE = 0.8;\nconst INITIAL_Y = 10;\nconst SEPARATOR_DELAY = 0.15;\n\nexport interface AnimatedInputOTPProps {\n  \"aria-describedby\"?: string;\n  \"aria-label\"?: string;\n  className?: string;\n  containerClassName?: string;\n  maxLength?: number;\n  onChange?: (value: string) => void;\n  onComplete?: (value: string) => void;\n  value?: string;\n}\n\nfunction AnimatedInputOTP({\n  className,\n  containerClassName,\n  value,\n  onChange,\n  onComplete,\n  maxLength = 6,\n  children,\n  ...props\n}: AnimatedInputOTPProps & { children: ReactNode }) {\n  const handleChange = (newValue: string) => {\n    // Only allow numeric characters\n    const numericValue = newValue.replace(/[^0-9]/g, \"\");\n    onChange?.(numericValue);\n  };\n\n  return (\n    <OTPInput\n      aria-describedby={props[\"aria-describedby\"]}\n      aria-label={props[\"aria-label\"] || \"One-time password input\"}\n      className={cn(\"disabled:cursor-not-allowed\", className)}\n      containerClassName={cn(\n        \"flex items-center gap-2 has-disabled:opacity-50\",\n        containerClassName\n      )}\n      data-slot=\"input-otp\"\n      maxLength={maxLength}\n      onChange={handleChange}\n      onComplete={onComplete}\n      value={value}\n      {...props}\n    >\n      {children}\n    </OTPInput>\n  );\n}\n\ninterface AnimatedInputOTPGroupProps {\n  children?: ReactNode;\n  className?: string;\n}\n\nfunction AnimatedInputOTPGroup({\n  className,\n  children,\n}: AnimatedInputOTPGroupProps) {\n  const shouldReduceMotion = useReducedMotion();\n  return (\n    <motion.div\n      animate={shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }}\n      className={cn(\"flex items-center gap-2\", className)}\n      data-slot=\"input-otp-group\"\n      initial={\n        shouldReduceMotion ? { opacity: 1 } : { opacity: 0, y: INITIAL_Y }\n      }\n      transition={\n        shouldReduceMotion\n          ? { duration: 0 }\n          : {\n              duration: ANIMATION_DURATION_LONG,\n              ease: EASE_OUT_QUINT,\n            }\n      }\n    >\n      {children}\n    </motion.div>\n  );\n}\n\ninterface AnimatedInputOTPSlotProps {\n  className?: string;\n  index: number;\n}\n\nfunction AnimatedInputOTPSlot({ index, className }: AnimatedInputOTPSlotProps) {\n  const inputOTPContext = useContext(OTPInputContext);\n  const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {};\n  const [isFilled, setIsFilled] = useState(false);\n  const shouldReduceMotion = useReducedMotion();\n\n  useEffect(() => {\n    if (char && !isFilled) {\n      setIsFilled(true);\n    } else if (!char && isFilled) {\n      setIsFilled(false);\n    }\n  }, [char, isFilled]);\n\n  return (\n    <motion.div\n      animate={\n        shouldReduceMotion\n          ? { opacity: 1 }\n          : {\n              opacity: 1,\n              scale: isFilled ? SCALE_FILLED : 1,\n              y: 0,\n            }\n      }\n      className={cn(\n        \"relative flex h-9 w-9 items-center justify-center rounded-md border border-zinc-300 bg-background text-sm shadow-sm outline-none transition-all aria-invalid:border-destructive data-[active=true]:z-10 data-[active=true]:border-ring data-[active=true]:ring-[3px] data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:border-destructive data-[active=true]:aria-invalid:ring-destructive/20 dark:border-zinc-700 dark:bg-input/30 dark:data-[active=true]:aria-invalid:ring-destructive/40\",\n        className\n      )}\n      data-active={isActive}\n      data-slot=\"input-otp-slot\"\n      initial={\n        shouldReduceMotion\n          ? { opacity: 1 }\n          : { opacity: 0, scale: INITIAL_SCALE, y: INITIAL_Y }\n      }\n      transition={\n        shouldReduceMotion\n          ? { duration: 0 }\n          : {\n              delay: index * STAGGER_DELAY,\n              duration: ANIMATION_DURATION_STANDARD,\n              ease: EASE_OUT_QUINT,\n              scale: {\n                duration: ANIMATION_DURATION_MEDIUM,\n                ease: EASE_OUT_QUINT,\n              },\n            }\n      }\n      whileHover={\n        shouldReduceMotion\n          ? {}\n          : {\n              scale: SCALE_HOVER,\n              transition: {\n                duration: ANIMATION_DURATION_MEDIUM,\n                ease: EASE_OUT_QUINT,\n              },\n            }\n      }\n      whileTap={\n        shouldReduceMotion\n          ? {}\n          : {\n              scale: SCALE_TAP,\n              transition: {\n                duration: ANIMATION_DURATION_SHORT,\n                ease: EASE_OUT_QUINT,\n              },\n            }\n      }\n    >\n      <AnimatePresence mode=\"wait\">\n        {char ? (\n          <motion.span\n            animate={\n              shouldReduceMotion\n                ? { opacity: 1, scale: 1 }\n                : { opacity: 1, rotateY: 0, scale: 1 }\n            }\n            className=\"font-medium\"\n            exit={\n              shouldReduceMotion\n                ? { opacity: 0, transition: { duration: 0 } }\n                : { opacity: 0, rotateY: 90, scale: 0.5 }\n            }\n            initial={\n              shouldReduceMotion\n                ? { opacity: 1, scale: 1 }\n                : { opacity: 0, rotateY: -90, scale: 0.5 }\n            }\n            key={char}\n            transition={\n              shouldReduceMotion\n                ? { duration: 0 }\n                : {\n                    duration: ANIMATION_DURATION_STANDARD,\n                    ease: EASE_OUT_QUINT,\n                  }\n            }\n          >\n            {char}\n          </motion.span>\n        ) : null}\n      </AnimatePresence>\n\n      {hasFakeCaret && !shouldReduceMotion && (\n        <motion.div\n          animate={{ opacity: 1 }}\n          className=\"pointer-events-none absolute inset-0 flex items-center justify-center\"\n          exit={{ opacity: 0 }}\n          initial={{ opacity: 0 }}\n          transition={{ duration: ANIMATION_DURATION_SHORT }}\n        >\n          <motion.div\n            animate={{ opacity: [0, 1, 0] }}\n            className=\"h-4 w-px bg-foreground\"\n            transition={{\n              duration: 1,\n              ease: [0.645, 0.045, 0.355, 1],\n              repeat: Number.POSITIVE_INFINITY,\n            }}\n          />\n        </motion.div>\n      )}\n    </motion.div>\n  );\n}\n\nfunction AnimatedInputOTPSeparator() {\n  const shouldReduceMotion = useReducedMotion();\n  return (\n    <motion.div\n      animate={shouldReduceMotion ? { opacity: 1 } : { opacity: 1, scale: 1 }}\n      data-slot=\"input-otp-separator\"\n      initial={\n        shouldReduceMotion\n          ? { opacity: 1 }\n          : { opacity: 0, scale: INITIAL_SCALE }\n      }\n      transition={\n        shouldReduceMotion\n          ? { duration: 0 }\n          : {\n              delay: SEPARATOR_DELAY,\n              duration: ANIMATION_DURATION_LONG,\n              ease: EASE_OUT_QUINT,\n            }\n      }\n    >\n      <MinusIcon className=\"h-4 w-4 text-muted-foreground\" />\n    </motion.div>\n  );\n}\n\n// Main component that combines everything\nexport function AnimatedOTPInput({\n  maxLength = 6,\n  className,\n  value,\n  onChange,\n  onComplete,\n  ...props\n}: AnimatedInputOTPProps) {\n  return (\n    <AnimatedInputOTP\n      className={className}\n      maxLength={maxLength}\n      onChange={onChange}\n      onComplete={onComplete}\n      value={value}\n      {...props}\n    >\n      <AnimatedInputOTPGroup>\n        <AnimatedInputOTPSlot index={0} />\n        <AnimatedInputOTPSlot index={1} />\n        <AnimatedInputOTPSlot index={2} />\n      </AnimatedInputOTPGroup>\n      <AnimatedInputOTPSeparator />\n      <AnimatedInputOTPGroup>\n        <AnimatedInputOTPSlot index={3} />\n        <AnimatedInputOTPSlot index={4} />\n        <AnimatedInputOTPSlot index={5} />\n      </AnimatedInputOTPGroup>\n    </AnimatedInputOTP>\n  );\n}\n\nexport {\n  AnimatedInputOTP,\n  AnimatedInputOTPGroup,\n  AnimatedInputOTPSeparator,\n  AnimatedInputOTPSlot,\n};\n\nexport default AnimatedOTPInput;\n","path":"index.tsx","target":"components/smoothui/animated-o-t-p-input/index.tsx","type":"registry:ui"}],"name":"animated-o-t-p-input","registryDependencies":[],"title":"Animated O T P Input","type":"registry:ui"}