/* ============================================================
   Media — reusable media library (Assets ↔ Cloudflare R2) + the
   type-specific readers/editors for the v16 section types:
     · IMAGE_GALLERY    → ImageGalleryReader / ImageGalleryEditor
     · DOCUMENT_LIBRARY → DocLibraryReader / DocLibraryEditor
     · AUDIO_LIBRARY    → AudioAlbumReader / AudioAlbumEditor
   Plus MediaPicker — browse the library (by folder / locale / search)
   and link an existing asset, or upload a new one to a chosen R2 path.
   ============================================================ */
const { useState: uSmd, useEffect: uEmd, useRef: uRmd, useMemo: uMmd } = React;

/* shared globals (defined by store.jsx / ui.jsx / admin.jsx, loaded first) */
const _Icon = (p) => window.Icon(p);
const M_Placeholder = (p) => window.Placeholder(p);

/* ---- read a File to a data URL ---- */
function fileToDataURL(file, cb) {
  if (!file) return;
  const r = new FileReader();
  r.onload = () => cb(r.result, file);
  r.readAsDataURL(file);
}

/* small type → icon/label helper for the library */
const MEDIA_TYPES = [
  { id: "image", label: "Images", icon: "compass" },
  { id: "audio", label: "Audio", icon: "headphones" },
  { id: "video", label: "Video", icon: "play" },
  { id: "document", label: "Documents", icon: "doc" },
];
function mtypeMeta(t) { return MEDIA_TYPES.find(m => m.id === t) || MEDIA_TYPES[0]; }

/* ============================================================
   MediaPicker — pick an existing asset from the R2 library, or
   upload a new file into a folder. Mirrors GET /media (filters:
   type, locale, media_key) + POST /media/upload.
   props: { type, defaultFolder, locale, title, onPick(assetId), onClose }
   ============================================================ */
const MEDIA_EXT = { image: ".webp", audio: ".mp3", video: ".mp4", document: ".pdf" };
const MEDIA_ACCEPT = { image: "image/*", audio: "audio/*", video: "video/*", document: "application/pdf" };
/* does a file's MIME type match the chosen business media_type? */
function mimeMatchesType(mime, type) {
  if (!mime) return true;                       // browser couldn't tell — let it through
  if (type === "document") return /pdf/i.test(mime);
  return mime.indexOf(type + "/") === 0;        // image/*, audio/*, video/*
}

/* ---- prepare the terrain for the real API ----
   POST /media/upload (ADMIN/EDITOR) inserts an Asset holding the RELATIVE R2 path
   and returns its asset_id; content_media then references it. Required:
   media_key (folder) + media_type; optional: locale. Here we write to the in-memory
   store — swap the body for `fetch(window.__VITRINE_API__ + '/media/upload', …)`. */
function mediaApiUpload(update, { media_type, media_key_folder, file_name, locale, url, label }) {
  const folder = (media_key_folder || "uploads").trim();
  const base = (file_name || (label || "asset").toLowerCase().replace(/[^a-z0-9]+/g, "-")).replace(/\.[^.]+$/, "");
  const media_key = `media/${media_type}/${folder}/${base}${MEDIA_EXT[media_type] || ""}`.replace(/\/+/g, "/");
  const asset_id = window.uid("md");            // asset_id stores the relative R2 path (media_key)
  update(d => { (d.media = d.media || []).unshift({ id: asset_id, key: media_key, folder, type: media_type, locale: locale || "", label: label || base, url: url || "", mime: "" }); });
  return asset_id;
}

function MediaPicker({ type = "audio", defaultFolder = "all", locale = "all", title, onPick, onClose }) {
  const { db, update } = window.useStore();
  const Modal = window.Modal;
  const [mtype, setMtype] = uSmd(type);          // browse media-type filter (auto-selected from context)
  const [folder, setFolder] = uSmd(defaultFolder || "all");
  const [loc, setLoc] = uSmd(locale || "all");
  const [q, setQ] = uSmd("");
  const [tab, setTab] = uSmd("browse"); // browse | upload
  // upload form — media_type is auto-selected (locked) to the calling context
  const [up, setUp] = uSmd({ label: "", folder: defaultFolder && defaultFolder !== "all" ? defaultFolder : "", locale: locale && locale !== "all" ? locale : "", url: "", key: "", err: "" });
  const folders = window.mediaFolders(db, mtype);
  const results = window.mediaInFolder(db, { type: mtype, folder, locale: loc, q });
  const mt = mtypeMeta(type);

  const onFile = (file) => {
    if (!file) return;
    if (!mimeMatchesType(file.type, type)) {
      setUp(p => ({ ...p, err: `“${file.type || "this file"}” doesn't match media type ${mtypeMeta(type).label} — choose a ${mtypeMeta(type).label.toLowerCase()} file.`, url: "" }));
      return;
    }
    fileToDataURL(file, (data, f) => setUp(p => ({ ...p, err: "", url: data, label: p.label || f.name, key: p.key || f.name.replace(/\.[^.]+$/, "") })));
  };
  const doUpload = () => {
    const id = mediaApiUpload(update, { media_type: type, media_key_folder: up.folder, file_name: up.key, locale: up.locale, url: up.url, label: up.label });
    onPick(id); onClose();
  };

  return (
    <Modal title={title || "Search media"} onClose={onClose} size="lg"
      footer={<button className="btn ghost" onClick={onClose}>Close</button>}>
      <div className="mp-tabs">
        <button className={"chip" + (tab === "browse" ? " on" : "")} onClick={() => setTab("browse")}><_Icon name="search" size={13} /> Browse library</button>
        <button className={"chip" + (tab === "upload" ? " on" : "")} onClick={() => setTab("upload")}><_Icon name="plus" size={13} /> Upload new</button>
        <span className="grow" />
        <span className="dim mono" style={{ fontSize: 11 }}>R2 · GET /media</span>
      </div>

      {tab === "browse" ? (
        <>
          <div className="mp-filters">
            <div className="row gap-8 wrap" style={{ alignItems: "center" }}>
              <span className="dim mono" style={{ fontSize: 11 }}>media type:</span>
              {MEDIA_TYPES.map(t => <button key={t.id} className={"chip" + (mtype === t.id ? " on" : "")} onClick={() => { setMtype(t.id); setFolder("all"); }}><_Icon name={t.icon} size={12} style={{ verticalAlign: "-2px", marginRight: 3 }} />{t.label}</button>)}
            </div>
            <div className="row gap-8 wrap" style={{ alignItems: "center", marginTop: 8 }}>
              <span className="dim mono" style={{ fontSize: 11 }}>media key folder:</span>
              <button className={"chip" + (folder === "all" ? " on" : "")} onClick={() => setFolder("all")}>all</button>
              {folders.map(f => <button key={f} className={"chip" + (folder === f ? " on" : "")} onClick={() => setFolder(f)}>{f}</button>)}
            </div>
            <div className="row gap-8" style={{ alignItems: "center", marginTop: 8 }}>
              <span className="dim mono" style={{ fontSize: 11 }}>locale:</span>
              {["all", "en", "fr"].map(l => <button key={l} className={"chip" + (loc === l ? " on" : "")} onClick={() => setLoc(l)}>{l.toUpperCase()}</button>)}
              <span className="grow" />
              <div style={{ position: "relative" }}>
                <span style={{ position: "absolute", left: 9, top: 8, color: "var(--muted)" }}><_Icon name="search" size={15} /></span>
                <input value={q} onChange={e => setQ(e.target.value)} placeholder="Search key / label…" style={{ paddingLeft: 30, width: 200 }} />
              </div>
            </div>
          </div>
          <div className="mp-grid">
            {results.map(m => (
              <button key={m.id} className="mp-asset" onClick={() => { onPick(m.id); onClose(); }}>
                <span className="mp-asset-ic"><_Icon name={mtypeMeta(m.type).icon} size={18} /></span>
                <span className="mp-asset-body">
                  <span className="mp-asset-label">{m.label}</span>
                  <span className="mp-asset-key mono">{m.key}</span>
                </span>
                {m.locale && <span className="mp-asset-loc">{m.locale.toUpperCase()}</span>}
                {m.url ? <span className="mp-asset-flag ok">file</span> : <span className="mp-asset-flag">stub</span>}
              </button>
            ))}
            {results.length === 0 && <div className="dim mono" style={{ fontSize: 12, padding: 20, textAlign: "center" }}>No matching asset — try “Upload new”.</div>}
          </div>
        </>
      ) : (
        <div className="col gap-10">
          <p className="dim" style={{ fontSize: 13, marginTop: -2 }}>Upload a file to the library — it becomes reusable across articles, languages and domains without duplication. <b>Media type</b> is set from where you opened this.</p>
          <div className="field-row">
            <div><label>Label</label><input value={up.label} onChange={e => setUp(p => ({ ...p, label: e.target.value }))} placeholder="Toolkit blog — my voice (EN)" /></div>
            <div><label>Media type <span className="dim mono" style={{ fontWeight: 400, fontSize: 11 }}>auto</span></label><div className="mp-typelock"><_Icon name={mt.icon} size={14} /> {mt.label}</div></div>
          </div>
          <div className="field-row">
            <div><label>Media Key Folder <span className="dim mono" style={{ fontWeight: 400, fontSize: 11 }}>R2 prefix</span></label><input value={up.folder} onChange={e => setUp(p => ({ ...p, folder: e.target.value }))} placeholder="readloud-blog" /></div>
            <div><label>File name <span className="dim mono" style={{ fontWeight: 400, fontSize: 11 }}>optional</span></label><input className="mono" value={up.key} onChange={e => setUp(p => ({ ...p, key: e.target.value }))} placeholder="toolkitblog_voice_version1_en" /></div>
          </div>
          <div className="field-row">
            <div><label>Locale <span className="dim mono" style={{ fontWeight: 400, fontSize: 11 }}>optional</span></label><select value={up.locale} onChange={e => setUp(p => ({ ...p, locale: e.target.value }))}><option value="">— none —</option><option value="en">EN</option><option value="fr">FR</option></select></div>
            <div>
              <label>File <span className="dim mono" style={{ fontWeight: 400, fontSize: 11 }}>{MEDIA_ACCEPT[type]}</span></label>
              <input type="file" accept={MEDIA_ACCEPT[type]} onChange={e => onFile(e.target.files && e.target.files[0])} />
            </div>
          </div>
          {up.err && <div className="mp-err"><_Icon name="bell" size={13} /> {up.err}</div>}
          {up.url && !up.err && <span className="leb-ok"><_Icon name="check" size={13} /> file loaded</span>}
          <div className="src-note" style={{ marginTop: 2 }}>
            <div className="mono dim" style={{ fontSize: 11, marginBottom: 3 }}>R2 KEY (generated) · asset_id stores this relative path</div>
            <div className="mono" style={{ fontSize: 12.5, color: "var(--accent-ink)", wordBreak: "break-all" }}>media/{type}/{(up.folder || "uploads")}/{(up.key || "asset") + (MEDIA_EXT[type] || "")}</div>
          </div>
          <div className="row" style={{ justifyContent: "flex-end" }}>
            <button className="btn accent" disabled={!up.label.trim() || !!up.err} style={{ opacity: (up.label.trim() && !up.err) ? 1 : .5 }} onClick={doUpload}><_Icon name="save" size={15} /> Add to library &amp; link</button>
          </div>
        </div>
      )}
    </Modal>
  );
}

/* small inline display of a linked asset (used by editors) */
function AssetRef({ assetId, onClear, onChange }) {
  const { db } = window.useStore();
  const m = window.assetById(db, assetId);
  if (!m) return <span className="dim mono" style={{ fontSize: 12 }}>no asset linked</span>;
  return (
    <span className="asset-ref">
      <span className="ar-ic"><_Icon name={mtypeMeta(m.type).icon} size={14} /></span>
      <span className="ar-body">
        <span className="ar-label">{m.label}{m.locale ? ` · ${m.locale.toUpperCase()}` : ""}</span>
        <span className="ar-key mono">{m.key}</span>
      </span>
      {onChange && <button className="iconbtn sm" title="change" onClick={onChange}><_Icon name="edit" size={13} /></button>}
      {onClear && <button className="iconbtn sm danger" title="unlink" onClick={onClear}><_Icon name="close" size={13} /></button>}
    </span>
  );
}

/* ============================================================
   IMAGE_GALLERY
   ============================================================ */
function galleryCoverIndex(list) { const i = (list || []).findIndex(g => g.cover); return i >= 0 ? i : 0; }
function galleryImgSrc(db, g) { if (!g) return ""; if (g.src) return g.src; const m = window.assetById(db, g.assetId); return m ? window.assetSrc(m) : ""; }

function ImageGalleryReader({ item }) {
  const { db } = window.useStore();
  const list = item.gallery || [];
  const [i, setI] = uSmd(() => galleryCoverIndex(list));
  const [paused, setPaused] = uSmd(false);
  const auto = item.galleryMode === "auto";
  const interval = (item.galleryInterval === 10 ? 10 : 5) * 1000;
  uEmd(() => { if (i >= list.length) setI(0); }, [list.length]);
  uEmd(() => {
    if (!auto || paused || list.length < 2) return;
    const id = setInterval(() => setI(x => (x + 1) % list.length), interval);
    return () => clearInterval(id);
  }, [auto, paused, list.length, interval]);
  if (!list.length) return <M_Placeholder label="image gallery" tag="add images in edit mode" style={{ width: "100%", aspectRatio: "16/10" }} />;
  const cur = list[Math.min(i, list.length - 1)];
  const src = galleryImgSrc(db, cur);
  const go = (d) => setI(x => (x + d + list.length) % list.length);
  return (
    <div className="gallery" onMouseEnter={() => setPaused(true)} onMouseLeave={() => setPaused(false)}>
      <div className="gallery-stage">
        {src ? <img src={src} alt={cur.caption || ""} className="gallery-img" />
             : <M_Placeholder label={cur.caption || "image"} tag={`frame ${i + 1}`} style={{ width: "100%", height: "100%" }} />}
        {list.length > 1 && <>
          <button className="gallery-nav prev" onClick={() => go(-1)} aria-label="previous"><_Icon name="left" size={22} /></button>
          <button className="gallery-nav next" onClick={() => go(1)} aria-label="next"><_Icon name="right" size={22} /></button>
        </>}
        {auto && list.length > 1 && <span className="gallery-auto"><_Icon name={paused ? "play" : "globe"} size={12} /> {paused ? "paused" : `auto · ${interval / 1000}s`}</span>}
      </div>
      <div className="gallery-foot">
        <span className="gallery-cap">{cur.caption || <span className="dim">—</span>}</span>
        <span className="gallery-count mono">{i + 1} / {list.length}</span>
      </div>
      {list.length > 1 && (
        <div className="gallery-dots">
          {list.map((g, gi) => <button key={g.id || gi} className={"gdot" + (gi === i ? " on" : "")} onClick={() => setI(gi)} aria-label={`go to ${gi + 1}`} />)}
        </div>
      )}
    </div>
  );
}

function ImageGalleryEditor({ item, onPatch }) {
  const { db } = window.useStore();
  const ImageUpload = window.ImageUpload, DragList = window.DragList;
  const list = item.gallery || [];
  const [pick, setPick] = uSmd(null); // index awaiting a media-library pick
  const set = (idx, patch) => onPatch(c => { c.gallery = (c.gallery || []).map((g, j) => j === idx ? { ...g, ...patch } : g); });
  const add = () => onPatch(c => { c.gallery = [...(c.gallery || []), { id: window.uid("gi"), src: null, caption: "", cover: (c.gallery || []).length === 0 }]; });
  const remove = (idx) => onPatch(c => { c.gallery = (c.gallery || []).filter((_, j) => j !== idx); });
  const setCover = (idx) => onPatch(c => { c.gallery = (c.gallery || []).map((g, j) => ({ ...g, cover: j === idx })); });
  return (
    <div className="col gap-10">
      <div className="row gap-8 wrap" style={{ alignItems: "center" }}>
        <span className="dim mono" style={{ fontSize: 12 }}>Display:</span>
        <button className={"chip" + (item.galleryMode !== "auto" ? " on" : "")} onClick={() => onPatch(c => c.galleryMode = "arrows")}>Cover + arrows</button>
        <button className={"chip" + (item.galleryMode === "auto" ? " on" : "")} onClick={() => onPatch(c => c.galleryMode = "auto")}>Auto-switch</button>
        {item.galleryMode === "auto" && <>
          <span className="dim mono" style={{ fontSize: 12, marginLeft: 6 }}>every</span>
          {[5, 10].map(s => <button key={s} className={"chip" + ((item.galleryInterval || 5) === s ? " on" : "")} onClick={() => onPatch(c => c.galleryInterval = s)}>{s}s</button>)}
        </>}
      </div>
      {list.length === 0 && <span className="dim mono" style={{ fontSize: 12 }}>No images yet.</span>}
      {list.length > 0 && (
        <DragList items={list} onReorder={n => onPatch(c => c.gallery = n)} renderRow={(g, idx) => (
          <div className="gallery-edit-row">
            <div style={{ width: 90, flex: "none" }}>
              <ImageUpload value={galleryImgSrc(db, g)} onChange={v => set(idx, { src: v, assetId: undefined })} label="image" aspect="4/3" />
            </div>
            <div className="col gap-8" style={{ flex: 1, minWidth: 0 }}>
              <input value={g.caption || ""} onChange={e => set(idx, { caption: e.target.value })} placeholder="Caption (optional)" />
              <div className="row gap-8 wrap">
                <button type="button" className={"chip" + (g.cover ? " on" : "")} onClick={() => setCover(idx)}><_Icon name="star" size={12} style={{ verticalAlign: "-2px", marginRight: 3 }} />Cover</button>
                <button type="button" className="chip" onClick={() => setPick(idx)}><_Icon name="compass" size={12} style={{ verticalAlign: "-2px", marginRight: 3 }} />From library</button>
                {g.assetId && <AssetRef assetId={g.assetId} onClear={() => set(idx, { assetId: undefined })} />}
                <span className="grow" />
                <button type="button" className="iconbtn sm danger" onClick={() => remove(idx)}><_Icon name="trash" size={13} /></button>
              </div>
            </div>
          </div>
        )} />
      )}
      <button className="btn ghost sm" style={{ alignSelf: "flex-start" }} onClick={add}><_Icon name="plus" size={14} /> Add image</button>
      {pick != null && <MediaPicker type="image" onPick={(id) => set(pick, { assetId: id, src: null })} onClose={() => setPick(null)} />}
    </div>
  );
}

/* ============================================================
   DOCUMENT_LIBRARY
   ============================================================ */
function docSrc(db, d) { if (!d) return ""; if (d.pdf) return d.pdf; const m = window.assetById(db, d.assetId); return m ? window.assetSrc(m) : ""; }

function DocLibraryReader({ item }) {
  const { db } = window.useStore();
  const Modal = window.Modal;
  const list = item.docs || [];
  const [open, setOpen] = uSmd(null);
  if (!list.length) return <M_Placeholder label="document library" tag="add documents in edit mode" style={{ width: "100%", height: 220 }} />;
  const od = open != null ? list[open] : null;
  const osrc = od ? docSrc(db, od) : "";
  return (
    <div className="doclib">
      {list.map((d, i) => (
        <button key={d.id || i} className="doc-card" onClick={() => setOpen(i)}>
          <span className="doc-card-ic"><_Icon name="doc" size={26} /></span>
          <span className="doc-card-body">
            <span className="doc-card-label">{d.label || `Document ${i + 1}`}</span>
            <span className="doc-card-meta mono">{docSrc(db, d) ? "PDF" : "no file yet"}</span>
          </span>
          <span className="doc-card-open"><_Icon name="ext" size={16} /> Open</span>
        </button>
      ))}
      {od && (
        <Modal title={od.label || "Document"} onClose={() => setOpen(null)} size="lg"
          footer={<button className="btn ghost" onClick={() => setOpen(null)}>Close</button>}>
          {osrc
            ? <iframe className="pdf-frame" src={osrc} title={od.label} style={{ width: "100%", height: "72vh", border: "1.5px solid var(--line)", borderRadius: "var(--r-md)", background: "#fff" }} />
            : <M_Placeholder label="PDF document" tag="upload a PDF in edit mode" style={{ width: "100%", height: 420 }} />}
        </Modal>
      )}
    </div>
  );
}

function DocLibraryEditor({ item, onPatch }) {
  const { db } = window.useStore();
  const DragList = window.DragList;
  const list = item.docs || [];
  const [pick, setPick] = uSmd(null);
  const set = (idx, patch) => onPatch(c => { c.docs = (c.docs || []).map((d, j) => j === idx ? { ...d, ...patch } : d); });
  const add = () => onPatch(c => { c.docs = [...(c.docs || []), { id: window.uid("dc"), label: "", pdf: "" }]; });
  const remove = (idx) => onPatch(c => { c.docs = (c.docs || []).filter((_, j) => j !== idx); });
  return (
    <div className="col gap-10">
      {list.length === 0 && <span className="dim mono" style={{ fontSize: 12 }}>No documents yet — one piece can hold several.</span>}
      {list.length > 0 && (
        <DragList items={list} onReorder={n => onPatch(c => c.docs = n)} renderRow={(d, idx) => (
          <div className="col gap-8" style={{ flex: 1, minWidth: 0 }}>
            <input value={d.label || ""} onChange={e => set(idx, { label: e.target.value })} placeholder="Document title" />
            <div className="row gap-8 wrap" style={{ alignItems: "center" }}>
              <label className="btn ghost sm" style={{ cursor: "pointer" }}>
                <_Icon name="save" size={13} /> Upload PDF
                <input type="file" accept="application/pdf" style={{ display: "none" }} onChange={e => fileToDataURL(e.target.files && e.target.files[0], (data, file) => set(idx, { pdf: data, assetId: undefined, label: d.label || file.name }))} />
              </label>
              <button type="button" className="chip" onClick={() => setPick(idx)}><_Icon name="compass" size={12} style={{ verticalAlign: "-2px", marginRight: 3 }} />From library</button>
              {docSrc(db, d) && <span className="leb-ok"><_Icon name="check" size={13} /> {d.assetId ? "linked" : "PDF loaded"}</span>}
              {d.assetId && <AssetRef assetId={d.assetId} onClear={() => set(idx, { assetId: undefined })} />}
              <span className="grow" />
              <button type="button" className="iconbtn sm danger" onClick={() => remove(idx)}><_Icon name="trash" size={14} /></button>
            </div>
          </div>
        )} />
      )}
      <button className="btn ghost sm" style={{ alignSelf: "flex-start" }} onClick={add}><_Icon name="plus" size={14} /> Add document</button>
      {pick != null && <MediaPicker type="document" onPick={(id) => set(pick, { assetId: id, pdf: "" })} onClose={() => setPick(null)} />}
    </div>
  );
}

/* ============================================================
   AUDIO_LIBRARY — album of sounds
   ============================================================ */
function trackSrc(db, t) { if (!t) return ""; if (t.file) return t.file; const m = window.assetById(db, t.assetId); return m ? window.assetSrc(m) : ""; }

function AudioAlbumReader({ item }) {
  const { db } = window.useStore();
  const list = item.tracks || [];
  const [cur, setCur] = uSmd(0);
  if (!list.length) return null;
  const t = list[Math.min(cur, list.length - 1)];
  const src = trackSrc(db, t);
  return (
    <div className="audio-album">
      <div className="aa-now">
        <span className="aa-now-ic"><_Icon name="music" size={20} /></span>
        <div className="aa-now-body">
          <div className="aa-now-title">{t.title || `Track ${cur + 1}`}{t.locale ? <span className="aa-loc">{t.locale.toUpperCase()}</span> : null}</div>
          {src ? <audio className="audio-player" controls src={src} /> : <FauxWave />}
        </div>
      </div>
      <ol className="aa-list">
        {list.map((tr, i) => (
          <li key={tr.id || i} className={"aa-track" + (i === cur ? " on" : "")} onClick={() => setCur(i)}>
            <span className="aa-num mono">{String(i + 1).padStart(2, "0")}</span>
            <span className="aa-t">{tr.title || `Track ${i + 1}`}</span>
            {tr.locale && <span className="aa-loc">{tr.locale.toUpperCase()}</span>}
            <span className="aa-play"><_Icon name={i === cur ? "globe" : "play"} size={15} /></span>
          </li>
        ))}
      </ol>
    </div>
  );
}

/* faux waveform shown when an asset has no playable file in this demo */
function FauxWave() {
  const bars = uMmd(() => Array.from({ length: 48 }, () => 0.25 + Math.random() * 0.75), []);
  return (
    <div className="faux-wave">
      {bars.map((h, i) => <span key={i} style={{ height: `${h * 100}%` }} />)}
      <span className="fw-demo mono">demo · no file loaded</span>
    </div>
  );
}

function AudioAlbumEditor({ item, onPatch }) {
  const { db } = window.useStore();
  const DragList = window.DragList;
  const list = item.tracks || [];
  const [pick, setPick] = uSmd(null);
  const set = (idx, patch) => onPatch(c => { c.tracks = (c.tracks || []).map((t, j) => j === idx ? { ...t, ...patch } : t); });
  const add = () => onPatch(c => { c.tracks = [...(c.tracks || []), { id: window.uid("tk"), title: "", assetId: "", locale: "" }]; });
  const remove = (idx) => onPatch(c => { c.tracks = (c.tracks || []).filter((_, j) => j !== idx); });
  return (
    <div className="col gap-10">
      {list.length === 0 && <span className="dim mono" style={{ fontSize: 12 }}>No tracks yet — one album holds several sounds.</span>}
      {list.length > 0 && (
        <DragList items={list} onReorder={n => onPatch(c => c.tracks = n)} renderRow={(t, idx) => (
          <div className="col gap-8" style={{ flex: 1, minWidth: 0 }}>
            <div className="row gap-8">
              <input style={{ flex: 1 }} value={t.title || ""} onChange={e => set(idx, { title: e.target.value })} placeholder="Track title" />
              <select value={t.locale || ""} onChange={e => set(idx, { locale: e.target.value })} style={{ flex: "none", width: 84 }}><option value="">—</option><option value="en">EN</option><option value="fr">FR</option></select>
              <button type="button" className="iconbtn sm danger" style={{ flex: "none" }} onClick={() => remove(idx)}><_Icon name="trash" size={14} /></button>
            </div>
            <div className="row gap-8 wrap" style={{ alignItems: "center" }}>
              <button type="button" className="chip" onClick={() => setPick(idx)}><_Icon name="compass" size={12} style={{ verticalAlign: "-2px", marginRight: 3 }} />From library</button>
              <label className="btn ghost sm" style={{ cursor: "pointer" }}>
                <_Icon name="save" size={13} /> Upload
                <input type="file" accept="audio/*" style={{ display: "none" }} onChange={e => fileToDataURL(e.target.files && e.target.files[0], (data, file) => set(idx, { file: data, assetId: "", title: t.title || file.name }))} />
              </label>
              {t.assetId && <AssetRef assetId={t.assetId} onClear={() => set(idx, { assetId: "" })} />}
              {t.file && <span className="leb-ok"><_Icon name="check" size={13} /> file loaded</span>}
            </div>
          </div>
        )} />
      )}
      <button className="btn ghost sm" style={{ alignSelf: "flex-start" }} onClick={add}><_Icon name="plus" size={14} /> Add track</button>
      {pick != null && <MediaPicker type="audio" onPick={(id) => set(pick, { assetId: id, file: "" })} onClose={() => setPick(null)} />}
    </div>
  );
}

Object.assign(window, {
  MediaPicker, AssetRef, fileToDataURL, mediaApiUpload, mimeMatchesType,
  ImageGalleryReader, ImageGalleryEditor,
  DocLibraryReader, DocLibraryEditor,
  AudioAlbumReader, AudioAlbumEditor, FauxWave,
  galleryImgSrc, docSrc, trackSrc,
});
