// React Bits' BorderGlow, ported to plain JSX. A ring and a halo light up along whichever
// edge the pointer is nearest, masked to a cone that points back at the cursor.
//
// Deviations from upstream, all deliberate:
//
// - **It wraps photographs, so the two layers that paint *over* the content are gone.**
//   Upstream's `::after` is a mesh-gradient fill at `mix-blend-mode: soft-light` across the
//   whole card. Over a photograph that is a tint, and this site already spends its one
//   allowed tint on the corner wash. Only the edge survives.
// - **No `mix-blend-mode: plus-lighter` on the glow.** Upstream assumes a near-black card;
//   plus-lighter against this site's cream slab blows the halo out to white. Normal blending
//   at a token colour instead — the same lesson the MagicBento port recorded before it.
// - **One accent per card, not a seven-stop mesh gradient.** Upstream distributes three hex
//   colours across seven radial gradients. Here each tile owns a single palette token
//   (`--gal-accent`) and the ring is built from it with `color-mix`, so both themes invert
//   with no second set of values.
// - **The pointer handler is rAF-throttled**, and it is one `getBoundingClientRect` per
//   frame rather than one per event. Same treatment VariableProximity and MagnetLines take.
// - **Nothing renders for a coarse pointer or under `prefers-reduced-motion`** — the effect
//   is entirely cursor-driven, so there is no paint layer at all on a phone.
// - Upstream's six-layer drop shadow and its own border/background are not ported: these
//   cards are pictures, and the site sizes its own shadows.
// `edgeSensitivity` and `coneSpread` deliberately have **no defaults**. They are written as
// inline custom properties, and an inline custom property outranks every stylesheet rule — so
// defaulting them here silently pinned the design values and made the `.bg-card` block in
// main.css unable to change them (retuning the CSS looked like it did nothing). Undefined
// means the property is not written at all and CSS owns it; pass one to override a single
// instance.
const BorderGlow = ({
  className = "",
  style,
  children,
  edgeSensitivity,
  coneSpread,
}) => {
  const { useRef, useCallback, useEffect, useState } = React;
  const ref = useRef(null);
  const frame = useRef(0);
  const [on, setOn] = useState(false);

  useEffect(() => {
    const fine = window.matchMedia("(hover: hover) and (pointer: fine)");
    const still = window.matchMedia("(prefers-reduced-motion: reduce)");
    const sync = () => setOn(fine.matches && !still.matches);
    sync();
    fine.addEventListener("change", sync);
    still.addEventListener("change", sync);
    return () => {
      fine.removeEventListener("change", sync);
      still.removeEventListener("change", sync);
      if (frame.current) cancelAnimationFrame(frame.current);
    };
  }, []);

  // Edge proximity is upstream's: how far along the ray from the centre through the pointer
  // the card's own boundary sits, clamped to 0..1. It reaches 1 exactly at the edge, which is
  // what makes the glow follow the frame rather than the cursor.
  const onMove = useCallback((e) => {
    if (!on || frame.current) return;
    const px = e.clientX;
    const py = e.clientY;
    frame.current = requestAnimationFrame(() => {
      frame.current = 0;
      const el = ref.current;
      if (!el) return;
      const r = el.getBoundingClientRect();
      const hw = r.width / 2;
      const hh = r.height / 2;
      const dx = px - r.left - hw;
      const dy = py - r.top - hh;
      let kx = Infinity;
      let ky = Infinity;
      if (dx !== 0) kx = hw / Math.abs(dx);
      if (dy !== 0) ky = hh / Math.abs(dy);
      const edge = Math.min(Math.max(1 / Math.min(kx, ky), 0), 1);
      let deg = Math.atan2(dy, dx) * (180 / Math.PI) + 90;
      if (deg < 0) deg += 360;
      el.style.setProperty("--edge-proximity", (edge * 100).toFixed(2));
      el.style.setProperty("--cursor-angle", deg.toFixed(2) + "deg");
    });
  }, [on]);

  // Reset on the way out, or a re-entry starts from wherever the pointer last left — the
  // opacity is already handled by :not(:hover), but the angle would jump.
  const onLeave = useCallback(() => {
    const el = ref.current;
    if (el) el.style.setProperty("--edge-proximity", "0");
  }, []);

  return (
    <div
      ref={ref}
      className={className + (on ? " bg-card" : "")}
      style={{
        ...style,
        ...(edgeSensitivity === undefined ? null : { "--edge-sensitivity": edgeSensitivity }),
        ...(coneSpread === undefined ? null : { "--cone": coneSpread }),
      }}
      onPointerMove={on ? onMove : undefined}
      onPointerLeave={on ? onLeave : undefined}
    >
      {children}
      {on && <span className="bg-edge" aria-hidden="true" />}
    </div>
  );
};

window.BorderGlow = BorderGlow;
