"use client"; import { useEffect, useId, useRef, useState, type CSSProperties, type InputHTMLAttributes } from "react"; import { motion, useReducedMotion } from "motion/react"; type PasswordRevealProps = Omit, "type" | "className"> & { /** Field name shown above the box. */ label: string; /** Small help text under the field. */ hint?: string; /** Error text under the field. Turns the field red and sets aria-invalid. */ error?: string; /** Colour of the focus border and eye button. */ accent?: string; /** Wrapper look (width etc.). */ className?: string; }; const FLIP_MS = 420; /* PasswordReveal: a password field whose eye button flips the dots into letters one by one, left to right. It stays a real password input the rest of the time, so password managers keep working. */ export function PasswordReveal({ label, hint, error, accent = "#a78bfa", className = "w-full max-w-sm", id, autoComplete = "current-password", ...rest }: PasswordRevealProps) { const reduce = useReducedMotion(); const autoId = useId(); const inputId = id ?? autoId; const noteId = `${inputId}-note`; const note = error ?? hint; const input = useRef(null); const timer = useRef(0); const [shown, setShown] = useState(false); const [flip, setFlip] = useState<{ chars: string[]; offset: number } | null>(null); useEffect(() => () => window.clearTimeout(timer.current), []); const toggle = () => { const el = input.current; setShown(!shown); if (reduce || !el?.value) return; setFlip({ chars: Array.from(el.value), offset: el.scrollLeft }); window.clearTimeout(timer.current); timer.current = window.setTimeout(() => setFlip(null), FLIP_MS + 120); }; const gap = flip ? Math.min(0.03, FLIP_MS / 1000 / flip.chars.length) : 0; return (
{flip && ( {flip.chars.map((c, i) => ( {c} • ))} )}
{note && (

{note}

)}
); }