/** * Pure helpers shared between `` and ``. These * mirror Base UI React's `packages/react/src/otp-field/utils/otp.ts` * one-for-one — they are plain, non-reactive functions, safe to call from any * scope. */ interface OTPValidationConfig { slotPattern: string; getRootPattern: (length: number) => string; regexp: RegExp; inputMode: "numeric" | "text"; } export type OTPValidationType = "numeric" | "alpha" | "alphanumeric" | "none"; const OTP_VALIDATION_CONFIG: Record< Exclude, OTPValidationConfig > = { numeric: { slotPattern: "\\d{1}", getRootPattern: (length) => `\\d{${length}}`, regexp: /[^\d]/g, inputMode: "numeric", }, alpha: { slotPattern: "[a-zA-Z]{1}", getRootPattern: (length) => `[a-zA-Z]{${length}}`, regexp: /[^a-zA-Z]/g, inputMode: "text", }, alphanumeric: { slotPattern: "[a-zA-Z0-9]{1}", getRootPattern: (length) => `[a-zA-Z0-9]{${length}}`, regexp: /[^a-zA-Z0-9]/g, inputMode: "text", }, }; export function getOTPValidationConfig( validationType: OTPValidationType, ): OTPValidationConfig | null { if (validationType === "none") return null; return OTP_VALIDATION_CONFIG[validationType]; } export function stripOTPWhitespace(value: string | null | undefined): string { return (value ?? "").replace(/\s/g, ""); } /** * Normalize user-entered OTP text by stripping whitespace, applying validation * or custom sanitization, then clamping the final value to the configured slot * count. Slicing uses `Array.from` so multi-byte characters stay atomic. */ export function normalizeOTPValue( value: string | null | undefined, length: number, validationType: OTPValidationType, sanitizeValue?: ((value: string) => string) | undefined, ): string { let sanitizedValue = stripOTPWhitespace(value); const validation = getOTPValidationConfig(validationType); if (validation) { sanitizedValue = sanitizedValue.replace(validation.regexp, ""); } else if (sanitizeValue) { sanitizedValue = sanitizeValue(sanitizedValue); } return Array.from(sanitizedValue).slice(0, Math.max(length, 0)).join(""); } /** * Replace characters starting at the provided slot index, then re-normalize * the result. Used by typing / paste flows where a multi-character edit must * stay contiguous and length-bounded. */ export function replaceOTPValue( currentValue: string, index: number, nextValue: string, length: number, validationType: OTPValidationType, sanitizeValue?: ((value: string) => string) | undefined, ): string { const normalizedValue = normalizeOTPValue( nextValue, length, validationType, sanitizeValue, ); const prefix = currentValue.slice(0, index); const suffix = currentValue.slice(index + normalizedValue.length); return normalizeOTPValue( `${prefix}${normalizedValue}${suffix}`, length, validationType, sanitizeValue, ); } export function removeOTPCharacter( currentValue: string, index: number, ): string { if (index < 0 || index >= currentValue.length) { return currentValue; } return `${currentValue.slice(0, index)}${currentValue.slice(index + 1)}`; }