feat(smooth-scroll): implement smooth scrolling functionality with Lenis integration

This commit is contained in:
2026-03-12 19:08:44 +01:00
parent 8f6a242273
commit ef9b277396
6 changed files with 160 additions and 1 deletions

View File

@@ -0,0 +1,111 @@
"use client";
import { useEffect, type ReactNode } from "react";
import { useReducedMotion } from "framer-motion";
import Lenis from "lenis";
interface SmoothScrollProviderProps {
children: ReactNode;
}
const DESKTOP_POINTER_QUERY = "(hover: hover) and (pointer: fine)";
const REDUCED_MOTION_QUERY = "(prefers-reduced-motion: reduce)";
export function SmoothScrollProvider({ children }: SmoothScrollProviderProps) {
const shouldReduceMotion = useReducedMotion();
useEffect(() => {
if (typeof window === "undefined") {
return;
}
const desktopPointerMedia = window.matchMedia(DESKTOP_POINTER_QUERY);
const reducedMotionMedia = window.matchMedia(REDUCED_MOTION_QUERY);
let lenis: Lenis | null = null;
let frameId = 0;
const stopLenis = () => {
if (frameId) {
window.cancelAnimationFrame(frameId);
frameId = 0;
}
if (lenis) {
lenis.destroy();
lenis = null;
}
};
const startLenis = () => {
if (lenis) {
return;
}
lenis = new Lenis({
lerp: 0.085,
smoothWheel: true,
syncTouch: false,
wheelMultiplier: 0.92,
anchors: true,
prevent: (node) => {
if (!(node instanceof HTMLElement)) {
return false;
}
return Boolean(
node.closest(
"[data-lenis-prevent], [data-radix-scroll-lock], [cmdk-list]",
),
);
},
});
const raf = (time: number) => {
lenis?.raf(time);
frameId = window.requestAnimationFrame(raf);
};
frameId = window.requestAnimationFrame(raf);
};
const syncState = () => {
if (
shouldReduceMotion ||
reducedMotionMedia.matches ||
!desktopPointerMedia.matches
) {
stopLenis();
return;
}
startLenis();
};
syncState();
const handleDesktopPointerChange = () => {
syncState();
};
const handleReducedMotionChange = () => {
syncState();
};
desktopPointerMedia.addEventListener("change", handleDesktopPointerChange);
reducedMotionMedia.addEventListener("change", handleReducedMotionChange);
return () => {
desktopPointerMedia.removeEventListener(
"change",
handleDesktopPointerChange,
);
reducedMotionMedia.removeEventListener(
"change",
handleReducedMotionChange,
);
stopLenis();
};
}, [shouldReduceMotion]);
return children;
}

View File

@@ -45,6 +45,7 @@ const CommandList = React.forwardRef<
>(({ className, ...props }, ref) => (
<CommandPrimitive.List
ref={ref}
data-lenis-prevent=""
className={cn("max-h-[280px] overflow-y-auto overflow-x-hidden", className)}
{...props}
/>