"use client"
import { ReactLenis, type LenisRef } from "lenis/react"
import {
cancelFrame,
easeInOut,
frame,
motion,
useScroll,
useTransform,
} from "motion/react"
import { useEffect, useRef } from "react"
export default function LenisSmoothScroll() {
const lenisRef = useRef<LenisRef>(null)
useEffect(() => {
// "setup" runs before Motion measures scroll, so useScroll
// sees the scroll position Lenis writes in the same frame.
const update = ({ timestamp }: { timestamp: number }) => {
lenisRef.current?.lenis?.raf(timestamp)
}
frame.setup(update, true)
return () => cancelFrame(update)
}, [])
const { scrollYProgress } = useScroll()
const progress = useTransform(
scrollYProgress,
[0, 1],
["scaleX(0)", "scaleX(1)"]
)
const scrollToSection = (id: string) => {
lenisRef.current?.lenis?.scrollTo(`#${id}`, {
duration: 1.6,
easing: easeInOut,
})
}
return (
<ReactLenis root options={{ autoRaf: false }} ref={lenisRef}>
<nav className="lenis-nav">
{SECTIONS.map(({ id }, i) => (
<button key={id} onClick={() => scrollToSection(id)}>
0{i + 1}
</button>
))}
</nav>
{SECTIONS.map((section) => (
<Section key={section.id} {...section} />
))}
<motion.div className="lenis-progress" style={{ transform: progress }} />
<Stylesheet />
</ReactLenis>
)
}
function Section({ id, image, title }: SectionData) {
const ref = useRef(null)
const { scrollYProgress } = useScroll({
target: ref,
offset: ["start end", "end start"],
})
const transform = useTransform(
scrollYProgress,
[0, 1],
["translateY(-300px)", "translateY(300px)"]
)
return (
<section id={id} ref={ref} className="lenis-section">
<div>
<img src={image} alt="" />
</div>
<motion.h2 style={{ transform }}>{title}</motion.h2>
</section>
)
}
/**
* ============== Data ================
*/
interface SectionData {
id: string
image: string
title: string
}
const SECTIONS: SectionData[] = [1, 2, 3, 4, 5].map((n) => ({
id: `lenis-${n}`,
image: `/photos/cityscape/${n}.jpg`,
title: `#00${n}`,
}))
/**
* ============== Styles ================
*/
function Stylesheet() {
return (
<style>{`
/* Recommended rules from lenis/dist/lenis.css */
html.lenis,
html.lenis body {
height: auto;
}
.lenis:not(.lenis-autoToggle).lenis-stopped {
overflow: clip;
}
.lenis.lenis-smooth iframe {
pointer-events: none;
}
.lenis-section {
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
position: relative;
}
.lenis-section > div {
width: 300px;
height: 400px;
margin: 20px;
background: var(--white);
overflow: hidden;
}
.lenis-section img {
width: 300px;
height: 400px;
object-fit: cover;
}
.lenis-section h2 {
color: var(--accent);
margin: 0;
font-family: "Geist Mono", monospace;
font-size: 50px;
font-weight: 700;
letter-spacing: -3px;
line-height: 1.2;
position: absolute;
top: calc(50% - 25px);
left: calc(50% + 120px);
}
.lenis-nav {
position: fixed;
top: 20px;
left: 50%;
translate: -50% 0;
display: flex;
gap: 4px;
padding: 4px;
border-radius: 999px;
background: var(--layer-transparent);
border: 1px solid var(--border);
backdrop-filter: blur(8px);
z-index: 1;
}
.lenis-nav button {
font-family: "Geist Mono", monospace;
font-size: 14px;
padding: 6px 12px;
border-radius: 999px;
color: var(--text);
}
.lenis-nav button:hover {
background: var(--accent-transparent);
color: var(--accent);
}
.lenis-progress {
position: fixed;
left: 0;
right: 0;
height: 5px;
background: var(--accent);
bottom: 50px;
transform: scaleX(0);
transform-origin: 0 50%;
}
`}</style>
)
}