"use client"; import React, { useEffect } from "react"; import { usePathname } from "next/navigation"; import gsap from "gsap"; import { ScrollTrigger } from "gsap/ScrollTrigger"; /** * AnimationProvider * Initializes GSAP + ScrollTrigger globally, refreshes on route changes, * and provides smooth defaults. */ export default function AnimationProvider({ children }: { children: React.ReactNode }) { const pathname = usePathname(); const initDecorativeBlocks = () => { // Clean up previous block triggers to avoid duplicates ScrollTrigger.getAll().forEach((t) => { if (t.vars && (t.vars as any).id === "block-deco") { t.kill(); } }); const vh = window.innerHeight || document.documentElement.clientHeight; const decorativeBlocks = document.querySelectorAll(".block-decoration"); decorativeBlocks.forEach((block) => { ScrollTrigger.create({ id: "block-deco", trigger: block, start: "top 92%", onEnter: () => { setTimeout(() => { block.classList.add("animated"); }, 150); }, onEnterBack: () => { setTimeout(() => { block.classList.add("animated"); }, 150); }, onLeave: () => { block.classList.remove("animated"); }, onLeaveBack: () => { block.classList.remove("animated"); }, }); // ScrollTrigger does not fire onEnter for blocks already in view at // creation — on larger / taller screens those stayed un-animated. // Reveal any block already intersecting the viewport. const r = block.getBoundingClientRect(); if (r.top < vh && r.bottom > 0) { block.classList.add("animated"); } }); }; useEffect(() => { gsap.registerPlugin(ScrollTrigger); // Global GSAP defaults for buttery animations gsap.defaults({ ease: "power3.out", duration: 0.8, }); // Mobile browsers fire `resize` when the address bar shows/hides while // scrolling. Refreshing ScrollTrigger on every one of those caused jumpy / // re-hidden animations on phones. Ignore those spurious resizes. ScrollTrigger.config({ ignoreMobileResize: true }); // Run initializations initDecorativeBlocks(); // Refresh on full window load (when all images, styles, and fonts are in place) const handleLoad = () => { initDecorativeBlocks(); ScrollTrigger.refresh(true); }; window.addEventListener("load", handleLoad); // Refresh on window resize — debounced so dragging the window across // breakpoints recomputes trigger positions once it settles, instead of // thrashing on every intermediate pixel. let resizeTimer: ReturnType; const handleResize = () => { clearTimeout(resizeTimer); resizeTimer = setTimeout(() => { initDecorativeBlocks(); ScrollTrigger.refresh(true); }, 200); }; window.addEventListener("resize", handleResize); return () => { clearTimeout(resizeTimer); window.removeEventListener("load", handleLoad); window.removeEventListener("resize", handleResize); ScrollTrigger.getAll().forEach((t) => t.kill()); }; }, []); // Listen for route changes and refresh ScrollTriggers so newly rendered content animates in correctly useEffect(() => { const refreshAnimations = () => { initDecorativeBlocks(); ScrollTrigger.refresh(true); }; // Run route change handler immediately on navigation refreshAnimations(); // Staggered refreshes to accommodate page layout calculations and paint frames const timers = [ setTimeout(refreshAnimations, 100), setTimeout(refreshAnimations, 400), setTimeout(refreshAnimations, 800), setTimeout(refreshAnimations, 1500), ]; return () => { timers.forEach(clearTimeout); }; }, [pathname]); return <>{children}; }