"use client"; import { ArrowRightIcon, FileTextIcon, MessageSquareIcon, SearchIcon, SparklesIcon, ZapIcon } from "lucide-react"; import { AnimatePresence, motion, MotionConfig, useInView, useReducedMotion } from "motion/react"; import { type ReactNode, useEffect, useRef, useState } from "react"; import { cn } from "@/lib/utils"; function useReducedMotionSafe() { const prefers = useReducedMotion(); const [mounted, setMounted] = useState(false); useEffect(() => setMounted(true), []); return mounted && Boolean(prefers); } export type FeatureDemo = "search" | "shortcuts" | "history" | "automation" | "insights"; export interface BentoFeature { title: string; description: string; demo: FeatureDemo; span?: 2 | 3 | 4 | 6; } export interface FeatureBentoProps { eyebrow?: string; title: ReactNode; description?: ReactNode; features: BentoFeature[]; className?: string; } const ease = [0.23, 1, 0.32, 1] as const; const spring = { type: "spring", stiffness: 380, damping: 34 } as const; const accent = "text-violet-600 dark:text-violet-400"; const docs = [ { title: "Q3 roadmap", meta: "Product · edited 2h ago" }, { title: "Onboarding checklist", meta: "People · edited yesterday" }, { title: "Pricing page rewrite", meta: "Marketing · edited Mon" }, { title: "Roadmap review notes", meta: "Product · edited Sep 30" }, { title: "Launch plan: Atlas", meta: "Marketing · edited Sep 28" }, { title: "Mobile app roadmap", meta: "Engineering · edited Sep 24" }, { title: "Launch retro, spring", meta: "Marketing · edited Jun 3" }, { title: "Onboarding emails v2", meta: "Lifecycle · edited Aug 14" }, ]; const queries = ["roadmap", "launch", "onboarding"]; function Highlight({ text, query }: { text: string; query: string }) { const at = query ? text.toLowerCase().indexOf(query.toLowerCase()) : -1; if (at < 0) return <>{text}; return ( <> {text.slice(0, at)} {text.slice(at, at + query.length)} {text.slice(at + query.length)} ); } function SearchDemo({ active }: { active: boolean }) { const [q, setQ] = useState(0); const [typed, setTyped] = useState(queries[0].length); useEffect(() => { if (!active) return; let cancelled = false; let timer: number | undefined; const run = (index: number, count: number) => { if (cancelled) return; const word = queries[index]; if (count <= word.length) { setQ(index); setTyped(count); timer = window.setTimeout(() => run(index, count + 1), count === 0 ? 260 : 55); } else { timer = window.setTimeout(() => run((index + 1) % queries.length, 0), 1500); } }; timer = window.setTimeout(() => run((q + 1) % queries.length, 0), 350); return () => { cancelled = true; window.clearTimeout(timer); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [active]); const query = queries[q].slice(0, typed); const results = docs.filter((d) => d.title.toLowerCase().includes(query.toLowerCase())).slice(0, 3); return (
{query} {results.length} {results.length === 1 ? "doc" : "docs"}
); } const combos = [ { keys: ["⌘", "K"], label: "Open anything" }, { keys: ["⌘", "⇧", "P"], label: "Publish page" }, { keys: ["G", "I"], label: "Go to inbox" }, ]; function Key({ children, down }: { children: ReactNode; down: boolean }) { return ( {children} ); } function ShortcutDemo({ active }: { active: boolean }) { const [combo, setCombo] = useState(0); const [pressed, setPressed] = useState(0); useEffect(() => { if (!active) { setPressed(0); return; } let cancelled = false; const timers: number[] = []; const later = (fn: () => void, ms: number) => timers.push(window.setTimeout(() => !cancelled && fn(), ms)); const play = (index: number) => { setCombo(index); setPressed(0); const n = combos[index].keys.length; for (let k = 1; k <= n; k++) later(() => setPressed(k), 200 + k * 170); later(() => setPressed(0), 200 + n * 170 + 900); later(() => play((index + 1) % combos.length), 200 + n * 170 + 1400); }; play((combo + 1) % combos.length); return () => { cancelled = true; timers.forEach(window.clearTimeout); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [active]); const current = combos[combo]; const fired = pressed === current.keys.length; return (
{current.keys.map((key, i) => ( i} key={`${combo}-${key}`}> {key} ))}
{current.label}
); } const versions = [ { label: "v9", who: "Iris", when: "Sep 12", lines: [0.9, 0.6, 0.75, 0.3] }, { label: "v10", who: "Marc", when: "Sep 19", lines: [0.9, 0.82, 0.5, 0.64] }, { label: "v11", who: "Maya", when: "Oct 2", lines: [0.7, 0.88, 0.62, 0.8] }, { label: "v12", who: "Iris", when: "Oct 9", lines: [0.96, 0.7, 0.86, 0.45] }, { label: "v13", who: "You", when: "Today", lines: [0.8, 0.94, 0.58, 0.72] }, ]; function HistoryDemo({ active }: { active: boolean }) { const [v, setV] = useState(versions.length - 1); useEffect(() => { if (!active) { setV(versions.length - 1); return; } let i = 0; setV(0); const id = window.setInterval(() => { i = (i + 1) % versions.length; setV(i); }, 900); return () => window.clearInterval(id); }, [active]); const version = versions[v]; return (
{version.label} {version.who} · {version.when}
{version.lines.map((w, i) => ( ))}
{versions.map((item, i) => ( ))}
); } const steps = [ { icon: MessageSquareIcon, label: "Form reply" }, { icon: SparklesIcon, label: "Summarise" }, { icon: ZapIcon, label: "Post to #sales" }, ]; function AutomationDemo({ active }: { active: boolean }) { const reduce = useReducedMotionSafe(); const [lit, setLit] = useState(-1); useEffect(() => { if (!active) { setLit(-1); return; } let i = 0; setLit(0); const id = window.setInterval(() => { i = (i + 1) % (steps.length + 1); setLit(i === steps.length ? -1 : i); }, 650); return () => window.clearInterval(id); }, [active]); return (
{steps.map((step, i) => (
i && "text-muted-foreground", )} transition={{ duration: 0.18, ease }} > {step.label} = i ? "bg-emerald-500" : "bg-border", )} /> {i < steps.length - 1 && ( i ? "0%" : "-100%" }} className="absolute inset-0 bg-violet-500" initial={false} transition={{ duration: 0.2, ease }} /> )}
))}
); } const rest = [38, 52, 44, 66, 58, 74, 88]; const days = ["M", "T", "W", "T", "F", "S", "S"]; function InsightsDemo({ active }: { active: boolean }) { const [bars, setBars] = useState(rest); useEffect(() => { if (!active) { setBars(rest); return; } const shuffle = () => setBars(rest.map((b) => Math.max(18, Math.min(100, b + Math.round((Math.random() - 0.4) * 40))))); shuffle(); const id = window.setInterval(shuffle, 1100); return () => window.clearInterval(id); }, [active]); const total = Math.round(bars.reduce((a, b) => a + b, 0) * 3.1); return (
Readers this week {total.toLocaleString("en-US")}
{bars.map((h, i) => ( // biome-ignore lint/suspicious/noArrayIndexKey: fixed days
{days[i]}
))}
); } const demos: Record ReactNode> = { search: SearchDemo, shortcuts: ShortcutDemo, history: HistoryDemo, automation: AutomationDemo, insights: InsightsDemo, }; const spans: Record = { 2: "md:col-span-2", 3: "md:col-span-3", 4: "sm:col-span-2 md:col-span-4", 6: "sm:col-span-2 md:col-span-6", }; function Tile({ feature, index }: { feature: BentoFeature; index: number }) { const reduce = useReducedMotionSafe(); const ref = useRef(null); const inView = useInView(ref, { amount: 0.7 }); const [hovered, setHovered] = useState(false); const [touch, setTouch] = useState(false); useEffect(() => { setTouch(window.matchMedia("(hover: none)").matches); }, []); const active = !reduce && (hovered || (touch && inView)); const Demo = demos[feature.demo]; return ( e.pointerType === "mouse" && setHovered(true)} onPointerLeave={() => setHovered(false)} ref={ref} transition={{ duration: 0.5, ease, delay: Math.min(index, 5) * 0.04 }} viewport={{ once: true, amount: 0.3 }} whileInView={{ opacity: 1, y: 0 }} >

{feature.title}

{feature.description}

); } export function FeatureBento({ eyebrow, title, description, features, className }: FeatureBentoProps) { return (
{eyebrow &&

{eyebrow}

}

{title}

{description && (

{description} Hover a tile to see it work

)}
{features.map((feature, i) => ( ))}
); }