Files
IdP/apps/frontend/components/ui/otp-input.tsx
2026-06-24 20:15:19 +03:00

114 lines
3.2 KiB
TypeScript

'use client';
import { useRef } from 'react';
import { cn } from '@/lib/utils';
const otpThemes = {
dark: {
box: 'border-[#555762] bg-transparent text-white focus:border-[#7b7f8f] focus:ring-white/10'
},
light: {
box: 'border-[#eceef4] bg-white text-[#111827] focus:border-[#c7cad6] focus:ring-[#eceef4]'
}
} as const;
interface OtpInputProps {
value: string;
onChange: (value: string) => void;
onComplete?: (value: string) => void;
length?: number;
disabled?: boolean;
variant?: keyof typeof otpThemes;
className?: string;
}
export function OtpInput({
value,
onChange,
onComplete,
length = 6,
disabled = false,
variant = 'dark',
className
}: OtpInputProps) {
const theme = otpThemes[variant];
const inputsRef = useRef<Array<HTMLInputElement | null>>([]);
const digits = Array.from({ length }, (_, index) => value[index] ?? '');
function commit(next: string) {
const clean = next.replace(/\D/g, '').slice(0, length);
onChange(clean);
if (clean.length === length) {
onComplete?.(clean);
}
}
function handleChange(index: number, raw: string) {
const char = raw.replace(/\D/g, '').slice(-1);
if (!char) return;
const chars = value.split('');
chars[index] = char;
const next = chars.join('').slice(0, length);
commit(next);
if (index < length - 1) {
inputsRef.current[index + 1]?.focus();
}
}
function handleKeyDown(index: number, event: React.KeyboardEvent<HTMLInputElement>) {
if (event.key === 'Backspace') {
event.preventDefault();
const chars = value.split('');
if (chars[index]) {
chars[index] = '';
onChange(chars.join(''));
} else if (index > 0) {
chars[index - 1] = '';
onChange(chars.join(''));
inputsRef.current[index - 1]?.focus();
}
}
if (event.key === 'ArrowLeft' && index > 0) {
inputsRef.current[index - 1]?.focus();
}
if (event.key === 'ArrowRight' && index < length - 1) {
inputsRef.current[index + 1]?.focus();
}
}
function handlePaste(event: React.ClipboardEvent<HTMLInputElement>) {
event.preventDefault();
const pasted = event.clipboardData.getData('text').replace(/\D/g, '').slice(0, length);
if (!pasted) return;
commit(pasted);
const focusIndex = Math.min(pasted.length, length - 1);
inputsRef.current[focusIndex]?.focus();
}
return (
<div className={cn('flex justify-between gap-2', className)}>
{digits.map((digit, index) => (
<input
key={index}
ref={(element) => {
inputsRef.current[index] = element;
}}
type="text"
inputMode="numeric"
autoComplete="one-time-code"
maxLength={1}
disabled={disabled}
value={digit}
onChange={(event) => handleChange(index, event.target.value)}
onKeyDown={(event) => handleKeyDown(index, event)}
onPaste={handlePaste}
className={cn(
'h-14 w-full rounded-2xl border text-center text-2xl font-semibold outline-none transition focus:ring-4 disabled:opacity-50',
theme.box
)}
/>
))}
</div>
);
}