108 lines
3.1 KiB
TypeScript
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>;
|
|
}
|