"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 (
{eyebrow}
{heading}
) : null}
{paragraph}
{eyebrow}
{quote}