// Main timetable: stages-as-rows × time-axis Gantt.
// - Click empty cell → AddPerformance modal.
// - Click block → floating Popover.
// - Drag block → move (snap 15min); drag right edge → resize.
// - Overlapping bookings stack vertically inside their stage row (lane-packed).
// - Parking column on the right shows pending availability + parked bookings;
//   drag from parking → timetable to schedule, drag block → parking to unschedule.

const TT = (function () {
  const STAGE_COL_W = 220;
  const PARKING_W = 280;
  const LANE_PAD = 4; // top/bottom padding inside row

  // ─── Block ──────────────────────────────────────────────────────────
  function Block({
    perf, artist, stage, sections, pxPerMin,
    laneTop, laneHeight,
    conflicting, capWarn, b2bRight, b2bLeft,
    isSelected, showPercent,
    onSelect, onStartDragMove, onStartDragResize,
  }) {
    const status = window.CrewliHelpers.STATUS[perf.status];
    const left  = perf.start * pxPerMin;
    const width = (perf.end - perf.start) * pxPerMin;
    const time = window.CrewliHelpers.fmtTime(perf.start, window.CrewliData.TIME.startHour) +
                 "–" + window.CrewliHelpers.fmtTime(perf.end, window.CrewliData.TIME.startHour);
    const adv = window.CrewliHelpers.advanceCount(artist, sections);
    const advPct = Math.round(adv.done / adv.total * 100);

    const blockStyle = {
      left, width,
      top: laneTop, height: laneHeight,
      background: status.blockBg,
      borderColor: conflicting ? "#d63d4b" : status.blockBorder,
      color: status.blockFg,
    };
    const isCancelled = perf.status === "cancelled";
    if (isCancelled) {
      blockStyle.borderColor = "#c4202f";
      blockStyle.background =
        `repeating-linear-gradient(135deg, rgba(214,61,75,0.32) 0 7px, rgba(214,61,75,0.10) 7px 15px), #ffffff`;
      blockStyle.color = "#7a1219";
    }

    const compact = laneHeight < 42;
    const showTime = width > 64;

    return (
      <div
        className={"cw-block" + (isSelected ? " is-selected" : "") + (conflicting ? " is-conflict" : "") + (compact ? " is-compact" : "") + (isCancelled ? " is-cancelled" : "")}
        style={blockStyle}
        onMouseDown={(e) => { if (e.button !== 0) return; onStartDragMove(e, perf); }}
        onClick={(e) => { e.stopPropagation(); onSelect(perf, e.currentTarget.getBoundingClientRect()); }}
      >
        <div className="cw-block-body">
          <div className="cw-block-row1">
            <span className="cw-block-name" title={artist.name}>{artist.name}</span>
            {!compact && artist.genre && width > 110 && (
              <span className="cw-block-genre" title={artist.genre}>{artist.genre}</span>
            )}
            <div className="cw-block-warn-row">
              {capWarn && (
                <span className="cw-block-warn" title={`Trekkracht ${artist.draw.toLocaleString('nl-NL')} > capaciteit ${stage.capacity.toLocaleString('nl-NL')}`}>
                  <svg width="12" height="12" viewBox="0 0 12 12">
                    <path d="M6 1 L11 10 L1 10 Z" fill="#e89a3c" stroke="#7a5018" strokeWidth=".7" strokeLinejoin="round"/>
                    <rect x="5.4" y="4.5" width="1.2" height="3" fill="#7a5018"/>
                    <rect x="5.4" y="8.2" width="1.2" height="1.2" fill="#7a5018"/>
                  </svg>
                </span>
              )}
              {conflicting && (
                <span className="cw-block-warn" title="Overlap op deze stage">
                  <svg width="12" height="12" viewBox="0 0 12 12">
                    <circle cx="6" cy="6" r="5" fill="#d63d4b"/>
                    <rect x="5.4" y="2.5" width="1.2" height="4" fill="white"/>
                    <rect x="5.4" y="7.8" width="1.2" height="1.2" fill="white"/>
                  </svg>
                </span>
              )}
              {showPercent && width > 86 && !compact && (
                <span className="cw-block-pct" title={`Advancing ${adv.done}/${adv.total}`}>{advPct}%</span>
              )}
            </div>
          </div>
          {!compact && (
            <div className="cw-block-row2">
              {showTime && <span className="cw-block-time">{time}</span>}
              {showPercent && width > 86 && (
                <span className="cw-block-adv-bar" title={`Advancing ${adv.done}/${adv.total}`}>
                  <span className="cw-block-adv-bar-fill" style={{ width: advPct + "%" }}></span>
                </span>
              )}
            </div>
          )}
          {compact && showTime && (
            <span className="cw-block-time cw-block-time-inline">{time}</span>
          )}
        </div>

        <span className="cw-block-resize"
              onMouseDown={(e) => { e.stopPropagation(); onStartDragResize(e, perf); }} />

        {b2bRight && <span className="cw-b2b-right" title="Back-to-back changeover" />}
        {b2bLeft && <span className="cw-b2b-left" />}
      </div>
    );
  }

  // ─── TimeAxis ───────────────────────────────────────────────────────
  function TimeAxis({ pxPerMin, totalMin, startHour }) {
    const hours = [];
    for (let m = 0; m <= totalMin; m += 60) {
      const h = (startHour + m / 60) % 24;
      hours.push({ m, h });
    }
    return (
      <div className="cw-axis" style={{ width: totalMin * pxPerMin }}>
        {hours.map(({ m, h }) => (
          <div key={m} className="cw-axis-tick" style={{ left: m * pxPerMin }}>
            <span className="cw-axis-label">{String(Math.floor(h)).padStart(2, "0")}:00</span>
          </div>
        ))}
      </div>
    );
  }

  function GridBg({ pxPerMin, totalMin, totalH, rowDividers }) {
    const halfHourW = 30 * pxPerMin;
    const hourW = 60 * pxPerMin;
    return (
      <>
        <div className="cw-grid-bg"
             style={{
               width: totalMin * pxPerMin, height: totalH,
               backgroundImage:
                 `linear-gradient(to right, rgba(20,30,45,.085) 1px, transparent 1px),` +
                 `linear-gradient(to right, rgba(20,30,45,.04) 1px, transparent 1px)`,
               backgroundSize: `${hourW}px 100%, ${halfHourW}px 100%`,
             }} />
        {rowDividers.map((y, i) => (
          <div key={i} className="cw-grid-row-divider"
               style={{ top: y, width: totalMin * pxPerMin }} />
        ))}
      </>
    );
  }

  // ─── Parking column ─────────────────────────────────────────────────

  // Multi-select dropdown for status filter (used inside ParkingColumn)
  function StatusMultiSelect({ statuses, STATUS, statusOn, counts, onSet }) {
    const [open, setOpen] = React.useState(false);
    const ref = React.useRef(null);
    React.useEffect(() => {
      if (!open) return;
      const onDown = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
      window.addEventListener("mousedown", onDown);
      return () => window.removeEventListener("mousedown", onDown);
    }, [open]);

    const onCount = statuses.filter(k => statusOn[k]).length;
    const total = statuses.length;
    const allOn = onCount === total;
    const lbl = allOn ? "Alle statussen" :
                onCount === 0 ? "Geen statussen" :
                onCount === 1 ? STATUS[statuses.find(k => statusOn[k])].label :
                `${onCount} statussen`;

    function toggle(k) {
      onSet({ ...statusOn, [k]: !statusOn[k] });
    }
    function setAll(v) {
      const next = {};
      statuses.forEach(k => { next[k] = v; });
      onSet(next);
    }

    return (
      <div ref={ref} className="cw-pq-msel">
        <button type="button"
                className={"cw-pq-msel-btn" + (open ? " is-open" : "")}
                onClick={() => setOpen(!open)}>
          <span className="cw-pq-msel-lbl">
            <span className="cw-pq-msel-icon">⚑</span>
            Status: <b>{lbl}</b>
          </span>
          <span className="cw-pq-msel-chev">▾</span>
        </button>
        {open && (
          <div className="cw-pq-msel-menu">
            <div className="cw-pq-msel-toolbar">
              <button className="cw-pq-msel-link" onClick={() => setAll(true)}>Alle aan</button>
              <span className="cw-pq-msel-sep">·</span>
              <button className="cw-pq-msel-link" onClick={() => setAll(false)}>Alle uit</button>
            </div>
            <div className="cw-pq-msel-list">
              {statuses.map(k => {
                const s = STATUS[k];
                const on = !!statusOn[k];
                const cnt = counts[k] || 0;
                return (
                  <label key={k} className={"cw-pq-msel-item" + (on ? " is-on" : "")}>
                    <input type="checkbox" checked={on} onChange={() => toggle(k)} />
                    <span className="cw-status-dot" style={{ background: s.dot }} />
                    <span className="cw-pq-msel-item-lbl">{s.label}</span>
                    <span className="cw-pq-msel-item-cnt">{cnt}</span>
                  </label>
                );
              })}
            </div>
          </div>
        )}
      </div>
    );
  }

  function ParkingColumn({ pending, parked, artists, onStartDragPending, onStartDragParked, isDropTarget, isDragOrigin, onSelectQueueItem, selectedQueueId }) {
    const STATUS = window.CrewliHelpers.STATUS;
    const GENRES = window.CrewliData.GENRES;
    // Closed status list. "requested" is mapped to pending items (they don't have an explicit status yet).
    const STATUSES = ["concept", "requested", "option", "confirmed", "contracted", "cancelled"];

    const [search, setSearch]             = React.useState("");
    const [filterGenre, setFilterGenre]   = React.useState("__all");
    // Multi-status set: cancelled is OFF by default.
    const [statusOn, setStatusOn]         = React.useState(() => {
      const s = {}; STATUSES.forEach(k => { s[k] = (k !== "cancelled"); }); return s;
    });
    const [groupBy, setGroupBy]           = React.useState("status"); // "status" | "none"
    const [genreOpen, setGenreOpen]       = React.useState(false);

    // Normalize: each item has { id, kind, artist, status, dur, src }
    const items = React.useMemo(() => {
      const out = [];
      pending.forEach(pa => {
        const a = artists.find(x => x.id === pa.artist_id);
        if (a) out.push({ id: pa.id, kind: "pending", artist: a, status: "requested", dur: null, src: pa, sortKey: a.name.toLowerCase() });
      });
      parked.forEach(p => {
        const a = artists.find(x => x.id === p.artist_id);
        if (a) {
          const computed = p.end - p.start;
          const dur = Number.isFinite(computed) ? computed : (p.dur || 60);
          out.push({ id: p.id, kind: "parked", artist: a, status: p.status, dur, src: p, sortKey: a.name.toLowerCase() });
        }
      });
      return out.sort((x, y) => x.sortKey.localeCompare(y.sortKey));
    }, [pending, parked, artists]);

    const q = search.trim().toLowerCase();

    // Apply filters
    const filtered = items.filter(it => {
      if (filterGenre  !== "__all" && it.artist.genre !== filterGenre)  return false;
      if (!statusOn[it.status]) return false;
      if (q && !it.artist.name.toLowerCase().includes(q)) return false;
      return true;
    });

    const hiddenByStatus = items.length - items.filter(it => statusOn[it.status]).length;

    // Group
    const groups = React.useMemo(() => {
      if (groupBy === "none") return [{ key: "_all", label: null, items: filtered }];
      const byStatus = STATUSES.map(s => ({ key: s, label: STATUS[s].label, items: filtered.filter(it => it.status === s) }))
                               .filter(g => g.items.length > 0);
      return byStatus;
    }, [filtered, groupBy]);

    const toggleStatus = (k) => setStatusOn(prev => ({ ...prev, [k]: !prev[k] }));

    const renderItem = (it) => {
      const isSelected = selectedQueueId === (it.kind + ":" + it.id);
      return (
        <div key={it.kind + ":" + it.id}
             className={"cw-parking-item" + (isSelected ? " is-selected" : "") + (it.status === "cancelled" ? " is-cancelled" : "")}
             onMouseDown={(e) => it.kind === "pending"
               ? onStartDragPending(e, it.src, it)
               : onStartDragParked(e, it.src, it)}>
          <span className="cw-avatar">{it.artist.initials}</span>
          <div className="cw-parking-item-body">
            <div className="cw-parking-item-name">{it.artist.name}</div>
            <div className="cw-parking-item-meta">
              <span className="cw-status-dot" style={{ background: STATUS[it.status].dot }} />
              <span className="cw-parking-item-status">{STATUS[it.status].label}</span>
              <span className="cw-parking-item-sep">·</span>
              <span className="cw-parking-item-genre">{it.artist.genre}</span>
              {it.dur != null && (
                <>
                  <span className="cw-parking-item-sep">·</span>
                  <span className="cw-parking-item-dur">{Math.floor(it.dur/60)}u{it.dur%60 ? ` ${it.dur%60}m` : ""}</span>
                </>
              )}
            </div>
          </div>
        </div>
      );
    };

    const genreLabel = filterGenre === "__all" ? "Alle genres" : filterGenre;

    return (
      <aside className={"cw-parking" + (isDropTarget ? " is-drop-target" : "") + (isDragOrigin ? " is-drag-origin" : "")}>
        <header className="cw-parking-hd">
          <div className="cw-parking-title">
            Wachtrij
            <span className="cw-parking-meta">{items.length}</span>
          </div>
        </header>

        <div className="cw-pq-filters">
          {/* Top row: search field + group-by-status toggle */}
          <div className="cw-pq-row">
            <label className="cw-pq-search">
              <svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round">
                <circle cx="7" cy="7" r="4"/><path d="M10 10 L13.5 13.5"/>
              </svg>
              <input type="text" placeholder="Zoek artiest…"
                     value={search} onChange={(e) => setSearch(e.target.value)} />
              {search && (
                <button className="cw-pq-search-x" onClick={() => setSearch("")} aria-label="Wissen">×</button>
              )}
            </label>
            <button type="button"
                    className={"cw-pq-icon-btn" + (groupBy === "status" ? " is-on" : "")}
                    onClick={() => setGroupBy(groupBy === "status" ? "none" : "status")}
                    title={groupBy === "status" ? "Groepering: status — klik om uit te zetten" : "Groepering: uit — klik voor groep op status"}>
              <svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round">
                <path d="M3 3.5 H11 M3 7 H11 M3 10.5 H11"/>
                <circle cx="1.5" cy="3.5" r=".7" fill="currentColor" stroke="none"/>
                <circle cx="1.5" cy="7" r=".7" fill="currentColor" stroke="none"/>
                <circle cx="1.5" cy="10.5" r=".7" fill="currentColor" stroke="none"/>
              </svg>
            </button>
          </div>

          {/* Status: multi-select dropdown */}
          <StatusMultiSelect
            statuses={STATUSES} STATUS={STATUS} statusOn={statusOn}
            counts={STATUSES.reduce((m, k) => { m[k] = items.filter(it => it.status === k).length; return m; }, {})}
            onToggle={toggleStatus}
            onSet={(next) => setStatusOn(next)}
          />

          {/* Genre pill row — horizontally scrollable */}
          <div className="cw-pq-genres" role="radiogroup" aria-label="Filter op genre">
            <button className={"cw-pq-genre-pill" + (filterGenre === "__all" ? " is-on" : "")}
                    onClick={() => setFilterGenre("__all")}>
              Alle
            </button>
            {GENRES.map(g => {
              const cnt = items.filter(it => it.artist.genre === g).length;
              if (cnt === 0) return null;
              return (
                <button key={g}
                        className={"cw-pq-genre-pill" + (filterGenre === g ? " is-on" : "")}
                        onClick={() => setFilterGenre(filterGenre === g ? "__all" : g)}>
                  {g}
                  <span className="cw-pq-genre-cnt">{cnt}</span>
                </button>
              );
            })}
          </div>
        </div>

        <div className="cw-parking-body">
          {filtered.length === 0 && (
            <div className="cw-parking-empty">
              {items.length === 0
                ? "Wachtrij is leeg"
                : hiddenByStatus === items.length
                  ? "Alle statussen uitgeschakeld"
                  : "Geen items voldoen aan dit filter"}
            </div>
          )}
          {groups.map(g => (
            <section key={g.key} className="cw-parking-section">
              {g.label && (
                <h5>
                  <span className="cw-parking-group-label">
                    <span className="cw-status-dot" style={{ background: STATUS[g.key].dot }} />
                    {g.label}
                  </span>
                  <span className="cw-parking-count">{g.items.length}</span>
                </h5>
              )}
              <div className="cw-parking-list">
                {g.items.map(renderItem)}
              </div>
            </section>
          ))}
        </div>

        <footer className="cw-parking-ft">
          {isDragOrigin
            ? <span><b>Loslaten = blijft in wachtrij.</b> Sleep naar een stage om te plannen.</span>
            : <span>Sleep een block hierheen om te <b>parkeren</b>, klik voor details</span>}
        </footer>
      </aside>
    );
  }

  // ─── Main Timetable ─────────────────────────────────────────────────
  function Timetable({
    activeDay, stages, performances,
    parked, pending,
    artists,
    dispatch,
    onSelectPerf, onClickEmptyCell, onEditStage, onScheduleFromParking,
    onSelectQueueItem, selectedQueueId,
    pxPerMin, baseRowHeight, density, showPercent,
  }) {
    const sections = window.CrewliData.ADVANCE_SECTIONS;
    const conflicts = window.CrewliHelpers.findConflicts(performances);
    const b2bLinks = window.CrewliHelpers.findB2B(performances, 5);
    const b2bRightSet = new Set(b2bLinks.map(l => l.leftId));
    const b2bLeftSet = new Set(b2bLinks.map(l => l.rightId));

    const [selectedId, setSelectedId] = React.useState(null);
    const [drag, setDrag] = React.useState(null); // { mode, perfId|paId, dx, dy, basePerf, sourceRect, fromParking }
    const [stageDrag, setStageDrag] = React.useState(null); // { fromIndex, toIndex, dy }

    // Compute the stages list to render — live-reordered while a stage drag is active.
    const displayedStages = React.useMemo(() => {
      if (!stageDrag) return stages;
      const arr = [...stages];
      const [moved] = arr.splice(stageDrag.fromIndex, 1);
      arr.splice(stageDrag.toIndex, 0, moved);
      return arr;
    }, [stages, stageDrag]);

    const startStageReorder = (e, fromIndex) => {
      const startY = e.clientY;
      const stagesEl = e.currentTarget.closest(".cw-tt-stages");
      // Snapshot original row geometries (in original order).
      const rows = stagesEl ? Array.from(stagesEl.querySelectorAll(".cw-tt-stage")) : [];
      const stagesRect = stagesEl ? stagesEl.getBoundingClientRect() : { top: 0 };
      const heights = rows.map(r => r.getBoundingClientRect().height);
      const total = heights.reduce((a, b) => a + b, 0);
      let toIndex = fromIndex;
      let moved = false;
      const onMove = (ev) => {
        if (!moved && Math.abs(ev.clientY - startY) < 4) return;
        moved = true;
        const yLocal = ev.clientY - stagesRect.top;
        // Find target index by midpoint of each row in ORIGINAL geometry
        let idx = rows.length - 1;
        let yAcc = 0;
        for (let i = 0; i < heights.length; i++) {
          const mid = yAcc + heights[i] / 2;
          if (yLocal < mid) { idx = i; break; }
          yAcc += heights[i];
        }
        idx = Math.max(0, Math.min(rows.length - 1, idx));
        toIndex = idx;
        setStageDrag({ fromIndex, toIndex, dy: ev.clientY - startY });
      };
      const onUp = () => {
        window.removeEventListener("mousemove", onMove);
        window.removeEventListener("mouseup", onUp);
        if (moved && toIndex !== fromIndex) {
          // Compute displayed (reordered) list of stage IDs and dispatch
          const arr = [...stages];
          const [m] = arr.splice(fromIndex, 1);
          arr.splice(toIndex, 0, m);
          dispatch({ type: "reorder_stages", dayStageIds: arr.map(s => s.id) });
        }
        setStageDrag(null);
      };
      window.addEventListener("mousemove", onMove);
      window.addEventListener("mouseup", onUp);
      e.preventDefault();
    };
    const totalMin = window.CrewliData.TIME.totalMinutes;
    const snap = window.CrewliData.TIME.snapMinutes;

    // ── Sync horizontal scroll between canvas and time-axis ───────
    const canvasRef = React.useRef(null);
    const axisInnerRef = React.useRef(null);
    React.useEffect(() => {
      const c = canvasRef.current; if (!c) return;
      const onScroll = () => {
        if (axisInnerRef.current) {
          axisInnerRef.current.style.transform = `translateX(${-c.scrollLeft}px)`;
        }
      };
      c.addEventListener("scroll", onScroll, { passive: true });
      return () => c.removeEventListener("scroll", onScroll);
    }, []);

    // ── Lane assignment per stage row (computed against displayedStages) ──
    // Lane size is FIXED — adding lanes grows the row, never shrinks the blocks.
    // Block size is FIXED & generous regardless of lane count.
    const LANE_STEP = density === "compact" ? 56 : density === "comfy" ? 76 : 64;
    const LANE_BLOCK_H = LANE_STEP - 4;
    const laneByStage = {};
    const rowHeights = [];
    const rowTops = [];
    let cumY = 0;
    for (const s of displayedStages) {
      const items = performances.filter(p => p.stage_id === s.id);
      const lanes = window.CrewliHelpers.assignLanes(items);
      laneByStage[s.id] = lanes;
      const innerH = lanes.laneCount * LANE_STEP + LANE_PAD * 2;
      const h = Math.max(baseRowHeight, innerH);
      rowHeights.push(h);
      rowTops.push(cumY);
      cumY += h;
    }
    const totalH = cumY;

    const stageRowIndex = {};
    displayedStages.forEach((s, i) => { stageRowIndex[s.id] = i; });

    function rowYToIndex(y) {
      for (let i = 0; i < rowTops.length; i++) {
        const top = rowTops[i];
        const bot = top + rowHeights[i];
        if (y >= top && y < bot) return i;
      }
      if (y < 0) return 0;
      return rowTops.length - 1;
    }

    // ── Block drag (move) ──────────────────────────────────────────
    const startDragMove = (e, perf) => {
      const startX = e.clientX, startY = e.clientY;
      let moved = false;
      const onMove = (ev) => {
        if (!moved && (Math.abs(ev.clientX - startX) > 3 || Math.abs(ev.clientY - startY) > 3)) moved = true;
        if (!moved) return;
        setDrag({ mode: "move", perfId: perf.id,
                  dx: ev.clientX - startX, dy: ev.clientY - startY,
                  basePerf: perf, mouseX: ev.clientX, mouseY: ev.clientY });
      };
      const onUp = (ev) => {
        const dx = ev.clientX - startX;
        const dy = ev.clientY - startY;
        window.removeEventListener("mousemove", onMove);
        window.removeEventListener("mouseup", onUp);
        if (!moved) { setDrag(null); return; }

        // Suppress the click event the browser fires after a drag
        const suppress = (e) => { e.stopPropagation(); e.preventDefault(); window.removeEventListener("click", suppress, true); };
        window.addEventListener("click", suppress, true);
        setTimeout(() => window.removeEventListener("click", suppress, true), 0);

        // Check if dropped on parking column
        const parkingEl = document.querySelector(".cw-parking");
        if (parkingEl) {
          const r = parkingEl.getBoundingClientRect();
          if (ev.clientX >= r.left && ev.clientX <= r.right &&
              ev.clientY >= r.top  && ev.clientY <= r.bottom) {
            dispatch({ type: "park_perf", id: perf.id });
            setDrag(null);
            return;
          }
        }
        const minDelta = window.CrewliHelpers.snap(dx / pxPerMin, snap);
        const newStart = Math.max(0, Math.min(totalMin - (perf.end - perf.start), perf.start + minDelta));
        const newEnd = newStart + (perf.end - perf.start);
        // Cursor-anchored target: row + lane derived from where the cursor is, not block-top.
        const canvasEl = document.querySelector(".cw-tt-canvas-inner");
        const cr = canvasEl.getBoundingClientRect();
        const targetY = ev.clientY - cr.top;
        const newRow = Math.max(0, Math.min(displayedStages.length - 1, rowYToIndex(targetY)));
        const newStageId = displayedStages[newRow]?.id || perf.stage_id;

        // Compute explicit lane assignment.
        let updated = { ...perf, start: newStart, end: newEnd, stage_id: newStageId };
        if ("lane_pref" in updated) delete updated.lane_pref;
        const cascadeUpdates = [];
        {
          // Drop logic: pick target lane from Y. If an existing block on that lane
          // OVERLAPS the new time → that's a "drop ON block": go to oLane + 1.
          // If target lane is free at that time → drop there exactly (no cascade).
          const targetStageId = newStageId;
          const targetLanes = laneByStage[targetStageId];
          const rowTop = rowTops[newRow];
          const yInRow = targetY - rowTop - LANE_PAD;
          // Round so a half-step movement snaps to the nearest lane.
          const rawLane = Math.max(0, Math.min(targetLanes.laneCount, Math.round(yInRow / LANE_STEP)));
          const stagePerfs = performances.filter(p => p.stage_id === targetStageId && p.id !== perf.id);
          const laneOfOther = (o) => Number.isInteger(o.lane) ? o.lane : (targetLanes.laneOf[o.id] || 0);
          const blockAtLaneTime = stagePerfs.find(o =>
            laneOfOther(o) === rawLane &&
            o.start < newEnd && o.end > newStart);

          let finalLane = rawLane;
          if (blockAtLaneTime) finalLane = laneOfOther(blockAtLaneTime) + 1;
          updated.lane = finalLane;

          // Cascade only kicks in if the chosen lane has a conflict (rare:
          // could happen if multiple items share the lane below).
          const queue = [{ lane: finalLane, start: newStart, end: newEnd }];
          const queued = new Set([perf.id]);
          while (queue.length > 0) {
            const cur = queue.shift();
            for (const o of stagePerfs) {
              if (queued.has(o.id)) continue;
              const oLane = laneOfOther(o);
              if (oLane !== cur.lane) continue;
              if (!(o.start < cur.end && o.end > cur.start)) continue;
              const bumpLane = oLane + 1;
              cascadeUpdates.push({ ...o, lane: bumpLane });
              queued.add(o.id);
              queue.push({ lane: bumpLane, start: o.start, end: o.end });
            }
          }
        }
        dispatch({ type: "update_perf", perf: updated });
        for (const u of cascadeUpdates) dispatch({ type: "update_perf", perf: u });
        setDrag(null);
      };
      window.addEventListener("mousemove", onMove);
      window.addEventListener("mouseup", onUp);
      e.preventDefault();
    };

    const startDragResize = (e, perf) => {
      const startX = e.clientX;
      let resized = false;
      const onMove = (ev) => {
        if (!resized && Math.abs(ev.clientX - startX) > 2) resized = true;
        setDrag({ mode: "resize", perfId: perf.id, dx: ev.clientX - startX });
      };
      const onUp = (ev) => {
        const dx = ev.clientX - startX;
        window.removeEventListener("mousemove", onMove);
        window.removeEventListener("mouseup", onUp);
        if (resized) {
          const suppress = (e) => { e.stopPropagation(); e.preventDefault(); window.removeEventListener("click", suppress, true); };
          window.addEventListener("click", suppress, true);
          setTimeout(() => window.removeEventListener("click", suppress, true), 0);
        }
        const delta = window.CrewliHelpers.snap(dx / pxPerMin, snap);
        const newEnd = Math.max(perf.start + 15, Math.min(totalMin, perf.end + delta));
        dispatch({ type: "update_perf", perf: { ...perf, end: newEnd }});
        setDrag(null);
      };
      window.addEventListener("mousemove", onMove);
      window.addEventListener("mouseup", onUp);
      e.preventDefault();
      e.stopPropagation();
    };

    // ── Parking → timetable drag (pending or parked) ───────────────
    const startDragFromParking = (e, source, fullItem) => {
      // source: { kind: 'pending' | 'parked', item }
      // fullItem: optional — the normalized queue row { kind, id, artist, status, src, ... }
      const startX = e.clientX, startY = e.clientY;
      const sourceEl = e.currentTarget;
      let moved = false;
      const onMove = (ev) => {
        if (!moved && (Math.abs(ev.clientX - startX) > 4 || Math.abs(ev.clientY - startY) > 4)) moved = true;
        if (!moved) return;
        setDrag({
          mode: "from_parking",
          kind: source.kind, item: source.item,
          mouseX: ev.clientX, mouseY: ev.clientY,
          moved: true,
        });
      };
      const onUp = (ev) => {
        window.removeEventListener("mousemove", onMove);
        window.removeEventListener("mouseup", onUp);
        if (!moved) {
          // Pure click → open queue popover
          setDrag(null);
          if (fullItem && onSelectQueueItem) {
            const r = sourceEl.getBoundingClientRect();
            onSelectQueueItem(fullItem, r);
          }
          return;
        }
        // Suppress the synthetic click after drop
        const suppress = (e) => { e.stopPropagation(); e.preventDefault(); window.removeEventListener("click", suppress, true); };
        window.addEventListener("click", suppress, true);
        setTimeout(() => window.removeEventListener("click", suppress, true), 0);
        // Drop within parking column = stay parked, do nothing
        const parkingEl = document.querySelector(".cw-parking");
        if (parkingEl) {
          const pr = parkingEl.getBoundingClientRect();
          if (ev.clientX >= pr.left && ev.clientX <= pr.right &&
              ev.clientY >= pr.top  && ev.clientY <= pr.bottom) { setDrag(null); return; }
        }
        // hit-test against canvas
        const canvasEl = document.querySelector(".cw-tt-canvas-inner");
        if (!canvasEl) { setDrag(null); return; }
        const r = canvasEl.getBoundingClientRect();
        const x = ev.clientX - r.left;
        const y = ev.clientY - r.top;
        if (x < 0 || y < 0 || x > r.width || y > totalH) { setDrag(null); return; }
        const minute = Math.max(0, window.CrewliHelpers.snap(x / pxPerMin, snap));
        const rowIdx = rowYToIndex(y);
        const stage = displayedStages[rowIdx];
        if (!stage) { setDrag(null); return; }

        // Determine target lane from cursor Y within the row.
        const dur = source.kind === "parked"
          ? (Number.isFinite(source.item.end - source.item.start) ? (source.item.end - source.item.start) : (source.item.dur || 60))
          : (source.item.dur || 60);
        const newStart = minute;
        const newEnd = minute + dur;
        const rowTop = rowTops[rowIdx];
        const yInRow = y - rowTop - LANE_PAD;
        const targetLanes = laneByStage[stage.id];
        const rawLane = Math.max(0, Math.min(targetLanes.laneCount, Math.round(yInRow / LANE_STEP)));
        const excludeId = source.kind === "parked" ? source.item.id : null;
        const stagePerfs = performances.filter(p => p.stage_id === stage.id && p.id !== excludeId);
        const laneOfOther = (o) => Number.isInteger(o.lane) ? o.lane : (targetLanes.laneOf[o.id] || 0);
        const blockAtLaneTime = stagePerfs.find(o =>
          laneOfOther(o) === rawLane &&
          o.start < newEnd && o.end > newStart);
        let finalLane = rawLane;
        if (blockAtLaneTime) finalLane = laneOfOther(blockAtLaneTime) + 1;

        // Cascade: bump conflicting blocks below.
        const cascadeUpdates = [];
        const queue = [{ lane: finalLane, start: newStart, end: newEnd }];
        const queued = new Set(excludeId ? [excludeId] : []);
        while (queue.length > 0) {
          const cur = queue.shift();
          for (const o of stagePerfs) {
            if (queued.has(o.id)) continue;
            const oLane = laneOfOther(o);
            if (oLane !== cur.lane) continue;
            if (!(o.start < cur.end && o.end > cur.start)) continue;
            const bumpLane = oLane + 1;
            cascadeUpdates.push({ ...o, lane: bumpLane });
            queued.add(o.id);
            queue.push({ lane: bumpLane, start: o.start, end: o.end });
          }
        }

        if (source.kind === "pending") {
          onScheduleFromParking({ pending: source.item, stage, minute, lane: finalLane });
        } else {
          // parked perf — restore with new stage + start + lane
          const updated = { ...source.item, stage_id: stage.id, start: newStart, end: newEnd, lane: finalLane };
          if ("lane_pref" in updated) delete updated.lane_pref;
          dispatch({ type: "update_perf", perf: updated });
        }
        for (const u of cascadeUpdates) dispatch({ type: "update_perf", perf: u });
        setDrag(null);
      };
      window.addEventListener("mousemove", onMove);
      window.addEventListener("mouseup", onUp);
      e.preventDefault();
    };

    function startCreateDrag(e, stage) {
      if (e.target !== e.currentTarget) return;
      if (e.button !== 0) return;
      const rowEl = e.currentTarget;
      const rect = rowEl.getBoundingClientRect();
      const startMinute = Math.max(0, window.CrewliHelpers.snap((e.clientX - rect.left) / pxPerMin, snap));
      const startY = e.clientY;
      const startX = e.clientX;
      // Lane derived from initial Y so the create-ghost lands on the correct sub-swimlane.
      // Use floor (not round) so anywhere within a lane = that lane, niet de volgende.
      const yInRow0 = e.clientY - rect.top - LANE_PAD;
      const stageLanes = laneByStage[stage.id];
      const initLane = Math.max(0, Math.min(stageLanes.laneCount, Math.floor(yInRow0 / LANE_STEP)));
      setDrag({ mode: "create", stage, startMinute, currentMinute: startMinute, lane: initLane, moved: false });
      const onMove = (ev) => {
        const dx = ev.clientX - startX;
        const dy = ev.clientY - startY;
        const moved = Math.abs(dx) > 3 || Math.abs(dy) > 3;
        const x = ev.clientX - rect.left;
        const m = Math.max(0, Math.min(totalMin, window.CrewliHelpers.snap(x / pxPerMin, snap)));
        // Live-recompute lane from cursor so ghost follows vertically too.
        const yInRow = ev.clientY - rect.top - LANE_PAD;
        const lane = Math.max(0, Math.min(stageLanes.laneCount, Math.floor(yInRow / LANE_STEP)));
        setDrag({ mode: "create", stage, startMinute, currentMinute: m, lane, moved });
      };
      const onUp = (ev) => {
        window.removeEventListener("mousemove", onMove);
        window.removeEventListener("mouseup", onUp);
        const x = ev.clientX - rect.left;
        const endMinute = Math.max(0, Math.min(totalMin, window.CrewliHelpers.snap(x / pxPerMin, snap)));
        const lo = Math.min(startMinute, endMinute);
        const hi = Math.max(startMinute, endMinute);
        // If no drag (pure click), default to 60 min.
        const finalEnd = (hi - lo) < 15 ? Math.min(totalMin, lo + 60) : hi;
        const yInRow = ev.clientY - rect.top - LANE_PAD;
        const lane = Math.max(0, Math.min(stageLanes.laneCount, Math.floor(yInRow / LANE_STEP)));
        setDrag(null);
        onClickEmptyCell(stage, lo, finalEnd, lane);
      };
      window.addEventListener("mousemove", onMove);
      window.addEventListener("mouseup", onUp);
      e.preventDefault();
    }

    const draggingOverParking = (() => {
      if (!drag || drag.mode !== "move") return false;
      const parkingEl = document.querySelector(".cw-parking");
      if (!parkingEl || !drag.mouseX) return false;
      const r = parkingEl.getBoundingClientRect();
      return drag.mouseX >= r.left && drag.mouseX <= r.right;
    })();

    const rowDividers = rowTops.slice(1).concat([totalH]);

    return (
      <div className="cw-tt">
        <div className="cw-tt-corner" style={{ width: STAGE_COL_W }}>
          <span className="cw-tt-corner-lbl">Stages</span>
          <span className="cw-tt-corner-meta">{displayedStages.length} actief</span>
        </div>

        <div className="cw-tt-axis-wrap">
          <div ref={axisInnerRef} className="cw-tt-axis-inner">
            <TimeAxis pxPerMin={pxPerMin} totalMin={totalMin} startHour={window.CrewliData.TIME.startHour} />
          </div>
        </div>

        <div className="cw-tt-stages" style={{ width: STAGE_COL_W }}>
          {displayedStages.map((s, i) => {
            const isDragging = stageDrag && stageDrag.toIndex === i;
            const transform = isDragging ? `translateY(${stageDrag.dy - (rowTops[stageDrag.toIndex] - rowTops[stageDrag.fromIndex])}px)` : null;
            return (
            <div key={s.id}
                 className={"cw-tt-stage" + (isDragging ? " is-dragging-row" : "") + (stageDrag ? " is-reflowing" : "")}
                 style={{ height: rowHeights[i], transform, zIndex: isDragging ? 5 : null }}>
              <span className="cw-stage-handle"
                    title="Sleep om volgorde te wijzigen"
                    onClick={(e) => e.stopPropagation()}
                    onMouseDown={(e) => { e.stopPropagation(); startStageReorder(e, i); }}>
                <svg width="10" height="14" viewBox="0 0 10 14"><circle cx="3" cy="3" r="1.1"/><circle cx="3" cy="7" r="1.1"/><circle cx="3" cy="11" r="1.1"/><circle cx="7" cy="3" r="1.1"/><circle cx="7" cy="7" r="1.1"/><circle cx="7" cy="11" r="1.1"/></svg>
              </span>
              <span className="cw-stage-swatch" style={{ background: s.color }} />
              <div className="cw-tt-stage-info">
                <span className="cw-tt-stage-name">{s.name}</span>
                <span className="cw-tt-stage-cap">cap. {s.capacity.toLocaleString("nl-NL")}</span>
              </div>
              {(() => {
                const stageConflicts = performances.filter(p => p.stage_id === s.id && conflicts.has(p.id)).length;
                if (stageConflicts > 0) {
                  return (
                    <span className="cw-tt-stage-conflict" title={`${stageConflicts} conflict${stageConflicts > 1 ? "en" : ""} op deze stage`}>
                      {stageConflicts}
                    </span>
                  );
                }
                return null;
              })()}
              <button className="cw-stage-edit-btn"
                      title="Stage bewerken"
                      onClick={(e) => { e.stopPropagation(); if (stageDrag) return; onEditStage(s); }}>
                Bewerken
              </button>
            </div>
          );})}
          {displayedStages.length === 0 && (
            <div className="cw-tt-empty-stages">Geen stages op deze dag.</div>
          )}
        </div>

        <div className="cw-tt-canvas" ref={canvasRef}>
          <div className="cw-tt-canvas-inner" style={{ width: totalMin * pxPerMin, height: totalH || 200 }}>
            <GridBg pxPerMin={pxPerMin} totalMin={totalMin} totalH={totalH} rowDividers={rowDividers} />

            {displayedStages.map((s, i) => {
              const lanes = laneByStage[s.id];
              const laneStep = LANE_STEP;
              const laneHeight = LANE_BLOCK_H;
              const items = performances.filter(p => p.stage_id === s.id);
              const isRowDragging = stageDrag && stageDrag.toIndex === i;
              const rowTransform = isRowDragging
                ? `translateY(${stageDrag.dy - (rowTops[stageDrag.toIndex] - rowTops[stageDrag.fromIndex])}px)`
                : null;
              return (
                <div key={s.id}
                     className={"cw-tt-row" + (isRowDragging ? " is-dragging-row" : "") + (stageDrag ? " is-reflowing" : "")}
                     style={{ top: rowTops[i], height: rowHeights[i], transform: rowTransform, zIndex: isRowDragging ? 4 : null }}
                     onMouseDown={(e) => startCreateDrag(e, s)}>
                  {items.map((p) => {
                    const artist = artists.find(a => a.id === p.artist_id);
                    let renderPerf = p;
                    let renderInThisRow = true;
                    if (drag && drag.perfId === p.id && drag.mode === "move") {
                      // Hide original block when dragged over parking column
                      const parkingEl = document.querySelector(".cw-parking");
                      if (parkingEl && drag.mouseX) {
                        const pr = parkingEl.getBoundingClientRect();
                        if (drag.mouseX >= pr.left && drag.mouseX <= pr.right) renderInThisRow = false;
                      }
                      const minDelta = window.CrewliHelpers.snap(drag.dx / pxPerMin, snap);
                      // Cursor-anchored: row from cursor Y, not from block-top + dy.
                      const canvasEl = document.querySelector(".cw-tt-canvas-inner");
                      let newStageId = p.stage_id;
                      if (canvasEl && drag.mouseY) {
                        const cr = canvasEl.getBoundingClientRect();
                        const targetY = drag.mouseY - cr.top;
                        const newRow = Math.max(0, Math.min(displayedStages.length - 1, rowYToIndex(targetY)));
                        newStageId = displayedStages[newRow]?.id || p.stage_id;
                      }
                      if (newStageId !== p.stage_id) renderInThisRow = false;
                      renderPerf = { ...p, start: p.start + minDelta, end: p.end + minDelta };
                    } else if (drag && drag.perfId === p.id && drag.mode === "resize") {
                      const delta = window.CrewliHelpers.snap(drag.dx / pxPerMin, snap);
                      renderPerf = { ...p, end: Math.max(p.start + 15, p.end + delta) };
                    }
                    if (!renderInThisRow) return null;
                    let lane = lanes.laneOf[p.id] ?? 0;
                    // Live lane shift while dragging within the same stage — cursor-anchored.
                    if (drag && drag.perfId === p.id && drag.mode === "move") {
                      const canvasEl = document.querySelector(".cw-tt-canvas-inner");
                      if (canvasEl && drag.mouseY) {
                        const cr = canvasEl.getBoundingClientRect();
                        const targetY = drag.mouseY - cr.top;
                        const newRow = Math.max(0, Math.min(displayedStages.length - 1, rowYToIndex(targetY)));
                        const newStageId = displayedStages[newRow]?.id;
                        if (newStageId === p.stage_id) {
                          const yInRow = targetY - rowTops[newRow] - LANE_PAD;
                          const rawLane = Math.max(0, Math.min(lanes.laneCount, Math.round(yInRow / LANE_STEP)));
                          const stagePerfs = performances.filter(o => o.stage_id === p.stage_id && o.id !== p.id);
                          const laneOfOther = (o) => Number.isInteger(o.lane) ? o.lane : (lanes.laneOf[o.id] || 0);
                          const newStart = renderPerf.start;
                          const newEnd = renderPerf.end;
                          const hit = stagePerfs.find(o => laneOfOther(o) === rawLane && o.start < newEnd && o.end > newStart);
                          lane = hit ? laneOfOther(hit) + 1 : rawLane;
                        }
                      }
                    }
                    const laneTop = LANE_PAD + lane * laneStep;
                    return (
                      <Block key={p.id}
                        perf={renderPerf} artist={artist} stage={s} sections={sections}
                        pxPerMin={pxPerMin}
                        laneTop={laneTop} laneHeight={laneHeight}
                        conflicting={conflicts.has(p.id)}
                        capWarn={window.CrewliHelpers.isCapacityWarn(artist, s)}
                        b2bRight={b2bRightSet.has(p.id)} b2bLeft={b2bLeftSet.has(p.id)}
                        isSelected={selectedId === p.id}
                        showPercent={showPercent}
                        onSelect={(perf, rect) => { setSelectedId(perf.id); onSelectPerf(perf, rect); }}
                        onStartDragMove={startDragMove}
                        onStartDragResize={startDragResize}
                      />
                    );
                  })}
                </div>
              );
            })}

            {/* Move-ghost in target row */}
            {drag && drag.mode === "move" && (() => {
              const p = performances.find(x => x.id === drag.perfId); if (!p) return null;
              const artist = artists.find(a => a.id === p.artist_id);
              const minDelta = window.CrewliHelpers.snap(drag.dx / pxPerMin, snap);
              const canvasEl = document.querySelector(".cw-tt-canvas-inner");
              if (!canvasEl || !drag.mouseY) return null;
              const cr = canvasEl.getBoundingClientRect();
              const targetY = drag.mouseY - cr.top;
              const newRow = Math.max(0, Math.min(displayedStages.length - 1, rowYToIndex(targetY)));
              const newStageId = displayedStages[newRow]?.id;
              if (!newStageId || newStageId === p.stage_id) return null;
              const targetStage = displayedStages[newRow];
              const newStart = p.start + minDelta;
              const newEnd = p.end + minDelta;
              const renderPerf = { ...p, start: newStart, end: newEnd };

              // Preview lane in the target row, mirror drop logic.
              const targetLanes = laneByStage[newStageId];
              const rowTop = rowTops[newRow];
              const yInRow = targetY - rowTop - LANE_PAD;
              const rawLane = Math.max(0, Math.min(targetLanes.laneCount, Math.round(yInRow / LANE_STEP)));
              const stagePerfs = performances.filter(o => o.stage_id === newStageId && o.id !== p.id);
              const laneOfOther = (o) => Number.isInteger(o.lane) ? o.lane : (targetLanes.laneOf[o.id] || 0);
              const blockAtLaneTime = stagePerfs.find(o =>
                laneOfOther(o) === rawLane &&
                o.start < newEnd && o.end > newStart);
              const finalLane = blockAtLaneTime ? laneOfOther(blockAtLaneTime) + 1 : rawLane;
              const ghostLaneTop = LANE_PAD + finalLane * LANE_STEP;

              return (
                <div className="cw-tt-row cw-tt-row-ghost"
                     style={{ top: rowTops[newRow], height: rowHeights[newRow] }}>
                  <Block perf={renderPerf} artist={artist} stage={targetStage} sections={sections}
                    pxPerMin={pxPerMin}
                    laneTop={ghostLaneTop} laneHeight={LANE_BLOCK_H}
                    conflicting={false} capWarn={false}
                    b2bRight={false} b2bLeft={false}
                    isSelected={false} showPercent={showPercent}
                    onSelect={() => {}} onStartDragMove={() => {}} onStartDragResize={() => {}}
                  />
                </div>
              );
            })()}

            {/* Create-drag ghost — visible while dragging out a new performance */}
            {drag && drag.mode === "create" && (() => {
              const stage = drag.stage;
              const rowIdx = stageRowIndex[stage.id];
              if (rowIdx == null) return null;
              const lo = Math.min(drag.startMinute, drag.currentMinute);
              const hi = Math.max(drag.startMinute, drag.currentMinute);
              const dur = Math.max(0, hi - lo);
              const previewDur = drag.moved ? dur : 60;
              const left = lo * pxPerMin;
              const width = Math.max(40, previewDur * pxPerMin);
              const ghostLaneTop = LANE_PAD + (drag.lane || 0) * LANE_STEP;
              return (
                <div className="cw-tt-row cw-tt-row-ghost"
                     style={{ top: rowTops[rowIdx], height: rowHeights[rowIdx] }}>
                  <div className="cw-tt-create-ghost"
                       style={{ left, width, top: ghostLaneTop, height: LANE_BLOCK_H, borderColor: stage.color }}>
                    <span className="cw-tt-create-ghost-time">
                      {drag.moved && dur >= 15 ? (
                        <><b>{dur}m</b> · {window.CrewliHelpers.fmtTime(lo, window.CrewliData.TIME.startHour)}–{window.CrewliHelpers.fmtTime(hi, window.CrewliData.TIME.startHour)}</>
                      ) : (
                        <b>{Math.max(0, dur)}m</b>
                      )}
                    </span>
                  </div>
                </div>
              );
            })()}

            {/* From-parking ghost */}
            {drag && drag.mode === "from_parking" && drag.moved && (() => {
              const canvasEl = document.querySelector(".cw-tt-canvas-inner");
              if (!canvasEl) return null;
              const r = canvasEl.getBoundingClientRect();
              const x = drag.mouseX - r.left;
              const y = drag.mouseY - r.top;
              if (x < 0 || y < 0 || y > totalH) return null;
              const minute = Math.max(0, window.CrewliHelpers.snap(x / pxPerMin, snap));
              const rowIdx = rowYToIndex(y);
              const stage = displayedStages[rowIdx]; if (!stage) return null;
              const dur = drag.kind === "parked"
                ? (Number.isFinite(drag.item.end - drag.item.start) ? (drag.item.end - drag.item.start) : (drag.item.dur || 60))
                : (drag.item.dur || 60);
              const artistId = drag.kind === "parked" ? drag.item.artist_id : drag.item.artist_id;
              const artist = artists.find(a => a.id === artistId);
              const status = drag.kind === "parked" ? drag.item.status : "concept";
              const newStart = minute;
              const newEnd = minute + dur;
              // Mirror drop logic: pick lane from Y, bump if a block on that lane overlaps in time.
              const rowTop = rowTops[rowIdx];
              const yInRow = y - rowTop - LANE_PAD;
              const targetLanes = laneByStage[stage.id];
              const rawLane = Math.max(0, Math.min(targetLanes.laneCount, Math.round(yInRow / LANE_STEP)));
              const excludeId = drag.kind === "parked" ? drag.item.id : null;
              const stagePerfs = performances.filter(p => p.stage_id === stage.id && p.id !== excludeId);
              const laneOfOther = (o) => Number.isInteger(o.lane) ? o.lane : (targetLanes.laneOf[o.id] || 0);
              const blockAtLaneTime = stagePerfs.find(o =>
                laneOfOther(o) === rawLane &&
                o.start < newEnd && o.end > newStart);
              const finalLane = blockAtLaneTime ? laneOfOther(blockAtLaneTime) + 1 : rawLane;
              const ghostLaneTop = LANE_PAD + finalLane * LANE_STEP;
              const renderPerf = { id: "ghost", start: minute, end: minute + dur, status, stage_id: stage.id, artist_id: artistId, lane: finalLane };
              return (
                <div className="cw-tt-row cw-tt-row-ghost"
                     style={{ top: rowTops[rowIdx], height: rowHeights[rowIdx] }}>
                  <Block perf={renderPerf} artist={artist} stage={stage} sections={sections}
                    pxPerMin={pxPerMin}
                    laneTop={ghostLaneTop} laneHeight={LANE_BLOCK_H}
                    conflicting={false} capWarn={false}
                    b2bRight={false} b2bLeft={false}
                    isSelected={false} showPercent={showPercent}
                    onSelect={() => {}} onStartDragMove={() => {}} onStartDragResize={() => {}}
                  />
                </div>
              );
            })()}

            {displayedStages.length === 0 && (
              <div className="cw-tt-empty">
                <h3>Geen stages op deze dag</h3>
                <p>Gebruik <b>Lineup per dag</b> om stages te activeren, of voeg een nieuwe stage toe.</p>
              </div>
            )}
          </div>
        </div>

        <ParkingColumn
          pending={pending} parked={parked}
          artists={artists}
          onStartDragPending={(e, item, full) => startDragFromParking(e, { kind: "pending", item }, full)}
          onStartDragParked={(e, item, full) => startDragFromParking(e, { kind: "parked", item }, full)}
          isDropTarget={draggingOverParking}
          isDragOrigin={drag && drag.mode === "from_parking" && drag.moved}
          onSelectQueueItem={onSelectQueueItem}
          selectedQueueId={selectedQueueId}
        />

        {/* Floating cursor chip when dragging a block toward the parking column */}
        {drag && drag.mode === "move" && draggingOverParking && (() => {
          const p = performances.find(x => x.id === drag.perfId); if (!p) return null;
          const artist = artists.find(a => a.id === p.artist_id);
          return (
            <div className="cw-floating-chip" style={{ left: drag.mouseX + 14, top: drag.mouseY + 6 }}>
              <span className="cw-avatar">{artist.initials}</span>
              <div>
                <div className="cw-floating-chip-name">{artist.name}</div>
                <div className="cw-floating-chip-hint">Loslaten = parkeren</div>
              </div>
            </div>
          );
        })()}

        {/* Floating cursor chip when dragging from parking — only show OUTSIDE canvas */}
        {drag && drag.mode === "from_parking" && drag.moved && (() => {
          const canvasEl = document.querySelector(".cw-tt-canvas-inner");
          if (!canvasEl) return null;
          const r = canvasEl.getBoundingClientRect();
          const insideCanvas = drag.mouseX >= r.left && drag.mouseX <= r.right &&
                               drag.mouseY >= r.top  && drag.mouseY <= r.top + totalH;
          if (insideCanvas) return null; // ghost in canvas already shows the target slot
          const artistId = drag.item.artist_id;
          const artist = artists.find(a => a.id === artistId);
          return (
            <div className="cw-floating-chip" style={{ left: drag.mouseX + 14, top: drag.mouseY + 6 }}>
              <span className="cw-avatar">{artist.initials}</span>
              <div>
                <div className="cw-floating-chip-name">{artist.name}</div>
                <div className="cw-floating-chip-hint">Sleep naar tijdvak · loslaten = annuleren</div>
              </div>
            </div>
          );
        })()}
      </div>
    );
  }

  return { Timetable };
})();

Object.assign(window, { CrewliTimetable: TT });
