"use client"; import { useEffect } from "react"; const PILLAR_CLASSES = [ "b-builder-ec3553f484d642e7ac0402bec8b0eaf0", // Next-Gen Optics "b-builder-8653fd9a098e4b9490a77501d6e9d546", // Adjustable IPD "b-builder-cd650e744cbd46dd81d56f78e9558ed6", // Eyetracking ] as const; /** * Cinematic scroll-reveal for the three pillar image cards. * * Timing note: we set data-reveal="hidden" synchronously so the browser * paints that state first, then use a double-RAF before starting the * IntersectionObserver. Without this gap the browser batches hidden→in * in one frame and the transition never plays. */ export function PillarAnimations() { useEffect(() => { if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return; const els = PILLAR_CLASSES.map((cls) => document.querySelector(`.${cls}`) ).filter((el): el is HTMLElement => el !== null); if (!els.length) return; // Arm hidden state + CSS vars NOW, before any RAF, so the browser can // paint the "hidden" visual on the very next frame. els.forEach((el, i) => { el.style.setProperty("--reveal-y", "36px"); el.style.setProperty("--reveal-dur", "1.1s"); el.style.setProperty("--reveal-delay", `${i * 90}ms`); el.dataset.reveal = "hidden"; }); let io: IntersectionObserver | null = null; let raf2 = -1; // Double-RAF: frame 1 paints "hidden", frame 2 starts the observer so // the transition has a visual baseline to animate from. const raf1 = requestAnimationFrame(() => { raf2 = requestAnimationFrame(() => { io = new IntersectionObserver( (entries) => { for (const e of entries) { if (e.isIntersecting) { (e.target as HTMLElement).dataset.reveal = "in"; io?.unobserve(e.target); } } }, { rootMargin: "0px 0px -8% 0px", threshold: 0.06 }, ); for (const el of els) io.observe(el); }); }); return () => { cancelAnimationFrame(raf1); cancelAnimationFrame(raf2); io?.disconnect(); }; }, []); return null; }