agency-web/app/components/SmoothScroll.tsx

55 lines
1.7 KiB
TypeScript

"use client";
/**
* Lenis smooth scroll wired into GSAP ScrollTrigger.
* - Lenis drives the rAF loop; ScrollTrigger.update() runs on every Lenis tick
* so pinned/scrubbed animations stay in lockstep with the smoothed scroll.
* - Disabled entirely when prefers-reduced-motion is set (native scroll).
* - Exposes a scroll-progress value on :root for the top progress bar (CSS).
*/
import { useEffect } from "react";
import Lenis from "lenis";
import { gsap, ScrollTrigger } from "./gsap";
export default function SmoothScroll() {
useEffect(() => {
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
// Always keep the CSS progress var fed (cheap), even without Lenis.
const setProgress = (p: number) =>
document.documentElement.style.setProperty("--scroll-progress", String(p));
if (reduce) {
const onScroll = () => {
const h = document.documentElement;
const max = h.scrollHeight - h.clientHeight;
setProgress(max > 0 ? h.scrollTop / max : 0);
};
window.addEventListener("scroll", onScroll, { passive: true });
onScroll();
return () => window.removeEventListener("scroll", onScroll);
}
const lenis = new Lenis({
duration: 1.1,
easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)),
smoothWheel: true,
});
lenis.on("scroll", (e: { progress: number }) => {
ScrollTrigger.update();
setProgress(e.progress);
});
const raf = (time: number) => lenis.raf(time * 1000);
gsap.ticker.add(raf);
gsap.ticker.lagSmoothing(0);
return () => {
gsap.ticker.remove(raf);
lenis.destroy();
};
}, []);
return null;
}