"use client"; import { useEffect } from "react"; import { usePathname } from "next/navigation"; import gsap from "gsap"; import { ScrollTrigger } from "gsap/ScrollTrigger"; import Lenis from "lenis"; /** * SmoothScroll * --------------------------------------------------------------------------- * One global Lenis instance, driven by a SINGLE rAF source (GSAP's ticker) and * kept locked to ScrollTrigger via `lenis.on("scroll", ScrollTrigger.update)`. * * Enabled on every route. /miletruth's three scroll-driven WebGL sections use a * self-managed `position: fixed` pin (toggled from ScrollTrigger.onUpdate via * `self.progress`) — NOT GSAP's pin-spacer — so Lenis driving real document * scroll keeps their progress correct and just smooths the wheel input. (It was * previously gated off here when those sections used GSAP pins; that no longer * applies, and native scroll there felt noticeably janky next to every Lenis * route.) * * Still gated OFF on: * - touch devices — native momentum is smoother than emulated inertia. * - prefers-reduced-motion. * * Re-evaluates on every route change: the effect cleanup destroys the previous * instance and re-inits on the next route. */ // /how-it-works runs its own tuned Lenis inside the embedded 3D experience // (src/modules/how-it-works-3d); the global instance is gated off there so two // Lenis instances don't fight over the same document scroll. const DISABLED_ROUTES: string[] = ["/how-it-works"]; export default function SmoothScroll() { const pathname = usePathname(); useEffect(() => { const routeDisabled = DISABLED_ROUTES.some( (r) => pathname === r || pathname.startsWith(`${r}/`), ); const prefersReduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches; // Mouse/desktop only — touch devices already have good native momentum. const isPointerFine = window.matchMedia("(hover: hover) and (pointer: fine)").matches; let hashTimer: ReturnType; if (routeDisabled || prefersReduced || !isPointerFine) { if (!window.location.hash) { window.scrollTo(0, 0); } else { const scrollToHash = () => { try { const target = document.querySelector(window.location.hash) as HTMLElement | null; if (target) { target.scrollIntoView(); } } catch (err) { console.warn(err); } }; scrollToHash(); hashTimer = setTimeout(scrollToHash, 100); } return () => { if (hashTimer) clearTimeout(hashTimer); }; } gsap.registerPlugin(ScrollTrigger); // /miletruth is one long stack of tall, pinned 3D sections, each with its own // ScrollTrigger `scrub`. The default duration-based momentum (1.05s) compounds // with that scrub, so the wheel feels heavy, slow and disconnected. On this // route we switch to a snappy `lerp`-based follow + a higher wheel multiplier: // the page travels fast and stays tightly locked to the wheel, while each // section's scrub supplies the visual smoothing. Other routes keep the softer // duration-based feel that suits their normal content. const isMileTruth = pathname === "/miletruth" || pathname.startsWith("/miletruth/"); const lenis = new Lenis( isMileTruth ? { lerp: 0.13, // snappy follow (higher = less smoothing lag) wheelMultiplier: 1.3, // travel further per wheel tick → fast touchMultiplier: 1.6, orientation: "vertical", gestureOrientation: "vertical", smoothWheel: true, } : { duration: 1.05, easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)), orientation: "vertical", gestureOrientation: "vertical", smoothWheel: true, }, ); if (!window.location.hash) { lenis.scrollTo(0, { immediate: true }); window.scrollTo(0, 0); } else { const scrollToHash = () => { try { const target = document.querySelector(window.location.hash) as HTMLElement | null; if (target) { lenis.scrollTo(target, { immediate: true }); } } catch (err) { console.warn(err); } }; scrollToHash(); hashTimer = setTimeout(scrollToHash, 100); } lenis.on("scroll", ScrollTrigger.update); const tickerCb = (time: number) => lenis.raf(time * 1000); // ticker is seconds, Lenis wants ms gsap.ticker.add(tickerCb); gsap.ticker.lagSmoothing(0); ScrollTrigger.refresh(); return () => { if (hashTimer) clearTimeout(hashTimer); gsap.ticker.remove(tickerCb); lenis.destroy(); }; }, [pathname]); return null; }