export interface TypewriterSentence { text: string style?: 'title' | 'citation' | 'text' stays?: boolean separator?: boolean } interface SequenceOptions { fadeMs?: number holdMs?: number gapMs?: number } export function useTypewriter(sentences: TypewriterSentence[], options: SequenceOptions = {}) { const { fadeMs = 1000, holdMs = 2800, gapMs = 300, } = options const currentText = ref('') const currentStyle = ref('title') const isVisible = ref(false) const lockedSentences = ref([]) const isComplete = ref(false) let currentIdx = -1 let timer: ReturnType | null = null function clearTimer() { if (timer) { clearTimeout(timer) timer = null } } function next() { currentIdx++ if (currentIdx >= sentences.length) { currentText.value = '' isComplete.value = true return } const sentence = sentences[currentIdx] if (sentence.separator) { lockedSentences.value = [...lockedSentences.value, { text: '', separator: true }] } // Set text while invisible currentText.value = sentence.text currentStyle.value = sentence.style || 'title' // Fade in on next frame requestAnimationFrame(() => { isVisible.value = true }) // After fade-in + hold → fade out timer = setTimeout(() => { isVisible.value = false // After fade-out completes → lock if stays, then next timer = setTimeout(() => { if (sentence.stays) { lockedSentences.value = [...lockedSentences.value, { ...sentence }] } timer = setTimeout(next, gapMs) }, fadeMs) }, fadeMs + holdMs) } function start() { next() } function skipToEnd() { clearTimer() isVisible.value = false currentText.value = '' const locked: TypewriterSentence[] = [] for (const sentence of sentences) { if (sentence.separator) { locked.push({ text: '', separator: true }) } if (sentence.stays) { locked.push({ ...sentence }) } } lockedSentences.value = locked currentIdx = sentences.length - 1 isComplete.value = true } onUnmounted(clearTimer) return { currentText: readonly(currentText), currentStyle: readonly(currentStyle), isVisible, lockedSentences: readonly(lockedSentences), isComplete: readonly(isComplete), start, skipToEnd, } }