nicholais-website/app/providers/LenisProvider.tsx

108 lines
3.1 KiB
TypeScript

"use client";
import React, { createContext, useContext, useEffect, useMemo, useRef } from "react";
import Lenis from "lenis";
import { useMotionValue, type MotionValue } from "motion/react";
type ScrollContextValue = {
lenis: Lenis | null;
scrollY: MotionValue<number>; // in px
progress: MotionValue<number>; // 0..1 for whole page
};
const ScrollContext = createContext<ScrollContextValue | null>(null);
type LenisScrollEvent = {
scroll: number;
limit: number;
progress?: number;
};
export function useScrollContext() {
const ctx = useContext(ScrollContext);
if (!ctx) {
throw new Error("useScrollContext must be used within <LenisProvider>");
}
return ctx;
}
export function LenisProvider({ children }: { children: React.ReactNode }) {
const lenisRef = useRef<Lenis | null>(null);
const scrollY = useMotionValue(0);
const progress = useMotionValue(0);
useEffect(() => {
// Respect user preferences
const prefersReducedMotion =
typeof window !== "undefined" &&
window.matchMedia &&
window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const lenis = new Lenis({
// Softer, less "floaty" feel
smoothWheel: !prefersReducedMotion,
syncTouch: true,
duration: 0.7, // was 1.2
easing: (t: number) => 1 - Math.pow(1 - t, 2.4), // gentler ease-out
wheelMultiplier: 0.9, // slightly reduce wheel amplitude
// Use native on reduced motion
wrapper: undefined,
content: undefined,
// If reduced motion, disable smoothing entirely
lerp: prefersReducedMotion ? 1 : 0.22, // was 0.1 (heavier smoothing)
});
lenisRef.current = lenis;
const onScroll = (e: unknown) => {
const { scroll, limit, progress: p } = (e as LenisScrollEvent);
scrollY.set(scroll);
// Some versions of Lenis may not send progress, compute fallback if needed
if (typeof p === "number" && Number.isFinite(p)) {
progress.set(p);
} else {
const fallback =
limit > 0
? Math.min(1, Math.max(0, scroll / limit))
: 0;
progress.set(fallback);
}
};
lenis.on("scroll", onScroll);
let rafId = 0;
const raf = (time: number) => {
lenis.raf(time);
rafId = requestAnimationFrame(raf);
};
rafId = requestAnimationFrame(raf);
// Initialize values
onScroll({
scroll: window.scrollY,
limit: Math.max(0, document.documentElement.scrollHeight - window.innerHeight),
progress: (document.documentElement.scrollHeight - window.innerHeight) > 0
? window.scrollY / (document.documentElement.scrollHeight - window.innerHeight)
: 0,
});
return () => {
cancelAnimationFrame(rafId);
lenis.off("scroll", onScroll);
lenis.destroy();
lenisRef.current = null;
};
}, [progress, scrollY]);
const value = useMemo<ScrollContextValue>(() => {
return {
lenis: lenisRef.current,
scrollY,
progress,
};
}, [scrollY, progress]);
return <ScrollContext.Provider value={value}>{children}</ScrollContext.Provider>;
}