/* ========================================================================
   LUMINA — Boards / Titles workspace
   Multi-element compositions (text + images + shapes) saved as boards.
   Each board is a triggerable live resource. Templates accelerate setup.
   ======================================================================== */
const { useState: useStateB, useEffect: useEffectB, useRef: useRefB, useMemo: useMemoB } = React;

const BOARD_ANIMATIONS = [
  { id: "none",       label: "Static" },
  { id: "fadeIn",     label: "Fade In" },
  { id: "slideIn",    label: "Slide In" },
  { id: "typewriter", label: "Typewriter" },
  { id: "glitch",     label: "Glitch" },
  { id: "pulse",      label: "Audio Pulse" },
  { id: "wave",       label: "Wave Distort" },
  { id: "scramble",   label: "Scramble" },
  { id: "marquee",    label: "Marquee" },
  { id: "popIn",      label: "Pop In" },
  { id: "bounce",     label: "Bounce In" },
  { id: "rainbow",    label: "Rainbow Cycle" },
  { id: "flicker",    label: "Neon Flicker" },
  { id: "zoomBlur",   label: "Zoom Blur" },
  { id: "float",      label: "Float Idle" },
  { id: "shake",      label: "Beat Shake" },
];

const FONT_CHOICES_B = [
  "Space Grotesk", "Inter", "JetBrains Mono",
  "Georgia", "Impact", "Courier New",
];

/* === Board templates === */
function makeTemplate(name) {
  const id = () => "e" + Math.random().toString(36).slice(2, 9);

  if (name === "Now Playing") {
    return {
      name: "Now Playing", bg: "#0a0a0c",
      elements: [
        { id: id(), kind: "shape", shape: "rect", x: 0, y: 0.78, w: 1, h: 0.22,
          color: "#000000aa", anim: "none" },
        { id: id(), kind: "text", text: "▸ NOW PLAYING",
          x: 0.04, y: 0.83, size: 28, color: "#ffae3a",
          font: "JetBrains Mono", weight: 700, align: "left", anim: "none" },
        { id: id(), kind: "text", text: "Track Name",
          x: 0.04, y: 0.88, size: 56, color: "#ffffff",
          font: "Space Grotesk", weight: 800, align: "left", anim: "typewriter" },
        { id: id(), kind: "text", text: "Artist · Album",
          x: 0.04, y: 0.94, size: 22, color: "#9b9a8c",
          font: "Space Grotesk", weight: 500, align: "left", anim: "none" },
        { id: id(), kind: "imageSlot", x: 0.82, y: 0.86, w: 0.14, h: 0.14 * (16/9),
          rounded: 8, anim: "none", source: "media:0" },
      ],
    };
  }
  if (name === "On Mic") {
    return {
      name: "On Mic", bg: "#0a0a0c",
      elements: [
        { id: id(), kind: "shape", shape: "circle", x: 0.3, y: 0.5, w: 0.32, h: 0.32 * (16/9),
          color: "#ff7a1a", anim: "pulse" },
        { id: id(), kind: "imageSlot", x: 0.3, y: 0.5, w: 0.3, h: 0.3 * (16/9),
          rounded: 999, anim: "popIn", source: "media:0" },
        { id: id(), kind: "text", text: "ON MIC",
          x: 0.62, y: 0.35, size: 32, color: "#ffae3a",
          font: "JetBrains Mono", weight: 700, align: "left", anim: "none" },
        { id: id(), kind: "text", text: "Speaker Name",
          x: 0.62, y: 0.5, size: 80, color: "#ffffff",
          font: "Space Grotesk", weight: 900, align: "left", anim: "slideIn" },
        { id: id(), kind: "text", text: "Role · Organization",
          x: 0.62, y: 0.63, size: 28, color: "#9b9a8c",
          font: "Space Grotesk", weight: 500, align: "left", anim: "fadeIn" },
      ],
    };
  }
  if (name === "Event Poster") {
    return {
      name: "Event Poster", bg: "#0a0a0c",
      elements: [
        { id: id(), kind: "shape", shape: "rect", x: 0.04, y: 0.04, w: 0.92, h: 0.92,
          color: "transparent", stroke: "#ffae3a", strokeWidth: 2, anim: "none" },
        { id: id(), kind: "text", text: "FESTIVAL",
          x: 0.5, y: 0.18, size: 36, color: "#ffae3a",
          font: "JetBrains Mono", weight: 700, align: "center", anim: "none" },
        { id: id(), kind: "text", text: "NIGHT 2026",
          x: 0.5, y: 0.36, size: 140, color: "#ffffff",
          font: "Impact", weight: 900, align: "center", anim: "popIn" },
        { id: id(), kind: "text", text: "SAT · MAR 14 · 23:00",
          x: 0.5, y: 0.52, size: 32, color: "#ff5ad6",
          font: "Space Grotesk", weight: 700, align: "center", anim: "none" },
        { id: id(), kind: "text", text: "Lumina Hall · Milano",
          x: 0.5, y: 0.6, size: 24, color: "#9b9a8c",
          font: "Space Grotesk", weight: 500, align: "center", anim: "none" },
        { id: id(), kind: "imageSlot", x: 0.18, y: 0.78, w: 0.18, h: 0.12,
          rounded: 4, anim: "none", source: "media:0" },
        { id: id(), kind: "imageSlot", x: 0.5, y: 0.78, w: 0.18, h: 0.12,
          rounded: 4, anim: "none", source: "media:1" },
        { id: id(), kind: "imageSlot", x: 0.82, y: 0.78, w: 0.18, h: 0.12,
          rounded: 4, anim: "none", source: "media:2" },
      ],
    };
  }
  if (name === "Thank You") {
    return {
      name: "Thank You", bg: "#0a0a0c",
      elements: [
        { id: id(), kind: "text", text: "♥",
          x: 0.5, y: 0.3, size: 200, color: "#ff5ad6", font: "Space Grotesk",
          weight: 800, align: "center", anim: "pulse" },
        { id: id(), kind: "text", text: "THANK YOU",
          x: 0.5, y: 0.55, size: 140, color: "#ffffff",
          font: "Impact", weight: 900, align: "center", anim: "popIn" },
        { id: id(), kind: "text", text: "for an unforgettable night",
          x: 0.5, y: 0.72, size: 32, color: "#9b9a8c",
          font: "Georgia", weight: 500, align: "center", italic: true, anim: "fadeIn" },
      ],
    };
  }
  if (name === "Birthday") {
    return {
      name: "Birthday", bg: "#0a0a0c",
      elements: [
        { id: id(), kind: "text", text: "🎂",
          x: 0.5, y: 0.28, size: 130, color: "#ff5ad6",
          font: "Space Grotesk", weight: 800, align: "center", anim: "pulse" },
        { id: id(), kind: "text", text: "HAPPY BIRTHDAY",
          x: 0.5, y: 0.5, size: 110, color: "#ffae3a",
          font: "Impact", weight: 900, align: "center", anim: "popIn" },
        { id: id(), kind: "text", text: "Name",
          x: 0.5, y: 0.65, size: 80, color: "#ffffff",
          font: "Space Grotesk", weight: 800, align: "center", anim: "slideIn" },
        { id: id(), kind: "imageSlot", x: 0.5, y: 0.85, w: 0.16, h: 0.16 * (16/9),
          rounded: 999, anim: "popIn", source: "media:0" },
      ],
    };
  }
  if (name === "Sponsor Reel") {
    return {
      name: "Sponsor Reel", bg: "#0a0a0c",
      elements: [
        { id: id(), kind: "text", text: "OUR SPONSORS",
          x: 0.5, y: 0.15, size: 56, color: "#ffae3a",
          font: "JetBrains Mono", weight: 700, align: "center", anim: "none" },
        ...[0,1,2,3].map((i) => ({
          id: id(), kind: "imageSlot",
          x: 0.15 + (i % 2) * 0.4, y: 0.4 + Math.floor(i/2) * 0.32,
          w: 0.3, h: 0.22, rounded: 6, anim: i === 0 ? "popIn" : "none",
          source: `media:${i}`,
        })),
      ],
    };
  }
  // Blank
  return {
    name: name || "New Board", bg: "#0a0a0c",
    elements: [
      { id: id(), kind: "text", text: "DOUBLE-CLICK TO EDIT",
        x: 0.5, y: 0.5, size: 64, color: "#ffae3a",
        font: "Space Grotesk", weight: 800, align: "center", anim: "none" },
    ],
  };
}

let _bid = 0;

function TextEditor({ audio, params, setVizId, onSendToLayers }) {
  const [boards, setBoards] = useStateB(() => [
    { id: ++_bid, ...makeTemplate("Now Playing") },
    { id: ++_bid, ...makeTemplate("On Mic") },
    { id: ++_bid, ...makeTemplate("Event Poster") },
    { id: ++_bid, ...makeTemplate("Thank You") },
  ]);
  const [activeBoardId, setActiveBoardId] = useStateB(boards[0].id);
  const board = boards.find(b => b.id === activeBoardId);
  const [selId, setSelId] = useStateB(board?.elements[0]?.id || null);
  const sel = board?.elements.find(e => e.id === selId);

  const updBoard = (patch) =>
    setBoards(arr => arr.map(b => b.id === activeBoardId ? { ...b, ...patch } : b));
  const updEl = (patch) =>
    updBoard({ elements: board.elements.map(e => e.id === selId ? { ...e, ...patch } : e) });

  const addElement = (kind) => {
    const id = "e" + Math.random().toString(36).slice(2, 9);
    let el;
    if (kind === "text") {
      el = { id, kind: "text", text: "TEXT", x: 0.5, y: 0.5, size: 80,
        color: "#ffffff", font: "Space Grotesk", weight: 800, align: "center", anim: "none" };
    } else if (kind === "shape") {
      el = { id, kind: "shape", shape: "rect", x: 0.5, y: 0.5,
        w: 0.3, h: 0.15, color: "#ffae3a", anim: "none" };
    } else if (kind === "imageSlot") {
      el = { id, kind: "imageSlot", x: 0.5, y: 0.5, w: 0.25, h: 0.25,
        rounded: 6, anim: "none", source: "media:0" };
    }
    updBoard({ elements: [...board.elements, el] });
    setSelId(id);
  };
  const dupElement = () => {
    if (!sel) return;
    const id = "e" + Math.random().toString(36).slice(2, 9);
    const ne = { ...sel, id, x: Math.min(0.95, sel.x + 0.04), y: Math.min(0.95, sel.y + 0.04) };
    updBoard({ elements: [...board.elements, ne] });
    setSelId(id);
  };
  const delElement = () => {
    if (!sel) return;
    updBoard({ elements: board.elements.filter(e => e.id !== selId) });
    setSelId(board.elements[0]?.id || null);
  };

  const moveElement = (dir) => {
    if (!sel) return;
    const arr = [...board.elements];
    const i = arr.findIndex(e => e.id === selId);
    if (i < 0) return;
    let j;
    if (dir === "up") j = Math.min(arr.length - 1, i + 1);
    else if (dir === "down") j = Math.max(0, i - 1);
    else if (dir === "front") j = arr.length - 1;
    else if (dir === "back") j = 0;
    if (j === i) return;
    arr.splice(j, 0, arr.splice(i, 1)[0]);
    updBoard({ elements: arr });
  };

  const copyElement = () => {
    if (!sel) return;
    window.__lumBoardClip = JSON.parse(JSON.stringify(sel));
  };
  const pasteElement = () => {
    const clip = window.__lumBoardClip;
    if (!clip) return;
    const id = "e" + Math.random().toString(36).slice(2, 9);
    const ne = { ...clip, id,
      x: Math.min(0.95, (clip.x ?? 0.5) + 0.04),
      y: Math.min(0.95, (clip.y ?? 0.5) + 0.04) };
    updBoard({ elements: [...board.elements, ne] });
    setSelId(id);
  };

  const addBoard = (templateName) => {
    const b = { id: ++_bid, ...makeTemplate(templateName) };
    setBoards(arr => [...arr, b]);
    setActiveBoardId(b.id);
    setSelId(b.elements[0]?.id || null);
  };
  const delBoard = () => {
    if (boards.length <= 1) return;
    setBoards(arr => arr.filter(b => b.id !== activeBoardId));
    setActiveBoardId(boards.find(b => b.id !== activeBoardId).id);
  };
  const dupBoard = () => {
    const b = { ...board, id: ++_bid, name: board.name + " copy",
      elements: board.elements.map(e => ({ ...e, id: "e" + Math.random().toString(36).slice(2, 9) })) };
    setBoards(arr => [...arr, b]);
    setActiveBoardId(b.id);
  };

  /* === Canvas === */
  const canvasRef = useRefB(null);
  const rafRef = useRefB(0);
  const startTimeRef = useRefB(performance.now());
  const dragRef = useRefB(null);

  useEffectB(() => {
    const cnv = canvasRef.current;
    if (!cnv) return;
    const ctx = cnv.getContext("2d");
    function resize() {
      const r = cnv.parentElement.getBoundingClientRect();
      const dpr = Math.min(2, window.devicePixelRatio || 1);
      cnv.width = Math.max(2, Math.floor(r.width * dpr));
      cnv.height = Math.max(2, Math.floor(r.height * dpr));
      cnv.style.width = r.width + "px"; cnv.style.height = r.height + "px";
      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    }
    resize();
    const ro = new ResizeObserver(resize);
    ro.observe(cnv.parentElement);

    const tick = () => {
      const t = (performance.now() - startTimeRef.current) / 1000;
      const w = parseFloat(cnv.style.width), h = parseFloat(cnv.style.height);
      // bg
      ctx.fillStyle = board?.bg || "#0a0a0c";
      ctx.fillRect(0, 0, w, h);
      // checker grid (subtle)
      ctx.strokeStyle = "rgba(255,255,255,0.04)";
      for (let x = 0; x < w; x += 40) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, h); ctx.stroke(); }
      for (let y = 0; y < h; y += 40) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(w, y); ctx.stroke(); }
      ctx.strokeStyle = "rgba(255,174,58,0.18)";
      ctx.setLineDash([6, 6]);
      ctx.strokeRect(w*0.05, h*0.05, w*0.9, h*0.9);
      ctx.setLineDash([]);

      const A = window.AudioEngine || { bass: 0, mid: 0, high: 0, energy: 0 };
      board?.elements.forEach(el => drawBoardElement(ctx, el, w, h, A, t));

      // selection overlay
      if (sel) {
        const cx = sel.x * w, cy = sel.y * h;
        const ew = (sel.w || 0.2) * w;
        const eh = (sel.h || 0.1) * h;
        ctx.strokeStyle = "rgba(255,174,58,0.9)";
        ctx.setLineDash([4, 4]); ctx.lineWidth = 1.5;
        if (sel.kind === "text") {
          ctx.strokeRect(cx - 8, cy - 8, 16, 16);
        } else {
          ctx.strokeRect(cx - ew/2, cy - eh/2, ew, eh);
        }
        ctx.setLineDash([]);
        ctx.fillStyle = "var(--accent)";
        ctx.fillRect(cx - 4, cy - 4, 8, 8);
      }
      rafRef.current = requestAnimationFrame(tick);
    };
    rafRef.current = requestAnimationFrame(tick);
    return () => { cancelAnimationFrame(rafRef.current); ro.disconnect(); };
  }, [board, sel]);

  const onMouseDown = (e) => {
    const rect = canvasRef.current.getBoundingClientRect();
    const x = (e.clientX - rect.left) / rect.width;
    const y = (e.clientY - rect.top) / rect.height;
    let bestId = null, bestD = 0.12;
    board.elements.forEach(el => {
      const d = Math.hypot(el.x - x, el.y - y);
      if (d < bestD) { bestD = d; bestId = el.id; }
    });
    if (bestId !== null) { setSelId(bestId); dragRef.current = bestId; }
  };
  const onMouseMove = (e) => {
    if (dragRef.current === null) return;
    const rect = canvasRef.current.getBoundingClientRect();
    const x = (e.clientX - rect.left) / rect.width;
    const y = (e.clientY - rect.top) / rect.height;
    updBoard({ elements: board.elements.map(el =>
      el.id === dragRef.current ? { ...el, x: Math.max(0, Math.min(1, x)), y: Math.max(0, Math.min(1, y)) } : el) });
  };
  const onMouseUp = () => { dragRef.current = null; };

  return (
    <div className="panel stage-bezel" style={{ padding: 0, display: "flex", flexDirection: "column", overflow: "hidden" }}>
      <Screws />
      <div className="stage-toolbar">
        <div className="stage-tabs"><div className="stage-tab active">Boards / Titles</div></div>
        <div style={{ flex: 1 }} />
        <span className="cap">+ Element:</span>
        <button className="nbtn compact glow pressed" onClick={() => addElement("text")}>+ Text</button>
        <button className="nbtn compact" onClick={() => addElement("imageSlot")}>+ Image</button>
        <button className="nbtn compact" onClick={() => addElement("shape")}>+ Shape</button>
        <div style={{ width: 1, height: 22, background: "rgba(255,255,255,0.1)" }} />
        <button className="nbtn compact" onClick={dupElement} disabled={!sel}>Dup</button>
        <button className="nbtn compact" onClick={copyElement} disabled={!sel} title="Copy">⎘</button>
        <button className="nbtn compact" onClick={pasteElement} title="Paste">⎗</button>
        <div style={{ width: 1, height: 22, background: "rgba(255,255,255,0.1)" }} />
        <button className="nbtn compact" onClick={() => moveElement("back")} disabled={!sel} title="Send to back">⤓</button>
        <button className="nbtn compact" onClick={() => moveElement("down")} disabled={!sel} title="Down">▼</button>
        <button className="nbtn compact" onClick={() => moveElement("up")} disabled={!sel} title="Up">▲</button>
        <button className="nbtn compact" onClick={() => moveElement("front")} disabled={!sel} title="Bring to front">⤒</button>
        <button className="nbtn compact danger" onClick={delElement} disabled={!sel}>×</button>
      </div>

      <div className="lum-wsgrid" style={{ display: "grid", gridTemplateColumns: "200px 1fr 240px", gap: 0, flex: 1, minHeight: 0 }}>
        {/* LEFT: Board library */}
        <div style={{
          padding: 10, background: "linear-gradient(180deg, #14141a, #0a0a0c)",
          borderRight: "1px solid rgba(255,255,255,0.04)",
          display: "flex", flexDirection: "column", gap: 8, overflowY: "auto",
        }}>
          <div className="cap" style={{ display: "flex", justifyContent: "space-between" }}>
            <span>Board Library</span>
            <span style={{ color: "var(--led-cyan)" }}>{boards.length}</span>
          </div>
          <div style={{ display: "flex", gap: 4 }}>
            <button className="nbtn compact glow pressed" onClick={() => addBoard("blank")} style={{ flex: 1 }}>+ New</button>
            <button className="nbtn compact" onClick={dupBoard}>Dup</button>
            <button className="nbtn compact danger" onClick={delBoard} disabled={boards.length <= 1}>×</button>
          </div>
          <div style={{ display: "flex", alignItems: "center", gap: 6, justifyContent: "space-between",
            padding: "4px 6px", background: "rgba(0,0,0,0.2)", borderRadius: 6 }}>
            <span className="cap" style={{ fontSize: 9 }}>Board BG</span>
            <div style={{ display: "flex", gap: 4 }}>
              {["#0a0a0c", "#ffffff", "#ff2d6f", "#1a1340", "#003322"].map(c => (
                <button key={c} onClick={() => updBoard({ bg: c })}
                  title={c}
                  style={{ width: 18, height: 18, borderRadius: 4, cursor: "pointer",
                    background: c, border: board?.bg === c ? "2px solid var(--accent)" : "1px solid rgba(255,255,255,0.15)" }} />
              ))}
              <input type="color" value={board?.bg || "#0a0a0c"} onChange={e => updBoard({ bg: e.target.value })}
                style={{ width: 22, height: 18, border: "none", background: "transparent", cursor: "pointer" }} />
            </div>
          </div>

          {/* Board thumbnails list */}
          <div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
            {boards.map(b => (
              <div key={b.id}
                onClick={() => { setActiveBoardId(b.id); setSelId(b.elements[0]?.id || null); }}
                className={`preset-card ${b.id === activeBoardId ? "active" : ""}`}
                style={{ padding: 5, gap: 6, cursor: "pointer" }}>
                <BoardThumb board={b} />
                <div style={{ flex: 1, minWidth: 0, textAlign: "left" }}>
                  <input value={b.name}
                    onClick={e => e.stopPropagation()}
                    onChange={e => setBoards(arr => arr.map(x => x.id === b.id ? { ...x, name: e.target.value } : x))}
                    style={{
                      background: "transparent", border: "none", outline: "none",
                      color: b.id === activeBoardId ? "var(--accent)" : "var(--ink)",
                      fontFamily: "var(--font-display)", fontSize: 11, fontWeight: 700,
                      width: "100%", padding: 0,
                    }} />
                  <div className="preset-tag" style={{ fontSize: 8 }}>
                    {b.elements.length} elements
                  </div>
                </div>
              </div>
            ))}
          </div>

          <div className="cap" style={{ marginTop: 4 }}>Templates</div>
          <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 4 }}>
            {["Now Playing", "On Mic", "Event Poster", "Thank You", "Birthday", "Sponsor Reel"].map(t => (
              <button key={t} className="nbtn compact"
                onClick={() => addBoard(t)}
                style={{ padding: "6px 4px", fontSize: 9, textAlign: "center", justifyContent: "center" }}>
                {t}
              </button>
            ))}
          </div>
        </div>

        {/* CENTER: canvas */}
        <div style={{ position: "relative", background: "#000" }}>
          <canvas ref={canvasRef}
            onMouseDown={onMouseDown} onMouseMove={onMouseMove} onMouseUp={onMouseUp} onMouseLeave={onMouseUp}
            style={{ display: "block", width: "100%", height: "100%", cursor: dragRef.current ? "grabbing" : "grab" }} />
          <div className="screen-corner" style={{ top: 10, left: 14 }}>
            ● BOARD · {board?.name} · {board?.elements.length} elements
          </div>
          <div className="screen-corner" style={{ bottom: 10, left: 14 }}>
            DRAG elements to move · CLICK to select
          </div>
        </div>

        {/* RIGHT: inspector */}
        <div style={{
          padding: 10, background: "linear-gradient(180deg, #14141a, #0a0a0c)",
          borderLeft: "1px solid rgba(255,255,255,0.04)",
          display: "flex", flexDirection: "column", gap: 8, overflowY: "auto",
        }}>
          {/* Element list (always visible) */}
          <div className="cap">Elements ({board?.elements.length})</div>
          <div style={{ display: "flex", flexDirection: "column", gap: 3, maxHeight: 140, overflowY: "auto" }}>
            {board?.elements.map((el, i) => (
              <button key={el.id}
                onClick={() => setSelId(el.id)}
                style={{
                  display: "flex", alignItems: "center", gap: 6, padding: "4px 6px",
                  background: el.id === selId
                    ? "linear-gradient(90deg, rgba(255,122,26,0.2), transparent)"
                    : "transparent",
                  border: el.id === selId
                    ? "1px solid rgba(255,122,26,0.4)"
                    : "1px solid rgba(255,255,255,0.04)",
                  borderRadius: 4, cursor: "pointer",
                  color: el.id === selId ? "var(--accent)" : "var(--ink-dim)",
                }}>
                <span style={{ fontFamily: "var(--font-mono)", fontSize: 9, opacity: 0.6 }}>{i+1}</span>
                <span style={{ fontSize: 12 }}>
                  {el.kind === "text" ? "T" : el.kind === "imageSlot" ? "▣" : "■"}
                </span>
                <span style={{ flex: 1, fontFamily: "var(--font-mono)", fontSize: 10,
                  whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", textAlign: "left" }}>
                  {el.kind === "text" ? el.text : el.kind === "imageSlot" ? "image" : el.shape || "shape"}
                </span>
              </button>
            ))}
          </div>

          {sel && (
            <>
              <div style={{ height: 1, background: "rgba(255,255,255,0.06)", margin: "4px 0" }} />
              {sel.kind === "text" && (<>
                <div className="cap">Text</div>
                <textarea value={sel.text}
                  onChange={e => updEl({ text: e.target.value })}
                  style={inspTextarea(sel)} />
                <div className="cap">Font</div>
                <select value={sel.font} onChange={e => updEl({ font: e.target.value })} style={inspSelect}>
                  {FONT_CHOICES_B.map(f => <option key={f}>{f}</option>)}
                </select>
                <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
                  <span className="cap">Color</span>
                  <input type="color" value={sel.color}
                    onChange={e => updEl({ color: e.target.value })}
                    style={{ width: 36, height: 22, border: "none", background: "transparent" }} />
                </div>
                <SI label="Size" value={sel.size} onChange={v => updEl({ size: v })} min={12} max={400} step={1} unit="px" />
                <SI label="Rot" value={sel.rotation || 0} onChange={v => updEl({ rotation: v })} min={-180} max={180} step={1} unit="°" />
                <SI label="Track" value={sel.letterSpacing || 0} onChange={v => updEl({ letterSpacing: v })} min={-10} max={40} step={0.5} unit="px" />
                <SI label="Glow" value={sel.shadow ?? 10} onChange={v => updEl({ shadow: v })} min={0} max={60} step={1} />
                <SI label="Alpha" value={sel.opacity ?? 1} onChange={v => updEl({ opacity: v })} min={0} max={1} step={0.01} />
                <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginTop: 4 }}>
                  <span className="cap">Outline</span>
                  <div style={{ display: "flex", gap: 4, alignItems: "center" }}>
                    <input type="color" value={sel.strokeText || "#000000"}
                      onChange={e => updEl({ strokeText: e.target.value, strokeWidth: sel.strokeWidth || 3 })}
                      style={{ width: 28, height: 22, border: "none", background: "transparent" }} />
                    <button className="nbtn compact" onClick={() => updEl({ strokeText: null })} title="Clear outline">×</button>
                  </div>
                </div>
                {sel.strokeText && (
                  <SI label="O.Width" value={sel.strokeWidth || 3} onChange={v => updEl({ strokeWidth: v })} min={1} max={20} step={0.5} />
                )}
              </>)}

              {sel.kind === "imageSlot" && (<>
                <div className="cap">Image Source</div>
                <select value={sel.source} onChange={e => updEl({ source: e.target.value })} style={inspSelect}>
                  <option value="media:0">Media Layer 1</option>
                  <option value="media:1">Media Layer 2</option>
                  <option value="media:2">Media Layer 3</option>
                  <option value="media:3">Media Layer 4</option>
                </select>
                <SI label="W" value={sel.w} onChange={v => updEl({ w: v })} min={0.05} max={1} step={0.01} />
                <SI label="H" value={sel.h} onChange={v => updEl({ h: v })} min={0.05} max={1} step={0.01} />
                <SI label="Round" value={sel.rounded} onChange={v => updEl({ rounded: v })} min={0} max={999} step={1} />
                <div className="cap" style={{ fontSize: 8, color: "var(--ink-faint)" }}>
                  Import images via Layers tab → they fill the slots in order.
                </div>
              </>)}

              {sel.kind === "shape" && (<>
                <div className="cap">Shape</div>
                <div style={{ display: "flex", background: "linear-gradient(180deg,#0a0a0c,#16161a)",
                  borderRadius: 6, padding: 2, boxShadow: "var(--nshadow-in-sm)" }}>
                  {["rect","circle","line"].map(s => (
                    <button key={s} onClick={() => updEl({ shape: s })}
                      style={{
                        flex: 1, padding: "3px 4px", border: "none",
                        background: sel.shape === s ? "linear-gradient(180deg, var(--panel-hi), var(--panel))" : "transparent",
                        color: sel.shape === s ? "var(--accent)" : "var(--ink-faint)",
                        borderRadius: 4, fontSize: 9, cursor: "pointer",
                        fontFamily: "var(--font-display)", letterSpacing: "0.08em",
                        textTransform: "uppercase", fontWeight: 600,
                      }}>{s}</button>
                  ))}
                </div>
                <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
                  <span className="cap">Fill</span>
                  <input type="color" value={sel.color === "transparent" ? "#ffae3a" : sel.color}
                    onChange={e => updEl({ color: e.target.value })}
                    style={{ width: 36, height: 22, border: "none", background: "transparent" }} />
                </div>
                <SI label="W" value={sel.w} onChange={v => updEl({ w: v })} min={0.02} max={1} step={0.01} />
                <SI label="H" value={sel.h} onChange={v => updEl({ h: v })} min={0.02} max={1} step={0.01} />
              </>)}

              <div className="cap">Animation</div>
              <select value={sel.anim} onChange={e => updEl({ anim: e.target.value })} style={inspSelect}>
                {BOARD_ANIMATIONS.map(a => <option key={a.id} value={a.id}>{a.label}</option>)}
              </select>

              <div className="cap">Position</div>
              <SI label="X" value={sel.x} onChange={v => updEl({ x: v })} min={0} max={1} step={0.001} />
              <SI label="Y" value={sel.y} onChange={v => updEl({ y: v })} min={0} max={1} step={0.001} />
              <div style={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: 3 }}>
                {[["TL", 0.1, 0.1], ["TC", 0.5, 0.1], ["TR", 0.9, 0.1],
                  ["ML", 0.1, 0.5], ["MC", 0.5, 0.5], ["MR", 0.9, 0.5],
                  ["BL", 0.1, 0.9], ["BC", 0.5, 0.9], ["BR", 0.9, 0.9]].map(([l, x, y]) => (
                  <button key={l} className="nbtn compact" style={{ padding: "3px 0", fontSize: 8 }}
                    onClick={() => updEl({ x, y })}>{l}</button>
                ))}
              </div>
            </>
          )}
        </div>
      </div>

      <div className="stage-strip" style={{ gap: 8, borderTop: "1px solid rgba(255,255,255,0.04)" }}>
        <span className="cap">TRIGGER:</span>
        <button className="nbtn glow pressed" style={{ padding: "6px 14px", fontWeight: 700 }}
          onClick={() => onSendToLayers?.(board)}>
          ▶ Send Board to Layers
        </button>
        <button className="nbtn compact" onClick={() => setVizId?.("type")}>
          Send to Live Output
        </button>
        <div style={{ flex: 1 }} />
        <span className="cap" style={{ color: "var(--ink-faint)" }}>
          Boards can be triggered from Live Loops · drag-drop to clip cells
        </span>
      </div>
    </div>
  );
}

/* ===== Element renderer ===== */
function drawBoardElement(ctx, el, W, H, A, t) {
  // animation transforms
  const cx = el.x * W, cy = el.y * H;
  let scale = 1, dx = 0, dy = 0;
  let opacity = el.opacity ?? 1;
  let text = el.text;
  let glitch = 0;
  let colorOverride = null;
  let blur = 0;

  if (el.anim === "fadeIn") {
    opacity *= Math.min(1, t / 0.6);
  } else if (el.anim === "slideIn") {
    const k = Math.min(1, t / 0.7);
    dx = (1 - k) * -80;
    opacity *= k;
  } else if (el.anim === "typewriter" && el.kind === "text") {
    const n = Math.floor(((t * 4) % (text.length + 5)));
    text = text.slice(0, Math.min(text.length, n));
    if ((t * 8) % 1 > 0.5 && text.length < el.text.length) text += "█";
  } else if (el.anim === "glitch") {
    glitch = (A.high || 0) > 0.2 || ((t * 6) % 1) < 0.1 ? 1 : 0;
  } else if (el.anim === "pulse") {
    scale = 1 + (A.bass || 0) * 0.35;
    opacity *= 0.85 + (A.energy || 0) * 0.4;
  } else if (el.anim === "wave") {
    dy = Math.sin(t * 2) * 12;
  } else if (el.anim === "scramble" && el.kind === "text") {
    if ((A.beat || 0) > 0.4) {
      const ch = "█▓▒░@#$%&*+=-";
      text = text.split("").map(c => Math.random() > 0.7 ? ch[Math.floor(Math.random() * ch.length)] : c).join("");
    }
  } else if (el.anim === "marquee") {
    dx = ((t * 200) % (W + 400)) - W * 0.5 - 200;
  } else if (el.anim === "popIn") {
    const k = Math.min(1, t / 0.5);
    scale = 0.3 + k * 0.7 + (k > 0.7 ? Math.sin((k - 0.7) * 10) * 0.05 : 0);
    opacity *= k;
  } else if (el.anim === "bounce") {
    const k = Math.min(1, t / 0.8);
    const e = k < 1 ? 1 - Math.pow(2, -10 * k) * Math.cos(k * 12) : 1;
    dy = (1 - e) * -H * 0.4;
    opacity *= Math.min(1, t / 0.3);
  } else if (el.anim === "rainbow") {
    const hue = (t * 80) % 360;
    colorOverride = `hsl(${hue}, 95%, 60%)`;
  } else if (el.anim === "flicker") {
    const f = Math.sin(t * 30) * 0.5 + 0.5;
    const rnd = Math.random() > 0.92 ? 0.3 : 1;
    opacity *= (0.6 + f * 0.4) * rnd;
  } else if (el.anim === "zoomBlur") {
    const k = Math.min(1, t / 0.6);
    scale = 1.6 - k * 0.6;
    blur = (1 - k) * 24;
    opacity *= k;
  } else if (el.anim === "float") {
    dy = Math.sin(t * 1.3) * 10;
    dx = Math.cos(t * 0.9) * 6;
  } else if (el.anim === "shake") {
    const amt = (A.beat || 0) > 0.3 ? (A.beat || 0) * 14 : 0;
    dx = (Math.random() - 0.5) * amt;
    dy = (Math.random() - 0.5) * amt;
    scale = 1 + (A.bass || 0) * 0.15;
  }

  if (el.kind === "text") {
    ctx.save();
    ctx.translate(cx + dx, cy + dy);
    ctx.rotate((el.rotation || 0) * Math.PI / 180);
    ctx.scale(scale, scale);
    ctx.globalAlpha = Math.max(0, Math.min(1, opacity));
    ctx.font = `${el.italic ? "italic " : ""}${el.weight || 800} ${el.size}px "${el.font}", sans-serif`;
    ctx.textAlign = el.align || "center";
    ctx.textBaseline = "middle";
    if (el.letterSpacing) ctx.letterSpacing = el.letterSpacing + "px";
    if (blur > 0) ctx.filter = `blur(${blur}px)`;
    const fillCol = colorOverride || el.color;
    if ((el.shadow ?? 10) > 0) {
      ctx.shadowColor = fillCol;
      ctx.shadowBlur = el.shadow ?? 10;
    }
    if (glitch) {
      ctx.shadowBlur = 0;
      ctx.fillStyle = "rgba(255,40,40,0.85)"; ctx.fillText(text, -3, 0);
      ctx.fillStyle = "rgba(40,200,255,0.85)"; ctx.fillText(text, 3, 0);
      ctx.fillStyle = fillCol; ctx.fillText(text, 0, 0);
    } else {
      if (el.strokeText && el.strokeWidth) {
        ctx.lineWidth = el.strokeWidth;
        ctx.strokeStyle = el.strokeText;
        ctx.strokeText(text, 0, 0);
      }
      ctx.fillStyle = fillCol; ctx.fillText(text, 0, 0);
    }
    ctx.filter = "none";
    ctx.letterSpacing = "0px";
    ctx.restore();
  }

  if (el.kind === "shape") {
    ctx.save();
    ctx.translate(cx + dx, cy + dy);
    ctx.scale(scale, scale);
    ctx.globalAlpha = Math.max(0, Math.min(1, opacity));
    const w = el.w * W, h = el.h * H;
    if (el.color && el.color !== "transparent") {
      ctx.fillStyle = el.color;
      if (el.shape === "rect") ctx.fillRect(-w/2, -h/2, w, h);
      else if (el.shape === "circle") {
        ctx.beginPath(); ctx.ellipse(0, 0, w/2, h/2, 0, 0, Math.PI*2); ctx.fill();
      } else if (el.shape === "line") {
        ctx.fillRect(-w/2, -2, w, 4);
      }
    }
    if (el.stroke) {
      ctx.strokeStyle = el.stroke;
      ctx.lineWidth = el.strokeWidth || 2;
      if (el.shape === "rect") ctx.strokeRect(-w/2, -h/2, w, h);
      else if (el.shape === "circle") { ctx.beginPath(); ctx.ellipse(0, 0, w/2, h/2, 0, 0, Math.PI*2); ctx.stroke(); }
    }
    ctx.restore();
  }

  if (el.kind === "imageSlot") {
    ctx.save();
    ctx.translate(cx + dx, cy + dy);
    ctx.scale(scale, scale);
    ctx.globalAlpha = Math.max(0, Math.min(1, opacity));
    const w = el.w * W, h = el.h * H;
    const idx = parseInt((el.source || "media:0").split(":")[1]) || 0;
    const imgs = (window.MediaStore?.layers || []).filter(l => l.kind === "image" && l.visible);
    const img = imgs[idx];
    // rounded clip
    ctx.save();
    if (el.rounded >= 999) {
      ctx.beginPath(); ctx.ellipse(0, 0, w/2, h/2, 0, 0, Math.PI*2); ctx.clip();
    } else if (el.rounded > 0) {
      const r = Math.min(el.rounded, Math.min(w, h) / 2);
      ctx.beginPath();
      ctx.moveTo(-w/2 + r, -h/2);
      ctx.arcTo(w/2, -h/2, w/2, h/2, r);
      ctx.arcTo(w/2, h/2, -w/2, h/2, r);
      ctx.arcTo(-w/2, h/2, -w/2, -h/2, r);
      ctx.arcTo(-w/2, -h/2, w/2, -h/2, r);
      ctx.closePath(); ctx.clip();
    }
    if (img && img.el) {
      // cover fit
      const sr = img.w / img.h, tr = w / h;
      let dw, dh;
      if (sr > tr) { dh = h; dw = h * sr; } else { dw = w; dh = w / sr; }
      ctx.drawImage(img.el, -dw/2, -dh/2, dw, dh);
    } else {
      // placeholder
      ctx.fillStyle = "rgba(255,174,58,0.1)";
      ctx.fillRect(-w/2, -h/2, w, h);
      ctx.fillStyle = "rgba(255,174,58,0.6)";
      ctx.font = '600 14px "Space Grotesk", sans-serif';
      ctx.textAlign = "center"; ctx.textBaseline = "middle";
      ctx.fillText("img slot " + (idx + 1), 0, 0);
    }
    ctx.restore();
    // border
    ctx.strokeStyle = "rgba(255,255,255,0.1)";
    ctx.lineWidth = 1;
    if (el.rounded >= 999) {
      ctx.beginPath(); ctx.ellipse(0, 0, w/2, h/2, 0, 0, Math.PI*2); ctx.stroke();
    } else {
      ctx.strokeRect(-w/2, -h/2, w, h);
    }
    ctx.restore();
  }
}

/* tiny board thumbnail */
function BoardThumb({ board }) {
  const ref = useRefB(null);
  useEffectB(() => {
    const cnv = ref.current; if (!cnv) return;
    const ctx = cnv.getContext("2d");
    const W = cnv.width = 60; const H = cnv.height = 36;
    ctx.fillStyle = board.bg || "#0a0a0c"; ctx.fillRect(0, 0, W, H);
    board.elements.forEach(el => {
      if (el.kind === "text") {
        ctx.fillStyle = el.color;
        ctx.font = `${(el.size || 80) * 0.05}px "Space Grotesk"`;
        ctx.textAlign = el.align || "center"; ctx.textBaseline = "middle";
        ctx.fillText(el.text.slice(0, 10), el.x * W, el.y * H);
      } else if (el.kind === "shape" && el.color !== "transparent") {
        ctx.fillStyle = el.color;
        const w = el.w * W, h = el.h * H;
        if (el.shape === "rect") ctx.fillRect(el.x*W - w/2, el.y*H - h/2, w, h);
        else if (el.shape === "circle") { ctx.beginPath(); ctx.arc(el.x*W, el.y*H, w/2, 0, Math.PI*2); ctx.fill(); }
      } else if (el.kind === "imageSlot") {
        ctx.fillStyle = "rgba(255,174,58,0.3)";
        const w = el.w * W, h = el.h * H;
        ctx.fillRect(el.x*W - w/2, el.y*H - h/2, w, h);
      }
    });
  }, [board]);
  return <canvas ref={ref} style={{
    width: 60, height: 36, borderRadius: 4, flexShrink: 0,
    background: "#0a0a0c", boxShadow: "var(--nshadow-in-sm)",
  }} />;
}

/* Inline slider for inspector */
function SI({ label, value, onChange, min, max, step, unit = "" }) {
  return (
    <div style={{ display: "grid", gridTemplateColumns: "40px 1fr 44px", gap: 6, alignItems: "center", marginTop: 4 }}>
      <span className="cap" style={{ fontSize: 8 }}>{label}</span>
      <HSlider value={value} onChange={onChange} min={min} max={max} />
      <span style={{ fontFamily: "var(--font-mono)", fontSize: 9, color: "var(--accent)",
        textShadow: "0 0 3px var(--accent-glow)", textAlign: "right" }}>
        {step >= 1 ? value.toFixed(0) : value.toFixed(2)}{unit}
      </span>
    </div>
  );
}

const inspTextarea = (sel) => ({
  width: "100%", padding: "6px 8px",
  background: "linear-gradient(180deg,#0a0a0c,#14141a)",
  border: "none", borderRadius: 4,
  color: sel.color, fontFamily: sel.font || "Space Grotesk",
  fontSize: 13, fontWeight: 600,
  boxShadow: "var(--nshadow-in-sm)", outline: "none",
  resize: "vertical", minHeight: 40, letterSpacing: "0.02em",
});
const inspSelect = {
  width: "100%", padding: "5px 6px",
  background: "linear-gradient(180deg,#0a0a0c,#14141a)",
  border: "none", borderRadius: 4,
  color: "var(--ink)", fontFamily: "var(--font-mono)", fontSize: 10,
  boxShadow: "var(--nshadow-in-sm)", outline: "none",
};

window.TextEditor = TextEditor;
window.drawBoardElement = drawBoardElement;
window.drawTextItem = drawBoardElement; // backward compat
