// src/MusicPlayer.jsx — the header MUSIC PLAYER (23 Sep 2026).
//
// Hugo: "for the music projects, we need some music player up top here we can play the albums
// tracks, in order with a nice player UI with a little audio wave bar thing and proper nice on one
// line ... i need to be able to choose the album and the tracks, by default plays the album we're on
// in order".
//
// - Shown only when GET /api/music/library says the project has a songs library (decided by the
//   template's data on the server, db/musicLibrary.js). Paradise Found never shows it.
// - It sits on the header's status-pill line, right-aligned with the round header buttons above.
//   It is position:absolute inside .project-header and measures the line it sits on, so it changes
//   no other box's size or position. At narrow widths it drops controls (volume, album picker,
//   progress, wave) instead of wrapping; on the phone (tier s) it is hidden (styles/music-player.css).
// - ONE <audio> element lives in a module-level engine (window.__ftMusic), not in React state, so a
//   page change — or a remount of the header — never stops the song.
// - It never starts by itself: only a click plays. Default queue = the album open in the header,
//   from track 1, in order, going on to the next track. When Hugo switches album in the header the
//   player follows, but only while nothing is playing.
// - 24 Sep 2026 — COMPACT AT REST (Hugo: "have just the track pill unless i hover it"). At rest the
//   bar is only as wide as the track pill, at the right end of its box; on hover, keyboard focus, an
//   open menu or a drag it grows LEFT to the full player, and shrinks back 300 ms after the pointer
//   leaves. The full row is always laid out at full width inside the bar (the bar clips it), so no
//   part reflows while it grows, and the player is position:absolute, so nothing else moves. The
//   pill is the row's LAST item, so it stays put. While a song plays the pill shows a tiny wave.
// - The wave bars read a Web Audio AnalyserNode for files served by this app (same origin); a cloud
//   file (another origin) gets a light fake wave, because an analysed cross-origin file plays silent.
(function () {
  "use strict";

  const LS_VOLUME = "ft-music-volume";
  const BAR_COUNT = 14;
  const clamp01 = (v) => Math.max(0, Math.min(1, v));
  const fmt = (s) => {
    if (!Number.isFinite(s) || s < 0) return "0:00";
    const m = Math.floor(s / 60), r = Math.floor(s % 60);
    return m + ":" + String(r).padStart(2, "0");
  };
  const readVolume = () => { try { const v = parseFloat(localStorage.getItem(LS_VOLUME)); return Number.isFinite(v) ? clamp01(v) : 0.8; } catch (_) { return 0.8; } };
  const sameOrigin = (src) => { try { return new URL(src, location.href).origin === location.origin; } catch (_) { return false; } };

  // ── the engine: one per page, survives any remount ───────────────────────
  function createEngine() {
    const subs = new Set();
    const E = {
      projectId: null, albums: [], albumId: null, index: 0,
      playing: false, time: 0, duration: 0, volume: readVolume(), muted: false,
      error: null, fake: false,
      local: null, remote: null, el: null,
      ctx: null, analyser: null, freq: null, silentFrames: 0,
    };
    const emit = () => { for (const fn of subs) { try { fn(); } catch (_) {} } };
    E.subscribe = (fn) => { subs.add(fn); return () => subs.delete(fn); };

    const makeEl = (withCors) => {
      const a = new Audio();
      a.preload = "metadata";
      if (withCors) a.crossOrigin = "anonymous";
      a.addEventListener("play", () => { if (a === E.el) { E.playing = true; emit(); } });
      a.addEventListener("pause", () => { if (a === E.el) { E.playing = false; emit(); } });
      a.addEventListener("timeupdate", () => { if (a === E.el) { E.time = a.currentTime || 0; emit(); } });
      a.addEventListener("durationchange", () => { if (a === E.el) { E.duration = Number.isFinite(a.duration) ? a.duration : 0; emit(); } });
      a.addEventListener("loadedmetadata", () => { if (a === E.el) { E.duration = Number.isFinite(a.duration) ? a.duration : 0; emit(); } });
      a.addEventListener("ended", () => { if (a === E.el) E.next(true); });
      a.addEventListener("error", () => {
        if (a !== E.el || !a.getAttribute("src")) return;
        const wasPlaying = E.playing;
        E.playing = false;
        E.error = "This song file could not be played.";
        emit();
        // a broken file in the middle of an album should not stop the album
        if (wasPlaying) setTimeout(() => { if (E.error && E.el === a) E.next(true); }, 1200);
      });
      return a;
    };

    E.album = () => E.albums.find((a) => a.id === E.albumId) || null;
    E.tracks = () => { const a = E.album(); return a ? a.tracks : []; };
    E.track = () => E.tracks()[E.index] || null;

    const applyVolume = () => {
      for (const a of [E.local, E.remote]) if (a) { a.volume = E.volume; a.muted = E.muted; }
    };

    // point the right element at the current track (paused, at the start)
    const loadCurrent = () => {
      const t = E.track();
      E.error = null; E.time = 0; E.duration = 0; E.silentFrames = 0; E.fake = false;
      for (const a of [E.local, E.remote]) if (a && a !== null) { try { a.pause(); } catch (_) {} }
      if (!t || !t.playable || !t.src) { if (E.el) { E.el.removeAttribute("src"); try { E.el.load(); } catch (_) {} } E.el = null; emit(); return; }
      const local = sameOrigin(t.src);
      if (local) { if (!E.local) E.local = makeEl(false); E.el = E.local; }
      else { if (!E.remote) E.remote = makeEl(false); E.el = E.remote; E.fake = true; }
      applyVolume();
      if (E.el.getAttribute("src") !== t.src) { E.el.setAttribute("src", t.src); try { E.el.load(); } catch (_) {} }
      else { try { E.el.currentTime = 0; } catch (_) {} }
      emit();
    };

    // the analyser is built on the first click (browsers only start audio after a user gesture)
    const ensureAnalyser = () => {
      if (E.el !== E.local || !E.local) return;
      try {
        if (!E.ctx) {
          const AC = window.AudioContext || window.webkitAudioContext;
          if (!AC) { E.fake = true; return; }
          E.ctx = new AC();
          const srcNode = E.ctx.createMediaElementSource(E.local);
          E.analyser = E.ctx.createAnalyser();
          E.analyser.fftSize = 64;
          E.analyser.smoothingTimeConstant = 0.72;
          E.freq = new Uint8Array(E.analyser.frequencyBinCount);
          srcNode.connect(E.analyser);
          E.analyser.connect(E.ctx.destination);
        }
        if (E.ctx.state === "suspended") E.ctx.resume().catch(() => {});
      } catch (_) { E.fake = true; }
    };

    const firstPlayable = (tracks, from = 0, step = 1) => {
      for (let i = from; i >= 0 && i < tracks.length; i += step) if (tracks[i] && tracks[i].playable) return i;
      return -1;
    };

    E.play = () => {
      E.albumDone = false;
      if (!E.el) {
        const i = firstPlayable(E.tracks(), E.index);
        const j = i >= 0 ? i : firstPlayable(E.tracks(), 0);
        if (j < 0) return;
        E.index = j; loadCurrent();
      }
      if (!E.el) return;
      ensureAnalyser();
      E.error = null;
      const p = E.el.play();
      if (p && p.catch) p.catch((err) => { if (err && err.name !== "AbortError") { E.error = "The browser would not start the song."; E.playing = false; emit(); } });
    };
    E.pause = () => { if (E.el) E.el.pause(); };
    E.toggle = () => { if (E.playing) E.pause(); else E.play(); };
    E.playIndex = (i) => { E.index = i; loadCurrent(); E.play(); };
    // next(true) = the song ended by itself; at the end of the album the player stops on track 1
    E.next = (auto) => {
      const tracks = E.tracks();
      const i = firstPlayable(tracks, E.index + 1);
      if (i < 0) {
        // albumDone: a carried album (another project's, see MusicPlayer) hands over now
        if (auto) { E.albumDone = true; E.index = Math.max(0, firstPlayable(tracks, 0)); loadCurrent(); }
        return;
      }
      const keepPlaying = auto || E.playing;
      E.index = i; loadCurrent();
      if (keepPlaying) E.play();
    };
    E.prev = () => {
      if (E.el && E.el.currentTime > 3) { E.el.currentTime = 0; return; }
      const i = firstPlayable(E.tracks(), E.index - 1, -1);
      if (i < 0) { if (E.el) E.el.currentTime = 0; return; }
      const keepPlaying = E.playing;
      E.index = i; loadCurrent();
      if (keepPlaying) E.play();
    };
    E.seek = (frac) => {
      if (!E.el || !E.duration) return;
      E.el.currentTime = clamp01(frac) * E.duration;
      E.time = E.el.currentTime; emit();
    };
    E.setVolume = (v) => {
      E.volume = clamp01(v); E.muted = E.volume === 0 ? E.muted : false;
      try { localStorage.setItem(LS_VOLUME, String(E.volume)); } catch (_) {}
      applyVolume(); emit();
    };
    E.toggleMute = () => { E.muted = !E.muted; applyVolume(); emit(); };

    // pick an album: its first playable track (or track 1), paused unless `andPlay`
    E.selectAlbum = (id, andPlay) => {
      E.albumId = id;
      const i = firstPlayable(E.tracks(), 0);
      E.index = i >= 0 ? i : 0;
      loadCurrent();
      if (andPlay) E.play();
    };

    // a fresh library for the project (a refetch keeps what is playing; a new project starts over)
    E.setLibrary = (projectId, albums) => {
      const sameProject = projectId === E.projectId;
      const cur = E.track();
      E.projectId = projectId;
      E.albums = Array.isArray(albums) ? albums : [];
      if (sameProject && cur && E.album()) {
        const at = E.tracks().findIndex((t) => t.id === cur.id);
        if (at >= 0) { E.index = at; emit(); return; }
      }
      if (sameProject && E.playing) { emit(); return; }
      if (!E.album()) E.albumId = null;
      emit();
    };
    E.stop = () => {
      for (const a of [E.local, E.remote]) if (a) { try { a.pause(); a.removeAttribute("src"); a.load(); } catch (_) {} }
      E.el = null; E.playing = false; E.time = 0; E.duration = 0; E.error = null;
      E.projectId = null; E.albums = []; E.albumId = null; E.index = 0; E.albumDone = false;
      emit();
    };

    // 0..1 levels for the wave bars, low → high frequencies
    let phase = 0;
    E.levels = (n, out) => {
      if (!E.playing) { for (let i = 0; i < n; i++) out[i] = 0; return out; }
      if (!E.fake && E.analyser && E.el === E.local) {
        E.analyser.getByteFrequencyData(E.freq);
        const bins = E.freq.length;
        let sum = 0;
        for (let i = 0; i < n; i++) {
          // skip the lowest bin (DC/rumble) and spread the bars over the useful range
          const b = Math.min(bins - 1, 1 + Math.floor(Math.pow(i / n, 1.5) * (bins * 0.62)));
          const v = E.freq[b] / 255;
          out[i] = v; sum += v;
        }
        // a silent analyser while the song runs (a browser that mutes the graph): fall back to the fake
        if (sum === 0 && E.el && E.el.currentTime > 0.5) { if (++E.silentFrames > 60) E.fake = true; }
        else E.silentFrames = 0;
        return out;
      }
      phase += 0.09;
      for (let i = 0; i < n; i++) {
        const x = i / n;
        const v = 0.42 + 0.26 * Math.sin(phase * 1.7 + i * 0.9) + 0.18 * Math.sin(phase * 3.1 - i * 1.7) + 0.12 * Math.sin(phase * 0.6 + x * 5);
        out[i] = clamp01(v * (1 - x * 0.35) + (Math.random() - 0.5) * 0.12);
      }
      return out;
    };
    E.currentTime = () => (E.el ? E.el.currentTime || 0 : 0);
    return E;
  }
  const engine = window.__ftMusic || (window.__ftMusic = createEngine());

  function useEngine() {
    const [, bump] = React.useReducer((n) => (n + 1) % 1e9, 0);
    React.useEffect(() => engine.subscribe(bump), []);
    return engine;
  }

  // ── icons (stroke = currentColor) ───────────────────────────────────────
  const I = {
    play: <svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true"><path d="M8 5.5v13a1 1 0 0 0 1.52.85l10.4-6.5a1 1 0 0 0 0-1.7L9.52 4.65A1 1 0 0 0 8 5.5z" fill="currentColor"/></svg>,
    pause: <svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true"><rect x="6.5" y="5" width="4" height="14" rx="1.2" fill="currentColor"/><rect x="13.5" y="5" width="4" height="14" rx="1.2" fill="currentColor"/></svg>,
    prev: <svg viewBox="0 0 24 24" width="15" height="15" aria-hidden="true"><path d="M17.5 6.2v11.6a.8.8 0 0 1-1.23.67L8 13.2a1.4 1.4 0 0 1 0-2.4l8.27-5.27a.8.8 0 0 1 1.23.67z" fill="currentColor"/><rect x="5.5" y="5.5" width="2.2" height="13" rx="1" fill="currentColor"/></svg>,
    next: <svg viewBox="0 0 24 24" width="15" height="15" aria-hidden="true"><path d="M6.5 6.2v11.6a.8.8 0 0 0 1.23.67L16 13.2a1.4 1.4 0 0 0 0-2.4L7.73 5.53A.8.8 0 0 0 6.5 6.2z" fill="currentColor"/><rect x="16.3" y="5.5" width="2.2" height="13" rx="1" fill="currentColor"/></svg>,
    caret: <svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="M6 9l6 6 6-6"/></svg>,
    vol: <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="M4 9.5h3.2L12 5.5v13l-4.8-4H4z" fill="currentColor" stroke="none"/><path d="M15.5 9a4 4 0 0 1 0 6"/><path d="M18 6.5a7.5 7.5 0 0 1 0 11"/></svg>,
    mute: <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="M4 9.5h3.2L12 5.5v13l-4.8-4H4z" fill="currentColor" stroke="none"/><path d="M16 9.5l5 5M21 9.5l-5 5"/></svg>,
    check: <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg>,
    note: <svg viewBox="0 0 24 24" width="13" height="13" aria-hidden="true"><path d="M9 17.5V6.8l10-2.3v10.7" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/><circle cx="6.8" cy="17.6" r="2.4" fill="currentColor"/><circle cx="16.8" cy="15.3" r="2.4" fill="currentColor"/></svg>,
  };

  // ── the live wave: bars driven by rAF straight on the DOM (no React render per frame) ──
  function Wave() {
    const ref = React.useRef(null);
    React.useEffect(() => {
      const bars = ref.current ? Array.from(ref.current.children) : [];
      const levels = new Float32Array(BAR_COUNT);
      const shown = new Float32Array(BAR_COUNT);
      let raf = 0;
      const IDLE = 0.16;
      const frame = () => {
        raf = 0;
        engine.levels(BAR_COUNT, levels);
        let moving = engine.playing;
        for (let i = 0; i < bars.length; i++) {
          const target = engine.playing ? IDLE + (1 - IDLE) * levels[i] : IDLE;
          // quick rise, soft fall
          shown[i] += (target - shown[i]) * (target > shown[i] ? 0.55 : 0.18);
          if (Math.abs(target - shown[i]) > 0.004) moving = true;
          bars[i].style.transform = "scaleY(" + shown[i].toFixed(3) + ")";
        }
        if (moving) raf = requestAnimationFrame(frame);
      };
      const kick = () => { if (!raf) raf = requestAnimationFrame(frame); };
      for (let i = 0; i < bars.length; i++) { shown[i] = IDLE; bars[i].style.transform = "scaleY(" + IDLE + ")"; }
      const off = engine.subscribe(kick);
      kick();
      return () => { off(); if (raf) cancelAnimationFrame(raf); };
    }, []);
    return (
      <span className={"hmp-wave" + (engine.playing ? " is-live" : "")} ref={ref} aria-hidden="true">
        {Array.from({ length: BAR_COUNT }, (_, i) => <i key={i} className="hmp-wave-bar"/>)}
      </span>
    );
  }

  // ── a thin bar you can click or drag (seek and volume share it) ─────────
  function Slider({ value, onChange, label, valueText, live, className = "" }) {
    const ref = React.useRef(null);
    const fillRef = React.useRef(null);
    const dragging = React.useRef(false);
    const at = (e) => { const b = ref.current.getBoundingClientRect(); return clamp01((e.clientX - b.left) / (b.width || 1)); };
    // the seek bar follows the song every frame while playing
    React.useEffect(() => {
      if (!live) return undefined;
      let raf = 0;
      const tick = () => {
        raf = 0;
        if (!dragging.current && fillRef.current) {
          const d = engine.duration || 0;
          fillRef.current.style.width = (d ? clamp01(engine.currentTime() / d) * 100 : 0) + "%";
        }
        if (engine.playing) raf = requestAnimationFrame(tick);
      };
      const kick = () => { if (!raf) raf = requestAnimationFrame(tick); };
      const off = engine.subscribe(kick);
      kick();
      return () => { off(); if (raf) cancelAnimationFrame(raf); };
    }, [live]);
    const onDown = (e) => {
      if (e.button !== undefined && e.button !== 0) return;
      dragging.current = true;
      try { ref.current.setPointerCapture(e.pointerId); } catch (_) {}
      const v = at(e);
      if (fillRef.current) fillRef.current.style.width = v * 100 + "%";
      if (!live) onChange(v);
    };
    const onMove = (e) => {
      if (!dragging.current) return;
      const v = at(e);
      if (fillRef.current) fillRef.current.style.width = v * 100 + "%";
      if (!live) onChange(v);
    };
    const onUp = (e) => {
      if (!dragging.current) return;
      dragging.current = false;
      try { ref.current.releasePointerCapture(e.pointerId); } catch (_) {}
      onChange(at(e));
    };
    const onKey = (e) => {
      const step = e.shiftKey ? 0.1 : 0.03;
      if (e.key === "ArrowRight" || e.key === "ArrowUp") { e.preventDefault(); onChange(clamp01(value + step)); }
      else if (e.key === "ArrowLeft" || e.key === "ArrowDown") { e.preventDefault(); onChange(clamp01(value - step)); }
      else if (e.key === "Home") { e.preventDefault(); onChange(0); }
      else if (e.key === "End") { e.preventDefault(); onChange(1); }
    };
    return (
      <span className={"hmp-slider " + className} ref={ref} role="slider" tabIndex={0}
        aria-label={label} aria-valuemin={0} aria-valuemax={100} aria-valuenow={Math.round(value * 100)} aria-valuetext={valueText}
        onPointerDown={onDown} onPointerMove={onMove} onPointerUp={onUp} onPointerCancel={onUp} onKeyDown={onKey}>
        <span className="hmp-slider-track"><span className="hmp-slider-fill" ref={fillRef} style={{ width: clamp01(value) * 100 + "%" }}/></span>
      </span>
    );
  }

  // a dropdown that closes on an outside click or Escape
  function useDismiss(ref, open, close) {
    React.useEffect(() => {
      if (!open) return undefined;
      const down = (e) => { if (ref.current && !ref.current.contains(e.target)) close(); };
      const key = (e) => { if (e.key === "Escape") close(); };
      document.addEventListener("mousedown", down);
      document.addEventListener("keydown", key);
      return () => { document.removeEventListener("mousedown", down); document.removeEventListener("keydown", key); };
    }, [open]);
  }

  // Where the player may sit: the status-pill line, from just after its last pill to the right
  // edge of the round header buttons. Read from the boxes, never assumed.
  const MAX_W = 780, H = 44, GAP = 32;
  // widest layout first; each step drops one part so nothing ever wraps or crops
  const TIERS = [[680, 5], [570, 4], [470, 3], [330, 2], [200, 1]];
  function useSlot(anchorRef, active) {
    const [slot, setSlot] = React.useState(null);
    React.useLayoutEffect(() => {
      if (!active) return undefined;
      const anchor = anchorRef.current;
      const header = anchor && anchor.closest(".project-header");
      if (!header) return undefined;
      let raf = 0;
      const measure = () => {
        raf = 0;
        const meta = header.querySelector(".ph-meta");
        const right = header.querySelector(".ph-right");
        if (!meta || !right) { setSlot(null); return; }
        const hb = header.getBoundingClientRect();
        const mb = meta.getBoundingClientRect();
        const rb = right.getBoundingClientRect();
        if (!mb.height || !rb.width) { setSlot(null); return; }
        let contentRight = mb.left;
        for (const k of meta.children) {
          const b = k.getBoundingClientRect();
          if (b.width > 0) contentRight = Math.max(contentRight, b.right);
        }
        // the app's size setting (the header's "85%" button) zooms the page: the boxes above are in
        // screen pixels, the style below is in the header's own pixels
        const z = header.offsetWidth ? hb.width / header.offsetWidth : 1;
        const k = z > 0.2 && z < 5 ? z : 1;
        const avail = Math.floor((rb.right - (contentRight + GAP)) / k);
        const width = Math.min(MAX_W, avail);
        const tier = (TIERS.find(([w]) => width >= w) || [0, 0])[1];
        const next = {
          tier, width,
          right: Math.round((hb.right - rb.right) / k),
          top: Math.round((mb.top - hb.top + mb.height / 2) / k - H / 2),
        };
        setSlot((prev) => (prev && prev.tier === next.tier && prev.width === next.width && prev.right === next.right && prev.top === next.top ? prev : next));
      };
      const kick = () => { if (!raf) raf = requestAnimationFrame(measure); };
      measure();
      const ro = typeof ResizeObserver !== "undefined" ? new ResizeObserver(kick) : null;
      if (ro) { ro.observe(header); const m = header.querySelector(".ph-meta"); if (m) ro.observe(m); }
      const mo = new MutationObserver(kick);
      mo.observe(header, { childList: true, subtree: true, characterData: true, attributes: true, attributeFilter: ["class", "style"] });
      header.addEventListener("transitionend", kick);
      window.addEventListener("resize", kick);
      return () => {
        if (raf) cancelAnimationFrame(raf);
        if (ro) ro.disconnect();
        mo.disconnect();
        header.removeEventListener("transitionend", kick);
        window.removeEventListener("resize", kick);
      };
    }, [active]);
    return slot;
  }

  function MusicPlayer({ projectId, activeGroupId = null, hidden = false }) {
    const E = useEngine();
    const anchorRef = React.useRef(null);
    const [lib, setLib] = React.useState(null);        // { projectId, enabled, local, songs_folder }
    // 24 Sep 2026 — the music keeps playing across a project switch (Hugo). A song that was playing
    // when the header switched project is CARRIED: the player stays on screen with that project's
    // album until the album ends (or it is paused and another project is opened). The new
    // project's own list waits in `pending` and takes over then. A carried song streams from its
    // own project (the link is signed for it), so nothing else changes.
    const libs = React.useRef({});                      // project id → its last lib, for a carried player
    const pending = React.useRef(null);                 // { projectId, lib, albums } of the open project
    const carried = !!(E.projectId && E.projectId !== projectId && E.el && !E.albumDone);
    const shownLib = carried ? (libs.current[E.projectId] || { enabled: true, local: true }) : lib;
    const on = carried || !!(lib && lib.enabled && lib.projectId === projectId);
    const slot = useSlot(anchorRef, on);
    const [menu, setMenu] = React.useState(null);      // "album" | "track" | null
    const menuRef = React.useRef(null);
    useDismiss(anchorRef, !!menu, () => setMenu(null));
    const followed = React.useRef(null);
    // compact at rest: hot = the pointer is over the bar (let go 300 ms after it leaves)
    const [hot, setHot] = React.useState(false);
    const [focusIn, setFocusIn] = React.useState(false);
    const leaveT = React.useRef(0);
    const trackRef = React.useRef(null);
    const [pillW, setPillW] = React.useState(0);
    React.useEffect(() => () => clearTimeout(leaveT.current), []);
    React.useLayoutEffect(() => {
      const el = trackRef.current;
      if (!on || !el) return undefined;
      const m = () => { const w = el.offsetWidth; setPillW((p) => (p === w ? p : w)); };
      m();
      const ro = typeof ResizeObserver !== "undefined" ? new ResizeObserver(m) : null;
      if (ro) ro.observe(el);
      return () => { if (ro) ro.disconnect(); };
    }, [on]);
    // NATIVE pointerenter/leave: React's emulated ones lose the leave when the element under the
    // pointer is replaced (the Play icon turns into Pause on the click), and the bar stayed open
    React.useEffect(() => {
      const el = anchorRef.current;
      if (!on || !el) return undefined;
      const enter = () => { clearTimeout(leaveT.current); setHot(true); };
      const leave = () => { clearTimeout(leaveT.current); leaveT.current = setTimeout(() => setHot(false), 300); };
      el.addEventListener("pointerenter", enter);
      el.addEventListener("pointerleave", leave);
      return () => { el.removeEventListener("pointerenter", enter); el.removeEventListener("pointerleave", leave); };
    }, [on]);

    const load = React.useCallback((pid) => {
      if (!pid || !window.authFetch) return;
      window.authFetch("/api/music/library", { headers: { "X-Active-Project": pid } })
        .then((r) => (r.ok ? r.json() : null))
        .then((j) => {
          if (window.__activeProjectId && window.__activeProjectId !== pid) return;   // switched meanwhile
          if (!j || !j.enabled) {
            pending.current = { projectId: pid, lib: { projectId: pid, enabled: false }, albums: null };
            setLib({ projectId: pid, enabled: false });
            if (engine.projectId === pid) engine.stop();
            return;
          }
          const next = { projectId: pid, enabled: true, local: !!j.local, songs_folder: j.songs_folder || "" };
          libs.current[pid] = next;
          // another project's song is playing: this list waits until that album is done
          if (engine.projectId && engine.projectId !== pid && engine.el && !engine.albumDone) {
            pending.current = { projectId: pid, lib: next, albums: j.albums || [] };
            setLib(next);
            return;
          }
          pending.current = null;
          engine.setLibrary(pid, j.albums || []);
          setLib(next);
        })
        .catch(() => {});
    }, []);

    // a new project: a song that is PLAYING goes on (carried); anything else of the old project
    // stops. Then the new project's list is loaded.
    React.useEffect(() => {
      if (engine.projectId && engine.projectId !== projectId && !engine.playing) engine.stop();
      pending.current = null;
      followed.current = null;
      setMenu(null);
      setLib(null);
      load(projectId);
    }, [projectId]);

    // a carried album that has finished (the engine stops on track 1 at the end): the open
    // project's own list takes over, or the player goes away when it has none
    React.useEffect(() => {
      if (!engine.projectId || engine.projectId === projectId || carried) return;
      const p = pending.current;
      engine.stop();
      if (p && p.projectId === projectId && p.albums) { engine.setLibrary(projectId, p.albums); pending.current = null; followed.current = null; setLib({ ...p.lib }); }
    });

    // follow the header's album while nothing is playing
    React.useEffect(() => {
      if (!lib || !lib.enabled || lib.projectId !== projectId || engine.projectId !== projectId) return;
      const key = projectId + "|" + (activeGroupId || "");
      if (followed.current === key && engine.album()) return;
      followed.current = key;
      // an album switch while a song plays does not interrupt it (and pausing later does not jump)
      if (engine.playing && engine.album()) return;
      const albums = engine.albums;
      const target = albums.find((a) => a.id === activeGroupId)
        || albums.find((a) => a.tracks.some((t) => t.playable)) || albums[0];
      if (target && (engine.albumId !== target.id || !engine.track())) engine.selectAlbum(target.id, false);
    }, [lib, activeGroupId, projectId]);

    if (!on) return null;

    const albums = E.albums;
    const album = E.album();
    const tracks = album ? album.tracks : [];
    const track = E.track();
    const playableCount = tracks.filter((t) => t.playable).length;
    const canPlay = playableCount > 0;
    const tier = slot ? slot.tier : 0;
    // 24 Sep 2026 — the album's own track number when the song has one ("01. Late" = 01), so the
    // list reads like the Drive folder; otherwise the place in the list
    const num = (i) => {
      const t = tracks[i];
      return String(t && Number.isFinite(t.track_no) && t.track_no > 0 ? t.track_no : i + 1).padStart(2, "0");
    };
    const trackLabel = !album ? "No songs yet"
      : !tracks.length ? "No songs yet"
      : !canPlay ? "No song files yet"
      : (track ? track.title : tracks[0].title);
    const openMenu = (k) => {
      setMenu((m) => (m === k ? null : k));
      if (menu !== k) load(projectId);                 // a song dropped in a moment ago shows up
    };
    const folderHint = shownLib && shownLib.local
      ? "Add the Google Drive song folders in Settings (Song folders), or put the files in " + (shownLib.songs_folder || "library/songs") + "/<album>/<song>/."
      : "Songs play here once they are uploaded.";
    const show = !!slot && tier > 0 && !hidden;
    const open = hot || focusIn || !!menu;
    const compact = !open && pillW > 0;
    // the pill + the bar's padding: 14 px on the right (the row's own), 8 px on the left
    const style = slot ? { top: slot.top + "px", right: slot.right + "px", width: slot.width + "px",
      "--mp-full-w": slot.width + "px", "--mp-rest-w": (pillW + 22) + "px" } : undefined;

    return (
      <div ref={anchorRef}
        className={"hmp-player hmp-t" + tier + (E.playing ? " is-playing" : "") + (menu ? " is-menu-open" : "") + (canPlay ? "" : " is-empty") + (show ? "" : " is-hidden") + (compact ? " is-compact" : "")}
        style={style} role="region" aria-label="Music player" aria-hidden={show ? undefined : true}
        onFocus={(e) => { let v = false; try { v = e.target.matches(":focus-visible"); } catch (_) {} if (v) setFocusIn(true); }}
        onBlur={(e) => { if (!e.currentTarget.contains(e.relatedTarget)) setFocusIn(false); }}>
        <div className="hmp-bar">
         <div className="hmp-inner">
          <div className="hmp-transport">
            <button type="button" className="hmp-btn hmp-prev" aria-label="Previous song" onClick={() => E.prev()} disabled={!canPlay}>{I.prev}</button>
            <button type="button" className="ph-iconbtn hmp-play" aria-label={E.playing ? "Pause" : "Play"} onClick={() => E.toggle()} disabled={!canPlay}>
              {E.playing ? I.pause : I.play}
            </button>
            <button type="button" className="hmp-btn hmp-next" aria-label="Next song" onClick={() => E.next(false)} disabled={!canPlay}>{I.next}</button>
          </div>

          <Wave/>

          <div className="hmp-pickers">
            <button type="button" className={"hmp-chip hmp-album" + (menu === "album" ? " is-open" : "")}
              aria-haspopup="listbox" aria-expanded={menu === "album"} aria-label={"Album: " + (album ? album.title : "none") + ". Choose an album"}
              onClick={() => openMenu("album")}>
              <span className="hmp-chip-eyebrow">Album</span>
              <span className="hmp-chip-text">{album ? album.title : "None"}</span>
              <span className="hmp-caret">{I.caret}</span>
            </button>
            <button type="button" ref={trackRef} className={"hmp-chip hmp-track" + (menu === "track" ? " is-open" : "")}
              aria-haspopup="listbox" aria-expanded={menu === "track"} aria-label={"Song: " + trackLabel + ". Choose a song"}
              onClick={() => openMenu("track")}>
              {E.playing && <span className="hmp-row-live is-playing hmp-pill-live" aria-hidden="true"><i/><i/><i/></span>}
              {canPlay && track && <span className="hmp-track-num">{num(E.index)}</span>}
              <span className="hmp-chip-text">{E.error || trackLabel}</span>
              {canPlay && track && track.quality && !E.error && <span className={"hmp-q hmp-q--" + track.quality}>{track.quality === "master" ? "Master" : "Demo"}</span>}
              <span className="hmp-caret">{I.caret}</span>
            </button>
          </div>

          <div className="hmp-progress">
            <span className="hmp-time">{fmt(E.time)}</span>
            <Slider className="hmp-seek" live value={E.duration ? E.time / E.duration : 0}
              onChange={(v) => E.seek(v)} label="Song position" valueText={fmt(E.time) + " of " + fmt(E.duration)}/>
            <span className="hmp-time hmp-time--total">{fmt(E.duration)}</span>
          </div>

          <div className="hmp-volume">
            <button type="button" className="hmp-btn hmp-mute" aria-label={E.muted || E.volume === 0 ? "Sound on" : "Sound off"} onClick={() => E.toggleMute()}>
              {E.muted || E.volume === 0 ? I.mute : I.vol}
            </button>
            <Slider className="hmp-vol" value={E.muted ? 0 : E.volume} onChange={(v) => E.setVolume(v)}
              label="Volume" valueText={Math.round((E.muted ? 0 : E.volume) * 100) + " percent"}/>
          </div>
         </div>
        </div>

        {/* the menus sit outside the bar (the bar clips its row while it grows) */}
        <div className="hmp-menus" ref={menuRef}>

            {menu === "album" && (
              <div className="ph-episode-menu hmp-menu" role="listbox" aria-label="Albums">
                <div className="ph-menu-eyebrow">ALBUMS</div>
                {albums.map((a) => {
                  const n = a.tracks.filter((t) => t.playable).length;
                  return (
                    <button type="button" key={a.id} role="option" aria-selected={album && a.id === album.id}
                      className={"ph-menu-row ph-episode-row hmp-row" + (album && a.id === album.id ? " is-active" : "")}
                      onClick={() => { setMenu(null); E.selectAlbum(a.id, n > 0); }}>
                      <span className="ph-episode-num hmp-row-icon">{I.note}</span>
                      <span className="ph-menu-row-text">
                        <span className="ph-menu-row-title">{a.title}</span>
                        <span className="ph-menu-row-sub">{a.tracks.length ? (n + " of " + a.tracks.length + " songs ready") : "No songs yet"}</span>
                      </span>
                      {album && a.id === album.id ? <span className="ph-episode-check">{I.check}</span> : <span/>}
                    </button>
                  );
                })}
              </div>
            )}

            {menu === "track" && (
              <div className="ph-episode-menu hmp-menu" role="listbox" aria-label="Songs">
                {tier <= 3 && albums.length > 1 && (
                  <div className="hmp-menu-albums">
                    {albums.map((a) => (
                      <button type="button" key={a.id} className={"hmp-menu-album" + (album && a.id === album.id ? " is-active" : "")}
                        onClick={() => E.selectAlbum(a.id, false)}>{a.title}</button>
                    ))}
                  </div>
                )}
                <div className="ph-menu-eyebrow">{album ? album.title.toUpperCase() : "SONGS"}</div>
                <div className="hmp-menu-list">
                  {tracks.map((t, i) => (
                    <button type="button" key={t.id} role="option" aria-selected={i === E.index} disabled={!t.playable}
                      className={"ph-menu-row ph-episode-row hmp-row" + (i === E.index && canPlay ? " is-active" : "") + (t.playable ? "" : " is-missing")}
                      onClick={() => { setMenu(null); E.playIndex(i); }}>
                      <span className="ph-episode-num">{num(i)}</span>
                      <span className="ph-menu-row-text">
                        {/* 24 Sep 2026 — Master = the mastered WAV, Demo = the unmastered MP3 (the Drive mirror) */}
                        <span className="ph-menu-row-title hmp-row-title">
                          <span className="hmp-row-name">{t.title}</span>
                          {t.playable && t.quality && <span className={"hmp-q hmp-q--" + t.quality}>{t.quality === "master" ? "Master" : "Demo"}</span>}
                        </span>
                        {!t.playable && <span className="ph-menu-row-sub">No song file yet</span>}
                      </span>
                      {i === E.index && canPlay
                        ? <span className={"hmp-row-live" + (E.playing ? " is-playing" : "")} aria-hidden="true"><i/><i/><i/></span>
                        : <span/>}
                    </button>
                  ))}
                  {!tracks.length && <div className="hmp-menu-empty">This album has no songs yet.</div>}
                </div>
                {playableCount < tracks.length || !tracks.length ? <div className="hmp-menu-hint">{folderHint}</div> : null}
              </div>
            )}
        </div>
      </div>
    );
  }

  window.MusicPlayer = MusicPlayer;
})();
