diff --git a/corpus/frontend/lenis/accessibility.yaml b/corpus/frontend/lenis/accessibility.yaml
new file mode 100644
index 0000000..37510b3
--- /dev/null
+++ b/corpus/frontend/lenis/accessibility.yaml
@@ -0,0 +1,32 @@
+name: accessibility
+description: >-
+ Respect prefers-reduced-motion by disabling smooth scrolling for users who
+ prefer it.
+code: |
+ "use client";
+
+ import { ReactLenis } from "lenis/react";
+ import "lenis/dist/lenis.css";
+
+ function useReducedMotion() {
+ if (typeof window === "undefined") return false;
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
+ }
+
+ export function AccessibleSmoothScrollProvider({ children }: { children: React.ReactNode }) {
+ const prefersReducedMotion = useReducedMotion();
+
+ if (prefersReducedMotion) {
+ return <>{children}>;
+ }
+
+ return (
+
+ {children}
+
+ );
+ }
+tips:
+ - Never force smooth scrolling on users who have opted out via prefers-reduced-motion.
+ - When skipping ReactLenis, native scroll is used - no polyfill needed.
+ - For a hook-based approach, use the useReducedMotion hook from Framer Motion or write your own with a useEffect + matchMedia listener.
diff --git a/corpus/frontend/lenis/custom-container.yaml b/corpus/frontend/lenis/custom-container.yaml
new file mode 100644
index 0000000..07e8955
--- /dev/null
+++ b/corpus/frontend/lenis/custom-container.yaml
@@ -0,0 +1,35 @@
+name: custom-container
+description: >-
+ Scoped scroll container using wrapper and content refs for non-window smooth
+ scroll.
+code: |
+ "use client";
+
+ import { useRef } from "react";
+ import { ReactLenis } from "lenis/react";
+ import "lenis/dist/lenis.css";
+
+ export function ScrollPanel({ children }: { children: React.ReactNode }) {
+ const wrapperRef = useRef(null);
+ const contentRef = useRef(null);
+
+ return (
+
+
+
+ );
+ }
+tips:
+ - "The wrapper element needs overflow: hidden and a fixed height for container scroll to work."
+ - The content element is the scrollable inner div - it should grow naturally with its children.
+ - This pattern is useful for split-panel layouts, side drawers, or modal scroll areas.
diff --git a/corpus/frontend/lenis/framer-motion-integration.yaml b/corpus/frontend/lenis/framer-motion-integration.yaml
new file mode 100644
index 0000000..6ec978c
--- /dev/null
+++ b/corpus/frontend/lenis/framer-motion-integration.yaml
@@ -0,0 +1,44 @@
+name: framer-motion-integration
+description: >-
+ Integrate Lenis with Framer Motion by disabling autoRaf and syncing via
+ frame.update.
+code: |
+ "use client";
+
+ import { useEffect } from "react";
+ import { ReactLenis, useLenis } from "lenis/react";
+ import "lenis/dist/lenis.css";
+ import { frame } from "motion";
+
+ function FramerSyncEffect() {
+ const lenis = useLenis();
+
+ useEffect(() => {
+ if (!lenis) return;
+
+ function update({ timestamp }: { timestamp: number }) {
+ lenis.raf(timestamp);
+ }
+
+ frame.update(update, true);
+
+ return () => {
+ frame.cancel(update);
+ };
+ }, [lenis]);
+
+ return null;
+ }
+
+ export function FramerLenisProvider({ children }: { children: React.ReactNode }) {
+ return (
+
+
+ {children}
+
+ );
+ }
+tips:
+ - "Use frame from 'motion' (not 'framer-motion') - this is the Framer Motion v11+ low-level scheduler."
+ - "frame.update(fn, true) schedules the update to run on every frame. The second argument (true) enables loop mode."
+ - "autoRaf: false is mandatory - same reasoning as with GSAP, prevent double-ticking."
diff --git a/corpus/frontend/lenis/full-page.yaml b/corpus/frontend/lenis/full-page.yaml
new file mode 100644
index 0000000..64a6b65
--- /dev/null
+++ b/corpus/frontend/lenis/full-page.yaml
@@ -0,0 +1,25 @@
+name: full-page
+description: >-
+ Standard root layout setup - ReactLenis wraps the entire app for full-page
+ smooth scrolling.
+code: |
+ // app/layout.tsx (Next.js App Router)
+ "use client"; // Must be client component
+
+ import { ReactLenis } from "lenis/react";
+ import "lenis/dist/lenis.css";
+
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
+ return (
+
+
+
+ {children}
+
+
+
+ );
+ }
+tips:
+ - "root={true} is required for full-page scroll - without it, Lenis creates an overflow:hidden container."
+ - The CSS import is mandatory - skip it and the layout breaks.
diff --git a/corpus/frontend/lenis/index.yaml b/corpus/frontend/lenis/index.yaml
index a9f0f74..1a865b8 100644
--- a/corpus/frontend/lenis/index.yaml
+++ b/corpus/frontend/lenis/index.yaml
@@ -2,3 +2,15 @@ namespace: frontend.lenis
patterns:
gsap-integration:
file: gsap-integration.yaml
+ full-page:
+ file: full-page.yaml
+ next-js:
+ file: next-js.yaml
+ framer-motion-integration:
+ file: framer-motion-integration.yaml
+ custom-container:
+ file: custom-container.yaml
+ scroll-to-nav:
+ file: scroll-to-nav.yaml
+ accessibility:
+ file: accessibility.yaml
diff --git a/corpus/frontend/lenis/next-js.yaml b/corpus/frontend/lenis/next-js.yaml
new file mode 100644
index 0000000..6f63ed1
--- /dev/null
+++ b/corpus/frontend/lenis/next-js.yaml
@@ -0,0 +1,36 @@
+name: next-js
+description: >-
+ Next.js App Router pattern using a dedicated SmoothScrollProvider client
+ component to wrap the layout.
+code: |
+ // components/smooth-scroll-provider.tsx
+ "use client";
+
+ import { ReactLenis } from "lenis/react";
+ import "lenis/dist/lenis.css";
+
+ export function SmoothScrollProvider({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ );
+ }
+
+ // app/layout.tsx
+ import { SmoothScrollProvider } from "@/components/smooth-scroll-provider";
+
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
+ return (
+
+
+
+ {children}
+
+
+
+ );
+ }
+tips:
+ - "Keep app/layout.tsx as a Server Component - extract the 'use client' directive into SmoothScrollProvider."
+ - This preserves RSC boundaries and avoids unnecessarily client-rendering the entire layout.
diff --git a/corpus/frontend/lenis/scroll-to-nav.yaml b/corpus/frontend/lenis/scroll-to-nav.yaml
new file mode 100644
index 0000000..575d074
--- /dev/null
+++ b/corpus/frontend/lenis/scroll-to-nav.yaml
@@ -0,0 +1,41 @@
+name: scroll-to-nav
+description: >-
+ Navigation link that uses lenis.scrollTo() for smooth in-page anchor
+ navigation.
+code: |
+ "use client";
+
+ import { useLenis } from "lenis/react";
+
+ interface NavLinkProps {
+ href: string;
+ children: React.ReactNode;
+ offset?: number;
+ duration?: number;
+ }
+
+ export function NavLink({ href, children, offset = -80, duration = 1.2 }: NavLinkProps) {
+ const lenis = useLenis();
+
+ function handleClick(e: React.MouseEvent) {
+ e.preventDefault();
+ lenis?.scrollTo(href, {
+ offset,
+ duration,
+ easing: (t) => 1 - Math.pow(1 - t, 4),
+ });
+ }
+
+ return (
+
+ {children}
+
+ );
+ }
+
+ // Usage:
+ // Features
+tips:
+ - offset compensates for sticky headers - pass a negative value equal to the header height.
+ - "lenis.scrollTo() accepts a CSS selector ('#section'), HTMLElement, or pixel number."
+ - For Next.js App Router, use usePathname to detect route changes and reset scroll position.
diff --git a/tests/lenis-pattern-corpus-backed-tools-behaviour.test.ts b/tests/lenis-pattern-corpus-backed-tools-behaviour.test.ts
index a039f55..f704e3f 100644
--- a/tests/lenis-pattern-corpus-backed-tools-behaviour.test.ts
+++ b/tests/lenis-pattern-corpus-backed-tools-behaviour.test.ts
@@ -13,11 +13,57 @@ test("lenis_get_pattern prefers corpus metadata for gsap-integration", async ()
expect(text).toContain('import { ReactLenis, useLenis } from "lenis/react"');
});
-test("lenis_get_pattern falls back to in-file data for non-corpus patterns", async () => {
+test("lenis_get_pattern prefers corpus metadata for full-page", async () => {
+ const result = await lenisGetPattern.invoke({ name: "full-page" });
+ const text = extractTextContent(result);
+
+ expect(text).toContain("# Lenis Pattern: full-page");
+ expect(text).toContain("**Corpus Source:** frontend.lenis");
+ expect(text).toContain('');
+});
+
+test("lenis_get_pattern prefers corpus metadata for next-js", async () => {
+ const result = await lenisGetPattern.invoke({ name: "next-js" });
+ const text = extractTextContent(result);
+
+ expect(text).toContain("# Lenis Pattern: next-js");
+ expect(text).toContain("**Corpus Source:** frontend.lenis");
+ expect(text).toContain("SmoothScrollProvider");
+});
+
+test("lenis_get_pattern prefers corpus metadata for framer-motion-integration", async () => {
+ const result = await lenisGetPattern.invoke({ name: "framer-motion-integration" });
+ const text = extractTextContent(result);
+
+ expect(text).toContain("# Lenis Pattern: framer-motion-integration");
+ expect(text).toContain("**Corpus Source:** frontend.lenis");
+ expect(text).toContain('import { frame } from "motion";');
+});
+
+test("lenis_get_pattern prefers corpus metadata for custom-container", async () => {
+ const result = await lenisGetPattern.invoke({ name: "custom-container" });
+ const text = extractTextContent(result);
+
+ expect(text).toContain("# Lenis Pattern: custom-container");
+ expect(text).toContain("**Corpus Source:** frontend.lenis");
+ expect(text).toContain("ScrollPanel");
+});
+
+test("lenis_get_pattern prefers corpus metadata for scroll-to-nav", async () => {
+ const result = await lenisGetPattern.invoke({ name: "scroll-to-nav" });
+ const text = extractTextContent(result);
+
+ expect(text).toContain("# Lenis Pattern: scroll-to-nav");
+ expect(text).toContain("**Corpus Source:** frontend.lenis");
+ expect(text).toContain("lenis?.scrollTo(href");
+});
+
+test("lenis_get_pattern prefers corpus metadata for accessibility", async () => {
const result = await lenisGetPattern.invoke({ name: "accessibility" });
const text = extractTextContent(result);
expect(text).toContain("# Lenis Pattern: accessibility");
- expect(text).toContain("## Key Notes");
- expect(text).not.toContain("**Corpus Source:** frontend.lenis");
+ expect(text).toContain("**Corpus Source:** frontend.lenis");
+ expect(text).toContain("prefers-reduced-motion");
+ expect(text).toContain("AccessibleSmoothScrollProvider");
});