diff --git a/app/globals.css b/app/globals.css
index d33aef9..69539dd 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -62,6 +62,23 @@ body::before {
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-family: "Space Grotesk", "Manrope", "Segoe UI", sans-serif;
}
diff --git a/app/layout.tsx b/app/layout.tsx
index 996590f..0802fc1 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -3,6 +3,8 @@ import type { Metadata } from "next";
import "./globals.css";
import "currency-flags/dist/currency-flags.min.css";
+import { SmoothScrollProvider } from "@/components/providers/smooth-scroll-provider";
+
export const metadata: Metadata = {
title: "NexCurrency | Modern Currency & Crypto Converter",
description:
@@ -17,7 +19,7 @@ export default function RootLayout({
return (
- {children}
+ {children}
);
diff --git a/components/providers/smooth-scroll-provider.tsx b/components/providers/smooth-scroll-provider.tsx
new file mode 100644
index 0000000..f2d2933
--- /dev/null
+++ b/components/providers/smooth-scroll-provider.tsx
@@ -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;
+}
diff --git a/components/ui/command.tsx b/components/ui/command.tsx
index 581a52a..10c6b0e 100644
--- a/components/ui/command.tsx
+++ b/components/ui/command.tsx
@@ -45,6 +45,7 @@ const CommandList = React.forwardRef<
>(({ className, ...props }, ref) => (
diff --git a/package-lock.json b/package-lock.json
index 5c4ffac..19667da 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -16,6 +16,7 @@
"cryptocurrency-icons": "^0.18.1",
"currency-flags": "github:vivekimsit/currency-flags",
"framer-motion": "^12.36.0",
+ "lenis": "^1.3.18",
"lucide-react": "^0.475.0",
"next": "^14.2.24",
"react": "^18.2.0",
@@ -4564,6 +4565,32 @@
"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": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
diff --git a/package.json b/package.json
index 8c1a0ee..9b00e08 100644
--- a/package.json
+++ b/package.json
@@ -18,6 +18,7 @@
"cryptocurrency-icons": "^0.18.1",
"currency-flags": "github:vivekimsit/currency-flags",
"framer-motion": "^12.36.0",
+ "lenis": "^1.3.18",
"lucide-react": "^0.475.0",
"next": "^14.2.24",
"react": "^18.2.0",