(null);
const { ref, visible } = useScrollVisible();
- const { displayed, done } = useTypewriter(text, visible, speed);
+ const [done, setDone] = useState(false);
+ const [started, setStarted] = useState(false);
+
+ useEffect(() => {
+ if (!visible || started) return;
+ setStarted(true);
+
+ if (prefersReducedMotion()) {
+ setDone(true);
+ return;
+ }
+
+ const el = tagRef.current;
+ if (!el) return;
+
+ // Wrap each character in an invisible span — layout stays correct
+ el.textContent = "";
+ const chars: HTMLSpanElement[] = [];
+ for (const char of text) {
+ const span = document.createElement("span");
+ span.textContent = char;
+ span.style.opacity = "0";
+ chars.push(span);
+ el.appendChild(span);
+ }
+
+ const textLength = text.length;
+ const charSpeed = Math.max(8, Math.min(speed, 600 / textLength));
+
+ let i = 0;
+ const step = () => {
+ if (i >= chars.length) {
+ el.textContent = text;
+ setDone(true);
+ return;
+ }
+ chars[i].style.opacity = "1";
+ i++;
+ setTimeout(step, charSpeed);
+ };
+ step();
+ }, [visible, started, text, speed]);
return (
-
- {visible ? displayed : "\u00A0"}
- {cursor && visible && !done && (
- |
- )}
+
+ {!started ? "\u00A0" : done ? text : null}
+ {cursor && started && !done && (
+ |
+ )}
);
}