"use client"; import { useEffect, useRef } from "react"; import { usePathname } from "next/navigation"; import { gsap } from "gsap"; import { ScrollTrigger } from "gsap/ScrollTrigger"; import { ScrollSmoother } from "gsap/ScrollSmoother"; gsap.registerPlugin(ScrollTrigger, ScrollSmoother); const MOBILE_BREAKPOINT = 768; export function ScrollSmootherProvider() { const smootherRef = useRef(null); const pathname = usePathname(); const isFirstRenderRef = useRef(true); useEffect(() => { const prefersReduced = window.matchMedia( "(prefers-reduced-motion: reduce)", ).matches; const isMobile = window.innerWidth < MOBILE_BREAKPOINT; if (prefersReduced) return; const smoother = ScrollSmoother.create({ wrapper: "#smooth-wrapper", content: "#smooth-content", smooth: isMobile ? 0.6 : 1.2, effects: !isMobile, normalizeScroll: !isMobile, ignoreMobileResize: true, }); smootherRef.current = smoother; return () => { smoother.kill(); smootherRef.current = null; }; }, []); // The layout that mounts this provider persists across client-side route // changes, so the ScrollSmoother instance above is created once and never // recreated when navigating between pages. Its internal scroll position // and measured content height stay tied to whatever page was on screen // before the navigation. If the next page is a different height, the // stale height/position pairing snaps into place the instant the new page // paints — most visible as the fixed header's active-nav-item background // jumping instead of easing, since it briefly renders against the old, // now-incorrect scroll transform. Resetting scroll on every pathname // change and refreshing ScrollTrigger's measurements once the new page has // painted keeps the smoother in sync with what's actually on screen. useEffect(() => { if (isFirstRenderRef.current) { isFirstRenderRef.current = false; return; } const smoother = smootherRef.current; if (!smoother) { return; } smoother.scrollTo(0, false); const refreshFrame = requestAnimationFrame(() => { ScrollTrigger.refresh(); }); return () => cancelAnimationFrame(refreshFrame); }, [pathname]); return null; }