"use client"; import { useEffect, useRef, useState } from "react"; import { motion, useInView, useReducedMotion } from "motion/react"; type TypeNovaProps = { text: string; className?: string; /** Milliseconds per letter. */ speed?: number; /** Colour of the flash each new letter lands with. */ glow?: string; /** Type only the first time it scrolls into view. */ once?: boolean; }; /* TypeNova: types the text one letter at a time; each new letter lands as a bright flash that cools down. */ export function TypeNova({ text, className = "", speed = 55, glow = "#a78bfa", once = true }: TypeNovaProps) { const reduce = useReducedMotion(); const ref = useRef(null); const inView = useInView(ref, { once, amount: 0.6 }); const [count, setCount] = useState(0); useEffect(() => { if (!inView || reduce) return; let n = 0; let timer = 0; const tick = () => { setCount(n); if (n++ < text.length) timer = window.setTimeout(tick, speed); }; timer = window.setTimeout(tick, 0); return () => window.clearTimeout(timer); }, [inView, reduce, text, speed]); const shown = reduce ? text.length : count; const letters = [...text]; return ( {text} {letters.slice(0, shown).map((ch, i) => ( // Plain inline letters (no transform), so long lines still wrap between words only. {ch} ))} {!reduce && shown < letters.length && ( // Zero-width holder, so the caret never pushes the text around. )} {/* Untyped letters hold their space, so the line never shifts while typing. */} {letters.slice(shown).join("")} ); }