Files
SculptedArcWebSite/components/typing-words.tsx

85 lines
2.6 KiB
TypeScript

'use client';
import { useEffect, useState } from 'react';
import './typing-words.css';
const WORDS = ['plan', 'idea', 'problem', 'goals'] as const;
const TYPING_MS = 90;
const DELETING_MS = 55;
const PAUSE_AFTER_TYPED_MS = 2000;
const PAUSE_AFTER_DELETED_MS = 400;
type Phase = 'typing' | 'pausing' | 'deleting' | 'waiting';
export default function TypingWords() {
const [wordIndex, setWordIndex] = useState(0);
const [charIndex, setCharIndex] = useState(0);
const [phase, setPhase] = useState<Phase>('typing');
const [prefersReducedMotion, setPrefersReducedMotion] = useState(false);
useEffect(() => {
const media = window.matchMedia('(prefers-reduced-motion: reduce)');
setPrefersReducedMotion(media.matches);
const onChange = () => setPrefersReducedMotion(media.matches);
media.addEventListener('change', onChange);
return () => media.removeEventListener('change', onChange);
}, []);
useEffect(() => {
if (prefersReducedMotion) return;
const currentWord = WORDS[wordIndex];
let timeoutId: ReturnType<typeof setTimeout>;
if (phase === 'typing') {
if (charIndex < currentWord.length) {
timeoutId = setTimeout(() => setCharIndex((i) => i + 1), TYPING_MS);
} else {
timeoutId = setTimeout(() => setPhase('pausing'), PAUSE_AFTER_TYPED_MS);
}
} else if (phase === 'pausing') {
timeoutId = setTimeout(() => setPhase('deleting'), 0);
} else if (phase === 'deleting') {
if (charIndex > 0) {
timeoutId = setTimeout(() => setCharIndex((i) => i - 1), DELETING_MS);
} else {
timeoutId = setTimeout(() => {
setWordIndex((i) => (i + 1) % WORDS.length);
setPhase('waiting');
}, PAUSE_AFTER_DELETED_MS);
}
} else if (phase === 'waiting') {
timeoutId = setTimeout(() => setPhase('typing'), 0);
}
return () => clearTimeout(timeoutId);
}, [charIndex, phase, wordIndex, prefersReducedMotion]);
useEffect(() => {
if (!prefersReducedMotion) return;
const intervalId = setInterval(() => {
setWordIndex((i) => (i + 1) % WORDS.length);
}, 3000);
return () => clearInterval(intervalId);
}, [prefersReducedMotion]);
const displayedWord = prefersReducedMotion
? WORDS[wordIndex]
: WORDS[wordIndex].slice(0, charIndex);
return (
<span className="typing-words" aria-live="polite">
<span className="typing-words-text text-transparent bg-clip-text bg-gradient-to-r from-sky-400 to-violet-800">
{displayedWord}
</span>
<span className="typing-words-cursor" aria-hidden="true">
|
</span>
</span>
);
}