feat(smooth-scroll): implement smooth scrolling functionality with Lenis integration
This commit is contained in:
@@ -62,6 +62,23 @@ body::before {
|
|||||||
will-change: transform;
|
will-change: transform;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html.lenis,
|
||||||
|
html.lenis body {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lenis.lenis-smooth {
|
||||||
|
scroll-behavior: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lenis.lenis-smooth [data-lenis-prevent] {
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lenis.lenis-stopped {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.font-heading {
|
.font-heading {
|
||||||
font-family: "Space Grotesk", "Manrope", "Segoe UI", sans-serif;
|
font-family: "Space Grotesk", "Manrope", "Segoe UI", sans-serif;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import type { Metadata } from "next";
|
|||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import "currency-flags/dist/currency-flags.min.css";
|
import "currency-flags/dist/currency-flags.min.css";
|
||||||
|
|
||||||
|
import { SmoothScrollProvider } from "@/components/providers/smooth-scroll-provider";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "NexCurrency | Modern Currency & Crypto Converter",
|
title: "NexCurrency | Modern Currency & Crypto Converter",
|
||||||
description:
|
description:
|
||||||
@@ -17,7 +19,7 @@ export default function RootLayout({
|
|||||||
return (
|
return (
|
||||||
<html lang="en" className="dark" suppressHydrationWarning>
|
<html lang="en" className="dark" suppressHydrationWarning>
|
||||||
<body className="font-sans antialiased">
|
<body className="font-sans antialiased">
|
||||||
{children}
|
<SmoothScrollProvider>{children}</SmoothScrollProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
111
components/providers/smooth-scroll-provider.tsx
Normal file
111
components/providers/smooth-scroll-provider.tsx
Normal 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;
|
||||||
|
}
|
||||||
@@ -45,6 +45,7 @@ const CommandList = React.forwardRef<
|
|||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<CommandPrimitive.List
|
<CommandPrimitive.List
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
data-lenis-prevent=""
|
||||||
className={cn("max-h-[280px] overflow-y-auto overflow-x-hidden", className)}
|
className={cn("max-h-[280px] overflow-y-auto overflow-x-hidden", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
|||||||
27
package-lock.json
generated
27
package-lock.json
generated
@@ -16,6 +16,7 @@
|
|||||||
"cryptocurrency-icons": "^0.18.1",
|
"cryptocurrency-icons": "^0.18.1",
|
||||||
"currency-flags": "github:vivekimsit/currency-flags",
|
"currency-flags": "github:vivekimsit/currency-flags",
|
||||||
"framer-motion": "^12.36.0",
|
"framer-motion": "^12.36.0",
|
||||||
|
"lenis": "^1.3.18",
|
||||||
"lucide-react": "^0.475.0",
|
"lucide-react": "^0.475.0",
|
||||||
"next": "^14.2.24",
|
"next": "^14.2.24",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
@@ -4564,6 +4565,32 @@
|
|||||||
"node": ">=0.10"
|
"node": ">=0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lenis": {
|
||||||
|
"version": "1.3.18",
|
||||||
|
"resolved": "https://registry.npmjs.org/lenis/-/lenis-1.3.18.tgz",
|
||||||
|
"integrity": "sha512-7KBl3V7vx5y1h05pu9fNFZS66I0+1eZ+zUGNNNBKtEn3BONZy+nkHWvdEe2b+zKT+6WX1x7zyOb1zbYYOs6tcg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/darkroomengineering"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@nuxt/kit": ">=3.0.0",
|
||||||
|
"react": ">=17.0.0",
|
||||||
|
"vue": ">=3.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@nuxt/kit": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"vue": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/levn": {
|
"node_modules/levn": {
|
||||||
"version": "0.4.1",
|
"version": "0.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
"cryptocurrency-icons": "^0.18.1",
|
"cryptocurrency-icons": "^0.18.1",
|
||||||
"currency-flags": "github:vivekimsit/currency-flags",
|
"currency-flags": "github:vivekimsit/currency-flags",
|
||||||
"framer-motion": "^12.36.0",
|
"framer-motion": "^12.36.0",
|
||||||
|
"lenis": "^1.3.18",
|
||||||
"lucide-react": "^0.475.0",
|
"lucide-react": "^0.475.0",
|
||||||
"next": "^14.2.24",
|
"next": "^14.2.24",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
|
|||||||
Reference in New Issue
Block a user