"use client";
import * as React from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import {
ArrowRight,
BarChart3,
BookOpen,
Boxes,
Building2,
ChevronDown,
CreditCard,
FileCode2,
GraduationCap,
LifeBuoy,
Menu,
MessagesSquare,
Rocket,
ShieldCheck,
ShoppingBag,
Sparkles,
Users,
Workflow,
X,
Zap,
type LucideIcon,
} from "lucide-react";
import { cn } from "@/lib/utils";
export interface MegaItem {
title: string;
description: string;
icon: LucideIcon;
href?: string;
}
export interface MegaMenu {
label: string;
columns: { title: string; items: MegaItem[] }[];
featured?: { eyebrow: string; title: string; description: string; cta: string; href?: string };
}
export interface NavbarMegaProps {
brand?: string;
brandHref?: string;
menus?: MegaMenu[];
links?: { label: string; href?: string }[];
signIn?: { label: string; href?: string };
cta?: { label: string; href?: string };
/** Label of a menu to open initially (useful for demos/screenshots). */
defaultOpen?: string;
className?: string;
}
const EASE = [0.22, 1, 0.36, 1] as const;
const DEFAULT_MENUS: MegaMenu[] = [
{
label: "Product",
columns: [
{
title: "Platform",
items: [
{ title: "Analytics", description: "Dashboards that update in real time", icon: BarChart3 },
{ title: "Automations", description: "No-code workflows triggered by events", icon: Workflow },
{ title: "Integrations", description: "Connect 120+ tools in a click", icon: Boxes },
],
},
{
title: "Capabilities",
items: [
{ title: "AI assistant", description: "Ask questions about your data", icon: Sparkles },
{ title: "Performance", description: "Sub-100ms queries at any scale", icon: Zap },
{ title: "Security", description: "SSO, SCIM, audit logs, SOC 2", icon: ShieldCheck },
],
},
],
featured: {
eyebrow: "Launch week",
title: "Northwind 4.0 is here",
description: "A rebuilt query engine, a new canvas and 40 other upgrades.",
cta: "Read the announcement",
},
},
{
label: "Solutions",
columns: [
{
title: "By team",
items: [
{ title: "Product", description: "Understand adoption and retention", icon: Rocket },
{ title: "Growth", description: "Experiment and measure every funnel", icon: BarChart3 },
{ title: "Support", description: "Resolve issues with full context", icon: LifeBuoy },
],
},
{
title: "By industry",
items: [
{ title: "E-commerce", description: "Carts, cohorts and lifetime value", icon: ShoppingBag },
{ title: "Fintech", description: "Compliant analytics for payments", icon: CreditCard },
{ title: "Enterprise", description: "Governance for global teams", icon: Building2 },
],
},
],
featured: {
eyebrow: "Customer story",
title: "How Acme cut churn by 32%",
description: "Their growth team replaced four tools with one workspace.",
cta: "Read the case study",
},
},
{
label: "Resources",
columns: [
{
title: "Learn",
items: [
{ title: "Documentation", description: "Guides, recipes and API reference", icon: BookOpen },
{ title: "Academy", description: "Free courses with certificates", icon: GraduationCap },
{ title: "Changelog", description: "Everything we shipped this month", icon: FileCode2 },
],
},
{
title: "Connect",
items: [
{ title: "Community", description: "12k builders swapping ideas", icon: Users },
{ title: "Support", description: "Talk to a human in minutes", icon: MessagesSquare },
{ title: "Status", description: "Uptime and incident history", icon: ShieldCheck },
],
},
],
featured: {
eyebrow: "Webinar · Oct 14",
title: "Metrics that matter in 2027",
description: "A live session with our data science team.",
cta: "Save your seat",
},
},
];
export function NavbarMega({
brand = "Northwind",
brandHref = "#",
menus = DEFAULT_MENUS,
links = [
{ label: "Pricing", href: "#" },
{ label: "Enterprise", href: "#" },
],
signIn = { label: "Sign in", href: "#" },
cta = { label: "Start free", href: "#" },
defaultOpen,
className,
}: NavbarMegaProps) {
const reduce = useReducedMotion();
const uid = React.useId();
const [open, setOpen] = React.useState<string | null>(defaultOpen ?? null);
const [dir, setDir] = React.useState(0);
const [mobile, setMobile] = React.useState(false);
const rootRef = React.useRef<HTMLElement>(null);
const triggerRefs = React.useRef<Record<string, HTMLButtonElement | null>>({});
const hoverTimer = React.useRef<ReturnType<typeof setTimeout> | null>(null);
const idx = (label: string | null) => menus.findIndex((m) => m.label === label);
const show = React.useCallback(
(label: string | null) => {
setOpen((prev) => {
if (prev && label) setDir(Math.sign(menus.findIndex((m) => m.label === label) - menus.findIndex((m) => m.label === prev)));
else setDir(0);
return label;
});
},
[menus],
);
const clearTimer = () => {
if (hoverTimer.current) clearTimeout(hoverTimer.current);
};
const hoverOpen = (label: string) => {
clearTimer();
hoverTimer.current = setTimeout(() => show(label), open ? 0 : 90);
};
const hoverClose = () => {
clearTimer();
hoverTimer.current = setTimeout(() => show(null), 160);
};
React.useEffect(() => () => clearTimer(), []);
const focusPanelFirst = () =>
requestAnimationFrame(() => rootRef.current?.querySelector<HTMLElement>(`#${CSS.escape(`${uid}-panel`)} a`)?.focus());
const onTriggerKey = (e: React.KeyboardEvent<HTMLButtonElement>, label: string) => {
const i = idx(label);
if (e.key === "ArrowDown") {
e.preventDefault();
show(label);
focusPanelFirst();
} else if (e.key === "ArrowRight" || e.key === "ArrowLeft") {
e.preventDefault();
const next = menus[(i + (e.key === "ArrowRight" ? 1 : -1) + menus.length) % menus.length];
triggerRefs.current[next.label]?.focus();
if (open) show(next.label);
}
};
const onRootKey = (e: React.KeyboardEvent) => {
if (e.key === "Escape") {
if (open) {
const label = open;
show(null);
triggerRefs.current[label]?.focus();
}
setMobile(false);
}
};
const active = menus.find((m) => m.label === open) ?? null;
return (
<header
ref={rootRef}
onKeyDown={onRootKey}
onBlur={(e) => {
if (!e.currentTarget.contains(e.relatedTarget as Node | null)) show(null);
}}
onMouseLeave={hoverClose}
className={cn("relative z-40 w-full border-b bg-background/80 text-foreground backdrop-blur-xl", className)}
>
<nav aria-label="Main" className="mx-auto flex h-16 max-w-6xl items-center gap-6 px-5 sm:px-8">
<a href={brandHref} className="flex items-center gap-2 rounded-md font-semibold tracking-tight focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">
<span className="grid size-7 place-items-center rounded-lg bg-foreground text-background">
<svg viewBox="0 0 16 16" className="size-3.5" fill="currentColor" aria-hidden>
<path d="M2 14V2l12 12V2" stroke="currentColor" strokeWidth="2.4" fill="none" strokeLinejoin="round" />
</svg>
</span>
{brand}
</a>
<ul className="hidden items-center gap-0.5 md:flex">
{menus.map((m) => (
<li key={m.label} onMouseEnter={() => hoverOpen(m.label)}>
<button
ref={(el) => {
triggerRefs.current[m.label] = el;
}}
type="button"
aria-expanded={open === m.label}
aria-controls={`${uid}-panel`}
onClick={() => show(open === m.label ? null : m.label)}
onKeyDown={(e) => onTriggerKey(e, m.label)}
className={cn(
"flex items-center gap-1 rounded-md px-3 py-2 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
open === m.label ? "bg-muted text-foreground" : "text-foreground/70 hover:text-foreground",
)}
>
{m.label}
<ChevronDown className={cn("size-3.5 opacity-60 transition-transform duration-200", open === m.label && "rotate-180")} aria-hidden />
</button>
</li>
))}
{links.map((l) => (
<li key={l.label} onMouseEnter={hoverClose}>
<a href={l.href ?? "#"} className="block rounded-md px-3 py-2 text-sm font-medium text-foreground/70 transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">
{l.label}
</a>
</li>
))}
</ul>
<div className="ml-auto hidden items-center gap-2 md:flex">
<a href={signIn.href ?? "#"} className="rounded-md px-3 py-2 text-sm font-medium text-foreground/70 transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">
{signIn.label}
</a>
<a href={cta.href ?? "#"} className="inline-flex h-9 items-center rounded-md bg-foreground px-4 text-sm font-semibold text-background transition hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background">
{cta.label}
</a>
</div>
<button
type="button"
aria-label={mobile ? "Close menu" : "Open menu"}
aria-expanded={mobile}
aria-controls={`${uid}-mobile`}
onClick={() => setMobile((v) => !v)}
className="ml-auto grid size-10 place-items-center rounded-md transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring md:hidden"
>
<AnimatePresence mode="wait" initial={false}>
<motion.span key={mobile ? "x" : "m"} initial={{ rotate: -90, opacity: 0 }} animate={{ rotate: 0, opacity: 1 }} exit={{ rotate: 90, opacity: 0 }} transition={{ duration: 0.15 }}>
{mobile ? <X className="size-5" aria-hidden /> : <Menu className="size-5" aria-hidden />}
</motion.span>
</AnimatePresence>
</button>
</nav>
{/* Desktop mega panel */}
<AnimatePresence>
{active && (
<motion.div
key="panel"
id={`${uid}-panel`}
initial={reduce ? { opacity: 0 } : { opacity: 0, y: -8 }}
animate={{ opacity: 1, y: 0 }}
exit={reduce ? { opacity: 0 } : { opacity: 0, y: -8 }}
transition={{ duration: 0.22, ease: EASE }}
onMouseEnter={clearTimer}
className="absolute inset-x-0 top-full hidden border-b bg-popover text-popover-foreground shadow-2xl shadow-black/5 md:block dark:shadow-black/40"
>
<AutoHeight>
<AnimatePresence mode="popLayout" initial={false} custom={dir}>
<motion.div
key={active.label}
custom={dir}
variants={{
enter: (d: number) => ({ opacity: 0, x: reduce ? 0 : d * 40 }),
center: { opacity: 1, x: 0 },
exit: (d: number) => ({ opacity: 0, x: reduce ? 0 : d * -40 }),
}}
initial="enter"
animate="center"
exit="exit"
transition={{ duration: 0.25, ease: EASE }}
className="mx-auto grid max-w-6xl grid-cols-[1fr_1fr_1.1fr] gap-6 px-8 py-8"
>
{active.columns.map((col) => (
<div key={col.title}>
<p className="px-3 text-xs font-medium uppercase tracking-wider text-muted-foreground">{col.title}</p>
<ul className="mt-3 space-y-1">
{col.items.map((it, i) => (
<motion.li key={it.title} initial={{ opacity: 0, y: 6 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.04 * i + 0.05, duration: 0.25 }}>
<a
href={it.href ?? "#"}
className="group flex gap-3 rounded-xl p-3 transition-colors hover:bg-muted focus-visible:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
<span className="grid size-10 shrink-0 place-items-center rounded-lg border bg-background shadow-sm transition-colors group-hover:border-primary/40 group-hover:text-primary">
<it.icon className="size-[18px]" aria-hidden />
</span>
<span className="min-w-0">
<span className="flex items-center gap-1 text-sm font-medium">
{it.title}
<ArrowRight className="size-3 -translate-x-1 opacity-0 transition-all group-hover:translate-x-0 group-hover:opacity-100" aria-hidden />
</span>
<span className="mt-0.5 block text-sm leading-snug text-muted-foreground">{it.description}</span>
</span>
</a>
</motion.li>
))}
</ul>
</div>
))}
{active.featured && <FeaturedCard {...active.featured} />}
</motion.div>
</AnimatePresence>
</AutoHeight>
</motion.div>
)}
</AnimatePresence>
{/* Mobile menu */}
<AnimatePresence>
{mobile && (
<motion.div
id={`${uid}-mobile`}
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.3, ease: EASE }}
className="absolute inset-x-0 top-full overflow-hidden border-b bg-background shadow-2xl shadow-black/10 md:hidden"
>
<div className="max-h-[70dvh] overflow-y-auto px-5 pb-6 pt-2">
{menus.map((m) => (
<MobileSection key={m.label} menu={m} />
))}
{links.map((l) => (
<a key={l.label} href={l.href ?? "#"} className="flex h-12 items-center border-b text-base font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">
{l.label}
</a>
))}
<div className="mt-5 grid gap-2">
<a href={cta.href ?? "#"} className="inline-flex h-11 items-center justify-center rounded-lg bg-foreground text-sm font-semibold text-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">
{cta.label}
</a>
<a href={signIn.href ?? "#"} className="inline-flex h-11 items-center justify-center rounded-lg border text-sm font-semibold focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">
{signIn.label}
</a>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</header>
);
}
function AutoHeight({ children }: { children: React.ReactNode }) {
const ref = React.useRef<HTMLDivElement>(null);
const [h, setH] = React.useState<number | "auto">("auto");
React.useEffect(() => {
const el = ref.current;
if (!el) return;
const ro = new ResizeObserver(([e]) => setH(e.contentRect.height));
ro.observe(el);
return () => ro.disconnect();
}, []);
return (
<motion.div animate={{ height: h }} transition={{ duration: 0.25, ease: EASE }} className="overflow-hidden">
<div ref={ref}>{children}</div>
</motion.div>
);
}
function FeaturedCard({ eyebrow, title, description, cta, href }: NonNullable<MegaMenu["featured"]>) {
return (
<a
href={href ?? "#"}
className="group relative flex flex-col justify-end overflow-hidden rounded-2xl border bg-linear-to-br from-primary/90 via-violet-600 to-fuchsia-600 p-6 text-white shadow-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
>
<div aria-hidden className="absolute inset-0 bg-[radial-gradient(circle_at_80%_10%,rgba(255,255,255,0.35),transparent_45%)]" />
<svg aria-hidden viewBox="0 0 200 120" className="absolute right-3 top-5 w-44 opacity-40 transition-transform duration-500 group-hover:-translate-y-1 group-hover:rotate-3">
<rect x="20" y="20" width="120" height="80" rx="12" fill="none" stroke="white" strokeWidth="1.5" />
<rect x="60" y="6" width="120" height="80" rx="12" fill="white" fillOpacity="0.12" stroke="white" strokeWidth="1.5" />
<path d="M76 64 L96 48 L116 58 L150 32" stroke="white" strokeWidth="2.5" fill="none" strokeLinecap="round" />
</svg>
<span className="relative mt-24 w-fit rounded-full bg-white/20 px-2.5 py-0.5 text-xs font-medium backdrop-blur">{eyebrow}</span>
<span className="relative mt-3 text-lg font-semibold leading-tight">{title}</span>
<span className="relative mt-1 text-sm text-white/80">{description}</span>
<span className="relative mt-4 inline-flex items-center gap-1 text-sm font-semibold">
{cta}
<ArrowRight className="size-4 transition-transform group-hover:translate-x-1" aria-hidden />
</span>
</a>
);
}
function MobileSection({ menu }: { menu: MegaMenu }) {
const [open, setOpen] = React.useState(false);
const id = React.useId();
return (
<div className="border-b">
<button
type="button"
aria-expanded={open}
aria-controls={id}
onClick={() => setOpen((v) => !v)}
className="flex h-12 w-full items-center justify-between text-base font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
{menu.label}
<ChevronDown className={cn("size-4 text-muted-foreground transition-transform duration-200", open && "rotate-180")} aria-hidden />
</button>
<AnimatePresence initial={false}>
{open && (
<motion.div
id={id}
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.25, ease: EASE }}
className="overflow-hidden"
>
<div className="space-y-4 pb-4">
{menu.columns.map((col) => (
<div key={col.title}>
<p className="text-xs font-medium uppercase tracking-wider text-muted-foreground">{col.title}</p>
<ul className="mt-1">
{col.items.map((it) => (
<li key={it.title}>
<a href={it.href ?? "#"} className="flex items-center gap-3 rounded-lg py-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">
<span className="grid size-8 place-items-center rounded-md border bg-card">
<it.icon className="size-4" aria-hidden />
</span>
<span>
<span className="block text-sm font-medium">{it.title}</span>
<span className="block text-xs text-muted-foreground">{it.description}</span>
</span>
</a>
</li>
))}
</ul>
</div>
))}
</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
}