/**
 * ScrollReveal — reveals children with a soft fade-up when they enter the viewport.
 *
 * Used across marketing pages to give the site a "running product" feel rather
 * than the typical static-screenshot landing page. Honors `prefers-reduced-motion`
 * via CSS (see app.css `[data-reveal]`).
 *
 * Default usage:
 *   <ScrollReveal>...</ScrollReveal>
 *
 * Advanced:
 *   <ScrollReveal delay={120}>...</ScrollReveal>     // ms delay
 *   <ScrollReveal as="section">...</ScrollReveal>    // change wrapper element
 *   <ScrollReveal once={false}>...</ScrollReveal>    // re-trigger when re-entering
 *
 * Note: this is purely visual. For analytics-instrumented visibility, use
 * `useSectionVisibility` from hooks instead.
 */
import { type ElementType, type PropsWithChildren, useEffect, useRef, useState } from 'react';

import { cn } from '@/lib/utils';

interface ScrollRevealProps extends PropsWithChildren {
  className?: string;
  /** Delay reveal in milliseconds. Useful for staggered groups. */
  delay?: number;
  /** Wrapper element. Defaults to <div>. */
  as?: ElementType;
  /** Reveal once and stay revealed (default true). */
  once?: boolean;
  /** Visibility threshold (0-1). Default 0.15 — fires early enough to feel snappy. */
  threshold?: number;
}

export function ScrollReveal({
  children,
  className,
  delay = 0,
  as: Tag = 'div',
  once = true,
  threshold = 0.15,
}: ScrollRevealProps) {
  // Polymorphic ref typing: the actual element is whatever `as` resolves to.
  // Using HTMLElement keeps things accurate without lying to TypeScript via
  // a cast. Tag below accepts the same ref type via React's polymorphic
  // signature.
  const ref = useRef<HTMLElement | null>(null);
  const timeoutRef = useRef<number | null>(null);
  const [revealed, setRevealed] = useState(false);

  useEffect(() => {
    const el = ref.current;
    if (!el) return;

    // Respect reduced-motion: just reveal immediately.
    const prefersReduce =
      typeof window !== 'undefined' &&
      window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
    if (prefersReduce) {
      setRevealed(true);
      return;
    }

    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) {
          if (delay > 0) {
            timeoutRef.current = window.setTimeout(() => setRevealed(true), delay);
          } else {
            setRevealed(true);
          }
          if (once) observer.disconnect();
        } else if (!once) {
          setRevealed(false);
        }
      },
      { threshold },
    );

    observer.observe(el);
    return () => {
      observer.disconnect();
      if (timeoutRef.current !== null) {
        clearTimeout(timeoutRef.current);
        timeoutRef.current = null;
      }
    };
  }, [delay, once, threshold]);

  // The React typings for polymorphic refs aren't perfect — a runtime cast on
  // the ref attribute (not on the underlying useRef call) keeps the types
  // honest at the call site.
  return (
    <Tag
      ref={ref as React.Ref<HTMLElement>}
      data-reveal=""
      className={cn(revealed && 'is-revealed', className)}
    >
      {children}
    </Tag>
  );
}
