// React Bits' LineSidebar, ported to plain JSX (no imports, no TypeScript). A vertical list whose
// entries lean toward the pointer — each label slides right and warms toward the accent as the
// cursor nears it, its marker line stretches, the ticks between items grow — all driven by one
// per-item `--effect` value (0..1) that a single rAF loop eases toward its target, so colour,
// shift and scale move together with no CSS transitions to stagger. The loop stops the moment
// everything has settled; there is no permanent frame loop.
//
// Used as the legal page's contents list (legal-page.jsx). Deviations from upstream, on purpose:
//   - Items are `{ label, href }` and render as real <a> anchors, not <li onClick>: a contents
//     list has to work for the keyboard, for a no-JS reader and for the browser's own anchor
//     navigation. `onItemClick` still fires.
//   - `activeIndex` is a controlled prop. Upstream keeps the clicked item active; here the page
//     owns it (a scroll-spy), which is what "current section" means in a table of contents.
//   - Colours default to the site's tokens (they are `var()` strings and resolve inside the
//     color-mix() the CSS uses), not upstream's purple-on-grey.
//   - The pointer effect runs only for a fine pointer that can hover; under reduced motion the
//     values are set outright instead of eased, so the active entry still shows without motion.
//   - Item refs are indexed, not pushed (see the VariableProximity notes in AGENTS.md).
//   - **A running loop is never restarted.** Upstream's startLoop cancels the frame and resets its
//     clock on every pointermove, so while the cursor moves the easing gets time steps of a few
//     milliseconds instead of a frame — it crawls, then speeds up the moment the cursor stops,
//     which reads as lag and inconsistency. Here a move only writes new targets; the loop that is
//     already running picks them up on its next frame.
//   - `snap` mode: the entry under the pointer goes to 1 and every other to 0, instead of the
//     upstream proximity gradient that half-lights the neighbours. The legal page uses it — one
//     entry moving at a time is what a contents list wants.
//   - The pointer area is the whole component (markers included), not the list alone, and the
//     upstream `::before` that extended each item's hit box 48px sideways is gone: an entry
//     lighting up before the cursor is on the list felt like a bug.

const LS_FALLOFF = {
  linear: (p) => p,
  smooth: (p) => p * p * (3 - 2 * p),
  sharp: (p) => p * p * p,
};

const LineSidebar = ({
  items = [],
  accentColor = "var(--sage-dark)",
  textColor = "var(--ink-2)",
  markerColor = "var(--line-strong)",
  showIndex = true,
  showMarker = true,
  proximityRadius = 100,
  maxShift = 30,
  falloff = "smooth",
  markerLength = 60,
  markerGap = 0,
  tickScale = 0.5,
  scaleTick = true,
  itemGap = 20,
  fontSize = 1.1,
  smoothing = 100,
  snap = false,
  activeIndex = null,
  onItemClick,
  className = "",
  ariaLabel,
}) => {
  const { useRef, useCallback, useEffect } = React;
  const listRef = useRef(null);
  const itemRefs = useRef([]);
  const targetsRef = useRef([]);
  const currentRef = useRef([]);
  const rafRef = useRef(null);
  const lastRef = useRef(0);
  const activeRef = useRef(activeIndex);
  const smoothingRef = useRef(smoothing);
  // Decided once: pointer proximity needs a pointer that hovers; reduced motion keeps the
  // state changes and drops the easing.
  const gates = useRef({ pointer: false, instant: false });
  activeRef.current = activeIndex;
  smoothingRef.current = smoothing;

  useEffect(() => {
    const mq = (q) => window.matchMedia && window.matchMedia(q).matches;
    gates.current = {
      pointer: mq("(hover: hover) and (pointer: fine)"),
      instant: mq("(prefers-reduced-motion: reduce)"),
    };
  }, []);

  // One rAF loop eases every item's --effect toward its target with frame-rate independent
  // exponential smoothing, and exits as soon as nothing is moving.
  const runFrame = useCallback((now) => {
    const dt = Math.min((now - lastRef.current) / 1000, 0.05);
    lastRef.current = now;
    const tau = Math.max(smoothingRef.current, 1) / 1000;
    const k = gates.current.instant ? 1 : 1 - Math.exp(-dt / tau);

    let moving = false;
    const els = itemRefs.current;
    for (let i = 0; i < els.length; i++) {
      const el = els[i];
      if (!el) continue;
      const target = Math.max(targetsRef.current[i] || 0, activeRef.current === i ? 1 : 0);
      const cur = currentRef.current[i] || 0;
      const next = cur + (target - cur) * k;
      const settled = Math.abs(target - next) < 0.0015;
      const value = settled ? target : next;
      currentRef.current[i] = value;
      el.style.setProperty("--effect", value.toFixed(4));
      if (!settled) moving = true;
    }
    rafRef.current = moving ? requestAnimationFrame(runFrame) : null;
  }, []);

  // Only starts an idle loop. A running one keeps its clock and reads the new targets itself.
  const startLoop = useCallback(() => {
    if (rafRef.current != null) return;
    lastRef.current = performance.now();
    rafRef.current = requestAnimationFrame(runFrame);
  }, [runFrame]);

  const handlePointerMove = useCallback((e) => {
    const list = listRef.current;
    if (!list || !gates.current.pointer) return;
    const rect = list.getBoundingClientRect();
    const pointerY = e.clientY - rect.top;
    const ease = LS_FALLOFF[falloff] || LS_FALLOFF.linear;
    const els = itemRefs.current;
    for (let i = 0; i < els.length; i++) {
      const el = els[i];
      if (!el) continue;
      const center = el.offsetTop + el.offsetHeight / 2;
      const distance = Math.abs(pointerY - center);
      // snap: the entry whose row (its own height plus half the gap either side) holds the
      // pointer is on, the rest are off — no gradient across neighbours.
      targetsRef.current[i] = snap
        ? (distance < (el.offsetHeight + itemGap) / 2 ? 1 : 0)
        : ease(Math.max(0, 1 - distance / proximityRadius));
    }
    startLoop();
  }, [falloff, proximityRadius, snap, itemGap, startLoop]);

  const handlePointerLeave = useCallback(() => {
    if (!gates.current.pointer) return;
    targetsRef.current = targetsRef.current.map(() => 0);
    startLoop();
  }, [startLoop]);

  // The active entry changing is motion too: run the loop so it eases to 1 (or snaps, under
  // reduced motion) and the previous one eases back.
  useEffect(() => { startLoop(); }, [activeIndex, startLoop]);
  useEffect(() => () => { if (rafRef.current != null) cancelAnimationFrame(rafRef.current); rafRef.current = null; }, []);

  const cls = "line-sidebar"
    + (showMarker ? " line-sidebar--markers" : "")
    + (scaleTick ? " line-sidebar--scale-tick" : "")
    + (className ? ` ${className}` : "");
  const style = {
    "--accent-color": accentColor,
    "--text-color": textColor,
    "--marker-color": markerColor,
    "--marker-length": `${markerLength}px`,
    "--marker-gap": `${markerGap}px`,
    "--tick-scale": tickScale,
    "--max-shift": `${maxShift}px`,
    "--item-gap": `${itemGap}px`,
    "--font-size": `${fontSize}rem`,
    "--smoothing": `${smoothing}ms`,
  };

  return (
    <nav className={cls} style={style} aria-label={ariaLabel}
         onPointerMove={handlePointerMove} onPointerLeave={handlePointerLeave}>
      <ul ref={listRef} className="line-sidebar__list">
        {items.map((item, index) => (
          <li key={index} ref={(el) => { itemRefs.current[index] = el; }}
              className="line-sidebar__item"
              aria-current={activeIndex === index ? "true" : undefined}>
            {showMarker && <span className="line-sidebar__marker" aria-hidden="true" />}
            <a className="line-sidebar__label" href={item.href}
               onClick={() => { if (onItemClick) onItemClick(index, item.label); }}>
              {showIndex && <span className="line-sidebar__index">{String(index + 1).padStart(2, "0")}</span>}
              <span className="line-sidebar__text">{item.label}</span>
            </a>
          </li>
        ))}
      </ul>
    </nav>
  );
};
window.LineSidebar = LineSidebar;
