"use client"; import { motion, useReducedMotion } from "motion/react"; import { type ReactNode, useMemo } from "react"; import { AnimatedNumber } from "@/components/motion/animated-number"; import { EASE_OUT, SPRING_SWAP } from "@/lib/ease"; import { cn } from "@/lib/utils"; export interface ContentStat { value: number; label: ReactNode; /** Short line under the label. */ description?: ReactNode; prefix?: string; suffix?: string; /** Fixed decimal places while rolling and at rest. Default 0. */ decimals?: number; } export interface ContentQuoteAuthor { name: string; role?: ReactNode; /** Avatar node (image, illustration). Falls back to initials. */ avatar?: ReactNode; } export interface ContentSectionProps { /** * "split": copy beside media (media sticks while the copy scrolls on wide * containers). "stats": copy plus rolling stat callouts. "quote": a pull * quote with large quote marks and its author. */ variant?: "split" | "stats" | "quote"; eyebrow?: ReactNode; heading?: ReactNode; /** Body copy; each entry is revealed as it scrolls into view. */ paragraphs?: ReactNode[]; /** Extra content after the paragraphs (CTA, list). */ children?: ReactNode; /** `variant="split"`: media node (image, mock UI, illustration). */ media?: ReactNode; mediaPosition?: "left" | "right"; /** `variant="split"`: keep the media pinned while the copy scrolls. Default true. */ stickyMedia?: boolean; /** `variant="stats"`: up to 3 reads best. */ stats?: ContentStat[]; /** `variant="quote"`: the quote itself, without quote marks. */ quote?: ReactNode; author?: ContentQuoteAuthor; /** Number formatting locale. Default "en-US" so server and client agree. */ locale?: string; className?: string; } const REVEAL_DURATION = 0.55; function Reveal({ children, delay = 0, className, }: { children: ReactNode; delay?: number; className?: string; }) { const reduce = useReducedMotion(); return ( {children} ); } function Header({ eyebrow, heading, center = false, }: { eyebrow?: ReactNode; heading?: ReactNode; center?: boolean; }) { if (!eyebrow && !heading) return null; return ( {eyebrow ? (

{eyebrow}

) : null} {heading ? (

{heading}

) : null}
); } function Paragraphs({ paragraphs }: { paragraphs?: ReactNode[] }) { if (!paragraphs?.length) return null; return (
{paragraphs.map((paragraph, index) => ( // biome-ignore lint/suspicious/noArrayIndexKey: paragraphs are positional copy.

{paragraph}

))}
); } function StatValue({ stat, locale }: { stat: ContentStat; locale: string }) { const decimals = stat.decimals ?? 0; const formatter = useMemo( () => new Intl.NumberFormat(locale, { minimumFractionDigits: decimals, maximumFractionDigits: decimals, }), [locale, decimals], ); return ( {stat.prefix} formatter.format(n)} /> {stat.suffix} ); } function initials(name: string) { const parts = name.trim().split(/\s+/); const last = parts.length > 1 ? (parts[parts.length - 1]?.[0] ?? "") : ""; return ((parts[0]?.[0] ?? "") + last).toUpperCase(); } /** * Long-form marketing content block in three variants. "split" sets copy * beside a media slot that stays pinned while the copy scrolls (on wide * containers); "stats" pairs copy with stat callouts whose numbers roll up * when they scroll into view; "quote" is a pull quote framed by large * typographic quote marks with its author. Paragraphs reveal on scroll. */ export function ContentSection({ variant = "split", eyebrow, heading, paragraphs, children, media, mediaPosition = "right", stickyMedia = true, stats = [], quote, author, locale = "en-US", className, }: ContentSectionProps) { const reduce = useReducedMotion() ?? false; if (variant === "quote") { return (
{eyebrow ? (

{eyebrow}

) : null}
{/* Decorative marks: a transform pop on view, nothing else moves. */} “ ”

{quote}

{author ? (
{author.avatar ?? initials(author.name)} {author.name} {author.role ? ( {author.role} ) : null}
) : null} {children}
); } if (variant === "stats") { return (
{children}
{stats.length > 0 ? (
{stats.map((stat, index) => ( 0 && "border-t border-border @lg:border-t-0 @lg:border-l", )} >
{stat.label}
{stat.description ? (
{stat.description}
) : null}
))}
) : null}
); } return (
{children}
{media ? (
{media}
) : null}
); }