"use client";
import * as React from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import {
ArrowRight,
BarChart3,
Bell,
ChevronRight,
Home,
Inbox,
Layers,
Search,
Settings,
Users,
} from "lucide-react";
import { cn } from "@/lib/utils";
export interface HeroRotatingWordsProps {
/** Text before the rotating word. */
prefix?: string;
/** Words that cycle in the second line. */
words?: string[];
/** Milliseconds each word stays on screen. */
interval?: number;
description?: string;
primaryCta?: { label: string; href?: string };
secondaryCta?: { label: string; href?: string };
/** Show the tilted app screenshot. */
showScreenshot?: boolean;
className?: string;
}
const EASE = [0.22, 1, 0.36, 1] as const;
export function HeroRotatingWords({
prefix = "One workspace for",
words = ["product teams", "founders", "designers", "engineers", "agencies"],
interval = 2400,
description = "Plan roadmaps, track metrics and ship updates in one calm place. Northwind replaces six tools with a single, blazing-fast workspace.",
primaryCta = { label: "Get started — it's free", href: "#" },
secondaryCta = { label: "Book a demo", href: "#" },
showScreenshot = true,
className,
}: HeroRotatingWordsProps) {
const reduce = useReducedMotion();
const [index, setIndex] = React.useState(0);
React.useEffect(() => {
if (words.length < 2) return;
const id = setInterval(() => setIndex((i) => (i + 1) % words.length), interval);
return () => clearInterval(id);
}, [words.length, interval]);
const word = words[index % Math.max(1, words.length)] ?? "";
return (
<section className={cn("relative isolate w-full overflow-hidden bg-background text-foreground", className)}>
<div aria-hidden className="pointer-events-none absolute inset-0 -z-10">
<div className="absolute left-1/2 top-[-12rem] h-[34rem] w-[60rem] max-w-[160%] -translate-x-1/2 rounded-full bg-[radial-gradient(closest-side,color-mix(in_oklch,var(--color-primary)_22%,transparent),transparent)]" />
<div className="absolute inset-0 bg-[radial-gradient(var(--color-border)_1px,transparent_1px)] bg-[size:22px_22px] [mask-image:linear-gradient(to_bottom,#000,transparent_70%)]" />
</div>
<div className="mx-auto flex max-w-6xl flex-col items-center px-5 pt-16 text-center sm:px-8 sm:pt-20">
<motion.h1
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.7, ease: EASE }}
className="text-balance text-[2.5rem] font-semibold leading-[1.08] tracking-tight sm:text-6xl lg:text-7xl"
>
<span className="block">{prefix}</span>
<span className="sr-only">{words.join(", ")}</span>
<span aria-hidden className="relative block h-[1.2em] overflow-hidden">
<AnimatePresence mode="popLayout" initial={false}>
<motion.span
key={word}
className="absolute inset-x-0 block whitespace-nowrap bg-linear-to-r from-primary via-fuchsia-500 to-orange-400 bg-clip-text pb-2 text-transparent"
initial={reduce ? { opacity: 0 } : { y: "100%", opacity: 0, filter: "blur(8px)" }}
animate={{ y: "0%", opacity: 1, filter: "blur(0px)" }}
exit={reduce ? { opacity: 0 } : { y: "-100%", opacity: 0, filter: "blur(8px)" }}
transition={{ duration: 0.6, ease: EASE }}
>
{word}
</motion.span>
</AnimatePresence>
</span>
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 14 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.7, delay: 0.15, ease: EASE }}
className="mt-6 max-w-xl 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.25, ease: EASE }}
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-11 w-full items-center justify-center gap-2 rounded-lg bg-primary px-5 text-sm font-semibold text-primary-foreground shadow-md shadow-primary/25 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 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-11 w-full items-center justify-center gap-1 rounded-lg border bg-card px-5 text-sm font-semibold transition hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring sm:w-auto"
>
{secondaryCta.label}
<ChevronRight className="size-4 text-muted-foreground" aria-hidden />
</a>
</motion.div>
{showScreenshot && (
<div className="mt-14 w-full [perspective:1600px] sm:mt-16">
<motion.div
initial={reduce ? { opacity: 0 } : { opacity: 0, rotateX: 28, y: 80, scale: 0.94 }}
animate={{ opacity: 1, rotateX: 0, y: 0, scale: 1 }}
transition={{ duration: 1.3, delay: 0.35, ease: EASE }}
style={{ transformOrigin: "50% 0%" }}
className="relative mx-auto max-w-5xl"
>
<div aria-hidden className="absolute -inset-x-8 -top-8 bottom-0 -z-10 rounded-[2rem] bg-linear-to-b from-primary/25 via-fuchsia-500/10 to-transparent blur-3xl" />
<AppScreenshot />
</motion.div>
</div>
)}
</div>
</section>
);
}
const NAV = [
{ icon: Home, label: "Home" },
{ icon: Inbox, label: "Inbox", badge: "4" },
{ icon: BarChart3, label: "Analytics", active: true },
{ icon: Layers, label: "Projects" },
{ icon: Users, label: "Team" },
{ icon: Settings, label: "Settings" },
];
// 12 points of a smooth growth curve, 0–100.
const SERIES_A = [22, 30, 27, 38, 35, 48, 44, 58, 62, 57, 72, 84];
const SERIES_B = [14, 18, 20, 22, 26, 25, 31, 33, 38, 36, 42, 46];
function toPath(values: number[], w: number, h: number) {
const step = w / (values.length - 1);
const pts = values.map((v, i) => [i * step, h - (v / 100) * h] as const);
return pts.reduce((d, [x, y], i) => {
if (i === 0) return `M${x},${y}`;
const [px, py] = pts[i - 1];
const cx = (px + x) / 2;
return `${d} C${cx},${py} ${cx},${y} ${x},${y}`;
}, "");
}
function AppScreenshot() {
const W = 600;
const H = 180;
const a = toPath(SERIES_A, W, H);
const b = toPath(SERIES_B, W, H);
return (
<div aria-hidden className="overflow-hidden rounded-t-2xl border border-b-0 bg-card text-left shadow-2xl shadow-black/10 [mask-image:linear-gradient(to_bottom,#000_70%,transparent)] dark:shadow-black/40">
<div className="flex">
{/* Sidebar */}
<aside className="hidden w-52 shrink-0 border-r bg-muted/40 p-3 md:block">
<div className="flex items-center gap-2 px-2 py-1.5">
<span className="grid size-6 place-items-center rounded-md bg-primary text-[11px] font-bold text-primary-foreground">N</span>
<span className="text-sm font-semibold">Northwind</span>
</div>
<div className="mt-3 flex items-center gap-2 rounded-md border bg-background px-2 py-1.5 text-xs text-muted-foreground">
<Search className="size-3.5" /> Search
<span className="ml-auto rounded border px-1 text-[10px]">⌘K</span>
</div>
<nav className="mt-4 space-y-0.5">
{NAV.map(({ icon: Icon, label, badge, active }) => (
<div
key={label}
className={cn(
"flex items-center gap-2 rounded-md px-2 py-1.5 text-xs",
active ? "bg-background font-medium text-foreground shadow-sm" : "text-muted-foreground",
)}
>
<Icon className="size-3.5" />
{label}
{badge && <span className="ml-auto rounded-full bg-primary/15 px-1.5 text-[10px] font-medium text-primary">{badge}</span>}
</div>
))}
</nav>
<p className="mt-6 px-2 text-[10px] font-medium uppercase tracking-wider text-muted-foreground">Favorites</p>
{["Q4 launch", "Growth board", "Design system"].map((t, i) => (
<div key={t} className="flex items-center gap-2 px-2 py-1.5 text-xs text-muted-foreground">
<span className={cn("size-2 rounded-sm", ["bg-violet-500", "bg-emerald-500", "bg-amber-500"][i])} />
{t}
</div>
))}
</aside>
{/* Main */}
<div className="min-w-0 flex-1 p-4 sm:p-6">
<div className="flex items-center justify-between gap-3">
<div>
<p className="text-[11px] text-muted-foreground">Analytics / Overview</p>
<p className="text-base font-semibold sm:text-lg">Revenue performance</p>
</div>
<div className="flex items-center gap-2">
<span className="hidden rounded-md border px-2 py-1 text-[11px] text-muted-foreground sm:inline">Last 12 months</span>
<span className="grid size-7 place-items-center rounded-md border"><Bell className="size-3.5 text-muted-foreground" /></span>
</div>
</div>
<div className="mt-4 grid grid-cols-2 gap-3 lg:grid-cols-4">
{[
{ k: "MRR", v: "$84.2k", d: "+18%" },
{ k: "Customers", v: "2,914", d: "+6%" },
{ k: "Churn", v: "1.8%", d: "-0.4%" },
{ k: "NPS", v: "67", d: "+5" },
].map((s, i) => (
<div key={s.k} className={cn("rounded-lg border bg-background p-3", i > 1 && "hidden lg:block")}>
<p className="text-[11px] text-muted-foreground">{s.k}</p>
<div className="mt-1 flex items-baseline gap-2">
<span className="text-lg font-semibold tabular-nums">{s.v}</span>
<span className="text-[11px] font-medium text-emerald-600 dark:text-emerald-400">{s.d}</span>
</div>
</div>
))}
</div>
<div className="mt-4 rounded-lg border bg-background p-3 sm:p-4">
<div className="mb-3 flex items-center gap-4 text-[11px] text-muted-foreground">
<span className="flex items-center gap-1.5"><span className="size-2 rounded-full bg-primary" /> This year</span>
<span className="flex items-center gap-1.5"><span className="size-2 rounded-full bg-muted-foreground/40" /> Last year</span>
</div>
<svg viewBox={`0 0 ${W} ${H + 4}`} className="h-36 w-full sm:h-48" preserveAspectRatio="none">
<defs>
<linearGradient id="hrw-fill" x1="0" x2="0" y1="0" y2="1">
<stop offset="0%" stopColor="var(--color-primary)" stopOpacity="0.28" />
<stop offset="100%" stopColor="var(--color-primary)" stopOpacity="0" />
</linearGradient>
</defs>
{[0.25, 0.5, 0.75].map((f) => (
<line key={f} x1="0" x2={W} y1={H * f} y2={H * f} stroke="var(--color-border)" strokeDasharray="4 4" />
))}
<motion.path
d={`${a} L${W},${H} L0,${H} Z`}
fill="url(#hrw-fill)"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.8, delay: 1.4 }}
/>
<motion.path
d={b}
fill="none"
stroke="var(--color-muted-foreground)"
strokeOpacity="0.4"
strokeWidth="2"
vectorEffect="non-scaling-stroke"
initial={{ pathLength: 0 }}
animate={{ pathLength: 1 }}
transition={{ duration: 1.4, delay: 0.9, ease: "easeInOut" }}
/>
<motion.path
d={a}
fill="none"
stroke="var(--color-primary)"
strokeWidth="2.5"
strokeLinecap="round"
vectorEffect="non-scaling-stroke"
initial={{ pathLength: 0 }}
animate={{ pathLength: 1 }}
transition={{ duration: 1.6, delay: 0.9, ease: "easeInOut" }}
/>
</svg>
</div>
</div>
</div>
</div>
);
}