"use client";
import * as React from "react";
import {
AnimatePresence,
motion,
useMotionTemplate,
useMotionValue,
useReducedMotion,
useSpring,
} from "motion/react";
import { ArrowRight, CheckCircle2, Loader2 } from "lucide-react";
import { cn } from "@/lib/utils";
export interface HeroSpotlightLogo {
name: string;
/** Any SVG/element; inherits `currentColor`. */
logo: React.ReactNode;
}
export interface HeroSpotlightProps {
eyebrow?: string;
/** First line of the headline (solid). */
headline?: string;
/** Second line of the headline (gradient). */
highlight?: string;
description?: string;
placeholder?: string;
buttonLabel?: string;
successMessage?: string;
trustLabel?: string;
logos?: HeroSpotlightLogo[];
/** Called with the email. Return a promise to show the loading state. */
onSubmit?: (email: string) => void | Promise<void>;
className?: string;
}
const EASE = [0.22, 1, 0.36, 1] as const;
export function HeroSpotlight({
eyebrow = "Private beta · 2,400 teams waiting",
headline = "Infrastructure that",
highlight = "scales with your ambition",
description = "Orbit deploys your apps to 40 regions with zero config. Preview every branch, roll back in one click, and sleep through traffic spikes.",
placeholder = "[email protected]",
buttonLabel = "Join the waitlist",
successMessage = "You're on the list — we'll be in touch soon.",
trustLabel = "Trusted by engineering teams at",
logos = DEFAULT_LOGOS,
onSubmit,
className,
}: HeroSpotlightProps) {
const reduce = useReducedMotion();
const ref = React.useRef<HTMLElement>(null);
const mx = useMotionValue(640);
const my = useMotionValue(180);
const x = useSpring(mx, { stiffness: 140, damping: 22, mass: 0.4 });
const y = useSpring(my, { stiffness: 140, damping: 22, mass: 0.4 });
const glow = useMotionTemplate`radial-gradient(520px circle at ${x}px ${y}px, color-mix(in oklch, var(--color-primary) 22%, transparent), transparent 70%)`;
const reveal = useMotionTemplate`radial-gradient(300px circle at ${x}px ${y}px, #000 0%, transparent 100%)`;
React.useEffect(() => {
const el = ref.current;
if (!el) return;
const r = el.getBoundingClientRect();
mx.jump(r.width / 2);
my.jump(Math.min(220, r.height * 0.35));
x.jump(r.width / 2);
y.jump(Math.min(220, r.height * 0.35));
}, [mx, my, x, y]);
const onMove = (e: React.PointerEvent<HTMLElement>) => {
if (reduce) return;
const r = e.currentTarget.getBoundingClientRect();
mx.set(e.clientX - r.left);
my.set(e.clientY - r.top);
};
return (
<section
ref={ref}
onPointerMove={onMove}
className={cn("relative isolate w-full overflow-hidden bg-background text-foreground", className)}
>
{/* Grid + spotlight */}
<div aria-hidden className="pointer-events-none absolute inset-0 -z-10">
<div className="absolute inset-0 bg-[linear-gradient(to_right,var(--color-border)_1px,transparent_1px),linear-gradient(to_bottom,var(--color-border)_1px,transparent_1px)] bg-[size:48px_48px] [mask-image:radial-gradient(ellipse_80%_70%_at_50%_40%,#000_30%,transparent_100%)]" />
<motion.div
className="absolute inset-0 bg-[linear-gradient(to_right,var(--color-primary)_1px,transparent_1px),linear-gradient(to_bottom,var(--color-primary)_1px,transparent_1px)] bg-[size:48px_48px] opacity-50"
style={{ maskImage: reveal, WebkitMaskImage: reveal }}
/>
<motion.div className="absolute inset-0" style={{ background: glow }} />
<div className="absolute left-1/2 top-0 h-px w-2/3 -translate-x-1/2 bg-linear-to-r from-transparent via-primary/60 to-transparent" />
<div className="absolute inset-x-0 bottom-0 h-40 bg-linear-to-b from-transparent to-background" />
</div>
<div className="mx-auto flex max-w-5xl flex-col items-center px-5 py-20 text-center sm:px-8 sm:py-28">
<motion.p
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, ease: EASE }}
className="inline-flex items-center gap-2 rounded-full border bg-card/60 px-3 py-1 text-xs font-medium text-muted-foreground backdrop-blur"
>
<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>
{eyebrow}
</motion.p>
<motion.h1
initial={reduce ? { opacity: 0 } : { opacity: 0, y: 24, filter: "blur(10px)" }}
animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
transition={{ duration: 0.9, delay: 0.1, ease: EASE }}
className="mt-7 max-w-4xl text-balance text-[2.5rem] font-semibold leading-[1.04] tracking-tight sm:text-6xl lg:text-7xl"
>
<span className="bg-linear-to-b from-foreground to-foreground/70 bg-clip-text text-transparent">{headline}</span>{" "}
<span className="bg-linear-to-r from-primary via-sky-500 to-emerald-400 bg-clip-text pb-1 text-transparent dark:via-sky-400 dark:to-emerald-300">
{highlight}
</span>
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 14 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.7, delay: 0.3, ease: EASE }}
className="mt-6 max-w-2xl text-pretty text-base leading-relaxed text-muted-foreground sm:text-lg"
>
{description}
</motion.p>
<motion.div
initial={{ opacity: 0, y: 14 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.7, delay: 0.45, ease: EASE }}
className="mt-9 w-full max-w-md"
>
<EmailCapture placeholder={placeholder} buttonLabel={buttonLabel} successMessage={successMessage} onSubmit={onSubmit} />
</motion.div>
{logos.length > 0 && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.8, delay: 0.7 }}
className="mt-16 w-full sm:mt-20"
>
<p className="text-xs font-medium uppercase tracking-[0.18em] text-muted-foreground">{trustLabel}</p>
<ul className="mt-6 flex flex-wrap items-center justify-center gap-x-10 gap-y-6 sm:gap-x-14">
{logos.map((l, i) => (
<motion.li
key={l.name}
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.8 + i * 0.07, ease: EASE }}
className="text-foreground/45 transition-colors hover:text-foreground/80"
title={l.name}
>
<span className="sr-only">{l.name}</span>
<span aria-hidden className="flex h-7 items-center">{l.logo}</span>
</motion.li>
))}
</ul>
</motion.div>
)}
</div>
</section>
);
}
function EmailCapture({
placeholder,
buttonLabel,
successMessage,
onSubmit,
}: Required<Pick<HeroSpotlightProps, "placeholder" | "buttonLabel" | "successMessage">> & Pick<HeroSpotlightProps, "onSubmit">) {
const id = React.useId();
const [email, setEmail] = React.useState("");
const [state, setState] = React.useState<"idle" | "loading" | "done" | "error">("idle");
const submit = async (e: React.FormEvent) => {
e.preventDefault();
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) return setState("error");
setState("loading");
await (onSubmit ? onSubmit(email) : new Promise((r) => setTimeout(r, 900)));
setState("done");
};
return (
<AnimatePresence mode="wait" initial={false}>
{state === "done" ? (
<motion.p
key="done"
role="status"
initial={{ opacity: 0, scale: 0.96 }}
animate={{ opacity: 1, scale: 1 }}
className="flex h-12 items-center justify-center gap-2 rounded-full border border-emerald-500/30 bg-emerald-500/10 px-4 text-sm font-medium text-emerald-700 dark:text-emerald-300"
>
<CheckCircle2 className="size-4" aria-hidden />
{successMessage}
</motion.p>
) : (
<motion.form key="form" exit={{ opacity: 0, scale: 0.96 }} onSubmit={submit} noValidate>
<div
className={cn(
"flex flex-col gap-2 rounded-2xl border bg-card/70 p-1.5 shadow-lg shadow-primary/5 backdrop-blur-md transition focus-within:border-primary/50 focus-within:ring-4 focus-within:ring-primary/15 sm:flex-row sm:rounded-full",
state === "error" && "border-destructive/60",
)}
>
<label htmlFor={id} className="sr-only">
Email address
</label>
<input
id={id}
type="email"
autoComplete="email"
value={email}
onChange={(e) => (setEmail(e.target.value), state === "error" && setState("idle"))}
placeholder={placeholder}
aria-invalid={state === "error"}
aria-describedby={`${id}-msg`}
className="h-11 w-full min-w-0 bg-transparent sm:w-auto sm:flex-1 px-4 text-sm outline-none placeholder:text-muted-foreground"
/>
<button
type="submit"
disabled={state === "loading"}
className="group inline-flex h-11 shrink-0 items-center justify-center gap-2 rounded-xl bg-primary px-5 text-sm font-semibold text-primary-foreground transition hover:brightness-110 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:opacity-70 sm:rounded-full"
>
{state === "loading" ? <Loader2 className="size-4 animate-spin" aria-hidden /> : null}
{buttonLabel}
{state !== "loading" && <ArrowRight className="size-4 transition-transform group-hover:translate-x-0.5" aria-hidden />}
</button>
</div>
<p id={`${id}-msg`} aria-live="polite" className={cn("mt-2 h-4 text-xs", state === "error" ? "text-destructive" : "text-muted-foreground")}>
{state === "error" ? "Please enter a valid email address." : "No spam. Unsubscribe anytime."}
</p>
</motion.form>
)}
</AnimatePresence>
);
}
/* Invented brands, drawn by hand. */
const DEFAULT_LOGOS: HeroSpotlightLogo[] = [
{
name: "Northwind",
logo: (
<svg viewBox="0 0 114 28" className="h-6 w-auto" fill="currentColor">
<path d="M4 22 14 4l10 18h-6l-4-7-4 7z" />
<text x="30" y="20" fontSize="16" fontWeight="700" letterSpacing="-0.5" fontFamily="inherit">Northwind</text>
</svg>
),
},
{
name: "Acme",
logo: (
<svg viewBox="0 0 84 28" className="h-6 w-auto" fill="currentColor">
<circle cx="12" cy="14" r="9" fill="none" stroke="currentColor" strokeWidth="3.5" />
<circle cx="12" cy="14" r="3" />
<text x="28" y="20" fontSize="17" fontWeight="800" letterSpacing="1" fontFamily="inherit">ACME</text>
</svg>
),
},
{
name: "Lumen",
logo: (
<svg viewBox="0 0 80 28" className="h-6 w-auto" fill="currentColor">
<path d="M12 3a11 11 0 1 0 0 22V3z" />
<path d="M14 7a7 7 0 0 1 0 14z" opacity=".55" />
<text x="30" y="20" fontSize="17" fontWeight="600" fontFamily="inherit">lumen</text>
</svg>
),
},
{
name: "Orbit",
logo: (
<svg viewBox="0 0 76 28" className="h-6 w-auto" fill="currentColor">
<ellipse cx="12" cy="14" rx="11" ry="5" fill="none" stroke="currentColor" strokeWidth="2.5" transform="rotate(-25 12 14)" />
<circle cx="12" cy="14" r="4" />
<text x="30" y="20" fontSize="17" fontWeight="700" fontStyle="italic" fontFamily="inherit">Orbit</text>
</svg>
),
},
{
name: "Halcyon",
logo: (
<svg viewBox="0 0 118 28" className="h-6 w-auto" fill="currentColor">
<rect x="2" y="4" width="8" height="20" rx="2" />
<rect x="12" y="10" width="8" height="14" rx="2" opacity=".6" />
<text x="28" y="20" fontSize="16" fontWeight="700" letterSpacing="2" fontFamily="inherit">HALCYON</text>
</svg>
),
},
];