"use client";

import { motion, useReducedMotion, type Variants } from "motion/react";
import type { ElementType, ReactNode } from "react";

type Direction = "up" | "down" | "left" | "right" | "none";

const OFFSET: Record<Direction, { x: number; y: number }> = {
  up: { x: 0, y: 28 },
  down: { x: 0, y: -28 },
  left: { x: 28, y: 0 },
  right: { x: -28, y: 0 },
  none: { x: 0, y: 0 },
};

export interface RevealProps {
  children: ReactNode;
  as?: ElementType;
  direction?: Direction;
  delay?: number;
  duration?: number;
  /** Stagger direct children instead of animating the wrapper itself. */
  stagger?: number;
  blur?: boolean;
  once?: boolean;
  amount?: number;
  className?: string;
}

/**
 * The workhorse enter animation. Every section on the site uses it.
 *
 * With reduced motion requested the element still renders in exactly the same
 * place in the tree, it just arrives instantly. Only the transition changes,
 * never the markup, so this hydrates cleanly.
 */
export function Reveal({
  children,
  as = "div",
  direction = "up",
  delay = 0,
  duration = 0.75,
  stagger,
  blur = true,
  once = true,
  amount = 0.25,
  className,
}: RevealProps) {
  const reduced = useReducedMotion();
  const Component = motion[as as "div"] ?? motion.div;
  const offset = OFFSET[direction];

  if (stagger != null) {
    const container: Variants = {
      hidden: {},
      shown: {
        transition: {
          staggerChildren: reduced ? 0 : stagger,
          delayChildren: reduced ? 0 : delay,
        },
      },
    };

    // `amount` is the fraction of this element that must be on screen. A
    // stagger container wraps a whole grid, and a tall one can never reach the
    // 0.25 default: a 4000px grid in a 900px viewport tops out at 0.22, so it
    // would stay hidden forever. Callers can still pass an explicit amount.
    const containerAmount = amount === 0.25 ? 0.05 : amount;

    return (
      <Component
        className={className}
        variants={container}
        initial="hidden"
        whileInView="shown"
        viewport={{ once, amount: containerAmount }}
      >
        {children}
      </Component>
    );
  }

  return (
    <Component
      className={className}
      initial={{
        opacity: 0,
        x: offset.x,
        y: offset.y,
        filter: blur ? "blur(8px)" : "blur(0px)",
      }}
      whileInView={{ opacity: 1, x: 0, y: 0, filter: "blur(0px)" }}
      viewport={{ once, amount }}
      transition={{
        duration: reduced ? 0 : duration,
        delay: reduced ? 0 : delay,
        ease: [0.16, 1, 0.3, 1],
      }}
    >
      {children}
    </Component>
  );
}

/** Child of a <Reveal stagger={...}> container. */
export const revealItem: Variants = {
  hidden: { opacity: 0, y: 24, filter: "blur(6px)" },
  shown: {
    opacity: 1,
    y: 0,
    filter: "blur(0px)",
    transition: { duration: 0.7, ease: [0.16, 1, 0.3, 1] },
  },
};

const revealItemInstant: Variants = {
  hidden: { opacity: 0, y: 24, filter: "blur(6px)" },
  shown: {
    opacity: 1,
    y: 0,
    filter: "blur(0px)",
    transition: { duration: 0 },
  },
};

export function RevealItem({
  children,
  className,
  as = "div",
}: {
  children: ReactNode;
  className?: string;
  as?: ElementType;
}) {
  const reduced = useReducedMotion();
  const Component = motion[as as "div"] ?? motion.div;

  return (
    <Component
      className={className}
      variants={reduced ? revealItemInstant : revealItem}
    >
      {children}
    </Component>
  );
}
