"use client";
import * as React from "react";
import { motion, useReducedMotion, type MotionProps } from "motion/react";
import { ArrowRight, Check, Play, Sparkles, TrendingUp } from "lucide-react";
import { cn } from "@/lib/utils";
export interface HeroAuroraLink {
label: string;
href?: string;
}
export interface HeroAuroraProps {
/** Small pill above the headline. Pass `null` to hide it. */
announcement?: (HeroAuroraLink & { tag?: string }) | null;
/** Headline. Words wrapped in *asterisks* get the gradient accent. */
headline?: string;
description?: string;
primaryCta?: HeroAuroraLink;
secondaryCta?: HeroAuroraLink;
/** Show the floating product mockup below the CTAs. */
showMockup?: boolean;
className?: string;
}
const EASE = [0.22, 1, 0.36, 1] as const;
// Aurora ribbons: deterministic paths so server and client render the same.
const BLOBS = [
{ className: "left-[-10%] top-[-20%] h-[520px] w-[620px] bg-violet-500/50 dark:bg-violet-600/40", x: [0, 120, -40, 0], y: [0, 60, 20, 0], d: 18 },
{ className: "right-[-15%] top-[-10%] h-[480px] w-[560px] bg-cyan-400/45 dark:bg-cyan-500/30", x: [0, -100, 40, 0], y: [0, 40, 90, 0], d: 22 },
{ className: "left-[25%] top-[10%] h-[420px] w-[520px] bg-fuchsia-400/40 dark:bg-fuchsia-500/30", x: [0, 80, -90, 0], y: [0, -40, 50, 0], d: 26 },
{ className: "left-[5%] top-[30%] h-[360px] w-[420px] bg-sky-300/40 dark:bg-indigo-500/30", x: [0, -60, 70, 0], y: [0, 50, -30, 0], d: 20 },
] as const;
function parseHeadline(text: string) {
return text.split(/\s+/).filter(Boolean).map((raw) => {
const accent = raw.startsWith("*") || raw.endsWith("*");
return { word: raw.replace(/\*/g, ""), accent };
});
}
export function HeroAurora({
announcement = { tag: "New", label: "Lumen 3.0 is live — meet the realtime canvas", href: "#" },
headline = "The analytics platform your *whole team* will actually use",
description = "Lumen turns raw events into answers in seconds. Build live dashboards, share insights, and ship decisions without waiting on a data team.",
primaryCta = { label: "Start free trial", href: "#" },
secondaryCta = { label: "Watch the demo", href: "#" },
showMockup = true,
className,
}: HeroAuroraProps) {
const reduce = useReducedMotion();
const words = React.useMemo(() => parseHeadline(headline), [headline]);
const rise = (delay: number): Pick<MotionProps, "initial" | "animate" | "transition"> => ({
initial: reduce ? { opacity: 0 } : { opacity: 0, y: 18, filter: "blur(6px)" },
animate: { opacity: 1, y: 0, filter: "blur(0px)" },
transition: { duration: 0.7, delay, ease: EASE },
});
const wordsDone = 0.25 + words.length * 0.06;
return (
<section className={cn("relative isolate w-full overflow-hidden bg-background text-foreground", className)}>
{/* Aurora background */}
<div aria-hidden className="pointer-events-none absolute inset-0 -z-10">
{BLOBS.map((b, i) => (
<motion.div
key={i}
className={cn("absolute rounded-full blur-[90px]", b.className)}
animate={reduce ? undefined : { x: [...b.x], y: [...b.y], scale: [1, 1.12, 0.95, 1] }}
transition={{ duration: b.d, repeat: Infinity, ease: "easeInOut" }}
/>
))}
{/* grid texture + fade into the page */}
<div className="absolute inset-0 bg-[linear-gradient(to_right,var(--color-foreground)_1px,transparent_1px),linear-gradient(to_bottom,var(--color-foreground)_1px,transparent_1px)] bg-[size:56px_56px] opacity-[0.04] [mask-image:radial-gradient(ellipse_70%_60%_at_50%_30%,#000_40%,transparent_100%)]" />
<div className="absolute inset-x-0 bottom-0 h-2/3 bg-linear-to-b from-transparent via-background/70 to-background" />
</div>
<div className="mx-auto flex max-w-6xl flex-col items-center px-5 pb-16 pt-16 text-center sm:px-8 sm:pt-24">
{announcement && (
<motion.a
href={announcement.href ?? "#"}
{...rise(0)}
className="group mb-8 inline-flex max-w-full items-center gap-2 rounded-full border border-foreground/10 bg-background/60 py-1 pl-1 pr-3 text-xs font-medium shadow-sm backdrop-blur-md transition hover:border-foreground/20 hover:bg-background/80 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring sm:text-sm"
>
{announcement.tag && (
<span className="inline-flex items-center gap-1 rounded-full bg-foreground px-2 py-0.5 text-[11px] font-semibold text-background">
<Sparkles className="size-3" aria-hidden />
{announcement.tag}
</span>
)}
<span className="truncate text-foreground/80">{announcement.label}</span>
<ArrowRight className="size-3.5 shrink-0 text-foreground/60 transition-transform group-hover:translate-x-0.5" aria-hidden />
</motion.a>
)}
<h1 className="max-w-4xl text-balance text-[2.6rem] font-semibold leading-[1.05] tracking-tight sm:text-6xl lg:text-7xl">
<span className="sr-only">{words.map((w) => w.word).join(" ")}</span>
<span aria-hidden>
{words.map((w, i) => (
<React.Fragment key={i}>
<motion.span
className={cn(
"inline-block",
w.accent && "bg-linear-to-r from-violet-600 via-fuchsia-500 to-cyan-500 bg-clip-text pb-1 text-transparent dark:from-violet-400 dark:via-fuchsia-400 dark:to-cyan-300",
)}
initial={reduce ? { opacity: 0 } : { opacity: 0, y: "0.45em", filter: "blur(8px)" }}
animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
transition={{ duration: 0.65, delay: 0.15 + i * 0.06, ease: EASE }}
>
{w.word}
</motion.span>
{i < words.length - 1 && " "}
</React.Fragment>
))}
</span>
</h1>
<motion.p {...rise(wordsDone)} className="mt-6 max-w-2xl text-pretty text-base leading-relaxed text-muted-foreground sm:text-lg">
{description}
</motion.p>
<motion.div {...rise(wordsDone + 0.1)} className="mt-9 flex w-full flex-col items-center justify-center gap-3 sm:w-auto sm:flex-row">
<a
href={primaryCta.href ?? "#"}
className="group inline-flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-6 text-sm font-semibold text-background shadow-lg shadow-foreground/10 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 sm:w-auto"
>
{primaryCta.label}
<ArrowRight className="size-4 transition-transform group-hover:translate-x-0.5" aria-hidden />
</a>
<a
href={secondaryCta.href ?? "#"}
className="inline-flex h-12 w-full items-center justify-center gap-2 rounded-full border border-foreground/10 bg-background/60 px-6 text-sm font-semibold backdrop-blur-md transition hover:bg-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring sm:w-auto"
>
<span className="grid size-5 place-items-center rounded-full bg-foreground/10">
<Play className="size-2.5 fill-current" aria-hidden />
</span>
{secondaryCta.label}
</a>
</motion.div>
{showMockup && (
<motion.div
initial={reduce ? { opacity: 0 } : { opacity: 0, y: 60, scale: 0.96 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
transition={{ duration: 1, delay: wordsDone + 0.25, ease: EASE }}
className="relative mt-14 w-full max-w-4xl"
>
<motion.div
animate={reduce ? undefined : { y: [0, -10, 0] }}
transition={{ duration: 6, repeat: Infinity, ease: "easeInOut" }}
>
<Mockup />
</motion.div>
</motion.div>
)}
</div>
</section>
);
}
const BARS = [38, 52, 44, 63, 58, 71, 66, 82, 76, 90, 84, 96];
function Mockup() {
return (
<div aria-hidden className="relative">
<div className="absolute -inset-4 -z-10 rounded-[2rem] bg-linear-to-b from-violet-500/20 to-cyan-400/10 blur-2xl" />
<div className="overflow-hidden rounded-2xl border border-foreground/10 bg-card/80 text-left shadow-2xl shadow-violet-950/10 backdrop-blur-xl">
<div className="flex items-center gap-2 border-b border-foreground/5 px-4 py-3">
<span className="size-2.5 rounded-full bg-rose-400/80" />
<span className="size-2.5 rounded-full bg-amber-400/80" />
<span className="size-2.5 rounded-full bg-emerald-400/80" />
<div className="mx-auto h-5 w-40 rounded-md bg-muted sm:w-64" />
</div>
<div className="grid gap-4 p-4 sm:grid-cols-[1fr_1.6fr] sm:p-6">
<div className="grid grid-cols-2 gap-3 sm:grid-cols-1">
{[
{ k: "Active users", v: "48,210", d: "+12.4%" },
{ k: "Conversion", v: "6.82%", d: "+1.9%" },
].map((s) => (
<div key={s.k} className="rounded-xl border border-foreground/5 bg-background/60 p-3 sm:p-4">
<p className="text-[11px] text-muted-foreground sm:text-xs">{s.k}</p>
<p className="mt-1 text-lg font-semibold tabular-nums sm:text-2xl">{s.v}</p>
<p className="mt-1 inline-flex items-center gap-1 text-[11px] font-medium text-emerald-600 dark:text-emerald-400">
<TrendingUp className="size-3" /> {s.d}
</p>
</div>
))}
<div className="col-span-2 hidden rounded-xl border border-foreground/5 bg-background/60 p-4 sm:col-span-1 sm:block">
{["Onboarding flow", "Pricing page", "Checkout"].map((t, i) => (
<div key={t} className="flex items-center gap-2 py-1 text-xs">
<span className="grid size-4 place-items-center rounded-full bg-violet-500/15 text-violet-600 dark:text-violet-300">
<Check className="size-2.5" />
</span>
<span className="text-foreground/80">{t}</span>
<span className="ml-auto tabular-nums text-muted-foreground">{[92, 78, 64][i]}%</span>
</div>
))}
</div>
</div>
<div className="rounded-xl border border-foreground/5 bg-background/60 p-4">
<div className="flex items-center justify-between">
<p className="text-xs font-medium">Weekly events</p>
<div className="flex gap-1">
{["7d", "30d", "90d"].map((t, i) => (
<span key={t} className={cn("rounded-md px-2 py-0.5 text-[10px]", i === 1 ? "bg-foreground text-background" : "text-muted-foreground")}>
{t}
</span>
))}
</div>
</div>
<div className="mt-4 flex h-32 items-end gap-1.5 sm:h-44 sm:gap-2">
{BARS.map((h, i) => (
<motion.div
key={i}
className="flex-1 rounded-t-md bg-linear-to-t from-violet-500/70 to-fuchsia-400/80 dark:from-violet-500/80 dark:to-cyan-300/80"
initial={{ height: 0 }}
animate={{ height: `${h}%` }}
transition={{ duration: 0.9, delay: 1.3 + i * 0.04, ease: EASE }}
/>
))}
</div>
</div>
</div>
</div>
</div>
);
}