'use client' import { MinusIcon } from 'lucide-react'; import * as React from 'react'; import { InputOTP, InputOTPGroup, InputOTPSlot } from '../input-otp'; import { cn } from '../../../lib'; import { useIsMobile } from '../../../hooks'; import { createPasteHandler, useSmartOTP } from './use-otp-input'; import type { SmartOTPProps } from './types' /** * Size variants for OTP slots — fixed square dimensions (Vercel/Linear look). * The boxes never stretch to the container width: a code is a small, tidy * cluster of fixed cells, centered, not a row of full-width fields. */ const sizeVariants = { sm: 'h-10 w-10 text-base', default: 'h-12 w-12 text-lg', lg: 'h-14 w-14 text-xl', } /** * OTP Separator Component */ const InputOTPSeparator = React.forwardRef< HTMLDivElement, React.ComponentPropsWithoutRef<'div'> >(({ className, ...props }, ref) => (
)) InputOTPSeparator.displayName = 'InputOTPSeparator' /** * Smart OTP Input Component * * Features: * - Automatic paste handling with cleaning * - Validation (numeric, alphanumeric, alpha, custom) * - Auto-submit on completion * - Customizable appearance * - Error/success states * - Optional separator * * @example * ```tsx * console.log('Complete:', value)} * /> * ``` * * @example With custom validation * ```tsx * /[A-F0-9]/i.test(char)} * value={hexCode} * onChange={setHexCode} * /> * ``` */ export const OTPInput = React.forwardRef< React.ComponentRef, SmartOTPProps >( ( { length = 6, value, onChange, onComplete, validationMode = 'numeric', customValidator, pasteBehavior = 'clean', autoSubmit = false, disabled = false, showSeparator = false, separatorIndex, containerClassName, slotClassName, separatorClassName, autoFocus = true, size, error = false, success = false, ...props }, ref ) => { const isMobile = useIsMobile() // Vercel-tuned default — 48×48 even on desktop. Caller can still // pass size="lg" for marketing surfaces. const resolvedSize = size ?? (isMobile ? 'default' : 'default') const { value: otpValue, handleChange, handleComplete, } = useSmartOTP({ length, value, onChange, onComplete, validationMode, customValidator, autoSubmit, }) // Create paste handler const pasteHandler = React.useMemo( () => createPasteHandler( length, validationMode, pasteBehavior, handleChange, customValidator ), [length, validationMode, pasteBehavior, handleChange, customValidator] ) // Calculate separator position (default to middle) const separatorPosition = separatorIndex ?? Math.floor(length / 2) // Render slots const slots = React.useMemo(() => { const slotElements: React.ReactNode[] = [] for (let i = 0; i < length; i++) { // Add separator if needed if (showSeparator && i === separatorPosition && i !== 0) { slotElements.push( ) } // Add slot slotElements.push( ) } return slotElements }, [length, showSeparator, separatorPosition, separatorClassName, resolvedSize, error, success, slotClassName]) return ( {slots} ) } ) OTPInput.displayName = 'OTPInput' /** * Re-export base components for advanced usage */ export { InputOTPGroup, InputOTPSlot } from '../input-otp' export { InputOTPSeparator } /** * Re-export types */ export type { SmartOTPProps as OTPInputProps, OTPValidationMode, OTPPasteBehavior, OTPValidator, } from './types' /** * Re-export hook for advanced usage */ export { useSmartOTP } from './use-otp-input'