"use client"; import { motion, type Variants } from "motion/react"; import type { CSSProperties, ReactNode } from "react"; // Plain fade, no y-translate — fixed 2026-07-24. Used to animate opacity // 0→1 *and* y 28→0 together ("fade up"), which read as the whole section // visibly hopping/jumping into place on top of the fade — one motion cue // too many. The fade alone is already a clear enough "this just appeared" // signal without the extra jump. const fadeIn: Variants = { hidden: { opacity: 0 }, show: { opacity: 1, transition: { duration: 0.6, ease: [0.22, 1, 0.36, 1] } }, }; type RevealProps = { children: ReactNode; className?: string; style?: CSSProperties; /** Delay in seconds, e.g. for staggering siblings that aren't inside a RevealGroup. */ delay?: number; }; /** Fades a section into view once, the first time it scrolls into view. */ export function Reveal({ children, className, style, delay = 0 }: RevealProps) { return ( {children} ); } const staggerContainer: Variants = { hidden: {}, show: { transition: { staggerChildren: 0.12 } }, }; /** Wraps a grid/row of cards — reveals children one-by-one as it scrolls into view. */ export function RevealGroup({ children, className }: { children: ReactNode; className?: string }) { return ( {children} ); } /** Child item for use inside a RevealGroup — same fade motion, driven by the parent's stagger. */ export function RevealItem({ children, className }: { children: ReactNode; className?: string }) { return ( {children} ); } const popIn: Variants = { hidden: { x: 140, y: -70, scale: 0.5, opacity: 0 }, show: { // x is a single continuous glide (140 → 0), deliberately NOT // keyframed in step with y's bounce schedule. It used to share y's // per-segment alternating ease so it would decelerate to a dead stop // at every bounce peak alongside y — physically correct for y (a // ball's vertical speed really is zero at the top of each arc) but // wrong for x, which has no reason to pause horizontally just because // it's momentarily at peak height. That shared stop-start was read as // "stops unnaturally in between". Giving x its own transition (below) // decouples it completely, so it glides smoothly the whole time while // y still bounces underneath it. x: 0, y: [-70, 0, -28, 0, -12, 0, -3, 0], scale: [0.5, 1.05, 0.95, 1.03, 0.97, 1.02, 0.99, 1], // Array, not a bare 1 — a scalar target here would fade opacity // linearly across the *entire* 1s duration against the shared `times` // grid below, so the dot would still be half-transparent through its // first, fastest bounces. Snap to fully opaque at the first keyframe // instead, matching how a real solid ball would look immediately. opacity: [0, 1, 1, 1, 1, 1, 1, 1], transition: { // Applies to y/scale/opacity (anything without its own override // below) — the actual bounce schedule. default: { duration: 1, times: [0, 0.22, 0.4, 0.55, 0.68, 0.8, 0.9, 1], // Per-segment, not a single ease for the whole animation — a real // bounce accelerates while falling (gravity) and decelerates // while rising toward each peak. Alternating fall (easeIn) / rise // (easeOut), one entry per keyframe-to-keyframe transition. ease: ["easeIn", "easeOut", "easeIn", "easeOut", "easeIn", "easeOut", "easeIn"], }, // x's own transition — one smooth decelerating tween across the // full duration, independent of the y bounce's stop-start rhythm. x: { duration: 1, ease: "easeOut" }, }, }, }; /** * One-shot "bouncing ball" drop-in for a small accent glyph (e.g. the * brand's orange dot) — arrives from the top right and bounces to rest, * not a looping pulse. `motion.span` needs a Client Component, same reason * Reveal itself exists: keep "use client" isolated here so callers like * Hero.tsx can stay Server Components. */ export function PopIn({ children, className, delay = 0 }: RevealProps) { return ( {children} ); }