// shared.jsx — helpers, hooks, common components
const { useState, useEffect, useRef, useMemo, useCallback } = React;

// ---------- Hooks ----------
function useInView(threshold = 0.2) {
  const ref = useRef(null);
  const [inView, setInView] = useState(false);
  useEffect(() => {
    if (!ref.current) return;
    // Immediate check: if already in viewport at mount, reveal now
    const rect = ref.current.getBoundingClientRect();
    if (rect.top < window.innerHeight && rect.bottom > 0) {
      setInView(true);
      return;
    }
    const obs = new IntersectionObserver(
      ([e]) => { if (e.isIntersecting) setInView(true); },
      { threshold }
    );
    obs.observe(ref.current);
    // Safety fallback in case observer never fires
    const tid = setTimeout(() => setInView(true), 1200);
    return () => { obs.disconnect(); clearTimeout(tid); };
  }, [threshold]);
  return [ref, inView];
}

function useScrollProgress() {
  const [p, setP] = useState(0);
  useEffect(() => {
    const onScroll = () => {
      const h = document.documentElement.scrollHeight - window.innerHeight;
      setP(h > 0 ? (window.scrollY / h) * 100 : 0);
    };
    onScroll();
    window.addEventListener("scroll", onScroll, { passive: true });
    return () => window.removeEventListener("scroll", onScroll);
  }, []);
  return p;
}

function useScrollY() {
  const [y, setY] = useState(0);
  useEffect(() => {
    const onScroll = () => setY(window.scrollY);
    onScroll();
    window.addEventListener("scroll", onScroll, { passive: true });
    return () => window.removeEventListener("scroll", onScroll);
  }, []);
  return y;
}

// Active section for nav
function useActiveSection(ids) {
  const [active, setActive] = useState(ids[0]);
  useEffect(() => {
    const onScroll = () => {
      const y = window.scrollY + window.innerHeight * 0.35;
      let cur = ids[0];
      for (const id of ids) {
        const el = document.getElementById(id);
        if (el && el.offsetTop <= y) cur = id;
      }
      setActive(cur);
    };
    onScroll();
    window.addEventListener("scroll", onScroll, { passive: true });
    return () => window.removeEventListener("scroll", onScroll);
  }, [ids.join(",")]);
  return active;
}

// Counter — anime de 0 vers `to`, déclenche un glow burst en fin d'animation
function Counter({ to, duration = 1600, suffix = "" }) {
  const [ref, inView] = useInView(0.4);
  const [n, setN] = useState(0);
  useEffect(() => {
    if (!inView) return;
    const start = performance.now();
    let raf;
    const tick = (t) => {
      const p = Math.min(1, (t - start) / duration);
      const eased = 1 - Math.pow(1 - p, 3);
      setN(Math.round(to * eased));
      if (p < 1) raf = requestAnimationFrame(tick);
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, [inView, to, duration]);
  return <span ref={ref}>{n}{suffix}</span>;
}

// Reveal wrapper — variantes : "up" (defaut), "blur", "scale", "left", "right", "mask"
function Reveal({ children, delay = 0, variant = "up", as: Tag = "div", className = "", style, ...rest }) {
  const [ref, inView] = useInView(0.15);
  return (
    <Tag
      ref={ref}
      className={`reveal ${inView ? "in" : ""} ${className}`}
      data-variant={variant}
      style={{ transitionDelay: `${delay}ms`, ...(style || {}) }}
      {...rest}
    >
      {children}
    </Tag>
  );
}

// RevealStagger — applique `.reveal` directement aux enfants (cloneElement)
// pour rester compatible avec n'importe quel parent (ul, grid, etc.) sans
// introduire de div intermédiaire qui casserait le HTML sémantique.
function RevealStagger({ children, stagger = 120, baseDelay = 0, variant = "up", className = "", as: Tag = "div", ...rest }) {
  const [ref, inView] = useInView(0.15);
  const arr = React.Children.toArray(children);
  return (
    <Tag ref={ref} className={`reveal-stagger ${inView ? "in" : ""} ${className}`} {...rest}>
      {arr.map((child, i) => (
        <div
          key={i}
          className="reveal"
          data-variant={variant}
          style={{ transitionDelay: `${baseDelay + i * stagger}ms` }}
        >
          {child}
        </div>
      ))}
    </Tag>
  );
}

// Magnetic — le wrapper "tire" vers le curseur dans un rayon donné.
// Utilisé pour CTAs primaires. Ne se déclenche pas sur pointer:coarse (mobile).
function useMagnetic({ strength = 0.35, radius = 100 } = {}) {
  const ref = useRef(null);
  useEffect(() => {
    const el = ref.current;
    if (!el) return;
    if (window.matchMedia && window.matchMedia("(pointer: coarse)").matches) return;
    let raf;
    let tx = 0, ty = 0, cx = 0, cy = 0;
    const onMove = (e) => {
      const r = el.getBoundingClientRect();
      const ecx = r.left + r.width / 2;
      const ecy = r.top + r.height / 2;
      const dx = e.clientX - ecx;
      const dy = e.clientY - ecy;
      const dist = Math.hypot(dx, dy);
      if (dist < radius) {
        tx = dx * strength;
        ty = dy * strength;
      } else {
        tx = 0; ty = 0;
      }
    };
    const tick = () => {
      cx += (tx - cx) * 0.18;
      cy += (ty - cy) * 0.18;
      el.style.transform = `translate(${cx.toFixed(2)}px, ${cy.toFixed(2)}px)`;
      raf = requestAnimationFrame(tick);
    };
    tick();
    window.addEventListener("mousemove", onMove);
    return () => {
      cancelAnimationFrame(raf);
      window.removeEventListener("mousemove", onMove);
      el.style.transform = "";
    };
  }, [strength, radius]);
  return ref;
}

// MagneticButton — wrap n'importe quel bouton/lien pour effet aimanté.
function MagneticButton({ children, strength = 0.32, radius = 90, className = "", ...rest }) {
  const ref = useMagnetic({ strength, radius });
  return (
    <span ref={ref} className={`magnetic ${className}`} style={{ display: "inline-block", willChange: "transform" }} {...rest}>
      {children}
    </span>
  );
}

// Tilt 3D — perspective + rotateX/Y vers le curseur quand il survole.
function useTilt({ max = 8, scale = 1.02 } = {}) {
  const ref = useRef(null);
  useEffect(() => {
    const el = ref.current;
    if (!el) return;
    if (window.matchMedia && window.matchMedia("(pointer: coarse)").matches) return;
    let raf;
    let rx = 0, ry = 0, cx = 0, cy = 0, active = false, scaleTarget = 1, scaleCur = 1;
    const onMove = (e) => {
      const r = el.getBoundingClientRect();
      const px = (e.clientX - r.left) / r.width;
      const py = (e.clientY - r.top) / r.height;
      rx = (py - 0.5) * -2 * max;
      ry = (px - 0.5) * 2 * max;
    };
    const onEnter = () => { active = true; scaleTarget = scale; };
    const onLeave = () => { active = false; rx = 0; ry = 0; scaleTarget = 1; };
    const tick = () => {
      cx += (rx - cx) * 0.15;
      cy += (ry - cy) * 0.15;
      scaleCur += (scaleTarget - scaleCur) * 0.15;
      el.style.transform = `perspective(900px) rotateX(${cx.toFixed(2)}deg) rotateY(${cy.toFixed(2)}deg) scale(${scaleCur.toFixed(3)})`;
      raf = requestAnimationFrame(tick);
    };
    tick();
    el.addEventListener("mousemove", onMove);
    el.addEventListener("mouseenter", onEnter);
    el.addEventListener("mouseleave", onLeave);
    return () => {
      cancelAnimationFrame(raf);
      el.removeEventListener("mousemove", onMove);
      el.removeEventListener("mouseenter", onEnter);
      el.removeEventListener("mouseleave", onLeave);
      el.style.transform = "";
    };
  }, [max, scale]);
  return ref;
}

// TiltCard — wrap qui applique le tilt 3D et préserve les transforms internes.
function TiltCard({ children, max = 7, scale = 1.025, className = "", ...rest }) {
  const ref = useTilt({ max, scale });
  return (
    <div ref={ref} className={`tilt-card ${className}`} style={{ willChange: "transform", transformStyle: "preserve-3d" }} {...rest}>
      {children}
    </div>
  );
}

// Parallax — translate Y inversement proportionnel au scroll, basé sur position
// de l'élément à l'écran. `speed` positif = élément remonte plus lentement.
function useParallax(speed = 0.25) {
  const ref = useRef(null);
  useEffect(() => {
    const el = ref.current;
    if (!el) return;
    let raf;
    const tick = () => {
      const r = el.getBoundingClientRect();
      const winH = window.innerHeight;
      const center = r.top + r.height / 2;
      const offset = (center - winH / 2) * speed;
      el.style.transform = `translate3d(0, ${(-offset).toFixed(2)}px, 0)`;
      raf = requestAnimationFrame(tick);
    };
    tick();
    return () => cancelAnimationFrame(raf);
  }, [speed]);
  return ref;
}

// Scroll-driven : retourne une progression 0→1 selon que l'élément est
// loin/proche du centre du viewport. Utilisé pour interpoler des effets
// continus (scale, blur) pendant que l'élément entre à l'écran.
function useScrollProgressInView({ start = 0.95, end = 0.45 } = {}) {
  const ref = useRef(null);
  const [p, setP] = useState(0);
  useEffect(() => {
    const el = ref.current;
    if (!el) return;
    let raf;
    const tick = () => {
      const r = el.getBoundingClientRect();
      const winH = window.innerHeight;
      const top = r.top / winH;
      let prog = (start - top) / (start - end);
      if (prog < 0) prog = 0;
      if (prog > 1) prog = 1;
      setP(prog);
      raf = requestAnimationFrame(tick);
    };
    tick();
    return () => cancelAnimationFrame(raf);
  }, [start, end]);
  return [ref, p];
}

// ScrollTitle — titre qui émerge dramatiquement : scale 0.65 → 1, translateY 80→0,
// blur 16px → 0, opacity 0 → 1 pendant qu'il entre dans le viewport.
function ScrollTitle({ children, as: Tag = "h2", className = "", style, ...rest }) {
  const [ref, p] = useScrollProgressInView({ start: 0.95, end: 0.5 });
  const scale = 0.94 + 0.06 * p;
  const blur = (1 - p) * 6;
  const opacity = 0.4 + 0.6 * p;
  return (
    <Tag
      ref={ref}
      className={className}
      style={{
        transform: `scale(${scale.toFixed(3)})`,
        filter: `blur(${blur.toFixed(2)}px)`,
        opacity,
        transformOrigin: "left center",
        willChange: "transform, filter, opacity",
        ...(style || {}),
      }}
      {...rest}
    >
      {children}
    </Tag>
  );
}


// Nav scrolled — true dès que window.scrollY > seuil.
function useScrolled(threshold = 80) {
  const [scrolled, setScrolled] = useState(false);
  useEffect(() => {
    const onScroll = () => setScrolled(window.scrollY > threshold);
    onScroll();
    window.addEventListener("scroll", onScroll, { passive: true });
    return () => window.removeEventListener("scroll", onScroll);
  }, [threshold]);
  return scrolled;
}

// Cursor custom
function CustomCursor() {
  const dot = useRef(null);
  const ring = useRef(null);
  useEffect(() => {
    let rx = 0, ry = 0, dx = 0, dy = 0;
    const onMove = (e) => {
      dx = e.clientX; dy = e.clientY;
      if (dot.current) {
        dot.current.style.transform = `translate(${dx}px, ${dy}px) translate(-50%, -50%)`;
      }
    };
    let raf;
    const animate = () => {
      rx += (dx - rx) * 0.18;
      ry += (dy - ry) * 0.18;
      if (ring.current) {
        ring.current.style.transform = `translate(${rx}px, ${ry}px) translate(-50%, -50%)`;
      }
      raf = requestAnimationFrame(animate);
    };
    animate();
    const hoverables = "a, button, .hoverable, [data-hover]";
    const magnetics = ".magnetic, [data-magnetic]";
    const onOver = (e) => {
      if (!e.target.closest) return;
      if (e.target.closest(magnetics)) {
        ring.current && ring.current.classList.add("magnetic");
      } else if (e.target.closest(hoverables)) {
        ring.current && ring.current.classList.add("hovering");
      }
    };
    const onOut = (e) => {
      if (!e.target.closest) return;
      if (e.target.closest(magnetics)) {
        ring.current && ring.current.classList.remove("magnetic");
      } else if (e.target.closest(hoverables)) {
        ring.current && ring.current.classList.remove("hovering");
      }
    };
    window.addEventListener("mousemove", onMove);
    document.addEventListener("mouseover", onOver);
    document.addEventListener("mouseout", onOut);
    return () => {
      cancelAnimationFrame(raf);
      window.removeEventListener("mousemove", onMove);
      document.removeEventListener("mouseover", onOver);
      document.removeEventListener("mouseout", onOut);
    };
  }, []);
  return (
    <>
      <div ref={ring} className="cursor-ring" />
      <div ref={dot} className="cursor-dot" />
    </>
  );
}

// Dust particles
function DustParticles() {
  const canvasRef = useRef(null);
  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const ctx = canvas.getContext("2d");
    let w = canvas.width = window.innerWidth;
    let h = canvas.height = window.innerHeight;
    const parts = Array.from({ length: 60 }, () => ({
      x: Math.random() * w,
      y: Math.random() * h,
      r: Math.random() * 1.8 + 0.3,
      vx: (Math.random() - 0.5) * 0.15,
      vy: (Math.random() - 0.5) * 0.1 - 0.05,
      a: Math.random() * 0.6 + 0.2,
      hue: Math.random() > 0.7 ? 40 : (Math.random() > 0.5 ? 25 : 0),
    }));
    let raf;
    const tick = () => {
      ctx.clearRect(0, 0, w, h);
      for (const p of parts) {
        p.x += p.vx; p.y += p.vy;
        if (p.x < 0) p.x = w; if (p.x > w) p.x = 0;
        if (p.y < 0) p.y = h; if (p.y > h) p.y = 0;
        ctx.beginPath();
        ctx.fillStyle = `hsla(${p.hue + 30}, 70%, 75%, ${p.a})`;
        ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);
        ctx.fill();
      }
      raf = requestAnimationFrame(tick);
    };
    tick();
    const onResize = () => { w = canvas.width = window.innerWidth; h = canvas.height = window.innerHeight; };
    window.addEventListener("resize", onResize);
    return () => { cancelAnimationFrame(raf); window.removeEventListener("resize", onResize); };
  }, []);
  return <canvas ref={canvasRef} className="dust" />;
}

// Drapeau officiel Burkina Faso — rouge (haut), vert (bas), étoile jaune 5 branches au centre
function FlagDot({ size = 10 }) {
  const star = "M0,-1 L0.294,-0.309 L0.951,-0.309 L0.397,0.118 L0.588,0.809 L0,0.382 L-0.588,0.809 L-0.397,0.118 L-0.951,-0.309 L-0.294,-0.309 Z";
  return (
    <span aria-label="Drapeau du Burkina Faso" style={{
      display: "inline-block",
      width: size, height: size, borderRadius: "50%",
      overflow: "hidden", position: "relative",
      boxShadow: "0 0 10px rgba(245,158,11,.35), inset 0 0 0 0.5px rgba(244,236,224,.3)",
      background: "linear-gradient(#ef2b2d 0 50%, #009e49 50% 100%)"
    }}>
      <svg viewBox="-1 -1 2 2" style={{ position: "absolute", inset: 0, width: "100%", height: "100%" }}>
        <path d={star} fill="#fcd116" style={{ filter: "drop-shadow(0 0 0.05px rgba(0,0,0,.25))" }}/>
      </svg>
    </span>
  );
}

// Logo 3D — pièce qui tourne continuellement (gauche→droite), recto = verso
function LogoCoin({ size = 220 }) {
  // Version simple : juste le logo qui tourne lentement sur lui-même.
  // Plus d'effet "verre volumétrique" — l'utilisateur préfère un rendu net,
  // que le logo lui-même soit la vedette sans matière autour.
  return (
    <div className="logo-simple" style={{ width: size, height: size }}>
      <img src="assets/logo.png" alt="FasoTravel"/>
    </div>
  );
}

// Store button
function StoreButton({ store, label, sub, onNotify }) {
  return (
    <button className="btn btn-store hoverable" onClick={onNotify} data-hover>
      <span className="store-icon" aria-hidden>
        {store === "apple" ? (
          <svg viewBox="0 0 24 24" fill="currentColor" width="28" height="28"><path d="M17.05 12.536c-.02-2.74 2.236-4.05 2.337-4.115-1.272-1.857-3.25-2.112-3.956-2.142-1.687-.17-3.292.99-4.147.99-.854 0-2.17-.964-3.568-.939-1.836.027-3.527 1.067-4.47 2.71-1.905 3.305-.486 8.19 1.37 10.868.906 1.312 1.985 2.787 3.398 2.734 1.367-.054 1.883-.884 3.534-.884s2.117.884 3.568.856c1.475-.025 2.408-1.336 3.309-2.655 1.044-1.522 1.474-2.997 1.5-3.073-.032-.015-2.876-1.105-2.903-4.35zM14.353 4.592c.755-.917 1.265-2.186 1.125-3.46-1.088.044-2.404.726-3.184 1.637-.7.808-1.31 2.1-1.144 3.342 1.211.094 2.45-.614 3.203-1.52z"/></svg>
        ) : (
          <svg viewBox="0 0 24 24" width="28" height="28">
            <defs>
              <linearGradient id="gp1" x1="0" y1="0" x2="1" y2="1">
                <stop offset="0" stopColor="#34A853"/>
                <stop offset="1" stopColor="#FBBC05"/>
              </linearGradient>
            </defs>
            <path fill="#EA4335" d="M3.6 2.3c-.3.3-.5.8-.5 1.4v16.6c0 .6.2 1.1.5 1.4l9.5-9.7L3.6 2.3z"/>
            <path fill="#FBBC05" d="M16.8 15.9l-3.7-3.8L3.6 21.7c.4.4 1.1.4 1.9 0l11.3-5.8z"/>
            <path fill="#34A853" d="M16.8 8.1L5.5 2.3c-.8-.5-1.5-.4-1.9 0l9.5 9.8 3.7-4z"/>
            <path fill="#4285F4" d="M20.5 10.1l-3.7-2-4.1 4 4.1 4.1 3.7-2.1c1.1-.6 1.1-1.6 0-2z"/>
          </svg>
        )}
      </span>
      <span className="store-txt">
        <span className="s1">{sub}</span>
        <span className="s2">{label}</span>
      </span>
      <span className="badge-soon">{window.FT_T ? window.FT_T.soon : "Bientôt"}</span>
    </button>
  );
}

Object.assign(window, {
  useInView, useScrollProgress, useScrollY, useActiveSection, useScrolled,
  useMagnetic, useTilt, useParallax, useScrollProgressInView,
  Counter, Reveal, RevealStagger, MagneticButton, TiltCard, ScrollTitle,
  CustomCursor, DustParticles, FlagDot, LogoCoin, StoreButton,
});
