import { Text } from '@/components/ui/text'; import { useColor } from '@/hooks/useColor'; import { CORNERS, FONT_SIZE } from '@/theme/globals'; import React, { forwardRef, useCallback, useImperativeHandle, useRef, useState, } from 'react'; import { NativeSyntheticEvent, Pressable, TextInput, TextInputKeyPressEventData, TextInputProps, TextStyle, View, ViewStyle, } from 'react-native'; export interface InputOTPProps extends Omit { /** Number of OTP digits */ length?: number; /** Current OTP value */ value?: string; /** Called when OTP value changes */ onChangeText?: (value: string) => void; /** Called when OTP is complete */ onComplete?: (value: string) => void; /** Error message to display */ error?: string; /** Disabled state */ disabled?: boolean; /** Container style */ containerStyle?: ViewStyle; /** Individual slot style */ slotStyle?: ViewStyle; /** Error style */ errorStyle?: TextStyle; /** Whether to mask the input (show dots instead of numbers) */ masked?: boolean; /** Separator component between slots */ separator?: React.ReactNode; /** Whether to show cursor in active slot */ showCursor?: boolean; } export interface InputOTPRef { focus: () => void; blur: () => void; clear: () => void; getValue: () => string; } export const InputOTP = forwardRef( ( { length = 6, value = '', onChangeText, onComplete, error, disabled = false, containerStyle, slotStyle, errorStyle, masked = false, separator, showCursor = true, onFocus, onBlur, ...textInputProps }, ref ) => { const [isFocused, setIsFocused] = useState(false); const [activeIndex, setActiveIndex] = useState(0); const inputRef = useRef(null); // Theme colors const cardColor = useColor('card'); const textColor = useColor('text'); const muted = useColor('textMuted'); const borderColor = useColor('border'); const primary = useColor('primary'); const danger = useColor('red'); const background = useColor('background'); // Normalize value to ensure it doesn't exceed length const normalizedValue = value.slice(0, length); // Calculate active index based on current value const currentActiveIndex = Math.min(normalizedValue.length, length - 1); // Expose methods via ref useImperativeHandle(ref, () => ({ focus: () => inputRef.current?.focus(), blur: () => inputRef.current?.blur(), clear: () => { onChangeText?.(''); setActiveIndex(0); }, getValue: () => normalizedValue, })); const handleChangeText = useCallback( (text: string) => { // Only allow numeric input const cleanText = text.replace(/[^0-9]/g, ''); const limitedText = cleanText.slice(0, length); onChangeText?.(limitedText); setActiveIndex(Math.min(limitedText.length, length - 1)); // Call onComplete when OTP is fully entered if (limitedText.length === length) { onComplete?.(limitedText); } }, [length, onChangeText, onComplete] ); const handleKeyPress = useCallback( (e: NativeSyntheticEvent) => { const { key } = e.nativeEvent; if (key === 'Backspace' && normalizedValue.length > 0) { const newValue = normalizedValue.slice(0, -1); onChangeText?.(newValue); setActiveIndex(Math.max(0, newValue.length)); } }, [normalizedValue, onChangeText] ); const handleFocus = useCallback( (e: any) => { setIsFocused(true); setActiveIndex(normalizedValue.length); onFocus?.(e); }, [normalizedValue.length, onFocus] ); const handleBlur = useCallback( (e: any) => { setIsFocused(false); onBlur?.(e); }, [onBlur] ); const handleSlotPress = useCallback(() => { if (!disabled) { inputRef.current?.focus(); } }, [disabled]); // Generate slots const slots = Array.from({ length }, (_, index) => { const hasValue = index < normalizedValue.length; const isActive = isFocused && index === currentActiveIndex; const displayValue = hasValue ? masked ? '•' : normalizedValue[index] : ''; return ( {displayValue} {/* Cursor */} {showCursor && isActive && !hasValue && ( )} {/* Separator */} {separator && index < length - 1 && ( {separator} )} ); }); const renderContent = () => ( {/* Hidden TextInput for handling input */} {/* OTP Slots */} {slots} {/* Error Message */} {error && ( {error} )} ); return renderContent(); } ); InputOTP.displayName = 'InputOTP'; // Optional: Export a preset with separator export const InputOTPWithSeparator = forwardRef< InputOTPRef, Omit >((props, ref) => ( - } {...props} /> )); InputOTPWithSeparator.displayName = 'InputOTPWithSeparator';