"use client"; import { usePathname } from "next/navigation"; import { useEffect } from "react"; // Selectors for elements that should animate in on scroll (scoped to
). const TARGETS = [ "h1", "h2", "h3", "h4", ".eyebrow", ".eyebrow-light", "p", ".btn-primary", ".btn-ghost", ".link-arrow", ".chip", '[class*="card"]', '[class*="__tile"]', '[class*="__item"]', '[class*="__pillar"]', '[class*="__step"]', '[class*="__cat"]', ] .map((s) => `main ${s}`) .join(","); // Sections that manage their own motion / must not be hidden. const EXCLUDE = ".home-hero, .jbanner, .nav, .footer, nav, footer, header"; export default function ScrollReveal() { const pathname = usePathname(); useEffect(() => { if (typeof window === "undefined") return; if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return; let cleanup = () => {}; const id = window.requestAnimationFrame(() => { const all = Array.from(document.querySelectorAll(TARGETS)).filter( (el) => !el.closest(EXCLUDE) ); // Keep only the outermost targets so a card animates as one unit // (its inner heading/paragraph don't animate separately). const set = new Set(all); const targets = all.filter((el) => { let p = el.parentElement; while (p) { if (set.has(p)) return false; p = p.parentElement; } return true; }); // Small stagger between siblings that share a parent. const byParent = new Map(); targets.forEach((el) => { const parent = el.parentElement; if (!parent) return; const arr = byParent.get(parent) ?? []; arr.push(el); byParent.set(parent, arr); }); byParent.forEach((arr) => { arr.forEach((el, idx) => { el.style.setProperty("--reveal-delay", `${Math.min(idx * 0.08, 0.4)}s`); }); }); const observer = new IntersectionObserver( (entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { entry.target.classList.add("is-visible"); observer.unobserve(entry.target); } }); }, { rootMargin: "0px 0px -8% 0px", threshold: 0.08 } ); const variantFor = (el: HTMLElement) => { const cls = el.className || ""; if (el.matches(".eyebrow, .eyebrow-light")) return "reveal--left"; if ( /card|__tile|__item|__pillar|__step|__cat/.test( typeof cls === "string" ? cls : "" ) ) { return "reveal--zoom"; } return "reveal--up"; }; targets.forEach((el) => { el.classList.add("reveal", variantFor(el)); const rect = el.getBoundingClientRect(); const inView = rect.top < window.innerHeight && rect.bottom > 0; if (inView) { // Already visible on load — reveal instantly, no hidden flash. el.classList.add("is-visible"); } else { observer.observe(el); } }); cleanup = () => observer.disconnect(); }); return () => { window.cancelAnimationFrame(id); cleanup(); }; }, [pathname]); return null; }