import { onMounted, onUnmounted, ref } from 'vue'; // Portage 1:1 de la timeline animée du mockup hero (Arboretum.dc.html). export type SessionState = 'idle' | 'busy' | 'waiting'; export interface AnimLine { text: string; k: string; } export interface HeroAnim { sess: SessionState; lines: AnimLine[]; att: number; toast: boolean; dlg: boolean; hl: number; ans: number; cur: boolean; } const TOTAL = 13000; const TICK = 80; interface ScriptEntry { at: number; text: string; k: string; } function script(): ScriptEntry[] { return [ { at: 850, text: '$ claude --resume feat/auth', k: 'cmd' }, { at: 1400, text: '> Reading src/auth/login.ts', k: 'tool' }, { at: 1950, text: '● Edit login.ts', k: 'edit' }, { at: 2400, text: " + import { verifyToken } from './jwt'", k: 'add' }, { at: 2850, text: ' + const user = await verifyToken(req)', k: 'add' }, { at: 3350, text: '● Bash npm test -- auth', k: 'edit' }, { at: 3850, text: ' ✓ 14 passed (1.24s)', k: 'ok' }, { at: 4300, text: '● Permission required to edit login.ts', k: 'warn' }, { at: 7950, text: '✓ approved · applying change', k: 'ok' }, { at: 8500, text: '● Edit login.ts', k: 'edit' }, { at: 9050, text: '● Running…', k: 'run' }, ]; } export function deriveAnim(clock: number): HeroAnim { const sess: SessionState = clock < 700 ? 'idle' : clock < 4500 ? 'busy' : clock < 7700 ? 'waiting' : 'busy'; const sc = script(); const lines: AnimLine[] = []; for (const entry of sc) { if (clock >= entry.at) lines.push({ text: entry.text, k: entry.k }); } const att = clock >= 4500 && clock < 7700 ? 1 : 0; const toast = clock >= 4650 && clock < 7700; const dlg = clock >= 4600 && clock < 7700; const hl = clock >= 6000 && clock < 7050 ? 0 : -1; const ans = clock >= 7050 && clock < 7700 ? 0 : -1; const cur = sess !== 'waiting'; return { sess, lines, att, toast, dlg, hl, ans, cur }; } export function useHeroTimeline(stageId = 'hero-stage') { const anim = ref(deriveAnim(0)); let clock = 0; let visible = true; let everVisible = false; let timer: ReturnType | undefined; let io: IntersectionObserver | undefined; onMounted(() => { let reduced = false; try { reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches; } catch { /* ignore */ } if (reduced) { anim.value = deriveAnim(5200); return; } const stage = document.getElementById(stageId); if (stage && 'IntersectionObserver' in window) { io = new IntersectionObserver( (entries) => { const e = entries[0]; if (!e) return; if (e.isIntersecting) { visible = true; everVisible = true; } else if (everVisible) { visible = false; } }, { threshold: 0, rootMargin: '200px 0px 200px 0px' }, ); io.observe(stage); } timer = setInterval(() => { if (!visible) return; clock += TICK; if (clock >= TOTAL) clock = 0; anim.value = deriveAnim(clock); }, TICK); }); onUnmounted(() => { if (timer) clearInterval(timer); if (io) io.disconnect(); }); return { anim }; }