"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 ? ( {content} ) : ( ); } function HeroBackground({ reduce }: { reduce: boolean | null }) { return (
{/* 40px grid drawn with two repeating hairline gradients. */}
{/* Soft glow at the top. */}
{reduce ? null : ( <> {/* Beams ride grid lines (multiples of 40px) and only translate. */} )}
); } /** * Landing hero: an eyebrow badge with a "New" pill, a headline revealed word * by word with a blur-in, supporting copy, primary/secondary CTAs, an optional * install command with copy, and an optional media slot that tilts up into * place on view, over a CSS-only animated grid. Type scales with the * section's own container width. */ export function HeroSection({ headline, description, badge, primaryAction, secondaryAction, installCommand, media, align = "center", background = "grid", headingAs = "h1", className, mediaClassName, }: HeroSectionProps) { const reduce = useReducedMotion(); const centered = align === "center"; // Supporting content enters as the headline finishes. const after = Math.min(1.2, 0.15 + wordCount(headline) * WORD_STAGGER); const enter = (delay: number) => ({ initial: reduce ? { opacity: 0 } : { opacity: 0, y: 12 }, animate: reduce ? { opacity: 1 } : { opacity: 1, y: 0 }, transition: { ...ENTER, delay }, }); const badgeInner = badge ? ( <> {badge.pill === null ? null : ( {badge.pill ?? "New"} )} {badge.label} ) : null; const badgeClass = "group inline-flex max-w-full items-center gap-2 rounded-full border border-border bg-card/80 py-1 pr-3 pl-1 text-xs font-medium text-foreground shadow-xs backdrop-blur transition-colors hover:border-border-strong outline-none focus-visible:ring-2 focus-visible:ring-ring/50"; return (
{background === "grid" ? : null}
{badge ? ( {badge.href ? ( {badgeInner} ) : badge.onClick ? ( ) : ( {badgeInner} )} ) : null} {description ? ( {description} ) : null} {primaryAction || secondaryAction ? ( {primaryAction ? ( ) : null} {secondaryAction ? ( ) : null} ) : null} {installCommand ? ( $ {installCommand} ) : null}
{media ? (
{media}
) : null}
); }