Fazekit

Code

"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&apos;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>
  );
}

More in Footers

View all →