"use client";
import { ArrowRight } from "lucide-react";
import { motion, type Transition, useReducedMotion } from "motion/react";
import type { ElementType, ReactNode } from "react";
import { Button, ButtonLink } from "@/components/motion/button";
import { CopyButton } from "@/components/motion/copy-button";
import { TextReveal } from "@/components/motion/text-reveal";
import { EASE_OUT } from "@/lib/ease";
import { cn } from "@/lib/utils";
export interface HeroAction {
label: ReactNode;
href?: string;
onClick?: () => void;
/** Trailing icon. */
icon?: ReactNode;
}
export interface HeroBadge {
label: ReactNode;
/** Pill before the label. Default "New"; null hides it. */
pill?: ReactNode;
href?: string;
onClick?: () => void;
}
export interface HeroSectionProps {
/** Revealed word by word with a blur-in. */
headline: string | string[];
description?: ReactNode;
badge?: HeroBadge;
primaryAction?: HeroAction;
secondaryAction?: HeroAction;
/** Shell command shown in a chip with a copy button. */
installCommand?: string;
/** Screenshot, video or mock UI below the copy. Tilts in on view. */
media?: ReactNode;
align?: "center" | "left";
/** CSS-only animated grid with sweeping beams, or nothing. Default "grid". */
background?: "grid" | "none";
/** Heading element for the headline. Default "h1". */
headingAs?: ElementType;
className?: string;
mediaClassName?: string;
}
const WORD_STAGGER = 0.06;
const ENTER: Transition = { duration: 0.5, ease: EASE_OUT };
// Media tilt-in: a heavier, slower spring than the UI tokens because it moves
// a large surface through a 3D tilt; the snappier tokens read as a jolt here.
const MEDIA_SPRING: Transition = {
type: "spring",
stiffness: 110,
damping: 22,
mass: 1,
};
// Beam sweep: decorative, slow and linear so the eye never locks onto it.
const BEAM_TRANSITION: Transition = {
duration: 7,
ease: "linear",
repeat: Number.POSITIVE_INFINITY,
repeatDelay: 2,
};
function wordCount(text: string | string[]) {
return (Array.isArray(text) ? text.join(" ") : text)
.split(/\s+/)
.filter(Boolean).length;
}
function ActionButton({
action,
variant,
}: {
action: HeroAction;
variant: "primary" | "secondary";
}) {
const content = (
<>
{action.label}
{action.icon ?? null}
>
);
return action.href ? (
{installCommand}