"use client";
import * as React from "react";
import { AnimatePresence, motion, useInView, useReducedMotion } from "motion/react";
import { ArrowRight, Check, Mail, MessagesSquare, Podcast, Rss, Video, type LucideIcon } from "lucide-react";
import { cn } from "@/lib/utils";
export interface FooterBigLink {
label: string;
href?: string;
badge?: string;
}
export interface FooterBigColumn {
title: string;
links: FooterBigLink[];
}
export interface FooterBigSocial {
label: string;
href?: string;
icon: LucideIcon;
}
export interface FooterBigProps {
brand?: string;
tagline?: string;
columns?: FooterBigColumn[];
socials?: FooterBigSocial[];
legal?: FooterBigLink[];
copyright?: string;
/** Giant wordmark text; defaults to `brand`. Pass `""` to hide it. */
wordmark?: string;
newsletterLabel?: string;
onSubscribe?: (email: string) => void | Promise<void>;
status?: string;
className?: string;
}
const EASE = [0.22, 1, 0.36, 1] as const;
const DEFAULT_COLUMNS: FooterBigColumn[] = [
{ title: "Product", links: [{ label: "Features" }, { label: "Integrations" }, { label: "Pricing" }, { label: "Changelog", badge: "New" }, { label: "Roadmap" }] },
{ title: "Company", links: [{ label: "About" }, { label: "Customers" }, { label: "Careers", badge: "4" }, { label: "Press" }, { label: "Contact" }] },
{ title: "Resources", links: [{ label: "Documentation" }, { label: "Guides" }, { label: "Blog" }, { label: "Community" }, { label: "Templates" }] },
{ title: "Developers", links: [{ label: "API reference" }, { label: "SDKs" }, { label: "Status" }, { label: "Open source" }] },
];
const DEFAULT_SOCIALS: FooterBigSocial[] = [
{ label: "Community forum", icon: MessagesSquare },
{ label: "Video tutorials", icon: Video },
{ label: "Podcast", icon: Podcast },
{ label: "RSS feed", icon: Rss },
{ label: "Email us", icon: Mail },
];
const DEFAULT_LEGAL: FooterBigLink[] = [{ label: "Privacy" }, { label: "Terms" }, { label: "Cookies" }, { label: "Security" }];
export function FooterBig({
brand = "Northwind",
tagline = "The operating system for modern product teams. Plan, build and measure — all in one place.",
columns = DEFAULT_COLUMNS,
socials = DEFAULT_SOCIALS,
legal = DEFAULT_LEGAL,
copyright = "© 2026 Northwind Labs, Inc.",
wordmark,
newsletterLabel = "Get product updates",
onSubscribe,
status = "All systems operational",
className,
}: FooterBigProps) {
const mark = wordmark ?? brand;
return (
<footer className={cn("relative w-full overflow-hidden border-t bg-background text-foreground", className)}>
<div className="mx-auto max-w-6xl px-5 pt-16 sm:px-8 sm:pt-20">
<div className="grid gap-12 lg:grid-cols-[1.1fr_2fr] lg:gap-16">
<div className="max-w-sm">
<a href="#" className="inline-flex items-center gap-2 rounded-md text-lg font-semibold tracking-tight focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">
<span className="grid size-8 place-items-center rounded-lg bg-foreground text-background">
<svg viewBox="0 0 16 16" className="size-4" aria-hidden>
<path d="M3 13V3l10 10V3" stroke="currentColor" strokeWidth="2.2" fill="none" strokeLinejoin="round" strokeLinecap="round" />
</svg>
</span>
{brand}
</a>
<p className="mt-4 text-sm leading-relaxed text-muted-foreground">{tagline}</p>
<MiniNewsletter label={newsletterLabel} onSubscribe={onSubscribe} />
{socials.length > 0 && (
<ul className="mt-6 flex gap-2">
{socials.map((s) => (
<li key={s.label}>
<a
href={s.href ?? "#"}
aria-label={s.label}
title={s.label}
className="grid size-9 place-items-center rounded-full border text-muted-foreground transition hover:-translate-y-0.5 hover:border-foreground/20 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
<s.icon className="size-4" aria-hidden />
</a>
</li>
))}
</ul>
)}
</div>
<nav aria-label="Footer" className="grid grid-cols-2 gap-x-6 gap-y-10 sm:grid-cols-4">
{columns.map((col) => (
<div key={col.title}>
<h3 className="text-sm font-semibold">{col.title}</h3>
<ul className="mt-4 space-y-3">
{col.links.map((l) => (
<li key={l.label}>
<a
href={l.href ?? "#"}
className="inline-flex items-center gap-2 rounded-sm text-sm text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
{l.label}
{l.badge && (
<span className="rounded-full bg-primary/10 px-1.5 py-px text-[10px] font-semibold text-primary">{l.badge}</span>
)}
</a>
</li>
))}
</ul>
</div>
))}
</nav>
</div>
</div>
{mark && <Wordmark text={mark} />}
<div className="relative border-t">
<div className="mx-auto flex max-w-6xl flex-col gap-4 px-5 py-6 text-sm text-muted-foreground sm:px-8 md:flex-row md:items-center md:justify-between">
<p>{copyright}</p>
<ul className="flex flex-wrap gap-x-5 gap-y-2">
{legal.map((l) => (
<li key={l.label}>
<a href={l.href ?? "#"} className="rounded-sm transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">
{l.label}
</a>
</li>
))}
</ul>
{status && (
<p className="inline-flex w-fit items-center gap-2 rounded-full border px-3 py-1 text-xs">
<span className="relative flex size-2">
<span className="absolute inset-0 animate-ping rounded-full bg-emerald-500/60 motion-reduce:animate-none" />
<span className="relative size-2 rounded-full bg-emerald-500" />
</span>
{status}
</p>
)}
</div>
</div>
</footer>
);
}
function Wordmark({ text }: { text: string }) {
const reduce = useReducedMotion();
const ref = React.useRef<HTMLDivElement>(null);
const inView = useInView(ref, { once: true, amount: 0.4 });
const letters = Array.from(text);
return (
<div ref={ref} aria-hidden className="pointer-events-none relative mx-auto mt-10 max-w-7xl select-none overflow-hidden px-2 sm:mt-14">
<p className="flex justify-center whitespace-nowrap text-[clamp(4rem,19vw,15rem)] font-bold leading-none tracking-[-0.06em]">
{letters.map((ch, i) => (
<motion.span
key={i}
className="inline-block bg-linear-to-b from-foreground/[0.13] to-foreground/[0.01] bg-clip-text px-[0.02em] pb-[0.1em] pt-[0.06em] text-transparent dark:from-foreground/[0.16]"
initial={reduce ? { opacity: 0 } : { opacity: 0, y: "55%", filter: "blur(10px)" }}
animate={inView ? { opacity: 1, y: "18%", filter: "blur(0px)" } : undefined}
transition={{ duration: 1, delay: i * 0.05, ease: EASE }}
>
{ch === " " ? " " : ch}
</motion.span>
))}
</p>
</div>
);
}
function MiniNewsletter({ label, onSubscribe }: { label: string; onSubscribe?: (email: string) => void | Promise<void> }) {
const id = React.useId();
const [email, setEmail] = React.useState("");
const [state, setState] = React.useState<"idle" | "error" | "done">("idle");
const submit = async (e: React.FormEvent) => {
e.preventDefault();
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) return setState("error");
await onSubscribe?.(email);
setState("done");
};
return (
<form onSubmit={submit} noValidate className="mt-6">
<label htmlFor={id} className="text-sm font-medium">
{label}
</label>
<AnimatePresence mode="wait" initial={false}>
{state === "done" ? (
<motion.p
key="done"
role="status"
initial={{ opacity: 0, y: 4 }}
animate={{ opacity: 1, y: 0 }}
className="mt-2 flex h-10 items-center gap-2 text-sm text-emerald-700 dark:text-emerald-400"
>
<Check className="size-4" aria-hidden /> Thanks — you're in.
</motion.p>
) : (
<motion.div key="form" exit={{ opacity: 0 }} className="mt-2 flex h-10 items-center rounded-full border bg-card pl-4 pr-1 transition focus-within:border-primary/50 focus-within:ring-4 focus-within:ring-primary/10">
<input
id={id}
type="email"
autoComplete="email"
placeholder="[email protected]"
value={email}
onChange={(e) => (setEmail(e.target.value), setState("idle"))}
aria-invalid={state === "error"}
aria-describedby={`${id}-hint`}
className="h-full min-w-0 flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground"
/>
<button
type="submit"
aria-label="Subscribe"
className="grid size-8 shrink-0 place-items-center rounded-full bg-foreground text-background transition hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
<ArrowRight className="size-4" aria-hidden />
</button>
</motion.div>
)}
</AnimatePresence>
<p id={`${id}-hint`} aria-live="polite" className={cn("mt-1.5 h-4 text-xs", state === "error" ? "text-destructive" : "text-transparent")}>
{state === "error" ? "Enter a valid email address." : ""}
</p>
</form>
);
}