diff --git a/website/package-lock.json b/website/package-lock.json
index 1bc97fb9..c8d5513d 100644
--- a/website/package-lock.json
+++ b/website/package-lock.json
@@ -18,9 +18,9 @@
"@vercel/analytics": "^2.0.1",
"@vercel/speed-insights": "^2.0.0",
"codemirror": "^6.0.2",
- "framer-motion": "^12.36.0",
"fuse.js": "^7.1.0",
"gray-matter": "^4.0.3",
+ "motion": "^12.36.0",
"next": "16.1.7",
"next-mdx-remote": "^6.0.0",
"react": "19.2.3",
@@ -4417,12 +4417,12 @@
"license": "MIT"
},
"node_modules/framer-motion": {
- "version": "12.36.0",
- "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.36.0.tgz",
- "integrity": "sha512-4PqYHAT7gev0ke0wos+PyrcFxI0HScjm3asgU8nSYa8YzJFuwgIvdj3/s3ZaxLq0bUSboIn19A2WS/MHwLCvfw==",
+ "version": "12.38.0",
+ "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.38.0.tgz",
+ "integrity": "sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==",
"license": "MIT",
"dependencies": {
- "motion-dom": "^12.36.0",
+ "motion-dom": "^12.38.0",
"motion-utils": "^12.36.0",
"tslib": "^2.4.0"
},
@@ -6327,10 +6327,36 @@
"integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==",
"license": "MIT"
},
+ "node_modules/motion": {
+ "version": "12.38.0",
+ "resolved": "https://registry.npmjs.org/motion/-/motion-12.38.0.tgz",
+ "integrity": "sha512-uYfXzeHlgThchzwz5Te47dlv5JOUC7OB4rjJ/7XTUgtBZD8CchMN8qEJ4ZVsUmTyYA44zjV0fBwsiktRuFnn+w==",
+ "license": "MIT",
+ "dependencies": {
+ "framer-motion": "^12.38.0",
+ "tslib": "^2.4.0"
+ },
+ "peerDependencies": {
+ "@emotion/is-prop-valid": "*",
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/is-prop-valid": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/motion-dom": {
- "version": "12.36.0",
- "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.36.0.tgz",
- "integrity": "sha512-Ep1pq8P88rGJ75om8lTCA13zqd7ywPGwCqwuWwin6BKc0hMLkVfcS6qKlRqEo2+t0DwoUcgGJfXwaiFn4AOcQA==",
+ "version": "12.38.0",
+ "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.38.0.tgz",
+ "integrity": "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==",
"license": "MIT",
"dependencies": {
"motion-utils": "^12.36.0"
diff --git a/website/package.json b/website/package.json
index 697f875b..30c3d921 100644
--- a/website/package.json
+++ b/website/package.json
@@ -22,7 +22,7 @@
"@vercel/analytics": "^2.0.1",
"@vercel/speed-insights": "^2.0.0",
"codemirror": "^6.0.2",
- "framer-motion": "^12.36.0",
+ "motion": "^12.36.0",
"fuse.js": "^7.1.0",
"gray-matter": "^4.0.3",
"next": "16.1.7",
diff --git a/website/src/app/benchmarks/BenchmarksContent.tsx b/website/src/app/benchmarks/BenchmarksContent.tsx
index f847cfe6..80ac936a 100644
--- a/website/src/app/benchmarks/BenchmarksContent.tsx
+++ b/website/src/app/benchmarks/BenchmarksContent.tsx
@@ -1,6 +1,7 @@
'use client';
import { useState } from 'react';
+import { motion, AnimatePresence } from 'motion/react';
import { FadeIn } from '@/components/ui/FadeIn';
import { GlassCard } from '@/components/ui/GlassCard';
import { Button } from '@/components/ui/Button';
@@ -41,22 +42,33 @@ function RawDataToggle({ id, children }: { id: string; children: React.ReactNode
aria-expanded={open}
aria-controls={`raw-data-${id}`}
>
-
+
{open ? 'Hide' : 'View'} raw data
- {open && (
-
- {children}
-
- )}
+
+ {open && (
+
+ {children}
+
+ )}
+
);
}
diff --git a/website/src/app/blog/BlogList.tsx b/website/src/app/blog/BlogList.tsx
index 181f1099..e7e23198 100644
--- a/website/src/app/blog/BlogList.tsx
+++ b/website/src/app/blog/BlogList.tsx
@@ -1,7 +1,7 @@
'use client';
import Link from 'next/link';
-import { motion } from 'framer-motion';
+import { motion } from 'motion/react';
import type { BlogPost } from '@/lib/blog';
export function BlogList({ posts }: { posts: BlogPost[] }) {
diff --git a/website/src/app/docs/page.tsx b/website/src/app/docs/page.tsx
index c124627e..0aaf9ad6 100644
--- a/website/src/app/docs/page.tsx
+++ b/website/src/app/docs/page.tsx
@@ -2,6 +2,7 @@ import type { Metadata } from 'next';
import Link from 'next/link';
import { DOCS_SIDEBAR } from '@/lib/constants';
import { DocsSearchTrigger } from '@/components/docs/DocsSearchTrigger';
+import { FadeIn } from '@/components/ui/FadeIn';
export const metadata: Metadata = {
title: 'Documentation',
@@ -43,30 +44,31 @@ export default function DocsPage() {
- {DOCS_SIDEBAR.map((group) => (
-
-
-
{group.category}
-
- {group.items.length} {group.items.length === 1 ? 'article' : 'articles'}
-
-
- {group.items.slice(0, 3).map((item) => (
- - {item.label}
- ))}
- {group.items.length > 3 && (
- - +{group.items.length - 3} more
- )}
-
-
+ {DOCS_SIDEBAR.map((group, i) => (
+
+
+
+ {group.category}
+
+ {group.items.length} {group.items.length === 1 ? 'article' : 'articles'}
+
+
+ {group.items.slice(0, 3).map((item) => (
+ - {item.label}
+ ))}
+ {group.items.length > 3 && (
+ - +{group.items.length - 3} more
+ )}
+
+
+
))}
diff --git a/website/src/app/layout.tsx b/website/src/app/layout.tsx
index c2597a7e..03ac377d 100644
--- a/website/src/app/layout.tsx
+++ b/website/src/app/layout.tsx
@@ -3,6 +3,7 @@ import { spaceGrotesk, inter, jetbrainsMono } from '@/lib/fonts';
import { Navbar } from '@/components/layout/Navbar';
import { Footer } from '@/components/layout/Footer';
import { ServiceWorkerRegister } from '@/components/ServiceWorkerRegister';
+import { ScrollProgressBar } from '@/components/ui/ScrollProgressBar';
import { Analytics } from '@vercel/analytics/next';
import { SpeedInsights } from '@vercel/speed-insights/next';
import './globals.css';
@@ -96,6 +97,7 @@ export default function RootLayout({
}),
}}
/>
+
{children}
diff --git a/website/src/app/not-found.tsx b/website/src/app/not-found.tsx
index 37099d3f..bc479522 100644
--- a/website/src/app/not-found.tsx
+++ b/website/src/app/not-found.tsx
@@ -1,5 +1,6 @@
import type { Metadata } from 'next';
import Link from 'next/link';
+import { FadeIn } from '@/components/ui/FadeIn';
export const metadata: Metadata = {
title: '404 - Page Not Found',
@@ -11,19 +12,27 @@ export default function NotFound() {
return (
-
404
-
- Page not found
-
-
- The page you're looking for doesn't exist.
-
-
- Back to Home
-
+
+ 404
+
+
+
+ Page not found
+
+
+
+
+ The page you're looking for doesn't exist.
+
+
+
+
+ Back to Home
+
+
);
diff --git a/website/src/components/docs/CopyButton.tsx b/website/src/components/docs/CopyButton.tsx
index 878570dc..78920486 100644
--- a/website/src/components/docs/CopyButton.tsx
+++ b/website/src/components/docs/CopyButton.tsx
@@ -1,6 +1,7 @@
'use client';
import { useState } from 'react';
+import { motion, AnimatePresence } from 'motion/react';
export function CopyButton({ text }: { text: string }) {
const [copied, setCopied] = useState(false);
@@ -16,26 +17,41 @@ export function CopyButton({ text }: { text: string }) {
});
};
+ const iconKey = failed ? 'failed' : copied ? 'copied' : 'copy';
+
return (
-
+
+
+ {copied ? (
+
+ ) : failed ? (
+
+ ) : (
+
+ )}
+
+
+
);
}
diff --git a/website/src/components/home/CodeExamples.tsx b/website/src/components/home/CodeExamples.tsx
index 6099317f..977bef9d 100644
--- a/website/src/components/home/CodeExamples.tsx
+++ b/website/src/components/home/CodeExamples.tsx
@@ -1,9 +1,9 @@
'use client';
import { useState } from 'react';
-import { motion, AnimatePresence } from 'framer-motion';
+import { motion, AnimatePresence } from 'motion/react';
import { GlassCard } from '@/components/ui/GlassCard';
-import { FadeInCSS } from '@/components/ui/FadeInCSS';
+import { FadeIn } from '@/components/ui/FadeIn';
type Segment = { text: string; cls: string };
type CodeLine = Segment[];
@@ -103,12 +103,12 @@ export function CodeExamples() {
return (
-
+
Simple, Powerful API
-
-
+
+
{tabs.map((tab, i) => (
-
+
);
diff --git a/website/src/components/home/CtaBanner.tsx b/website/src/components/home/CtaBanner.tsx
index 33484e8a..60f7b3f6 100644
--- a/website/src/components/home/CtaBanner.tsx
+++ b/website/src/components/home/CtaBanner.tsx
@@ -1,4 +1,4 @@
-import { FadeInCSS } from '@/components/ui/FadeInCSS';
+import { FadeIn } from '@/components/ui/FadeIn';
import { Button } from '@/components/ui/Button';
export function CtaBanner() {
@@ -12,7 +12,7 @@ export function CtaBanner() {
-
+
Ready to parse SQL at the speed of Go?
@@ -24,7 +24,7 @@ export function CtaBanner() {
Try Playground
-
+
);
diff --git a/website/src/components/home/DialectShowcase.tsx b/website/src/components/home/DialectShowcase.tsx
index 26646f85..418d86fd 100644
--- a/website/src/components/home/DialectShowcase.tsx
+++ b/website/src/components/home/DialectShowcase.tsx
@@ -1,5 +1,5 @@
import { GlassCard } from '@/components/ui/GlassCard';
-import { FadeInCSS } from '@/components/ui/FadeInCSS';
+import { FadeIn } from '@/components/ui/FadeIn';
interface Dialect {
name: string;
@@ -40,17 +40,17 @@ export function DialectShowcase() {
return (
-
+
8 SQL Dialects, One Parser
From PostgreSQL to ClickHouse — parse them all
-
+
{DIALECTS.map((dialect, i) => (
-
+
-
+
))}
diff --git a/website/src/components/home/FeatureGrid.tsx b/website/src/components/home/FeatureGrid.tsx
index 8f058451..69fd2d24 100644
--- a/website/src/components/home/FeatureGrid.tsx
+++ b/website/src/components/home/FeatureGrid.tsx
@@ -1,5 +1,5 @@
import { GlassCard } from '@/components/ui/GlassCard';
-import { FadeInCSS } from '@/components/ui/FadeInCSS';
+import { FadeIn } from '@/components/ui/FadeIn';
import { FEATURES } from '@/lib/constants';
const icons: Record
= {
@@ -48,14 +48,14 @@ export function FeatureGrid() {
return (
-
+
Built for Production
-
+
{FEATURES.map((feature, i) => (
-
+
{icons[feature.icon]}
@@ -63,7 +63,7 @@ export function FeatureGrid() {
{feature.title}
{feature.description}
-
+
))}
diff --git a/website/src/components/home/GitHubStarButton.tsx b/website/src/components/home/GitHubStarButton.tsx
index 26e630a4..ec78a425 100644
--- a/website/src/components/home/GitHubStarButton.tsx
+++ b/website/src/components/home/GitHubStarButton.tsx
@@ -1,6 +1,9 @@
'use client';
import { useState, useEffect } from 'react';
+import { motion } from 'motion/react';
+
+const spring = { type: 'spring' as const, stiffness: 400, damping: 17 };
export function GitHubStarButton() {
const [stars, setStars] = useState
(null);
@@ -15,11 +18,14 @@ export function GitHubStarButton() {
}, []);
return (
-
+
);
}
diff --git a/website/src/components/home/Hero.tsx b/website/src/components/home/Hero.tsx
index 425976e6..131f6ded 100644
--- a/website/src/components/home/Hero.tsx
+++ b/website/src/components/home/Hero.tsx
@@ -1,4 +1,4 @@
-import { FadeInCSS } from '@/components/ui/FadeInCSS';
+import { FadeIn } from '@/components/ui/FadeIn';
import { GlassCard } from '@/components/ui/GlassCard';
import { GradientText } from '@/components/ui/GradientText';
import { VersionBadge } from '@/components/ui/VersionBadge';
@@ -41,31 +41,31 @@ export function Hero() {
{/* Content */}
{/* Version badge */}
-
+
-
+
{/* Headline */}
-
+
Parse SQL at the speed of Go
-
+
{/* Subtitle */}
-
+
Production-ready SQL parsing with zero-copy tokenization, object pooling, and multi-dialect support
-
+
{/* Buttons */}
-
+
-
+
{/* Live mini playground */}
-
+
-
+
diff --git a/website/src/components/home/McpSection.tsx b/website/src/components/home/McpSection.tsx
index 9aa4cef3..97bf9221 100644
--- a/website/src/components/home/McpSection.tsx
+++ b/website/src/components/home/McpSection.tsx
@@ -1,5 +1,5 @@
import Link from 'next/link';
-import { FadeInCSS } from '@/components/ui/FadeInCSS';
+import { FadeIn } from '@/components/ui/FadeIn';
import { TerminalMockup } from '@/components/ui/TerminalMockup';
const tools = [
@@ -16,21 +16,21 @@ export function McpSection() {
return (
-
+
AI-Ready SQL Tools
Connect 7 SQL tools to Claude, Cursor, or any MCP client — no installation, no API key.
-
-
+
+
-
-
+
+
{tools.map((tool) => (
))}
-
-
+
+
-
+
);
diff --git a/website/src/components/home/PerformanceSection.tsx b/website/src/components/home/PerformanceSection.tsx
index 8a16397e..b510d89f 100644
--- a/website/src/components/home/PerformanceSection.tsx
+++ b/website/src/components/home/PerformanceSection.tsx
@@ -1,5 +1,5 @@
import { GlassCard } from '@/components/ui/GlassCard';
-import { FadeInCSS } from '@/components/ui/FadeInCSS';
+import { FadeIn } from '@/components/ui/FadeIn';
import { AnimatedCounter } from '@/components/ui/AnimatedCounter';
import { AnimatedBars } from './AnimatedBars';
@@ -23,16 +23,16 @@ export function PerformanceSection() {
return (
-
+
Performance That Speaks for Itself
-
+
{/* Stat cards */}
{stats.map((stat, i) => (
-
+
{stat.prefix && (
@@ -42,19 +42,19 @@ export function PerformanceSection() {
{stat.label}
-
+
))}
{/* Bar chart */}
-
+
Based on BenchmarkParse, Apple M4, Go 1.26
-
+
);
diff --git a/website/src/components/home/SocialProof.tsx b/website/src/components/home/SocialProof.tsx
index 2d28d90e..488887e6 100644
--- a/website/src/components/home/SocialProof.tsx
+++ b/website/src/components/home/SocialProof.tsx
@@ -1,5 +1,5 @@
import Image from 'next/image';
-import { FadeInCSS } from '@/components/ui/FadeInCSS';
+import { FadeIn } from '@/components/ui/FadeIn';
const badges = [
{
@@ -32,7 +32,7 @@ export function SocialProof() {
return (
-
+
{badges.map((badge) => (
))}
-
+
);
diff --git a/website/src/components/home/StatsBar.tsx b/website/src/components/home/StatsBar.tsx
index 9f34afbc..1bc112fe 100644
--- a/website/src/components/home/StatsBar.tsx
+++ b/website/src/components/home/StatsBar.tsx
@@ -1,5 +1,5 @@
import { GlassCard } from '@/components/ui/GlassCard';
-import { FadeInCSS } from '@/components/ui/FadeInCSS';
+import { FadeIn } from '@/components/ui/FadeIn';
import { AnimatedCounter } from '@/components/ui/AnimatedCounter';
const stats = [
@@ -15,7 +15,7 @@ export function StatsBar() {
{stats.map((stat, i) => (
-
+
{stat.prefix && (
@@ -25,7 +25,7 @@ export function StatsBar() {
{stat.label}
-
+
))}
diff --git a/website/src/components/home/TrustSection.tsx b/website/src/components/home/TrustSection.tsx
index b86a2c33..45a824da 100644
--- a/website/src/components/home/TrustSection.tsx
+++ b/website/src/components/home/TrustSection.tsx
@@ -1,4 +1,4 @@
-import { FadeInCSS } from '@/components/ui/FadeInCSS';
+import { FadeIn } from '@/components/ui/FadeIn';
import { GitHubStarCount } from './GitHubStarCount';
/* ── Inline SVG icons (Heroicons-style, 20x20) ────────────────────────── */
@@ -94,16 +94,16 @@ export function TrustSection() {
{/* Heading */}
-
+
Trusted by Developers
-
+
{/* Metric cards */}
{metrics.map((m, i) => (
-
+
{m.icon}
@@ -111,20 +111,20 @@ export function TrustSection() {
{m.label}
-
+
))}
{/* Integrations */}
-
+
Integrates with
-
+
{integrations.map((item, i) => (
-
+
{item.name}
@@ -133,7 +133,7 @@ export function TrustSection() {
{item.detail}
-
+
))}
diff --git a/website/src/components/home/VscodeSection.tsx b/website/src/components/home/VscodeSection.tsx
index c8b912b7..77f59a29 100644
--- a/website/src/components/home/VscodeSection.tsx
+++ b/website/src/components/home/VscodeSection.tsx
@@ -1,4 +1,4 @@
-import { FadeInCSS } from '@/components/ui/FadeInCSS';
+import { FadeIn } from '@/components/ui/FadeIn';
import { GlassCard } from '@/components/ui/GlassCard';
import { Button } from '@/components/ui/Button';
@@ -9,7 +9,7 @@ export function VscodeSection() {
{/* Left: Copy */}
-
+
IDE Integration
@@ -26,12 +26,12 @@ export function VscodeSection() {
Install Extension
-
+
{/* Right: VS Code Mockup */}
-
+
{/* Title bar */}
@@ -100,7 +100,7 @@ export function VscodeSection() {
0 issues
-
+
diff --git a/website/src/components/layout/Navbar.tsx b/website/src/components/layout/Navbar.tsx
index 5a7005d5..938239dd 100644
--- a/website/src/components/layout/Navbar.tsx
+++ b/website/src/components/layout/Navbar.tsx
@@ -3,7 +3,7 @@ import Link from 'next/link';
import Image from 'next/image';
import { useState, useEffect } from 'react';
import { usePathname } from 'next/navigation';
-import { motion, AnimatePresence, useScroll, useTransform } from 'framer-motion';
+import { motion, AnimatePresence, useScroll, useTransform } from 'motion/react';
import { NAV_LINKS } from '@/lib/constants';
import { Button } from '@/components/ui/Button';
import { SearchModal, useSearchShortcut } from '@/components/ui/SearchModal';
diff --git a/website/src/components/playground/Playground.tsx b/website/src/components/playground/Playground.tsx
index fd2a1ab0..5fbb7e23 100644
--- a/website/src/components/playground/Playground.tsx
+++ b/website/src/components/playground/Playground.tsx
@@ -1,6 +1,6 @@
'use client';
import React, { useState, useEffect, useCallback, useRef, Suspense } from "react";
-import { motion } from "framer-motion";
+import { motion } from 'motion/react';
import { useWasm } from "./WasmLoader";
import SqlEditor from "./SqlEditor";
import AstTab from "./AstTab";
diff --git a/website/src/components/ui/AnimatedCounter.tsx b/website/src/components/ui/AnimatedCounter.tsx
index 7068346b..7ad26178 100644
--- a/website/src/components/ui/AnimatedCounter.tsx
+++ b/website/src/components/ui/AnimatedCounter.tsx
@@ -1,5 +1,5 @@
'use client';
-import { useMotionValue, useSpring } from 'framer-motion';
+import { useMotionValue, useSpring } from 'motion/react';
import { useRef, useEffect, useState } from 'react';
export function AnimatedCounter({ value, suffix = '', color = 'text-white' }: { value: number; suffix?: string; color?: string }) {
diff --git a/website/src/components/ui/Button.tsx b/website/src/components/ui/Button.tsx
index db13715b..1e2e956f 100644
--- a/website/src/components/ui/Button.tsx
+++ b/website/src/components/ui/Button.tsx
@@ -1,4 +1,6 @@
+'use client';
import Link from 'next/link';
+import { motion } from 'motion/react';
type ButtonProps = {
variant?: 'primary' | 'ghost';
@@ -9,15 +11,46 @@ type ButtonProps = {
'aria-label'?: string;
};
+const spring = { type: 'spring' as const, stiffness: 400, damping: 17 };
+
export function Button({ variant = 'primary', href, children, className = '', external, 'aria-label': ariaLabel }: ButtonProps) {
const base = variant === 'primary'
? 'bg-white text-zinc-950 hover:bg-zinc-200'
: 'bg-white/[0.06] border border-white/[0.1] text-zinc-300 hover:bg-white/[0.1] hover:text-white';
- const cls = `inline-flex items-center gap-2 px-5 py-2.5 rounded-lg font-medium text-sm transition-all duration-200 ${base} ${className}`;
+ const cls = `inline-flex items-center gap-2 px-5 py-2.5 rounded-lg font-medium text-sm transition-colors duration-200 ${base} ${className}`;
if (href) {
- if (external) return {children};
- return {children};
+ if (external) {
+ return (
+
+ {children}
+
+ );
+ }
+ return (
+
+ {children}
+
+ );
}
- return ;
+ return (
+
+ {children}
+
+ );
}
diff --git a/website/src/components/ui/FadeIn.tsx b/website/src/components/ui/FadeIn.tsx
index ce1b4096..50917e5f 100644
--- a/website/src/components/ui/FadeIn.tsx
+++ b/website/src/components/ui/FadeIn.tsx
@@ -1,29 +1,54 @@
'use client';
-import { motion, useReducedMotion } from 'framer-motion';
+import { motion, useReducedMotion } from 'motion/react';
import { ReactNode, useState, useEffect, memo } from 'react';
+import { fadeInUp, defaultTransition } from '@/lib/motion-variants';
-export const FadeIn = memo(function FadeIn({ children, delay = 0, className = '' }: { children: ReactNode; delay?: number; className?: string }) {
+interface FadeInProps {
+ children: ReactNode;
+ delay?: number;
+ className?: string;
+ /** Use whileInView (scroll-triggered) instead of animate-on-mount */
+ viewport?: boolean;
+}
+
+export const FadeIn = memo(function FadeIn({ children, delay = 0, className = '', viewport = false }: FadeInProps) {
const shouldReduce = useReducedMotion();
const [isMounted, setIsMounted] = useState(false);
- // Defer Framer Motion's initial state until after hydration. Without this,
- // Framer Motion applies initial={{ opacity: 0 }} before React hydrates, causing
- // the server-rendered HTML (opacity:1) to mismatch the client DOM (opacity:0).
useEffect(() => {
setIsMounted(true);
}, []);
if (!isMounted) {
- // Server + hydration pass: render without Framer Motion so the DOM matches
- // the server HTML exactly. No opacity/transform styles applied.
return {children}
;
}
+ const transition = {
+ ...defaultTransition,
+ duration: shouldReduce ? 0 : 0.5,
+ delay: shouldReduce ? 0 : delay,
+ };
+
+ if (viewport) {
+ return (
+
+ {children}
+
+ );
+ }
+
return (
{children}
diff --git a/website/src/components/ui/GlassCard.tsx b/website/src/components/ui/GlassCard.tsx
index 1d3dd1e1..f3254ec2 100644
--- a/website/src/components/ui/GlassCard.tsx
+++ b/website/src/components/ui/GlassCard.tsx
@@ -1,12 +1,16 @@
'use client';
-import { motion } from 'framer-motion';
+import { motion } from 'motion/react';
import { ReactNode } from 'react';
+const spring = { type: 'spring' as const, stiffness: 400, damping: 17 };
+
export function GlassCard({ children, className = '', hover = true }: { children: ReactNode; className?: string; hover?: boolean }) {
return (
{children}
diff --git a/website/src/components/ui/ScrollProgressBar.tsx b/website/src/components/ui/ScrollProgressBar.tsx
new file mode 100644
index 00000000..a6830072
--- /dev/null
+++ b/website/src/components/ui/ScrollProgressBar.tsx
@@ -0,0 +1,25 @@
+'use client';
+import { motion, useScroll, useReducedMotion } from 'motion/react';
+
+export function ScrollProgressBar() {
+ const { scrollYProgress } = useScroll();
+ const shouldReduce = useReducedMotion();
+
+ if (shouldReduce) return null;
+
+ return (
+
+ );
+}
diff --git a/website/src/components/ui/SearchModal.tsx b/website/src/components/ui/SearchModal.tsx
index 508cc3ff..b647166a 100644
--- a/website/src/components/ui/SearchModal.tsx
+++ b/website/src/components/ui/SearchModal.tsx
@@ -2,6 +2,7 @@
import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
import { useRouter } from 'next/navigation';
+import { motion, AnimatePresence } from 'motion/react';
import Fuse from 'fuse.js';
import { buildSearchIndex, type SearchEntry } from '@/lib/search-index';
@@ -93,9 +94,9 @@ export function SearchModal({ open, onClose }: SearchModalProps) {
setSelectedIndex(0);
}, [results]);
- if (!open) return null;
-
return (
+
+ {open && (
{/* Backdrop */}
-
+
{/* Modal */}
-
+
+ )}
+
);
}
diff --git a/website/src/components/ui/ThemeToggle.tsx b/website/src/components/ui/ThemeToggle.tsx
index 835bfc90..c0bcdcbd 100644
--- a/website/src/components/ui/ThemeToggle.tsx
+++ b/website/src/components/ui/ThemeToggle.tsx
@@ -1,6 +1,7 @@
'use client';
import { useState, useEffect } from 'react';
+import { motion, AnimatePresence } from 'motion/react';
function SunIcon({ className = '' }: { className?: string }) {
return (
@@ -65,17 +66,39 @@ export function ThemeToggle() {
}
return (
-
+
+ {theme === 'dark' ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+
);
}
diff --git a/website/src/lib/motion-variants.ts b/website/src/lib/motion-variants.ts
new file mode 100644
index 00000000..fac55898
--- /dev/null
+++ b/website/src/lib/motion-variants.ts
@@ -0,0 +1,80 @@
+import type { Variants, Transition } from 'motion/react';
+
+// -- Shared animation variants --
+
+/** Fade in from below -- use for sections, cards, any scroll-triggered content */
+export const fadeInUp: Variants = {
+ hidden: { opacity: 0, y: 20 },
+ visible: { opacity: 1, y: 0 },
+};
+
+/** Fade in from left */
+export const slideInLeft: Variants = {
+ hidden: { opacity: 0, x: -30 },
+ visible: { opacity: 1, x: 0 },
+};
+
+/** Fade in from right */
+export const slideInRight: Variants = {
+ hidden: { opacity: 0, x: 30 },
+ visible: { opacity: 1, x: 0 },
+};
+
+/** Simple opacity fade */
+export const fadeIn: Variants = {
+ hidden: { opacity: 0 },
+ visible: { opacity: 1 },
+};
+
+/** Stagger container -- wraps children that each use fadeInUp/slideIn etc. */
+export const staggerContainer: Variants = {
+ hidden: {},
+ visible: {
+ transition: {
+ staggerChildren: 0.06,
+ delayChildren: 0.1,
+ },
+ },
+};
+
+/** Faster stagger for dense grids (8+ items) */
+export const staggerContainerFast: Variants = {
+ hidden: {},
+ visible: {
+ transition: {
+ staggerChildren: 0.04,
+ delayChildren: 0.05,
+ },
+ },
+};
+
+// -- Shared transitions --
+
+export const defaultTransition: Transition = {
+ duration: 0.4,
+ ease: [0.25, 0.1, 0.25, 1],
+};
+
+export const springTransition: Transition = {
+ type: 'spring',
+ stiffness: 400,
+ damping: 17,
+};
+
+// -- Shared gesture props (spread onto motion components) --
+
+export const hoverLift = {
+ whileHover: { y: -4 },
+ transition: defaultTransition,
+};
+
+export const hoverScale = {
+ whileHover: { scale: 1.02 },
+ whileTap: { scale: 0.98 },
+ transition: springTransition,
+};
+
+export const tapShrink = {
+ whileTap: { scale: 0.97 },
+ transition: springTransition,
+};