main
import { useState } from 'react'
import { t } from '@/shared/i18n'
export function Otp({
value,
onInput,
error,
label = t('ui.otp.label'),
helper = t('ui.otp.helper'),
}: {
value: string
onInput: (value: string) => void
error?: string
label?: string
helper?: string
}) {
const [focused, setFocused] = useState(false)
const [position, setPosition] = useState(0)
return (
<div className="flex flex-col gap-2">
<label className="text-sm font-medium" htmlFor="code">
{label}
</label>
<div className="relative h-14">
<div className="pointer-events-none flex h-full gap-1" aria-hidden="true">
{Array.from({ length: 6 }, (_, index) => (
<span
key={index}
className={`flex h-full min-w-0 flex-1 items-center justify-center rounded-lg border bg-field font-latin text-2xl font-medium ${error ? 'border-danger' : focused && index === position ? 'border-focus ring-1 ring-focus' : 'border-control-border'}`}
>
{value[index] || (focused && index === position ? <span className="h-6 w-0.5 bg-focus" /> : '')}
</span>
))}
</div>
<input
id="code"
name="code"
type="text"
inputMode="numeric"
autoComplete="one-time-code"
pattern="[0-9]{6}"
maxLength={6}
value={value}
aria-invalid={!!error}
aria-describedby="code-description"
onFocus={() => setFocused(true)}
onBlur={() => setFocused(false)}
onSelect={(event) => setPosition(Math.min(event.currentTarget.selectionStart ?? value.length, 5))}
onChange={(event) => {
const next = event.currentTarget.value.replace(/\D/g, '').slice(0, 6)
onInput(next)
setPosition(Math.min(next.length, 5))
}}
onPaste={(event) => {
const next = event.clipboardData?.getData('text').replace(/\D/g, '').slice(0, 6)
if (next) {
event.preventDefault()
onInput(next)
setPosition(Math.min(next.length, 5))
}
}}
className="absolute inset-0 h-full w-full cursor-text border-0 bg-transparent text-transparent caret-transparent opacity-[0.01] outline-none"
/>
</div>
<p id="code-description" className={`text-xs leading-normal ${error ? 'text-danger' : 'text-muted'}`}>
{error || helper}
</p>
</div>
)
}