type RevealOptions = { delay?: number; distance?: number; threshold?: number; }; const defaultOptions: Required = { delay: 0, distance: 24, threshold: 0.18 }; export function reveal(node: HTMLElement, options: RevealOptions = {}) { const settings = { ...defaultOptions, ...options }; const media = window.matchMedia('(prefers-reduced-motion: reduce)'); if (media.matches) { node.classList.add('reveal-visible'); return { destroy() {} }; } node.style.setProperty('--reveal-delay', `${settings.delay}ms`); node.style.setProperty('--reveal-distance', `${settings.distance}px`); node.classList.add('reveal-ready'); // If the element is already visible at all in the initial viewport, // reveal it immediately so the first section below the hero doesn't // appear blank on page load. const initialCheck = () => { const rect = node.getBoundingClientRect(); const viewportHeight = window.innerHeight || document.documentElement.clientHeight; if (rect.top < viewportHeight && rect.bottom > 0) { node.classList.add('reveal-visible'); return true; } return false; }; if (initialCheck()) { return { destroy() {} }; } const observer = new IntersectionObserver( (entries) => { for (const entry of entries) { if (!entry.isIntersecting) { continue; } node.classList.add('reveal-visible'); observer.disconnect(); break; } }, { threshold: settings.threshold, rootMargin: '0px 0px -8% 0px' } ); observer.observe(node); return { destroy() { observer.disconnect(); } }; }