"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 (
    <motion.div
      className={className}
      style={style}
      initial="hidden"
      whileInView="show"
      viewport={{ once: true, margin: "-80px" }}
      variants={fadeIn}
      transition={{ delay }}
    >
      {children}
    </motion.div>
  );
}

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 (
    <motion.div
      className={className}
      initial="hidden"
      whileInView="show"
      viewport={{ once: true, margin: "-80px" }}
      variants={staggerContainer}
    >
      {children}
    </motion.div>
  );
}

/** 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 (
    <motion.div className={className} variants={fadeIn}>
      {children}
    </motion.div>
  );
}

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 (
    <motion.span
      className={className}
      initial="hidden"
      // animate, not whileInView — its only caller (Hero.tsx's brand dot)
      // sits above the fold, already visible on load, so there's no real
      // "scrolls into view" moment to gate on. whileInView's -80px viewport
      // margin also broke on some phones: the popIn variant's own "hidden"
      // state translates x:+140, and on a narrow mobile viewport that could
      // push the dot's pre-animation bounding box past the right edge —
      // IntersectionObserver then never reports it as visible, so
      // whileInView never fires and the dot stays stuck off-screen
      // (reported 2026-07-24: dot invisible on a real phone). Firing on
      // mount sidesteps that geometry entirely.
      animate="show"
      variants={popIn}
      transition={{ delay }}
    >
      {children}
    </motion.span>
  );
}
