import type { ForwardedRef } from 'react'; import { forwardRef, useMemo } from 'react'; import { View, type TextInput, type ViewProps } from 'react-native'; import { OtpCellIndexProvider, OtpInputField, OtpInputProvider, dataAttributes, mergeDataAttributes, useInputContext, useOtpInput, type IOtpInputRootProps, type OtpInputFieldProps, } from '@cdx-ui/primitives'; import { ParentContext, cn, useParentContext, useStyleContext } from '@cdx-ui/utils'; import { Input, type InputProps } from '../Input'; import { type InputVariantProps, inputFieldVariants } from '../Input/styles'; import { otpCellVariants, otpFieldVariants, otpRootVariants, type OtpInputVariantProps, } from './styles'; const SCOPE = 'OTP_INPUT'; const useOtpInputStyleContext = () => useStyleContext(SCOPE) as Partial; export interface OtpInputCellProps extends ViewProps, InputVariantProps { className?: string; children?: React.ReactNode; isInvalid?: boolean; isReadOnly?: boolean; isDisabled?: boolean; } const OtpStyledCell = forwardRef( ( { className, children, variant: variantProp, size: sizeProp, ...props }, ref: ForwardedRef, ) => { const context = useOtpInputStyleContext(); const variant = variantProp ?? context.variant ?? 'outline'; const size = sizeProp ?? context.size ?? 'default'; return ( {children} ); }, ); OtpStyledCell.displayName = 'OtpInput.Cell'; export type OtpInputFieldStyledProps = OtpInputFieldProps & { className?: string }; /** * Bridges OtpInputField focus/blur events to the parent Input's context so * InputRoot emits `data-focus=true` and the `border-stroke-action` CSS activates. * The primitive OtpInputField doesn't know about InputContext — that coupling * belongs here in the styled layer. */ const OtpStyledField = forwardRef((props, ref) => { const { setIsFocused } = useInputContext(); return ( { setIsFocused?.(true); props.onFocus?.(e); }} onBlur={(e) => { setIsFocused?.(false); props.onBlur?.(e); }} /> ); }); OtpStyledField.displayName = 'OtpInput.Field'; export interface OtpInputProps extends Omit, OtpInputVariantProps { variant?: NonNullable; size?: NonNullable; className?: string; } const OtpInputRoot = forwardRef( ( { variant = 'outline', size, fullWidth, className, style, cellCount, value, onChangeText, onComplete, isDisabled, isInvalid, isReadOnly, smsOtpAutofill, filterInput, allowedCharacters, characterPattern, getCellProps, cellTestIdPrefix, accessibilityLabel, ...rest }, ref, ) => { const otpContext = useOtpInput({ cellCount, value, onChangeText, onComplete, isDisabled, isInvalid, isReadOnly, smsOtpAutofill, filterInput, allowedCharacters, characterPattern, getCellProps, cellTestIdPrefix, accessibilityLabel, }); const rootClassName = cn(otpRootVariants({ size, fullWidth }), className); const parent = useParentContext(); const ctx = useMemo(() => ({ ...parent, [SCOPE]: { variant, size } }), [parent, variant, size]); const fieldClassName = cn(inputFieldVariants({ size }), otpFieldVariants({ size })); const { isDisabled: resolvedDisabled, isInvalid: resolvedInvalid, isReadOnly: resolvedReadOnly, } = otpContext; const cells: React.ReactElement[] = []; for (let i = 0; i < cellCount; i++) { cells.push( , ); } return ( {cells} ); }, ); OtpInputRoot.displayName = 'OtpInput'; type OtpInputCompoundComponent = typeof OtpInputRoot & { Cell: typeof OtpStyledCell; Field: typeof OtpStyledField; }; export const OtpInput = Object.assign(OtpInputRoot, { Cell: OtpStyledCell, Field: OtpStyledField, }) as OtpInputCompoundComponent;