{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "otp-input",
  "title": "OTP Input",
  "description": "A component for entering one-time passwords with individual character slots.",
  "dependencies": [
    "react-native-reanimated"
  ],
  "registryDependencies": [
    "@tetra-ui/input",
    "@tetra-ui/text"
  ],
  "files": [
    {
      "path": "ui/otp-input.tsx",
      "content": "import { cva } from \"class-variance-authority\";\nimport {\n  createContext,\n  useCallback,\n  useContext,\n  useEffect,\n  useImperativeHandle,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport {\n  type BlurEvent,\n  type FocusEvent,\n  Pressable,\n  type TextInput as RNTextInput,\n  View,\n} from \"react-native\";\nimport Animated, {\n  FadeIn,\n  FadeOut,\n  FlipInXDown,\n  FlipOutXDown,\n  useAnimatedStyle,\n  useSharedValue,\n  withRepeat,\n  withTiming,\n} from \"react-native-reanimated\";\nimport { cn } from \"../lib/utils\";\nimport { Input } from \"./input\";\nimport { Text } from \"./text\";\n\n// Constants\nconst CARET_ANIMATION_DURATION = 500;\nconst SLOT_VALUE_ENTER_DURATION = 250;\nconst SLOT_VALUE_EXIT_DURATION = 100;\n\nexport const REGEXP_ONLY_DIGITS = \"^\\\\d+$\";\nexport const REGEXP_ONLY_CHARS = \"^[a-zA-Z]+$\";\nexport const REGEXP_ONLY_DIGITS_AND_CHARS = \"^[a-zA-Z0-9]+$\";\n\nconst AnimatedText = Animated.createAnimatedComponent(Text);\n\n// Types\ntype SlotData = {\n  index: number;\n  char: string | null;\n  placeholderChar: string | null;\n  isActive: boolean;\n  isCaretVisible: boolean;\n};\n\ntype OTPInputContextValue = {\n  value: string;\n  maxLength: number;\n  isFocused: boolean;\n  disabled?: boolean;\n  invalid?: boolean;\n  secureTextEntry?: boolean;\n  slots: SlotData[];\n  inputRef: React.RefObject<RNTextInput | null>;\n  focus: () => void;\n  onSlotPress: (index: number) => void;\n};\n\ntype OTPInputSlotContextValue = {\n  slot: SlotData;\n  isActive: boolean;\n  isCaretVisible: boolean;\n};\n\nexport type OTPInputRef = {\n  focus: () => void;\n  blur: () => void;\n  clear: () => void;\n  setValue: (value: string) => void;\n};\n\nexport type OTPInputProps = {\n  maxLength: number;\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (value: string) => void;\n  onComplete?: (value: string) => void;\n  disabled?: boolean;\n  invalid?: boolean;\n  pattern?: string;\n  inputMode?: React.ComponentProps<typeof RNTextInput>[\"inputMode\"];\n  placeholder?: string;\n  secureTextEntry?: boolean;\n  onFocus?: (e: FocusEvent) => void;\n  onBlur?: (e: BlurEvent) => void;\n  className?: string;\n  children?: React.ReactNode;\n  ref?: React.RefObject<OTPInputRef>;\n};\n\nexport type OTPInputGroupProps = React.ComponentProps<typeof View>;\n\nexport type OTPInputSlotProps = React.ComponentProps<typeof Pressable> & {\n  index: number;\n  children?: React.ReactNode;\n};\n\nexport type OTPInputSeparatorProps = React.ComponentProps<typeof View>;\n\nexport type OTPInputSlotPlaceholderProps = React.ComponentProps<typeof Text> & {\n  children?: string;\n};\n\nexport type OTPInputSlotValueProps = React.ComponentProps<typeof Text> & {\n  children?: string;\n};\n\nexport type OTPInputSlotCaretProps = React.ComponentProps<typeof Animated.View>;\n\n// Utils\nconst defaultPasteTransformer = (maxLength: number) => {\n  return (pasted: string): string => {\n    const otpRegex = new RegExp(`(?<!\\\\d)\\\\d{${maxLength}}(?!\\\\d)`);\n    const match = pasted.match(otpRegex);\n\n    if (match) {\n      return match[0];\n    }\n\n    return pasted.replace(/\\D/g, \"\").slice(0, maxLength);\n  };\n};\n\n// Context\nconst OTPInputContext = createContext<OTPInputContextValue | null>(null);\nconst OTPInputSlotContext = createContext<OTPInputSlotContextValue | null>(\n  null\n);\n\nconst useOTPInput = () => {\n  const context = useContext(OTPInputContext);\n\n  if (!context) {\n    throw new Error(\"useOTPInput must be used within an OTPInput component\");\n  }\n\n  return context;\n};\n\nconst useOTPInputSlot = () => {\n  const context = useContext(OTPInputSlotContext);\n\n  if (!context) {\n    throw new Error(\n      \"useOTPInputSlot must be used within an OTPInputSlot component\"\n    );\n  }\n\n  return context;\n};\n\n// Components\nexport const OTPInput = ({\n  maxLength,\n  value: valueProp,\n  defaultValue,\n  onValueChange,\n  onComplete,\n  disabled,\n  invalid,\n  pattern,\n  inputMode = \"numeric\",\n  placeholder,\n  secureTextEntry,\n  onFocus: onFocusProp,\n  onBlur: onBlurProp,\n  className,\n  children,\n  ref,\n}: OTPInputProps) => {\n  const [internalValue, setInternalValue] = useState(defaultValue ?? \"\");\n  const [isFocused, setIsFocused] = useState(false);\n  const inputRef = useRef<RNTextInput>(null);\n\n  const isControlled = valueProp !== undefined;\n  const value = isControlled ? valueProp : internalValue;\n\n  const setValue = useCallback(\n    (nextValue: string) => {\n      if (!isControlled) {\n        setInternalValue(nextValue);\n      }\n\n      onValueChange?.(nextValue);\n    },\n    [isControlled, onValueChange]\n  );\n\n  const regexp = useMemo(() => {\n    if (!pattern) {\n      return null;\n    }\n\n    return new RegExp(pattern);\n  }, [pattern]);\n\n  const pasteTransformFn = useMemo(\n    () => defaultPasteTransformer(maxLength),\n    [maxLength]\n  );\n\n  const onChangeText = useCallback(\n    (text: string) => {\n      const isPaste = text.length > value.length + 1;\n      const transformedText = isPaste ? pasteTransformFn(text) : text;\n      const newValue = transformedText.slice(0, maxLength);\n\n      if (newValue.length > 0 && regexp && !regexp.test(newValue)) {\n        return;\n      }\n\n      setValue(newValue);\n\n      if (newValue.length === maxLength) {\n        onComplete?.(newValue);\n      }\n    },\n    [maxLength, onComplete, pasteTransformFn, regexp, setValue, value.length]\n  );\n\n  const onFocus = useCallback(\n    (e: FocusEvent) => {\n      setIsFocused(true);\n      onFocusProp?.(e);\n    },\n    [onFocusProp]\n  );\n\n  const onBlur = useCallback(\n    (e: BlurEvent) => {\n      setIsFocused(false);\n      onBlurProp?.(e);\n    },\n    [onBlurProp]\n  );\n\n  const focus = useCallback(() => {\n    inputRef.current?.focus();\n  }, []);\n\n  const blur = useCallback(() => {\n    inputRef.current?.blur();\n  }, []);\n\n  const clear = useCallback(() => {\n    inputRef.current?.clear();\n    setValue(\"\");\n  }, [setValue]);\n\n  useImperativeHandle(\n    ref,\n    () => ({\n      blur,\n      clear,\n      focus,\n      setValue: onChangeText,\n    }),\n    [blur, clear, focus, onChangeText]\n  );\n\n  const slots = useMemo<SlotData[]>(() => {\n    return Array.from({ length: maxLength }, (_, slotIdx) => {\n      const char = value[slotIdx] ?? null;\n      const isActive = isFocused && slotIdx === value.length;\n      const placeholderChar =\n        isActive || char !== null ? null : (placeholder?.[slotIdx] ?? null);\n\n      return {\n        char,\n        index: slotIdx,\n        isActive,\n        isCaretVisible: isActive && char === null,\n        placeholderChar,\n      };\n    });\n  }, [isFocused, maxLength, placeholder, value]);\n\n  const onSlotPress = useCallback(\n    (_index: number) => {\n      focus();\n    },\n    [focus]\n  );\n\n  const contextValue = useMemo<OTPInputContextValue>(\n    () => ({\n      disabled,\n      focus,\n      inputRef,\n      invalid,\n      isFocused,\n      maxLength,\n      onSlotPress,\n      secureTextEntry,\n      slots,\n      value,\n    }),\n    [\n      disabled,\n      focus,\n      invalid,\n      isFocused,\n      maxLength,\n      onSlotPress,\n      secureTextEntry,\n      slots,\n      value,\n    ]\n  );\n\n  return (\n    <OTPInputContext.Provider value={contextValue}>\n      <Pressable\n        accessibilityRole=\"none\"\n        className={cn(\"relative flex-row items-center gap-2\", className)}\n        disabled={disabled}\n        onPress={focus}\n      >\n        <Input\n          autoComplete={secureTextEntry ? \"off\" : \"one-time-code\"}\n          caretHidden\n          className=\"absolute h-px w-px opacity-0\"\n          disabled={disabled}\n          inputMode={inputMode}\n          onBlur={onBlur}\n          onChangeText={onChangeText}\n          onFocus={onFocus}\n          ref={inputRef}\n          secureTextEntry={secureTextEntry}\n          textContentType={secureTextEntry ? \"password\" : \"oneTimeCode\"}\n          value={value}\n        />\n        {children}\n      </Pressable>\n    </OTPInputContext.Provider>\n  );\n};\n\nexport const OTPInputGroup = ({ className, ...props }: OTPInputGroupProps) => {\n  return (\n    <View className={cn(\"flex-row items-center gap-2\", className)} {...props} />\n  );\n};\n\nexport const OTPInputSlot = ({\n  index,\n  children,\n  className,\n  disabled: disabledProp,\n  ...props\n}: OTPInputSlotProps) => {\n  const { slots, disabled, invalid, onSlotPress } = useOTPInput();\n  const slot = slots[index];\n  const isDisabled = disabledProp ?? disabled;\n\n  const slotContextValue = useMemo<OTPInputSlotContextValue | null>(() => {\n    if (!slot) {\n      return null;\n    }\n\n    return {\n      isActive: slot.isActive,\n      isCaretVisible: slot.isCaretVisible,\n      slot,\n    };\n  }, [slot]);\n\n  if (!(slot && slotContextValue)) {\n    if (__DEV__) {\n      throw new Error(\n        `OTPInputSlot index ${index} is out of range. Must be between 0 and ${slots.length - 1}.`\n      );\n    }\n\n    return null;\n  }\n\n  return (\n    <OTPInputSlotContext.Provider value={slotContextValue}>\n      <Pressable\n        accessibilityRole=\"none\"\n        className={cn(\n          otpInputSlotVariants({\n            disabled: isDisabled,\n            invalid,\n            isActive: slot.isActive,\n          }),\n          className\n        )}\n        disabled={isDisabled}\n        onPress={() => onSlotPress(index)}\n        {...props}\n      >\n        {children ?? (\n          <>\n            <OTPInputSlotPlaceholder />\n            <OTPInputSlotValue />\n            <OTPInputSlotCaret />\n          </>\n        )}\n      </Pressable>\n    </OTPInputSlotContext.Provider>\n  );\n};\n\nexport const OTPInputSlotPlaceholder = ({\n  children,\n  className,\n  ...props\n}: OTPInputSlotPlaceholderProps) => {\n  const { slot, isActive } = useOTPInputSlot();\n  const displayChar = children ?? slot.placeholderChar ?? \"\";\n\n  if (slot.char || isActive || !displayChar) {\n    return null;\n  }\n\n  return (\n    <Text\n      className={cn(\"font-medium text-lg text-muted-foreground/50\", className)}\n      {...props}\n    >\n      {displayChar}\n    </Text>\n  );\n};\n\nexport const OTPInputSlotValue = ({\n  children,\n  className,\n  ...props\n}: OTPInputSlotValueProps) => {\n  const { slot } = useOTPInputSlot();\n  const { secureTextEntry } = useOTPInput();\n  const displayChar =\n    children ?? (secureTextEntry && slot.char ? \"•\" : slot.char) ?? \"\";\n\n  if (!displayChar) {\n    return null;\n  }\n\n  return (\n    <Animated.View\n      entering={FadeIn.duration(SLOT_VALUE_ENTER_DURATION)}\n      exiting={FadeOut.duration(SLOT_VALUE_EXIT_DURATION)}\n    >\n      <AnimatedText\n        className={cn(\"font-medium text-lg\", className)}\n        entering={FlipInXDown.duration(SLOT_VALUE_ENTER_DURATION)}\n        exiting={FlipOutXDown.duration(SLOT_VALUE_ENTER_DURATION)}\n        {...props}\n      >\n        {displayChar}\n      </AnimatedText>\n    </Animated.View>\n  );\n};\n\nexport const OTPInputSlotCaret = ({\n  className,\n  ...props\n}: OTPInputSlotCaretProps) => {\n  const { isCaretVisible } = useOTPInputSlot();\n  const opacity = useSharedValue(1);\n\n  useEffect(() => {\n    opacity.value = withRepeat(\n      withTiming(0, { duration: CARET_ANIMATION_DURATION }),\n      -1,\n      true\n    );\n  }, [opacity]);\n\n  const animatedStyle = useAnimatedStyle(() => {\n    return {\n      opacity: opacity.value,\n    };\n  });\n\n  if (!isCaretVisible) {\n    return null;\n  }\n\n  return (\n    <Animated.View\n      className={cn(\n        \"absolute h-4 w-0.5 rounded-full bg-muted-foreground\",\n        className\n      )}\n      pointerEvents=\"none\"\n      style={animatedStyle}\n      {...props}\n    />\n  );\n};\n\nexport const OTPInputSeparator = ({\n  className,\n  ...props\n}: OTPInputSeparatorProps) => {\n  useOTPInput();\n\n  return (\n    <View\n      className={cn(\"h-0.5 w-2 rounded-full bg-input\", className)}\n      {...props}\n    />\n  );\n};\n\n// Styles\nconst otpInputSlotVariants = cva(\n  \"relative h-12 w-11 items-center justify-center overflow-hidden rounded-lg border bg-background\",\n  {\n    defaultVariants: {\n      disabled: false,\n      invalid: false,\n      isActive: false,\n    },\n    variants: {\n      disabled: {\n        false: \"\",\n        true: \"pointer-events-none opacity-50\",\n      },\n      invalid: {\n        false: \"\",\n        true: \"border-destructive\",\n      },\n      isActive: {\n        false: \"border-input\",\n        true: \"border-2 border-ring\",\n      },\n    },\n  }\n);\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}
