const {
  useState,
  useRef,
  useEffect,
  useMemo,
  useCallback
} = React;

// ── FastAPI Backend Configuration ──
const API_BASE_URL = (window.API_BASE_URL || window.location.origin).replace(/\/+$/, '');
const API_AUDIO = `${API_BASE_URL}/api/v1/audio`;
const API_MULTITRACK = `${API_BASE_URL}/api/v1/multitrack`;
const API_TASKS = `${API_BASE_URL}/api/v1/audio/tasks`;

// ── Dedicated per-track MIDI channel allocation ──
// FluidSynth has 16 channels; if two tracks share a channel, arming one track
// re-selects the other track's program and its instrument changes. Every track
// gets its own stable, unique channel (0-15, skipping 9 which is the classic
// percussion slot) so multi-track ARM / playback never cross-contaminates
// instruments. Module-level so both App and the piano-roll sub-components
// (PianoRollTabEditor etc.) allocate the SAME channel for a track.
const trackMidiChannelsRef = { current: {} };
const ensureTrackMidiChannel = (track, tracks) => {
  if (!track) return 0;
  const trackList = tracks || [];
  const inUse = new Set();
  trackList.forEach(tr => { if (tr && tr.id !== track.id && tr.midiChannel !== undefined) inUse.add(tr.midiChannel); });
  const cached = trackMidiChannelsRef.current[track.id];
  if (cached !== undefined && !inUse.has(cached)) return cached;
  const preferred = track.midiChannel !== undefined && !inUse.has(track.midiChannel) ? track.midiChannel : null;
  if (preferred !== null) { trackMidiChannelsRef.current[track.id] = preferred; return preferred; }
  for (let c = 0; c < 16; c++) {
    if (c === 9) continue;
    if (!inUse.has(c)) { trackMidiChannelsRef.current[track.id] = c; return c; }
  }
  trackMidiChannelsRef.current[track.id] = 0;
  return 0;
};
const assignTrackMidiChannel = (track, tracks) => {
  const ch = ensureTrackMidiChannel(track, tracks);
  if (track && track.midiChannel !== ch) track.midiChannel = ch;
  return ch;
};

// ── Instrument context (PIANO ROLL play — nguồn duy nhất, mọi nơi dùng) ──
// Track "đã loaded instrument" = track.instrumentProgram (GM preset) HOẶC
// track.synth_engine (soundfont: soundfont_id/bank/program). Resolve thành
// context thống nhất cho schedulePianoRollMidi + preview (wheel/click/keybed)
// để note PHẢI chơi đúng instrument của track — không phụ thuộc st snapshot.
const resolveTrackInstrumentCtx = (track, tracks) => {
  const all = tracks || [];
  const ch = track ? assignTrackMidiChannel(track, all) : 0;
  const se = track ? track.synth_engine : undefined;
  const isSf = !!(se && (se.type === 'soundfont' || se.soundfont_id));
  if (isSf) {
    return {
      ch,
      program: undefined,               // SF path — synthEngine được ưu tiên trong _playNoteFluid
      synthEngine: se,
      sfId: se.soundfont_id,
      bank: se.soundfont_bank || 0,
      prog: se.soundfont_program || 0
    };
  }
  if (track && track.instrumentProgram !== undefined) {
    return { ch, program: track.instrumentProgram, synthEngine: undefined, sfId: undefined, bank: 0, prog: track.instrumentProgram };
  }
  return { ch, program: undefined, synthEngine: undefined, sfId: undefined, bank: 0, prog: 0 };
};

// ── Bridge program-change helper (D8/A12): gửi CC0/CC32 (bank THẬT) + PROGRAM
// qua bridge trước note-on — dedupe theo track+bank+program (không spam mỗi
// note; reset khi load instrument mới). Dùng chung mọi đường: timeline,
// preview, keybed. Trước đây bank hardcode 0 + preview/keybed KHÔNG gửi
// program change → SF2 preset ở bank≠0 ra piano sai.
window.__ensureBridgeProgram = (trkId, program, synthEngine) => {
  try {
    if (!window.SonicMidiRouter || !window.SonicMidiRouter.isBridgeActive()) return;
    const _bank = synthEngine && synthEngine.soundfont_bank !== undefined ? synthEngine.soundfont_bank : 0;
    const _prog = (program !== undefined && program !== null)
      ? program
      : (synthEngine && synthEngine.soundfont_program !== undefined ? synthEngine.soundfont_program : undefined);
    if (_prog === undefined || _prog === null) return;
    const _key = trkId + ':' + _bank + ':' + _prog;
    const _lp = window.__bridgeLastProgram || (window.__bridgeLastProgram = {});
    if (_lp[_key]) return;
    _lp[_key] = true;
    const _isPerc = _bank === 128;
    window.SonicMidiRouter.pushEvent({ cmd: 'CC', channel: trkId, pitch: 0, velocity: 0, data2: (_bank >> 7) & 0x7F, percussion: _isPerc });
    window.SonicMidiRouter.pushEvent({ cmd: 'CC', channel: trkId, pitch: 32, velocity: 0, data2: _bank & 0x7F, percussion: _isPerc });
    window.SonicMidiRouter.pushEvent({ cmd: 'PROGRAM', channel: trkId, pitch: 0, velocity: 0, data2: _prog, percussion: _isPerc });
  } catch (e) { console.warn('[Bridge] ensureBridgeProgram error:', e); }
};

// Đảm bảo FluidSynth channel của track đã select ĐÚNG instrument trước khi
// notes bắn. Fire-and-forget: playNote tự load + retry nếu SF chưa load xong
// (dedup sẵn trong loadSoundFont) — không chặn, không gây stall khi mở tab.
const ensureSonicInstrument = (ctx) => {
  try {
    if (!window.SonicSF || !window.SonicSF.selectInstrument) return;
    if (ctx.sfId) {
      window.SonicSF.selectInstrument(ctx.ch, ctx.bank, ctx.prog, ctx.sfId);
    } else if (ctx.program !== undefined) {
      window.SonicSF.selectInstrument(ctx.ch, 0, ctx.program, null);
    }
  } catch (e) {
    console.warn('[Instrument] ensureSonicInstrument error:', e);
  }
};

// ── Carla bridge MIDI scheduling helper ────────────────────────────────────
// Schedule note_on/note_off tới Carla (OSC qua backend) cho MIDI items khi
// track dùng VSTi + có Carla local. Dùng chung cho mọi đường playback
// (main timeline, local loop, piano roll, ghost notes) để MIDI item PHẢI play
// qua Carla bridge khi VSTi được loaded.
const fireCarlaNote = (ch, pitch, vel, delay, durMs) => {
  setTimeout(function () { window.SonicCarlaMidi.noteOn(ch, pitch, vel); }, delay);
  setTimeout(function () { window.SonicCarlaMidi.noteOff(ch, pitch); }, delay + durMs + 30);
};
// ⚠️ FIX (Bug 2): Carla mở ASYNC lúc play (ensureCarlaForPlayback) — nốt gửi
// trước khi OSC engine bind cổng bị mất → item câm dù keybed kêu. Nốt khi
// chưa ready được nhét queue, flush khi bridge OSC ready (giữ đúng thứ tự).
const flushCarlaNoteQueue = () => {
  const q = window.__carlaNoteQueue || [];
  if (!q.length) return;
  window.__carlaNoteQueue = [];
  q.forEach(item => {
    const remaining = item.delay - (Date.now() - item.scheduledAt);
    if (remaining <= 0) fireCarlaNote(item.ch, item.pitch, item.vel, 0, item.durMs);
    else fireCarlaNote(item.ch, item.pitch, item.vel, remaining, item.durMs);
  });
};
const scheduleCarlaNote = (synthEngine, channel, pitch, velocity, startWallTime, durMs) => {
  try {
    if (!window.SonicCarlaMidi || !window.SonicCarlaMidi.shouldRoutePlayback(synthEngine)) return;
    const ctx = getAudioContext();
    const delay = Math.max(0, (startWallTime - ctx.currentTime) * 1000);
    const carlaVel = Math.round((velocity || 0.8) * 127);
    const carlaCh = (synthEngine && synthEngine.midi_channel !== undefined) ? synthEngine.midi_channel : (channel || 0);
    // Chẩn đoán: note MIDI item → Carla (bật console.log để verify route)
    if (window.__carlaNoteLog === undefined) window.__carlaNoteLog = (window.__carlaNoteLog || 0) + 1;
    console.log('[Carla] item note ch=' + carlaCh + ' pitch=' + pitch + ' vel=' + carlaVel + ' delay=' + delay.toFixed(0) + 'ms dur=' + (durMs || 300) + 'ms');
    const dur = durMs || 300;
    if (window.__carlaRunning !== true) {
      window.__carlaNoteQueue = window.__carlaNoteQueue || [];
      window.__carlaNoteQueue.push({ ch: carlaCh, pitch, vel: carlaVel, scheduledAt: Date.now(), delay, durMs: dur });
      return;
    }
    fireCarlaNote(carlaCh, pitch, carlaVel, delay, dur);
  } catch (e) { console.warn('[Carla] scheduleCarlaNote error:', e); }
};

// ── Carla bridge alive tracking + auto-open ────────────────────────────────
// window.__carlaRunning: undefined = chưa biết | true = đang chạy | false = đã chết.
// window.__carlaNoteQueue: nốt chờ flush khi Carla chưa ready (cold start).
// Quyết định route MIDI item EXCLUSIVE qua Carla hay fallback FluidSynth —
// tránh "câm toàn phần" khi Carla bị đóng (route chỉ-Carla mà Carla chết = im lặng).
const refreshCarlaStatus = () => {
  try {
    if (!window.SonicAPI || !window.SonicAPI.carlaStatus) return;
    window.SonicAPI.carlaStatus().then(st => {
      window.__carlaRunning = !!(st && st.running);
      if (window.__carlaRunning) flushCarlaNoteQueue();
    }).catch(() => { window.__carlaRunning = false; });
  } catch (e) {}
};
// Carla chưa chạy → TỰ ĐỘNG mở với VSTi của track rồi CHỜ OSC ready (poll
// carla-status tối đa 10s) — nốt chỉ gửi khi bridge thực sự nhận được.
// Có gate __carlaOpening chống spawn trùng (mỗi play chỉ mở 1 lần).


// ── Môi trường chạy: docker vs standalone ─────────────────────────────────
// environment: "docker" → âm instrument qua FluidSynthWASM (client); "standalone"
// → xử lí trực tiếp trên OS (backend native pyfluidsynth / Carla). Quy tắc:
// KHÔNG phải docker = standalone (Windows/Linux/macOS chạy trực tiếp).
const sfEnv = () => {
  try {
    const r = window.SonicRuntime;
    if (r && r.environment) return r.environment;
    const c = r && r.capabilities;
    return (c && c.docker) ? 'docker' : 'standalone';
  } catch (e) { return 'docker'; }
};
const isDockerSf = () => sfEnv() === 'docker';
const isStandaloneSf = () => sfEnv() === 'standalone';
// Track dùng âm soundfont (không phải VSTi) — route native khi standalone.
const isSfTrackEngine = (se) => {
  if (!se) return false;
  return !!(se.soundfont_id || String(se.type || '').indexOf('soundfont') !== -1 || String(se.type || '').indexOf('sf3') !== -1);
};
const isVstTrackEngine = (se) => !!se && String(se.type || '').indexOf('vst') !== -1;
// Track dùng VSTi + Carla local → route Carla (native GUI, realtime).
const shouldRouteCarla = (se) => !!(window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(se));

// ── Native soundfont preview (standalone) ─────────────────────────────────
// Render 1 note bằng backend native FluidSynth (pyfluidsynth) → play WAV.
// Mỗi key (thường = track.id) một Audio element — note mới stop note cũ;
// token chống stale (response cũ không đè response mới).
const _nativeSfPreviews = {};
const playNativeSfNote = (track, pitch, velocity, durationMs, startTime, key) => {
  try {
    const eng = track && track.synth_engine;
    const sfId = (eng && eng.soundfont_id) || (track && track.instrumentId && String(track.instrumentId).startsWith('sf_') ? track.instrumentId : null);
    if (!sfId) return;
    const bank = (eng && eng.soundfont_bank) || 0;
    const program = (eng && eng.soundfont_program) || (track && track.instrumentProgram !== undefined ? track.instrumentProgram : 0);
    const k = key || (track ? track.id : 'global');
    const prev = _nativeSfPreviews[k];
    const token = (prev ? prev.token : 0) + 1;
    const durationSec = Math.max(0.2, (durationMs || 500) / 1000);
    window.SonicAPI.soundfontRender({
      soundfont_id: sfId,
      bank: bank,
      program: program,
      bpm: 120,
      notes: [{ pitch: pitch, start_beat: 0, duration_beats: durationSec * 2, velocity: velocity != null ? velocity : 0.8 }],
    }).then(function (res) {
      if (!res || !res.success || !res.url) return;
      if (_nativeSfPreviews[k] && _nativeSfPreviews[k].token !== token) return; // stale
      const audio = new Audio(API_BASE_URL + res.url);
      _nativeSfPreviews[k] = { audio: audio, token: token };
      const ctx = getAudioContext();
      const delay = startTime ? Math.max(0, (startTime - ctx.currentTime) * 1000) : 0;
      setTimeout(function () {
        if (!_nativeSfPreviews[k] || _nativeSfPreviews[k].audio !== audio) return;
        audio.play().catch(function () {});
        audio._sfStopTimer && clearTimeout(audio._sfStopTimer);
        audio._sfStopTimer = setTimeout(function () { try { audio.pause(); } catch (e) {} }, durationSec * 1000 + 400);
      }, delay);
    }).catch(function () {});
  } catch (e) { console.warn('[NativeSF] playNativeSfNote error:', e); }
};
const stopNativeSfNote = (key) => {
  try {
    const k = key || 'global';
    const prev = _nativeSfPreviews[k];
    if (!prev) return;
    prev.token++;
    try { if (prev.audio) { prev.audio.pause(); prev.audio.currentTime = 0; } } catch (e) {}
  } catch (e) {}
};
const stopAllNativeSfNotes = () => {
  try { Object.keys(_nativeSfPreviews).forEach(function (k) { stopNativeSfNote(k); }); } catch (e) {}
};

// ── Native soundfont item render (standalone, transport) ──────────────────
// Render TOÀN BỘ MIDI item bằng native FluidSynth → decode AudioBuffer →
// schedule nguồn audio đúng vị trí item (giống audio clip). opts:
//   baseOffsetSec: offset thêm (section: secStart) | limitSec: chặn tại secEnd
//   isActive(): guard stop giữa chừng | sources: mảng nguồn để stop.
const scheduleNativeSfItem = (track, item, offsetTime, context, destNode, bpm, opts) => {
  try {
    const eng = track && track.synth_engine;
    const sfId = eng && eng.soundfont_id;
    if (!sfId) return;
    const notes = (item && item.notes) || [];
    if (!notes.length) return;
    const secPerBeat = 60.0 / (parseInt(bpm) || 120);
    const baseOffsetSec = (opts && opts.baseOffsetSec) || 0;
    const itemStartAbs = baseOffsetSec + (item.startTime || 0);
    let itemEndAbs = itemStartAbs + 0.05;
    notes.forEach(function (n) {
      const end = itemStartAbs + ((n.start_beat || 0) + (n.duration_beats || 1)) * secPerBeat;
      if (end > itemEndAbs) itemEndAbs = end;
    });
    window.SonicAPI.soundfontRender({
      soundfont_id: sfId,
      bank: (eng && eng.soundfont_bank) || 0,
      program: (eng && eng.soundfont_program) || 0,
      bpm: parseFloat(bpm) || 120,
      notes: notes.map(function (n) { return { pitch: n.pitch || 60, start_beat: n.start_beat || 0, duration_beats: n.duration_beats || 1, velocity: n.velocity != null ? n.velocity : 0.8 }; }),
    }).then(function (res) {
      if (!res || !res.success || !res.url) return;
      fetch(API_BASE_URL + res.url).then(function (r) { return r.arrayBuffer(); }).then(function (buf) {
        context.decodeAudioData(buf, function (audioBuf) {
          try {
            if (opts && typeof opts.isActive === 'function' && !opts.isActive()) return;
            const src = context.createBufferSource();
            src.buffer = audioBuf;
            src.connect(destNode);
            if (offsetTime < itemStartAbs) {
              src.start(context.currentTime + (itemStartAbs - offsetTime), 0);
            } else if (offsetTime < itemEndAbs) {
              src.start(context.currentTime, Math.min(offsetTime - itemStartAbs, Math.max(0, audioBuf.duration - 0.05)));
            } else {
              return;
            }
            if (opts && opts.limitSec && opts.limitSec < itemStartAbs + audioBuf.duration) {
              src.stop(context.currentTime + Math.max(0.02, opts.limitSec - Math.max(offsetTime, itemStartAbs)));
            }
            if (opts && opts.sources && opts.sources.push) opts.sources.push(src);
          } catch (e) { console.warn('[NativeSF] schedule decode error:', e); }
        }, function () {});
      }).catch(function () {});
    }).catch(function () {});
  } catch (e) { console.warn('[NativeSF] scheduleNativeSfItem error:', e); }
};

// Handle ?sfs=<encoded> from double-clicking a .sfs file (opens domain -> loads project)
(function handleSfsDeepLink() {
  try {
    const params = new URLSearchParams(window.location.search);
    const sfsParam = params.get('sfs');
    if (!sfsParam) return;
    const decoded = JSON.parse(decodeURIComponent(sfsParam));
    window.__pendingSfsProject = decoded; // consumed after auth in App
    if (window.history.replaceState) {
      window.history.replaceState({}, document.title, window.location.pathname);
    }
  } catch (e) {
    window.__pendingSfsProject = null;
  }
})();

// Storage for server-side file IDs mapped to track IDs
let serverFileIdMap = {};
let audioCtx;
let masterBus = null; // { input, compressor, analyser, output, masteringActive, dryInput, dryOutput }

// Per-track mastering-bypass state (trackId -> bool), kept in sync with the
// tracks state so ANY audio path can route without holding the track object.
const trackMasteringBypassMap = {};
const trackAudioBypassMap = {};
const trackMidiBypassMap = {};

// Mastering chain ON? (masterConnected && !isBypassed)
const masteringChainOn = () => !!(window.currentMasteringSettings && window.currentMasteringSettings.masterConnected && !window.currentMasteringSettings.isBypassed);
// ♪ bypass hiệu lực CHỈ khi mastering chain TẮT — khi chain ON, MỌI track
// (solo/preview/play) PHẢI đi qua mastering chain (user requirement: âm phải
// qua chain để đủ lớn). Chain OFF → theo ♪ maps như cũ.
const effMidiBypass = (track) => {
  // GỠ override mastering-ON (17:00) — nút ♪ quyết định bypass (user rules).
  // ƯU TIÊN track object (field midiBypass): track object phân biệt đúng
  // context (main track vs section clone — id TRÙNG nhau); map keyed theo id
  // gây collision (clone section ghi đè map của main track cùng id → MIDI
  // section bị route theo ♪ main = bypass sai). Map chỉ là fallback realtime.
  if (track && track.midiBypass !== undefined) return !!track.midiBypass;
  if (track && track.id && trackMidiBypassMap[track.id] !== undefined) return !!trackMidiBypassMap[track.id];
  return false;
};
const effAudioBypass = (track) => {
  // GỠ override mastering-ON (17:00) — nút A quyết định bypass (user rules)
  if (track && track.audioBypass !== undefined) return !!track.audioBypass;
  if (track && track.id && trackAudioBypassMap[track.id] !== undefined) return !!trackAudioBypassMap[track.id];
  return false;
};

// Build the dual routing for one track: routeGain -> mastering chain (normal),
// dryGain -> dry bus (bypass). Gains start at complementary 1/0 values.
function createMasteringRoute(ctx, track, bus) {
  // Prefer the live bypass map (synced from the tracks state on every render),
  // falling back to the track object — this guarantees the A-button toggle is
  // picked up even if a stale track object is passed in.
  let bypass = false;
  // Bypass theo LOẠI nội dung track (rules nút A/♪): track MIDI-only → nút ♪
  // quyết định — KHÔNG dùng audioBypass (section track default audioBypass=true
  // → route dry → midi section bị bypass dù ♪ tắt — log: SF → track node
  // (sfEntry) + noteon chạy nhưng âm dry).
  const hasClips = track && ((track.clips && track.clips.length > 0) || !!track.buffer);
  const hasMidi = track && track.midiItems && track.midiItems.length > 0;
  if (hasMidi && !hasClips) {
    // MIDI-only → ♪; ƯU TIÊN field track object (context đúng — id section
    // clone TRÙNG main track; map keyed theo id bị ghi đè chéo → bypass sai).
    bypass = track.midiBypass !== undefined ? !!track.midiBypass : (track.id && trackMidiBypassMap[track.id] !== undefined ? !!trackMidiBypassMap[track.id] : false);
  } else if (track) {
    bypass = track.audioBypass !== undefined ? !!track.audioBypass : (track.id && trackAudioBypassMap[track.id] !== undefined ? !!trackAudioBypassMap[track.id] : false);
  }
  // Mastering chain ON → MỌI track qua chain (♪ bị override — user requirement)
  // ⚠️ GỠ 17:00 — override nuốt nút A/♪ (log: audioBypass=true nhưng
  // routeGain=1) — rules hiện tại: NÚT A/♪ quyết định bypass, không phụ thuộc
  // mastering ON.
  const routeGain = ctx.createGain();
  const dryGain = ctx.createGain();
  const masterDest = bus ? bus.input : ctx.destination;
  const dryDest = (bus && bus.dryInput) ? bus.dryInput : ctx.destination;
  routeGain.gain.value = bypass ? 0 : 1;
  dryGain.gain.value = bypass ? 1 : 0;
  routeGain.connect(masterDest);
  dryGain.connect(dryDest);
  const routeObj = { routeGain, dryGain };
  routeObj._trackId = track && track.id;
  routeObj._bypass = bypass;
  return routeObj;
}

// Bypass theo LOẠI nội dung track (dùng chung: createMasteringRoute, sync
// effect, startTrackPlayback re-apply) — MIDI-only track → nút ♪; có clips →
// nút A. Section track default audioBypass=true — nếu route đọc audioBypass
// cho track MIDI → midi bị kéo vào dry (bị bypass) dù ♪ tắt.
function routeBypassFor(track) {
  const hasC = track && ((track.clips && track.clips.length > 0) || !!track.buffer);
  const hasM = track && track.midiItems && track.midiItems.length > 0;
  if (hasM && !hasC) {
    // MIDI-only → ♪; ƯU TIÊN field track object (context đúng — id section
    // clone TRÙNG main track; map keyed theo id bị ghi đè chéo → bypass sai).
    routeBypassFor._lastType = 'midi';
    return track.midiBypass !== undefined ? !!track.midiBypass : (track.id && trackMidiBypassMap[track.id] !== undefined ? !!trackMidiBypassMap[track.id] : false);
  }
  routeBypassFor._lastType = 'audio';
  return track.audioBypass !== undefined ? !!track.audioBypass : (track.id && trackAudioBypassMap[track.id] !== undefined ? !!trackAudioBypassMap[track.id] : false);
}

// Live-toggle a route. HARD switch: cancel any pending automation and assign
// .value directly (instant, cannot be delayed by the automation queue).
function setMasteringRoute(route, bypass) {
  if (!route) return;
  const on = !!bypass;
  try {
    const ctx = (typeof getAudioContext === 'function') ? getAudioContext() : null;
    if (!ctx) return;
    const t = ctx.currentTime;
    route.routeGain.gain.cancelScheduledValues(t);
    route.dryGain.gain.cancelScheduledValues(t);
    route.routeGain.gain.value = on ? 0 : 1;
    route.dryGain.gain.value = on ? 1 : 0;
    route._bypass = on;
    console.log('[Bypass] track', route._trackId, 'audioBypass=' + on, '→', on ? 'DRY BUS (bỏ mastering + bỏ track FX)' : 'MASTERING CHAIN (qua FX + mastering)');
  } catch (e) {
    console.warn('setMasteringRoute error:', e);
  }
}

// Realtime mute/solo: audible linear gain for a track given the full track list
// of the CURRENT context. Solo semantics: if ANY track is soloed, only soloed
// tracks are audible; muted tracks are always silent.
function computeTrackAudibleGain(trackList, track) {
  if (!track) return 0;
  if (track.muted) return 0;
  const hasSolo = (trackList || []).some(t => t.solo);
  if (hasSolo && !track.solo) return 0;
  const volDb = Number(track.volumeDb);
  if (isNaN(volDb) || !isFinite(volDb)) return 1.0;
  return volDb <= -50 ? 0 : Math.pow(10, volDb / 20);
}

// Apply a gain to a track node's gain with a short crossfade (click-free).
function setTrackNodeGain(node, gainLinear) {
  if (!node || !node.gainNode || !audioCtx) return;
  const t = audioCtx.currentTime;
  const g = (typeof gainLinear === 'number' && isFinite(gainLinear) && !isNaN(gainLinear)) ? gainLinear : 1.0;
  node.gainNode.gain.cancelScheduledValues(t);
  node.gainNode.gain.setTargetAtTime(g, t, 0.02);
}

// MAIN SESSION end-time (seconds): endtime of the items ON the session's own
// tracks only — audio clips, MIDI items, and section-item bounds. Section-TAB
// content is deliberately IGNORED here: when played inside the main session the
// sub-track items are clamped to their section bounds, so a long SECTION-TAB
// must NOT stretch the main project. The SECTION-TAB duration is computed
// separately from ITS OWN tracks (see maxDuration useMemo).
function computeMainSessionEndTime(tracksList) {
  let max = 0;
  const midiEnd = m => {
    if (m && typeof m.endTime === 'number') return m.endTime;
    return (m && m.startTime || 0) + (m && typeof m.duration === 'number' ? m.duration : 4);
  };
  (tracksList || []).forEach(t => {
    const clips = t.clips && t.clips.length > 0 ? t.clips : t.buffer ? [{
      id: 'default', buffer: t.buffer, startTime: t.startTime || 0, speed: t.speed || 1.0
    }] : [];
    clips.forEach(c => {
      if (c.buffer) max = Math.max(max, (c.startTime || 0) + c.buffer.duration / (c.speed || 1.0));
    });
    (t.midiItems || []).forEach(m => { max = Math.max(max, midiEnd(m)); });
    (t.sections || []).forEach(s => { max = Math.max(max, (s.start || 0) + (s.duration || 0)); });
  });
  return max;
}

// Signature of all item positions/speeds/durations on the given tracks (+ the
// content inside section tabs). Compared against the signature captured at
// schedule time: when they differ mid-playback the loop re-schedules so moved
// items play at their NEW position instead of the stale one.
function buildItemsSignature(tracksList, tabsList) {
  const tSig = t => {
    const clips = (t.clips || []).map(c => c.id + ':' + Math.round((c.startTime || 0) * 100) + ':' + Math.round((c.speed || 1) * 100)).join(',');
    const midi = (t.midiItems || []).map(m => m.id + ':' + Math.round((m.startTime || 0) * 100)).join(',');
    const secs = (t.sections || []).map(s => s.id + ':' + Math.round((s.start || 0) * 100) + ':' + Math.round((s.duration || 0) * 100)).join(',');
    // Track-level default clip (track.buffer): vị trí lưu ở track.startTime —
    // KHÔNG nằm trong t.clips, phải đưa vào signature nếu không kéo default
    // clip sẽ không kích hoạt re-schedule (vẫn phát nội dung cũ).
    const defClip = (t.buffer ? 'def:' + Math.round((t.startTime || 0) * 100) + ':' + Math.round((t.speed || 1) * 100) : '');
    return clips + '|' + midi + '|' + secs + '|' + defClip;
  };
  let s = (tracksList || []).map(tSig).join(';');
  s += '##' + (tabsList || []).map(st => (st.tracks || []).map(tSig).join(';')).join('|');
  return s;
}

// Reusable time-domain buffers for the imager vectorscope/correlation meter
// (leftAnalyser/rightAnalyser are fixed at fftSize 2048) — allocated once so
// the 60fps render loop does not churn the GC.
const _imagerBufL = new Float32Array(2048);
const _imagerBufR = new Float32Array(2048);

// Real-time stereo correlation (−1.0 … +1.0) from the master output L/R
// analysers. +0.5…+1.0 safe, 0…+0.5 caution, <0 phase cancellation (spec §III).
function computeStereoCorrelation() {
  if (!masterBus || !masterBus.leftAnalyser || !masterBus.rightAnalyser) return 1.0;
  try {
    masterBus.leftAnalyser.getFloatTimeDomainData(_imagerBufL);
    masterBus.rightAnalyser.getFloatTimeDomainData(_imagerBufR);
    let sumLR = 0, sumL2 = 0, sumR2 = 0;
    for (let i = 0; i < 2048; i++) {
      sumLR += _imagerBufL[i] * _imagerBufR[i];
      sumL2 += _imagerBufL[i] * _imagerBufL[i];
      sumR2 += _imagerBufR[i] * _imagerBufR[i];
    }
    if (sumL2 < 1e-9 || sumR2 < 1e-9) return 1.0; // silence → neutral
    return Math.max(-1, Math.min(1, sumLR / Math.sqrt(sumL2 * sumR2)));
  } catch (e) {
    return 1.0;
  }
}

function makeDistortionCurve(k) {
  const n_samples = 44100;
  const curve = new Float32Array(n_samples);
  for (let i = 0; i < n_samples; ++i) {
    const x = (i * 2) / n_samples - 1;
    curve[i] = Math.atan(x * k) / (Math.atan(k) || 1);
  }
  return curve;
}

function applyMasteringSettings(s) {
  console.log('[Mastering] applyMasteringSettings active=', !!(s && s.masterConnected), 'bypass=', !!(s && s.isBypassed), 'eqActive=', !!(s && s.eqActive), 'masterBus=', !!masterBus);
  if (!masterBus || !audioCtx || !s) return;
  // getAudioContext() invokes this on EVERY call (stopAll, play, VU, …).
  // Re-applying identical values in rapid bursts is "fast parameter automation"
  // and makes Chromium flag the biquad EQ filters as unstable ("state is bad").
  // Only touch the graph when a value actually changed.
  const sig = [s.masterConnected, s.isBypassed, s.eqActive, s.eqLowGain, s.eqMid1Gain, s.eqMid2Gain, s.eqHighGain, s.imagerActive, s.w1, s.w2, s.w3, s.w4, s.maximizerActive, s.maxGain, s.maxSoftClip, s.maxUpward, s.ceiling, s.compActive, s.compThreshold, s.compRatio, s.compMakeup, s.limActive, s.limThreshold, s.excActive, s.excDrive, s.rebalActive, s.rebalMid, s.rebalSide].join('|');
  if (sig === _lastMasteringSig) return;
  _lastMasteringSig = sig;

  const now = audioCtx.currentTime;
  // Clamp every parameter so a stale/incomplete settings object can never push
  // NaN or an extreme value into the biquad filters — that puts the master
  // chain into a bad state and silences ALL audio (the "mất soundfont" symptom).
  const clamp = (v, lo, hi) => {
    const n = Number(v);
    if (!isFinite(n)) return 0; // missing/NaN → neutral, never a filter-breaking value
    return Math.min(hi, Math.max(lo, n));
  };

  // 1. EQ Settings
  // cancelScheduledValues + a slower time constant keeps rapid slider drags
  // from piling up automation events on the biquad filters (the trigger for
  // Chromium's "BiquadFilterNode: state is bad").
  // ⚠️ Guard NaN: setValueAtTime(NaN) trên biquad → Chromium "BiquadFilterNode:
  // state is bad" + câm. Field settings undefined/NaN → mặc định 0.
  const _g = (v, lo, hi) => (typeof v === 'number' && isFinite(v)) ? clamp(v, lo, hi) : 0;
  masterBus.eqLowFilter.gain.cancelScheduledValues(now);
  masterBus.eqLowFilter.gain.setValueAtTime(s.eqActive ? _g(s.eqLowGain, -24, 24) : 0, now);
  masterBus.eqMid1Filter.gain.cancelScheduledValues(now);
  masterBus.eqMid1Filter.gain.setValueAtTime(s.eqActive ? _g(s.eqMid1Gain, -24, 24) : 0, now);
  masterBus.eqMid2Filter.gain.cancelScheduledValues(now);
  masterBus.eqMid2Filter.gain.setValueAtTime(s.eqActive ? _g(s.eqMid2Gain, -24, 24) : 0, now);
  masterBus.eqHighFilter.gain.cancelScheduledValues(now);
  masterBus.eqHighFilter.gain.setValueAtTime(s.eqActive ? _g(s.eqHighGain, -24, 24) : 0, now);

  // 2. Imager Settings (Mid/Side width per band — imager_spec.md)
  // Width % semantics per the guide: 0% = MONO (S × 0), 100% = original
  // (S × 1), 200% = double width (S × 2). The L/R crossfeed gains below are
  // exactly equivalent to M/S scaling: L'=g1·L+g2·R, R'=g1·R+g2·L with
  // g1=(w+100)/200, g2=(100−w)/200 → Mid (L+R)/2 untouched, Side (L−R)/2
  // scaled by w/100. Bands: 1=20-100Hz, 2=100Hz-1kHz, 3=1k-6kHz, 4=6k-20kHz.
  const updateImagerBand = (w, active, gainLL, gainRL, gainLR, gainRR) => {
    const widthVal = (s.imagerActive && active) ? clamp(w, 0, 200) : 100;
    const g1 = (widthVal + 100) / 200;
    const g2 = (100 - widthVal) / 200;

    gainLL.gain.setTargetAtTime(g1, now, 0.01);
    gainRR.gain.setTargetAtTime(g1, now, 0.01);
    gainRL.gain.setTargetAtTime(g2, now, 0.01);
    gainLR.gain.setTargetAtTime(g2, now, 0.01);
  };

  updateImagerBand(s.w1, true, masterBus.gainLL1, masterBus.gainRL1, masterBus.gainLR1, masterBus.gainRR1);
  updateImagerBand(s.w2, true, masterBus.gainLL2, masterBus.gainRL2, masterBus.gainLR2, masterBus.gainRR2);
  updateImagerBand(s.w3, true, masterBus.gainLL3, masterBus.gainRL3, masterBus.gainLR3, masterBus.gainRR3);
  updateImagerBand(s.w4, true, masterBus.gainLL4, masterBus.gainRL4, masterBus.gainLR4, masterBus.gainRR4);

  // 3. Maximizer Settings
  const boostLinear = (s.maximizerActive) ? Math.pow(10, clamp(s.maxGain, -60, 30) / 20) : 1.0;
  masterBus.maximizerBoostGain.gain.setTargetAtTime(boostLinear, now, 0.01);

  // Soft Clipper (identity passthrough when off — never null curve)
  if (s.maximizerActive && s.maxSoftClip > 0) {
    const k = 1 + (clamp(s.maxSoftClip, 0, 100) / 100) * 10;
    masterBus.maximizerSoftClipper.curve = makeDistortionCurve(k);
  } else {
    masterBus.maximizerSoftClipper.curve = new Float32Array([-1, 1]);
  }

  // Upward Compressor
  const upwardGainLinear = (s.maximizerActive && s.maxUpward > 0) ? (Math.pow(10, clamp(s.maxUpward, 0, 30) / 20) - 1.0) : 0.0;
  masterBus.upwardGain.gain.setTargetAtTime(upwardGainLinear, now, 0.01);

  // Limiter Threshold (WaveShaper ceiling — _setCeiling rebuild curve)
  const ceilingVal = s.maximizerActive ? clamp(s.ceiling, -60, 0) : -0.1;
  if (masterBus.maximizerCompressor._setCeiling) masterBus.maximizerCompressor._setCeiling(ceilingVal);

  // 4. Bus Compressor module (mastering_expand.md §II.2)
  if (masterBus.compNode) {
    const compOn = !!s.compActive;
    masterBus.compNode.threshold.setTargetAtTime(compOn ? clamp(s.compThreshold, -60, 0) : 0, now, 0.02);
    masterBus.compNode.ratio.setTargetAtTime(compOn ? clamp(s.compRatio, 1, 20) : 1, now, 0.02);
    masterBus.compMakeup.gain.setTargetAtTime(compOn ? Math.pow(10, clamp(s.compMakeup, 0, 12) / 20) : 1.0, now, 0.02);
  }

  // 5. Brickwall Limiter module (WaveShaper tanh — threshold = mức clip; OFF = identity)
  if (masterBus.limNode) {
    const limOn = !!s.limActive;
    if (limOn) {
      if (masterBus.limNode._setThreshold) masterBus.limNode._setThreshold(clamp(s.limThreshold, -24, 0));
    } else {
      try { masterBus.limNode.curve = new Float32Array([-1, 1]); } catch (e) {}
    }
  }

  // 6. Harmonic Exciter module (dry/wet mix; dry stays 1.0 for subtle warmth)
  if (masterBus.excWet) {
    const excOn = !!s.excActive;
    const wetAmt = excOn ? clamp(s.excDrive, 0, 100) / 100 : 0;
    masterBus.excWet.gain.setTargetAtTime(wetAmt * 0.6, now, 0.02);
    masterBus.excDry.gain.setTargetAtTime(1.0, now, 0.02);
  }

  // 7. Master Rebalance module (M/S gains via L/R crossfeed)
  // L' = a·L + b·R, R' = b·L + a·R with a=(mid+side)/2, b=(mid−side)/2
  if (masterBus.gLLr) {
    const rebOn = !!s.rebalActive;
    const midLin = rebOn ? Math.pow(10, clamp(s.rebalMid, -24, 24) / 20) : 1.0;
    const sideLin = rebOn ? Math.pow(10, clamp(s.rebalSide, -24, 24) / 20) : 1.0;
    const a = (midLin + sideLin) / 2;
    const b = (midLin - sideLin) / 2;
    masterBus.gLLr.gain.setTargetAtTime(a, now, 0.01);
    masterBus.gRRr.gain.setTargetAtTime(a, now, 0.01);
    masterBus.gRLr.gain.setTargetAtTime(b, now, 0.01);
    masterBus.gLRr.gain.setTargetAtTime(b, now, 0.01);
  }
}

function initMasterBus(ctx) {
  if (masterBus) return masterBus;

  // Clamp every filter frequency below Nyquist (0.45 * sampleRate). A biquad
  // with frequency ≥ Nyquist gets NaN coefficients → "BiquadFilterNode: state
  // is bad" → the whole mastering chain outputs silence (IN peak yes, OUT no).
  // Low sample-rate devices (8/11/16 kHz audio drivers) would otherwise break
  // the 10 kHz highshelf / 6 kHz imager crossover filters.
  const maxFilterFreq = (ctx.sampleRate || 44100) * 0.45;
  const clampF = v => Math.max(20, Math.min(v, maxFilterFreq));

  // Create EQ filters
  const eqLowFilter = ctx.createBiquadFilter();
  eqLowFilter.type = 'lowshelf';
  eqLowFilter.frequency.value = clampF(100);

  const eqMid1Filter = ctx.createBiquadFilter();
  eqMid1Filter.type = 'peaking';
  eqMid1Filter.frequency.value = clampF(822);
  eqMid1Filter.Q.value = 0.7;

  const eqMid2Filter = ctx.createBiquadFilter();
  eqMid2Filter.type = 'peaking';
  eqMid2Filter.frequency.value = clampF(3200);
  eqMid2Filter.Q.value = 1.2;

  const eqHighFilter = ctx.createBiquadFilter();
  eqHighFilter.type = 'highshelf';
  eqHighFilter.frequency.value = clampF(10000);

  // Create Stereo Imager nodes
  const imagerInput = ctx.createGain();
  const imagerOutput = ctx.createGain();

  // Imager Crossover Filters
  const f1_lp = ctx.createBiquadFilter(); f1_lp.type = 'lowpass'; f1_lp.frequency.value = clampF(100);
  const f2_hp = ctx.createBiquadFilter(); f2_hp.type = 'highpass'; f2_hp.frequency.value = clampF(100);
  const f2_lp = ctx.createBiquadFilter(); f2_lp.type = 'lowpass'; f2_lp.frequency.value = clampF(1000);
  const f3_hp = ctx.createBiquadFilter(); f3_hp.type = 'highpass'; f3_hp.frequency.value = clampF(1000);
  const f3_lp = ctx.createBiquadFilter(); f3_lp.type = 'lowpass'; f3_lp.frequency.value = clampF(6000);
  const f4_hp = ctx.createBiquadFilter(); f4_hp.type = 'highpass'; f4_hp.frequency.value = clampF(6000);

  const split1 = ctx.createChannelSplitter(2);
  const split2 = ctx.createChannelSplitter(2);
  const split3 = ctx.createChannelSplitter(2);
  const split4 = ctx.createChannelSplitter(2);

  const merge1 = ctx.createChannelMerger(2);
  const merge2 = ctx.createChannelMerger(2);
  const merge3 = ctx.createChannelMerger(2);
  const merge4 = ctx.createChannelMerger(2);

  const gainLL1 = ctx.createGain(); const gainRL1 = ctx.createGain(); const gainLR1 = ctx.createGain(); const gainRR1 = ctx.createGain();
  const gainLL2 = ctx.createGain(); const gainRL2 = ctx.createGain(); const gainLR2 = ctx.createGain(); const gainRR2 = ctx.createGain();
  const gainLL3 = ctx.createGain(); const gainRL3 = ctx.createGain(); const gainLR3 = ctx.createGain(); const gainRR3 = ctx.createGain();
  const gainLL4 = ctx.createGain(); const gainRL4 = ctx.createGain(); const gainLR4 = ctx.createGain(); const gainRR4 = ctx.createGain();

  // Connections for Imager DSP
  imagerInput.connect(f1_lp);
  imagerInput.connect(f2_hp); f2_hp.connect(f2_lp);
  imagerInput.connect(f3_hp); f3_hp.connect(f3_lp);
  imagerInput.connect(f4_hp);

  // Band 1
  f1_lp.connect(split1);
  split1.connect(gainLL1, 0); split1.connect(gainLR1, 0);
  split1.connect(gainRL1, 1); split1.connect(gainRR1, 1);
  gainLL1.connect(merge1, 0, 0); gainRL1.connect(merge1, 0, 0);
  gainLR1.connect(merge1, 0, 1); gainRR1.connect(merge1, 0, 1);
  merge1.connect(imagerOutput);

  // Band 2
  f2_lp.connect(split2);
  split2.connect(gainLL2, 0); split2.connect(gainLR2, 0);
  split2.connect(gainRL2, 1); split2.connect(gainRR2, 1);
  gainLL2.connect(merge2, 0, 0); gainRL2.connect(merge2, 0, 0);
  gainLR2.connect(merge2, 0, 1); gainRR2.connect(merge2, 0, 1);
  merge2.connect(imagerOutput);

  // Band 3
  f3_lp.connect(split3);
  split3.connect(gainLL3, 0); split3.connect(gainLR3, 0);
  split3.connect(gainRL3, 1); split3.connect(gainRR3, 1);
  gainLL3.connect(merge3, 0, 0); gainRL3.connect(merge3, 0, 0);
  gainLR3.connect(merge3, 0, 1); gainRR3.connect(merge3, 0, 1);
  merge3.connect(imagerOutput);

  // Band 4
  f4_hp.connect(split4);
  split4.connect(gainLL4, 0); split4.connect(gainLR4, 0);
  split4.connect(gainRL4, 1); split4.connect(gainRR4, 1);
  gainLL4.connect(merge4, 0, 0); gainRL4.connect(merge4, 0, 0);
  gainLR4.connect(merge4, 0, 1); gainRR4.connect(merge4, 0, 1);
  merge4.connect(imagerOutput);

  // Maximizer nodes
  const maximizerBoostGain = ctx.createGain();
  const maximizerSoftClipper = ctx.createWaveShaper();
  // NEVER leave the curve null: a WaveShaper with a null/identity curve can
  // output silence in some engines, which would kill the whole mastering path.
  // Use an explicit linear identity table for passthrough.
  maximizerSoftClipper.curve = new Float32Array([-1, 1]);
  maximizerSoftClipper.oversample = '4x';

  const upwardCompressor = ctx.createDynamicsCompressor();
  upwardCompressor.threshold.value = -30;
  upwardCompressor.knee.value = 10;
  upwardCompressor.ratio.value = 4;
  upwardCompressor.attack.value = 0.01;
  upwardCompressor.release.value = 0.1;

  const upwardGain = ctx.createGain();
  upwardGain.gain.value = 0.0;

  const upwardSummingGain = ctx.createGain();

  maximizerBoostGain.connect(maximizerSoftClipper);
  maximizerSoftClipper.connect(upwardSummingGain);

  maximizerBoostGain.connect(upwardCompressor);
  upwardCompressor.connect(upwardGain);
  upwardGain.connect(upwardSummingGain);

  // Brickwall Limiter tại ceiling: WaveShaper HARD CLIP (slope 1 — không boost,
  // clip chính xác tại ceiling) — KHÔNG DynamicsCompressor (NaN trên bass
  // transient → chain state-bad → CÂM + stuck).
  const maximizerCompressor = ctx.createWaveShaper();
  maximizerCompressor.oversample = '2x';
  let _maxCeil = -0.1;
  const _buildMaxCurve = (db) => {
    const c = Math.pow(10, Math.max(-60, Math.min(0, db)) / 20);
    const _c = new Float32Array(4096);
    for (let _i = 0; _i < 4096; _i++) { const _x = (_i / 4095) * 2 - 1; _c[_i] = Math.max(-c, Math.min(c, _x)); }
    maximizerCompressor.curve = _c;
    _maxCeil = db;
  };
  _buildMaxCurve(-0.1);
  maximizerCompressor._setCeiling = (db) => { if (db !== _maxCeil) _buildMaxCurve(db); };

  upwardSummingGain.connect(maximizerCompressor);

  // ── Bus Compressor module (mastering_expand.md §II.2) ──
  const compInput = ctx.createGain();
  const compNode = ctx.createDynamicsCompressor();
  compNode.threshold.value = -16;
  compNode.knee.value = 8;
  compNode.ratio.value = 3;
  compNode.attack.value = 0.02;
  compNode.release.value = 0.25;
  const compMakeup = ctx.createGain();
  compMakeup.gain.value = 1.0;
  const compOutput = ctx.createGain();
  compInput.connect(compNode);
  compNode.connect(compMakeup);
  compMakeup.connect(compOutput);

  // ── Brickwall Limiter module (WaveShaper tanh soft-clip — KHÔNG
  // DynamicsCompressor: NaN trên bass transient → chain stuck) ──
  const limInput = ctx.createGain();
  const limNode = ctx.createWaveShaper();
  limNode.oversample = '2x';
  let _limLastThresh = null;
  const _buildLimCurve = (db) => {
    const tLin = Math.pow(10, Math.max(-24, Math.min(0, db)) / 20);
    const k = 1 / Math.max(0.02, tLin);
    const _c = new Float32Array(4096);
    const _tk = Math.tanh(k);
    for (let _i = 0; _i < 4096; _i++) { const _x = (_i / 4095) * 2 - 1; _c[_i] = Math.tanh(_x * k) / _tk; }
    limNode.curve = _c;
    _limLastThresh = db;
  };
  _buildLimCurve(-1.0);
  limNode._setThreshold = (db) => { if (db !== _limLastThresh) _buildLimCurve(db); };
  const limOutput = ctx.createGain();
  limInput.connect(limNode);
  limNode.connect(limOutput);

  // ── Harmonic Exciter module (WaveShaper saturator + high-pass, dry/wet) ──
  const excInput = ctx.createGain();
  const excHp = ctx.createBiquadFilter();
  excHp.type = 'highpass';
  excHp.frequency.value = clampF(2000);
  excHp.Q.value = 0.7;
  const excShaper = ctx.createWaveShaper();
  excShaper.curve = makeDistortionCurve(3);
  excShaper.oversample = '4x';
  const excDry = ctx.createGain();
  excDry.gain.value = 1.0;
  const excWet = ctx.createGain();
  excWet.gain.value = 0.0;
  const excOutput = ctx.createGain();
  excInput.connect(excDry);
  excDry.connect(excOutput);
  excInput.connect(excHp);
  excHp.connect(excShaper);
  excShaper.connect(excWet);
  excWet.connect(excOutput);

  // ── Master Rebalance module (M/S gains via L/R crossfeed) ──
  const rebalInput = ctx.createGain();
  const rebalSplit = ctx.createChannelSplitter(2);
  const rebalMerge = ctx.createChannelMerger(2);
  const rebalOutput = ctx.createGain();
  const gLLr = ctx.createGain(); const gRLr = ctx.createGain();
  const gLRr = ctx.createGain(); const gRRr = ctx.createGain();
  rebalInput.connect(rebalSplit);
  rebalSplit.connect(gLLr, 0); rebalSplit.connect(gRLr, 0);
  rebalSplit.connect(gLRr, 1); rebalSplit.connect(gRRr, 1);
  gLLr.connect(rebalMerge, 0, 0); gRLr.connect(rebalMerge, 0, 0);
  gLRr.connect(rebalMerge, 0, 1); gRRr.connect(rebalMerge, 0, 1);
  rebalMerge.connect(rebalOutput);

  // Setup Analysers
  const inputAnalyser = ctx.createAnalyser();
  inputAnalyser.fftSize = 2048;

  const outputAnalyser = ctx.createAnalyser();
  outputAnalyser.fftSize = 2048;

  // Global fader / output
  const output = ctx.createGain();
  output.gain.value = 1.0;

  // Per-track mastering-bypass dry bus: tracks with bypass ON feed into
  // dryInput -> dryOutput -> output, skipping the mastering modules
  // (EQ / Imager / Maximizer) while still passing the master volume fader
  // and the master output metering.
  const dryInput = ctx.createGain();
  const dryOutput = ctx.createGain();
  dryInput.connect(dryOutput);
  dryOutput.connect(output);

  const analyser = ctx.createAnalyser();
  analyser.fftSize = 256;

  const leftAnalyser = ctx.createAnalyser();
  leftAnalyser.fftSize = 2048;
  const rightAnalyser = ctx.createAnalyser();
  rightAnalyser.fftSize = 2048;

  const splitter = ctx.createChannelSplitter(2);
  output.connect(splitter);
  splitter.connect(leftAnalyser, 0);
  splitter.connect(rightAnalyser, 1);

  masterBus = {
    input: ctx.createGain(),
    compressor: ctx.createDynamicsCompressor(),
    analyser,
    output,
    masteringActive: false,
    dryInput,
    dryOutput,

    // Analysers for metering
    inputAnalyser,
    outputAnalyser,
    leftAnalyser,
    rightAnalyser,

    // Mastering nodes
    eqLowFilter, eqMid1Filter, eqMid2Filter, eqHighFilter,
    imagerInput, imagerOutput,
    gainLL1, gainRL1, gainLR1, gainRR1,
    gainLL2, gainRL2, gainLR2, gainRR2,
    gainLL3, gainRL3, gainLR3, gainRR3,
    gainLL4, gainRL4, gainLR4, gainRR4,
    maximizerBoostGain, maximizerSoftClipper,
    upwardCompressor, upwardGain, upwardSummingGain,
    maximizerCompressor,
    // Extension modules (mastering_expand.md §II.2)
    compInput, compNode, compMakeup, compOutput,
    limInput, limNode, limOutput,
    excInput, excHp, excShaper, excDry, excWet, excOutput,
    rebalInput, rebalSplit, rebalMerge, gLLr, gRLr, gLRr, gRRr, rebalOutput
  };

  // Connect EQ chain (internal — the module BOUNDARIES are wired dynamically
  // by rebuildMasteringGraph so modules can be reordered on the CHAIN bar)
  eqLowFilter.connect(eqMid1Filter);
  eqMid1Filter.connect(eqMid2Filter);
  eqMid2Filter.connect(eqHighFilter);

  // Setup default non-mastered routing (KHÔNG compressor mặc định trong path):
  // input -> inputAnalyser -> outputAnalyser -> output -> analyser -> destination
  // Compressor mặc định (ratio 12, threshold -24 — LUÔN-ON) vừa (a) pump-down
  // tín hiệu → âm nhỏ/méo, vừa (b) phát NaN khi gặp bass transient → 11 biquad
  // "state is bad" → CÂM + stuck. Mastering chain có comp/lim module riêng khi bật.
  masterBus.input.connect(masterBus.inputAnalyser);
  masterBus.inputAnalyser.connect(masterBus.outputAnalyser);
  masterBus.outputAnalyser.connect(masterBus.output);
  masterBus.output.connect(masterBus.analyser);
  masterBus.analyser.connect(ctx.destination);

  window.masterBus = masterBus;
  // Apply mastering once when the chain is first created (and on the
  // masteringSettings effect for subsequent changes — see useEffect).
  if (window.currentMasteringSettings) {
    try {
      // Reset sig-cache: chain MỚI (biquad/maximizer node mới) giữ giá trị
      // INIT (gain 0, width 100…) — applyMasteringSettings early-return vì
      // _lastMasteringSig không đổi (module-level, persist qua recreation) →
      // chain FLAT ("spectrum hiển thị nhưng không xử lí âm thanh").
      _lastMasteringSig = null;
      toggleMasteringOnMaster(window.currentMasteringSettings.masterConnected, window.currentMasteringSettings.isBypassed);
      applyMasteringSettings(window.currentMasteringSettings);
    } catch (e) {
      console.warn('initMasterBus apply mastering error:', e);
    }
  }
  return masterBus;
}

function setMasterVolume(linear) {
  if (masterBus) masterBus.output.gain.setValueAtTime(linear, audioCtx.currentTime);
}

let _lastMasteringActive = null;
let _lastMasteringSig = null;
let _lastChainSig = null;

// Module input/output boundary nodes for dynamic chain re-routing
// (mastering_expand.md §II.3 — rebuildAudioGraph). Each module's INTERNAL
// wiring is fixed; only the boundaries get re-connected per chain order.
const MASTER_MODULE_IO = {
  eq: { input: 'eqLowFilter', output: 'eqHighFilter' },
  imager: { input: 'imagerInput', output: 'imagerOutput' },
  maximizer: { input: 'maximizerBoostGain', output: 'maximizerCompressor' },
  compressor: { input: 'compInput', output: 'compOutput' },
  limiter: { input: 'limInput', output: 'limOutput' },
  exciter: { input: 'excInput', output: 'excOutput' },
  rebalance: { input: 'rebalInput', output: 'rebalOutput' }
};

const DEFAULT_MASTER_CHAIN = [
  { id: 'mod_eq', type: 'eq', name: 'Dynamic EQ', active: true },
  { id: 'mod_imager', type: 'imager', name: 'Imager', active: true },
  { id: 'mod_maximizer', type: 'maximizer', name: 'Maximizer', active: true }
];

function chainSignature(chainArray) {
  return (chainArray || []).map(m => (m.type || '') + (m.active ? '1' : '0')).join(',');
}

// ── Track FX module factory (mastering_expand.md §II.4) ──
// The SAME module DSP used in the mastering chain, instantiated per-track for
// the [FX] button on track strips. Returns { input, output, nodes, dispose }.
// ── Track FX module factory (mastering_expand.md §II.4 / unified_fx_rack_panel.md) ──
// The SAME module DSP used in the mastering chain, instantiated per-track for
// the [FX] button on track strips. `params` are stored per chain entry so the
// unified FX Rack panel edits them declaratively. Returns { input, output, nodes }.
// ── Parametric / Graphic EQ Pro module (graphic_EQ_interactive_module.md) ──
// Logarithmic freq mapping 20Hz–20kHz, ±24dB gain, RBJ biquad response math.
const EQPRO_F_MIN = 20, EQPRO_F_MAX = 20000, EQPRO_MAX_DB = 24, EQPRO_MAX_BANDS = 8;
const EQPRO_BAND_COLORS = [
  { stroke: '#ef4444', fill: 'rgba(239,68,68,0.16)', badge: '#ef4444' },
  { stroke: '#f59e0b', fill: 'rgba(245,158,11,0.16)', badge: '#f59e0b' },
  { stroke: '#a855f7', fill: 'rgba(168,85,247,0.16)', badge: '#a855f7' },
  { stroke: '#38bdf8', fill: 'rgba(56,189,248,0.16)', badge: '#38bdf8' },
  { stroke: '#10b981', fill: 'rgba(16,185,129,0.16)', badge: '#10b981' },
  { stroke: '#ec4899', fill: 'rgba(236,72,153,0.16)', badge: '#ec4899' }
];
const EQPRO_DEFAULT_BANDS = [
  { type: 'lowshelf', freq: 80, gain: 0, q: 0.7, active: true },
  { type: 'peaking', freq: 250, gain: 0, q: 1.0, active: true },
  { type: 'peaking', freq: 1000, gain: 0, q: 1.0, active: true },
  { type: 'peaking', freq: 4000, gain: 0, q: 1.0, active: true },
  { type: 'highshelf', freq: 10000, gain: 0, q: 0.7, active: true }
];
function eqproFreqToX(f, w) { return w * (Math.log10(f / EQPRO_F_MIN) / Math.log10(EQPRO_F_MAX / EQPRO_F_MIN)); }
function eqproClamp(v, lo, hi) { return (typeof v === 'number' && isFinite(v)) ? Math.min(hi, Math.max(lo, v)) : lo; }
function eqproXToFreq(x, w) { return EQPRO_F_MIN * Math.pow(EQPRO_F_MAX / EQPRO_F_MIN, eqproClamp(x, 0, w) / w); }
function eqproGainToY(g, h) { return (h / 2) - (g * ((h / 2) / EQPRO_MAX_DB)); }
function eqproYToGain(y, h) { return ((h / 2) - y) * (EQPRO_MAX_DB / (h / 2)); }
function eqproQToWing(q) { return Math.max(12, Math.min(80, 110 / Math.sqrt(q))); }
function eqproWingToQ(offset) { return eqproClamp(parseFloat(Math.pow(110 / Math.max(12, offset), 2).toFixed(2)), 0.1, 18); }
// Filter shapes with a real Gain control (dB). highpass/lowpass/notch/bandpass
// ignore gain in WebAudio — the UI keeps them meaningful by anchoring the node
// at the natural −3 dB / notch-dip point of their response curve.
function eqproBandHasGain(type) { return type === 'peaking' || type === 'lowshelf' || type === 'highshelf'; }
// Vertical dB position where the node handle sits for a band (matches the curve).
function eqproNodeDb(b) {
  if (eqproBandHasGain(b.type)) return b.gain;
  if (b.type === 'notch') return -30;
  if (b.type === 'bandpass') return 0;
  return -3; // highpass / lowpass at fc
}
// RBJ Audio-EQ-Cookbook biquad magnitude (dB) — analytic band response used for
// band fills + summed master curve rendering (no AudioContext needed).
function eqproBiquadMagDb(type, f, f0, gainDb, q, fs) {
  const w0 = 2 * Math.PI * f0 / fs, cw = Math.cos(w0), sw = Math.sin(w0);
  const alpha = sw / (2 * Math.max(0.05, q));
  const A = Math.pow(10, eqproClamp(gainDb || 0, -24, 24) / 40);
  let b0, b1, b2, a0, a1, a2;
  if (type === 'peaking') {
    b0 = 1 + alpha * A; b1 = -2 * cw; b2 = 1 - alpha * A;
    a0 = 1 + alpha / A; a1 = -2 * cw; a2 = 1 - alpha / A;
  } else if (type === 'lowshelf') {
    b0 = A * ((A + 1) - (A - 1) * cw + 2 * Math.sqrt(A) * alpha);
    b1 = 2 * A * ((A - 1) - (A + 1) * cw);
    b2 = A * ((A + 1) - (A - 1) * cw - 2 * Math.sqrt(A) * alpha);
    a0 = (A + 1) + (A - 1) * cw + 2 * Math.sqrt(A) * alpha;
    a1 = -2 * ((A - 1) + (A + 1) * cw);
    a2 = (A + 1) + (A - 1) * cw - 2 * Math.sqrt(A) * alpha;
  } else if (type === 'highshelf') {
    b0 = A * ((A + 1) + (A - 1) * cw + 2 * Math.sqrt(A) * alpha);
    b1 = -2 * A * ((A - 1) + (A + 1) * cw);
    b2 = A * ((A + 1) + (A - 1) * cw - 2 * Math.sqrt(A) * alpha);
    a0 = (A + 1) - (A - 1) * cw + 2 * Math.sqrt(A) * alpha;
    a1 = 2 * ((A - 1) - (A + 1) * cw);
    a2 = (A + 1) - (A - 1) * cw - 2 * Math.sqrt(A) * alpha;
  } else if (type === 'highpass') {
    b0 = (1 + cw) / 2; b1 = -(1 + cw); b2 = (1 + cw) / 2;
    a0 = 1 + alpha; a1 = -2 * cw; a2 = 1 - alpha;
  } else if (type === 'lowpass') {
    b0 = (1 - cw) / 2; b1 = 1 - cw; b2 = (1 - cw) / 2;
    a0 = 1 + alpha; a1 = -2 * cw; a2 = 1 - alpha;
  } else if (type === 'notch') {
    b0 = 1; b1 = -2 * cw; b2 = 1;
    a0 = 1 + alpha; a1 = -2 * cw; a2 = 1 - alpha;
  } else { // bandpass (constant 0dB peak)
    b0 = alpha; b1 = 0; b2 = -alpha;
    a0 = 1 + alpha; a1 = -2 * cw; a2 = 1 - alpha;
  }
  const w = 2 * Math.PI * f / fs, cw1 = Math.cos(w), cw2 = Math.cos(2 * w);
  const num = b0 * b0 + b1 * b1 + b2 * b2 + 2 * (b0 * b1 + b1 * b2) * cw1 + 2 * b0 * b2 * cw2;
  const den = a0 * a0 + a1 * a1 + a2 * a2 + 2 * (a0 * a1 + a1 * a2) * cw1 + 2 * a0 * a2 * cw2;
  return 10 * Math.log10(Math.max(1e-10, num / Math.max(1e-10, den)));
}
// DSP module: serial BiquadFilterNode cascade (input → band1 → … → bandN →
// output), + post-module analyser for the realtime FFT spectrum overlay.
function createEqProModule(ctx, params) {
  const input = ctx.createGain();
  const output = ctx.createGain();
  const analyser = ctx.createAnalyser();
  analyser.fftSize = 2048;
  output.connect(analyser);
  const bands = (params && Array.isArray(params.bands)) ? JSON.parse(JSON.stringify(params.bands)) : JSON.parse(JSON.stringify(EQPRO_DEFAULT_BANDS));
  const filters = [];
  let amount = (params && params.amount !== undefined) ? params.amount : 100;
  const rebuild = () => {
    try { input.disconnect(); } catch (e) { }
    filters.forEach(f => { try { f.disconnect(); } catch (e) { } });
    filters.length = 0;
    let tail = input;
    const maxF = (ctx.sampleRate || 44100) * 0.45;
    bands.forEach(b => {
      const f = ctx.createBiquadFilter();
      f.type = b.type || 'peaking';
      f.frequency.value = eqproClamp(b.freq !== undefined ? b.freq : 1000, EQPRO_F_MIN, Math.min(EQPRO_F_MAX, maxF));
      f.Q.value = eqproClamp(b.q !== undefined ? b.q : 1, 0.1, 18);
      f.gain.value = (b.active !== false) ? ((b.gain || 0) * amount / 100) : 0;
      tail.connect(f);
      tail = f;
      filters.push(f);
    });
    tail.connect(output);
  };
  rebuild();
  const setBand = (i, patch) => {
    const b = bands[i]; if (!b) return;
    Object.assign(b, patch);
    const f = filters[i]; if (!f) return;
    const now = ctx.currentTime;
    const maxF = (ctx.sampleRate || 44100) * 0.45;
    if (patch.type !== undefined) f.type = patch.type;
    if (patch.freq !== undefined) f.frequency.setValueAtTime(eqproClamp(b.freq, EQPRO_F_MIN, Math.min(EQPRO_F_MAX, maxF)), now);
    if (patch.q !== undefined) f.Q.setValueAtTime(eqproClamp(b.q, 0.1, 18), now);
    if (patch.gain !== undefined || patch.active !== undefined) f.gain.setValueAtTime((b.active !== false) ? ((b.gain || 0) * amount / 100) : 0, now);
  };
  const setAmount = (a) => {
    amount = eqproClamp(a, 0, 200);
    const now = ctx.currentTime;
    filters.forEach((f, i) => { const b = bands[i]; if (b) f.gain.setValueAtTime((b.active !== false) ? ((b.gain || 0) * amount / 100) : 0, now); });
  };
  // Replace the internal band model + rebuild DSP immediately (add/delete/reset
  // from the UI) — guarantees the audible result matches the added bands at once.
  const syncBands = (newBands) => {
    bands.length = 0;
    (newBands || []).forEach(b => bands.push({ ...b }));
    rebuild();
  };
  return {
    type: 'eqpro',
    input, output, analyser,
    get bands() { return bands; },
    get filters() { return filters; },
    get amount() { return amount; },
    setBand, setAmount, rebuild, syncBands,
    destroy() { try { input.disconnect(); output.disconnect(); analyser.disconnect(); } catch (e) { } }
  };
}

function createTrackFxModule(type, ctx, params) {
  const num = (v, def) => {
    const n = Number(v);
    return isFinite(n) && !isNaN(n) ? n : def;
  };
  const maxF = (ctx.sampleRate || 44100) * 0.45;
  const clampF = v => Math.max(20, Math.min(v, maxF));

  const input = ctx.createGain();
  const output = ctx.createGain();
  const p = params || {};
  let nodes = {};
  if (type === 'compressor') {
    const comp = ctx.createDynamicsCompressor();
    comp.threshold.value = num(p.threshold, -16);
    comp.knee.value = 8; comp.ratio.value = num(p.ratio, 3);
    comp.attack.value = 0.02; comp.release.value = 0.25;
    const makeup = ctx.createGain(); makeup.gain.value = Math.pow(10, num(p.makeup, 0) / 20);
    input.connect(comp); comp.connect(makeup); makeup.connect(output);
    nodes = { comp, makeup };
  } else if (type === 'limiter') {
    // Brickwall Limiter bằng WaveShaper tanh soft-clip — KHÔNG DynamicsCompressor:
    // Chromium compressor phát NaN với bass transient mạnh (pitch thấp + vel cao
    // đồng loạt) → NaN vào master chain → 11 biquad "state is bad" → CÂM + stuck.
    const shaper = ctx.createWaveShaper();
    shaper.oversample = '2x';
    const ceilingDb = Math.min(0, num(p.ceiling, -1.0));
    const threshLin = Math.pow(10, ceilingDb / 20);
    const k = 1 / Math.max(0.02, threshLin);
    const _curve = new Float32Array(4096);
    const _tanhK = Math.tanh(k);
    for (let _i = 0; _i < 4096; _i++) {
      const _x = (_i / 4095) * 2 - 1;
      _curve[_i] = Math.tanh(_x * k) / _tanhK;
    }
    shaper.curve = _curve;
    const makeup = ctx.createGain();
    makeup.gain.value = 1.0;
    input.connect(shaper); shaper.connect(makeup); makeup.connect(output);
    nodes = { shaper, makeup };
  } else if (type === 'exciter') {
    const hp = ctx.createBiquadFilter();
    hp.type = 'highpass'; hp.frequency.value = clampF(2000); hp.Q.value = 0.7;
    const shaper = ctx.createWaveShaper();
    shaper.curve = makeDistortionCurve(3); shaper.oversample = '4x';
    const dry = ctx.createGain(); dry.gain.value = 1.0;
    const wet = ctx.createGain(); wet.gain.value = (num(p.drive, 40) / 100) * 0.6;
    input.connect(dry); dry.connect(output);
    input.connect(hp); hp.connect(shaper); shaper.connect(wet); wet.connect(output);
    nodes = { hp, shaper, dry, wet };
  } else if (type === 'rebalance') {
    const split = ctx.createChannelSplitter(2);
    const merge = ctx.createChannelMerger(2);
    const gLL = ctx.createGain(), gRL = ctx.createGain(), gLR = ctx.createGain(), gRR = ctx.createGain();
    const midLin = Math.pow(10, num(p.mid, 0) / 20);
    const sideLin = Math.pow(10, num(p.side, 0) / 20);
    const a = (midLin + sideLin) / 2, b = (midLin - sideLin) / 2;
    gLL.gain.value = a; gRR.gain.value = a; gRL.gain.value = b; gLR.gain.value = b;
    input.connect(split);
    split.connect(gLL, 0); split.connect(gRL, 0);
    split.connect(gLR, 1); split.connect(gRR, 1);
    gLL.connect(merge, 0, 0); gRL.connect(merge, 0, 0);
    gLR.connect(merge, 0, 1); gRR.connect(merge, 0, 1);
    merge.connect(output);
    nodes = { gLL, gRL, gLR, gRR };
  } else if (type === 'eqpro') {
    return createEqProModule(ctx, params);
  } else if (type === 'carla') {
    // Carla Bridge = VST FX chạy NGOÀI (ứng dụng ngoài, user tự cài) — trong
    // WebAudio graph chỉ là pass-through (không thêm DSP): âm track đi thẳng.
    input.connect(output);
    nodes = { passthrough: input };
  } else {
    // 'eq' or default: 4-band EQ (params.g1..g4 = band gains in dB)
    const f1 = ctx.createBiquadFilter(); f1.type = 'lowshelf'; f1.frequency.value = clampF(100);
    const f2 = ctx.createBiquadFilter(); f2.type = 'peaking'; f2.frequency.value = clampF(800); f2.Q.value = 0.7;
    const f3 = ctx.createBiquadFilter(); f3.type = 'peaking'; f3.frequency.value = clampF(3200); f3.Q.value = 1.2;
    const f4 = ctx.createBiquadFilter(); f4.type = 'highshelf'; f4.frequency.value = clampF(10000);
    f1.gain.value = num(p.g1, 0);
    f2.gain.value = num(p.g2, 0);
    f3.gain.value = num(p.g3, 0);
    f4.gain.value = num(p.g4, 0);
    input.connect(f1); f1.connect(f2); f2.connect(f3); f3.connect(f4); f4.connect(output);
    nodes = { f1, f2, f3, f4 };
  }
  return { input, output, nodes, type };
}

// Offline-export helper: builds a track node IDENTICAL to playback (track FX
// chain + mastering route + legacy chorus/reverb) but inside an OfflineAudioContext,
// keyed in a LOCAL map so the live graph is never touched. Audio clips are then
// scheduled through node.gainNode, and the offline master bus applies the
// mastering chain — the exported file therefore matches what you hear.
function buildOfflineTrackNode(track, ctx, nodeMap) {
  if (!track || nodeMap[track.id]) return nodeMap[track.id] || null;
  const gainNode = ctx.createGain();
  const volDb = track.volumeDb ?? 0;
  gainNode.gain.setValueAtTime(volDb <= -50 ? 0 : Math.pow(10, volDb / 20), 0);
  const pannerNode = ctx.createStereoPanner();
  pannerNode.pan.setValueAtTime((track.pan ?? 0) / 100, 0);
  const analyserNode = ctx.createAnalyser();
  analyserNode.fftSize = 2048;
  pannerNode.connect(analyserNode);
  if (!masterBus) initMasterBus(ctx);
  const route = createMasteringRoute(ctx, track, masterBus);
  analyserNode.connect(route.routeGain);
  analyserNode.connect(route.dryGain);
  let fxStopFn;
  const fxEntry = ctx.createGain();
  const fxLegacyIn = ctx.createGain();
  gainNode.connect(fxEntry);
  const fxChain = track.fxChain || [];
  const fxEnabled = track.fxActive !== false;
  const chainMods = fxEnabled ? fxChain.filter(m => m && m.active !== false).map(m => {
    try { return createTrackFxModule(m.type || m, ctx, m.params); } catch (e) { return null; }
  }).filter(Boolean) : [];
  let fxChainTail = fxEntry;
  chainMods.forEach(mod => { fxChainTail.connect(mod.input); fxChainTail = mod.output; });
  fxChainTail.connect(fxLegacyIn);
  if (fxEnabled && track.fxType === 'chorus') {
    const fxInput = ctx.createGain();
    fxLegacyIn.connect(fxInput);
    const chorus = createChorusNode(ctx, fxInput, pannerNode);
    fxStopFn = chorus.stop;
  } else if (fxEnabled && track.fxType === 'reverb') {
    const fxInput = ctx.createGain();
    fxLegacyIn.connect(fxInput);
    createReverbNode(ctx, fxInput, pannerNode);
  } else {
    fxLegacyIn.connect(pannerNode);
  }
  const node = { gainNode, pannerNode, analyserNode, route, fxStopFn, sfEntry: null, sfOut: null, sfRouteGain: null, sfDryGain: null };
  // Soundfont (MIDI cache) chain — mirrors the live node so cached MIDI buffers
  // flow through the track FX modules + mastering exactly like playback.
  if ((track.midiItems && track.midiItems.length > 0)) {
    const sfEntry = ctx.createGain();
    sfEntry.gain.setValueAtTime(1, 0);
    const sfOut = ctx.createGain();
    const sfPan = ctx.createStereoPanner();
    sfPan.pan.setValueAtTime((track.pan ?? 0) / 100, 0);
    sfOut.connect(sfPan);
    const sfRouteGain = ctx.createGain();
    const sfDryGain = ctx.createGain();
    const sfBypass = effMidiBypass(track);
    sfRouteGain.gain.value = sfBypass ? 0 : 1;
    sfDryGain.gain.value = sfBypass ? 1 : 0;
    sfPan.connect(sfRouteGain);
    sfPan.connect(sfDryGain);
    sfRouteGain.connect(masterBus.input);
    sfDryGain.connect(masterBus.dryInput);
    const sfMods = fxEnabled ? fxChain.filter(m => m && m.active !== false).map(m => {
      try { return createTrackFxModule(m.type || m, ctx, m.params); } catch (e) { return null; }
    }).filter(Boolean) : [];
    let sfTail = sfEntry;
    sfMods.forEach(mod => { sfTail.connect(mod.input); sfTail = mod.output; });
    sfTail.connect(sfOut);
    node.sfEntry = sfEntry;
    node.sfOut = sfOut;
    node.sfRouteGain = sfRouteGain;
    node.sfDryGain = sfDryGain;
  }
  nodeMap[track.id] = node;
  return node;
}

// Track-EQ preset library (unified_fx_rack_panel.md §III.1) — band gains only.
const TRACK_EQ_PRESETS = {
  flat: { name: 'Flat / Reset', g: [0, 0, 0, 0] },
  vocal_clarity: { name: 'Vocal Unmask & Clarity', g: [-2.5, -1.8, 3.2, 2.0] },
  bass_punch: { name: 'EDM Low-End Punch', g: [4.0, -3.0, 1.5, 1.0] },
  warm_tape: { name: 'Warm Vintage Analog', g: [2.0, 1.0, -2.0, -3.0] },
  guitar_edge: { name: 'Guitar Cut & Presence', g: [-3.0, -2.0, 3.5, 1.5] }
};

// Dynamic signal-chain reconstruction (mastering_expand.md §II.3):
// disconnect every module boundary, then wire the ACTIVE modules in series
// between inputAnalyser (chain input) and outputAnalyser (chain output).
function rebuildMasteringGraph(activate, chainArray) {
  if (!masterBus) return;
  const active = !!(activate);
  try {
    // 1. Disconnect all module boundary outputs (breaks static + previous dynamic links)
    masterBus.inputAnalyser.disconnect();
    masterBus.eqHighFilter.disconnect();
    masterBus.imagerOutput.disconnect();
    masterBus.maximizerCompressor.disconnect();
    masterBus.compOutput.disconnect();
    masterBus.limOutput.disconnect();
    masterBus.excOutput.disconnect();
    masterBus.rebalOutput.disconnect();

    if (!active) {
      // 2a. No mastering → straight chain: inputAnalyser → outputAnalyser
      masterBus.inputAnalyser.connect(masterBus.outputAnalyser);
      masterBus.masteringActive = false;
      return;
    }

    // 2b. Filter active modules
    const activeMods = (chainArray || []).filter(m => m && m.active);
    if (activeMods.length === 0) {
      masterBus.inputAnalyser.connect(masterBus.outputAnalyser);
      masterBus.masteringActive = true;
      return;
    }

    // 3. Wire in series: Input → mod[0].input → mod[0].output → mod[1].input → … → Output
    let prev = masterBus.inputAnalyser;
    // Dynamic modules (eqpro): rebuilt on every graph rebuild; instances kept
    // in masterBus.eqProInstances so the Mastering panel can patch them live.
    const eqProStore = masterBus.eqProInstances = masterBus.eqProInstances || {};
    Object.keys(eqProStore).forEach(k => { try { eqProStore[k].destroy && eqProStore[k].destroy(); } catch (e) { } });
    Object.keys(eqProStore).forEach(k => delete eqProStore[k]);
    activeMods.forEach(mod => {
      if (mod.type === 'carla') {
        // Carla Bridge = VST FX chạy NGOÀI (Carla standalone) — không có node
        // WebAudio trong master chain: pass-through, prev giữ nguyên.
        return;
      }
      if (mod.type === 'eqpro') {
        const m = createEqProModule(getAudioContext(), mod.params || {});
        eqProStore[mod.id] = m;
        prev.connect(m.input);
        prev = m.output;
      } else {
        const io = MASTER_MODULE_IO[mod.type] || MASTER_MODULE_IO.eq;
        const inNode = masterBus[io.input];
        if (inNode) prev.connect(inNode);
        prev = masterBus[io.output] || prev;
      }
    });
    prev.connect(masterBus.outputAnalyser);
    masterBus.masteringActive = true;
  } catch (e) {
    console.warn('rebuildMasteringGraph error:', e);
  }
}

function toggleMasteringOnMaster(activate, isBypassed) {
  if (!masterBus) return;

  const active = !!(activate && !isBypassed);
  const chain = (window.currentMasteringSettings && window.currentMasteringSettings.chain) || DEFAULT_MASTER_CHAIN;
  const cSig = chainSignature(chain);
  // Idempotent: don't rebuild unless active state OR chain layout changed.
  if (_lastMasteringActive === active && masterBus.masteringActive === active && _lastChainSig === cSig) return;
  _lastMasteringActive = active;
  _lastChainSig = cSig;

  // Disconnect + immediately reconnect in one synchronous block so the master
  // routing can NEVER be left broken (a mid-swap exception would otherwise
  // disconnect inputAnalyser and silence ALL audio globally).
  try {
    rebuildMasteringGraph(active, chain);
  } catch (e) {
    console.warn('toggleMasteringOnMaster error:', e);
    // Restore a guaranteed-valid default routing regardless of the failure.
    try {
      masterBus.inputAnalyser.disconnect();
      masterBus.maximizerCompressor.disconnect();
      masterBus.inputAnalyser.connect(masterBus.outputAnalyser);
      masterBus.masteringActive = false;
    } catch (e2) {}
  }
}

function getAudioContext() {
  if (!audioCtx) {
    audioCtx = new (window.AudioContext || window.webkitAudioContext)();
    if (window.SonicAudio && window.SonicAudio.initAudioWorklet) {
      window.SonicAudio.initAudioWorklet();
    }
  }
  if (audioCtx.state === 'suspended') {
    audioCtx.resume();
  }
  if (!masterBus) {
    initMasterBus(audioCtx);
  }
  // The mastering chain is ONLY managed by the masteringSettings effect — NOT
  // here. getAudioContext runs constantly (play, stopAll, VU, double-click…);
  // re-toggling/re-automating the biquad EQ here in bursts is "fast parameter
  // automation" that makes Chromium flag the filters as unstable
  // ("BiquadFilterNode: state is bad") and can leave the master routing broken
  // → global silence. The React effect applies it once per settings change.
  if (window.SonicSF && window.SonicSF.init) {
    window.SonicSF.init(audioCtx);
  }
  // C6: bootstrap native bridge 1 lần — query status, nối audio sink + router.
  if (window.NativeBridgeService && window.SonicMidiRouter && !window.__bridgeBootstrapped) {
    window.__bridgeBootstrapped = true;
    window.SonicMidiRouter.onFallback = function (cmd, ch, pitch, vel) {
      if (!window.SonicSF) return;
      if (cmd === 'PANIC') { if (window.SonicSF.stopAll) window.SonicSF.stopAll(); return; }
      if (cmd === 'NOTE_ON') window.SonicSF.playNote(pitch, vel, undefined, undefined, undefined, undefined, ch);
      else window.SonicSF.stopNote(ch, pitch);
    };
    (async function () {
      try {
        var st = await window.NativeBridgeService.queryStatus();
        var connected = !!(st && st.connected);
        window.SonicMidiRouter.setBridgeConnected(connected);
        console.log('[Bridge] bootstrap connected=', connected);
        if (connected) {
          try {
            console.log('[Bridge] Requesting restart to match sampleRate:', audioCtx.sampleRate);
            await window.__TAURI__.core.invoke('restart_bridge_with_sample_rate', { sampleRate: audioCtx.sampleRate });
            // Bridge restart → mất hết instrument → reset map để reconnect reload.
            window.__bridgeLoadedChannels = {};
            for (var retry = 0; retry < 10; retry++) {
              await new Promise(r => setTimeout(r, 100));
              var st2 = await window.NativeBridgeService.queryStatus();
              if (st2 && st2.connected) {
                console.log('[Bridge] Reconnected at sampleRate:', audioCtx.sampleRate);
                break;
              }
            }
          } catch (err) {
            console.warn('[Bridge] restart error:', err);
          }
          window.BridgeAudioNode.init(audioCtx);
          window.NativeBridgeService.onAudio(function (l, r) { window.BridgeAudioNode.onAudio(l, r); });
          if (window.AudioRoutingEngine) window.AudioRoutingEngine.connect(window.BridgeAudioNode, null, null);
        }
      } catch (e) {
        console.warn('[Bridge] bootstrap error:', e);
        window.SonicMidiRouter.setBridgeConnected(false);
      }
    })();
  }
  return audioCtx;
}
const formatTime = secs => {
  if (isNaN(secs) || secs < 0) return "0:00.000";
  const m = Math.floor(secs / 60);
  const s = Math.floor(secs % 60);
  const ms = Math.floor(secs % 1 * 1000).toString().padStart(3, '0');
  return `${m}:${s.toString().padStart(2, '0')}.${ms}`;
};
const formatTimeSimple = secs => {
  if (isNaN(secs) || secs < 0) return "0.00s";
  return `${secs.toFixed(2)}s`;
};
const formatBeat = (secs, bpmVal) => {
  if (isNaN(secs) || secs < 0) return "0.1.1";
  const beatDuration = 60 / bpmVal;
  const barDuration = beatDuration * 4;
  const bar = Math.floor(secs / barDuration);
  const beat = Math.floor((secs % barDuration) / beatDuration) + 1;
  const sub = Math.floor((secs % beatDuration) / (beatDuration / 4)) + 1;
  return `${bar}.${beat}.${sub}`;
};
const midiPitchToName = pitch => {
  const names = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
  const octave = Math.floor(pitch / 12) - 1;
  return names[pitch % 12] + octave;
};
const getBeatMarkers = (maxDur, bpmVal) => {
  const beatDuration = 60 / bpmVal;
  const barDuration = beatDuration * 4;
  const markers = [];
  for (let t = 0; t <= maxDur; t += beatDuration) {
    const isBar = Math.abs(t % barDuration) < 0.001 || Math.abs(t % barDuration - barDuration) < 0.001;
    markers.push({
      time: t,
      isBar,
      beatNum: Math.floor(t / beatDuration) + 1
    });
  }
  return markers;
};
const findZeroCrossing = (buffer, targetTime) => {
  if (!buffer) return targetTime;
  const sampleRate = buffer.sampleRate;
  const data = buffer.getChannelData(0);
  const targetSample = Math.floor(targetTime * sampleRate);
  const windowSize = Math.floor(0.04 * sampleRate);
  const start = Math.max(0, targetSample - windowSize);
  const end = Math.min(data.length - 2, targetSample + windowSize);
  let bestSample = targetSample;
  let minDistance = Infinity;
  for (let i = start; i <= end; i++) {
    if (data[i] >= 0 && data[i + 1] <= 0 || data[i] <= 0 && data[i + 1] >= 0) {
      const dist = Math.abs(i - targetSample);
      if (dist < minDistance) {
        minDistance = dist;
        bestSample = i;
      }
    }
  }
  return bestSample / sampleRate;
};

class ClientMIDIRecorder {
  constructor(audioContext, bpm = 120, timeSigNumerator = 4) {
    this.audioCtx = audioContext;
    this.bpm = bpm;
    this.timeSigNum = timeSigNumerator;
    this.isRecording = false;
    this.tempMidiItemId = null;

    this.activeNotes = new Map(); // Store pitch -> { noteId, startBeat, velocity }
    this.recordedNotes = [];
    this.recStartAudioTime = 0.0;
    this.recStartBar = 0.0;
    this.selectedMidiInputId = null;

    // Compute round-trip browser latency
    this.latencyCompSec = (this.audioCtx.baseLatency || 0) + (this.audioCtx.outputLatency || 0);
  }

  start(startBar = 0.0, selectedMidiInputId = null) {
    this.isRecording = true;
    this.recordedNotes = [];
    this.activeNotes.clear();
    this.recStartBar = startBar;
    this.recStartAudioTime = this.audioCtx.currentTime;
    this.selectedMidiInputId = selectedMidiInputId;
  }

  handleMIDIMessage(event, sourceInputId = null) {
    if (!this.isRecording) return;
    if (this.selectedMidiInputId && this.selectedMidiInputId !== 'ALL' && sourceInputId && sourceInputId !== this.selectedMidiInputId) {
      console.log(`[DevLog] [MIDI Rec] Ignoring input message from "${sourceInputId}" (Selected: "${this.selectedMidiInputId}")`);
      return;
    }

    const [status, pitch, velocity] = event.data;
    const command = status >> 4;

    // Apply latency compensation formula
    const currentTimeSec = Math.max(0, this.audioCtx.currentTime - this.recStartAudioTime - this.latencyCompSec);
    const secondsPerBeat = 60.0 / this.bpm;
    const currentBeat = currentTimeSec / secondsPerBeat;

    // Command 0x9: Note On
    if (command === 0x9 && velocity > 0) {
      const noteId = `rec_${Date.now()}_${pitch}`;
      const scaledVel = Math.min(1.0, Math.max(0.5, Math.round(velocity / 127.0 * 100) / 100));
      const newActiveNote = {
        id: noteId,
        pitch: pitch,
        start_beat: currentBeat,
        velocity: scaledVel
      };
      this.activeNotes.set(pitch, newActiveNote);
      console.log(`[DevLog] [MIDI Rec] Note On - Pitch: ${pitch}, Velocity: ${velocity}, StartBeat: ${currentBeat.toFixed(3)}, latencyCompSec: ${this.latencyCompSec.toFixed(3)}, noteObj:`, newActiveNote);

      // Fire visual feedback callback
      if (this.onNoteOn) {
        this.onNoteOn(pitch, currentBeat);
      }
    }
    // Command 0x8: Note Off (or Note On with velocity = 0)
    else if (command === 0x8 || (command === 0x9 && velocity === 0)) {
      if (this.activeNotes.has(pitch)) {
        const note = this.activeNotes.get(pitch);
        const durationBeats = Math.max(0.125, currentBeat - note.start_beat); // Min 1/32 note

        const finishedNote = {
          id: note.id,
          pitch: note.pitch,
          start_beat: note.start_beat,
          duration_beats: durationBeats,
          velocity: note.velocity,
          pan: 0.0
        };

        this.recordedNotes.push(finishedNote);
        this.activeNotes.delete(pitch);
        console.log(`[DevLog] [MIDI Rec] Note Off - Pitch: ${pitch}, DurationBeats: ${durationBeats.toFixed(3)}, FinishedNote:`, finishedNote);

        if (this.onNoteOff) {
          this.onNoteOff(pitch, finishedNote);
        }
      }
    }
  }

  stop() {
    this.isRecording = false;

    // Flush remaining active keypresses when stop is triggered
    const currentTimeSec = Math.max(0, this.audioCtx.currentTime - this.recStartAudioTime - this.latencyCompSec);
    const currentBeat = currentTimeSec / (60.0 / this.bpm);

    for (let [pitch, note] of this.activeNotes.entries()) {
      const durationBeats = Math.max(0.25, currentBeat - note.start_beat);
      const finishedNote = {
        id: note.id,
        pitch: note.pitch,
        start_beat: note.start_beat,
        duration_beats: durationBeats,
        velocity: note.velocity,
        pan: 0.0
      };
      this.recordedNotes.push(finishedNote);
      console.log(`[DevLog] [MIDI Rec] Flushing active keypress on stop - Pitch: ${pitch}, DurationBeats: ${durationBeats.toFixed(3)}, FinishedNote:`, finishedNote);
    }
    this.activeNotes.clear();

    console.log(`[DevLog] [MIDI Rec] Recording stopped. Total notes: ${this.recordedNotes.length}. List:`, this.recordedNotes);
    return this.recordedNotes;
  }
}

class ClientAudioRecorder {
  constructor(audioContext) {
    this.audioCtx = audioContext;
    this.mediaStream = null;
    this.sourceNode = null;
    this.workletNode = null;
    this.pcmChunks = [];
    this.isRecording = false;
  }

  async initializeInput(deviceId = null) {
    const constraints = {
      audio: {
        deviceId: deviceId ? { exact: deviceId } : undefined,
        echoCancellation: false,
        noiseSuppression: false,
        autoGainControl: false
      }
    };
    this.mediaStream = await navigator.mediaDevices.getUserMedia(constraints);
    this.sourceNode = this.audioCtx.createMediaStreamSource(this.mediaStream);
  }

  async start(destinationTrackGainNode = null, enableMonitoring = true) {
    this.pcmChunks = [];
    this.isRecording = true;

    // Load Worklet Processor Module
    await this.audioCtx.audioWorklet.addModule('/static/processors/pcm-recorder-processor.js');
    this.workletNode = new AudioWorkletNode(this.audioCtx, 'pcm-recorder-processor');

    // Receive PCM data streams from AudioWorklet
    this.workletNode.port.onmessage = (event) => {
      if (this.isRecording && event.data.type === 'PCM_DATA') {
        const chunk = new Float32Array(event.data.buffer);
        this.pcmChunks.push(chunk);

        // VU Meter Level Callback
        if (this.onLevelUpdate) {
          let sum = 0;
          for (let i = 0; i < chunk.length; i++) {
            sum += chunk[i] * chunk[i];
          }
          const rms = Math.sqrt(sum / chunk.length);
          const db = rms > 0 ? 20 * Math.log10(rms) : -96;
          this.onLevelUpdate(db);
        }

        // Live visualization Callback
        if (this.onPCMChunk) {
          this.onPCMChunk(chunk);
        }
      }
    };

    // Route Audio Nodes
    this.sourceNode.connect(this.workletNode);

    // Enable Live Input Monitoring if requested
    if (enableMonitoring && destinationTrackGainNode) {
      this.sourceNode.connect(destinationTrackGainNode);
    }
  }

  async stop() {
    this.isRecording = false;

    if (this.sourceNode && this.workletNode) {
      try {
        this.sourceNode.disconnect(this.workletNode);
      } catch (e) { }
    }

    if (this.mediaStream) {
      this.mediaStream.getTracks().forEach(track => track.stop());
    }

    // Concatenate PCM Float32Array chunks into a single AudioBuffer
    const totalSamples = this.pcmChunks.reduce((sum, chunk) => sum + chunk.length, 0);
    if (totalSamples === 0) return null;

    const audioBuffer = this.audioCtx.createBuffer(1, totalSamples, this.audioCtx.sampleRate);
    const channelData = audioBuffer.getChannelData(0);

    let offset = 0;
    for (const chunk of this.pcmChunks) {
      channelData.set(chunk, offset);
      offset += chunk.length;
    }

    return audioBuffer; // Return compiled AudioBuffer for timeline insertion
  }
}

const VolumeKnob = ({
  value,
  onChange,
  min = 0,
  max = 1
}) => {
  const [isDragging, setIsDragging] = useState(false);
  const startY = useRef(0);
  const startValue = useRef(0);
  const rotation = useMemo(() => {
    const percent = (value - min) / (max - min);
    return -135 + percent * 270;
  }, [value, min, max]);
  const handleMouseDown = e => {
    setIsDragging(true);
    startY.current = e.clientY;
    startValue.current = value;
    document.addEventListener('mousemove', handleMouseMove);
    document.addEventListener('mouseup', handleMouseUp);
  };
  const handleMouseMove = e => {
    const deltaY = startY.current - e.clientY;
    const sensitivity = 0.005;
    const newValue = Math.max(min, Math.min(max, startValue.current + deltaY * sensitivity));
    onChange(parseFloat(newValue.toFixed(2)));
  };
  const handleMouseUp = () => {
    setIsDragging(false);
    document.removeEventListener('mousemove', handleMouseMove);
    document.removeEventListener('mouseup', handleMouseUp);
  };
  return /*#__PURE__*/React.createElement("div", {
    className: "knob-container cursor-ns-resize flex flex-col items-center",
    onMouseDown: handleMouseDown,
    title: `Volume: ${Math.round(value * 100)}%`
  }, /*#__PURE__*/React.createElement("svg", {
    className: "w-7 h-7",
    viewBox: "0 0 40 40"
  }, /*#__PURE__*/React.createElement("circle", {
    cx: "20",
    cy: "20",
    r: "16",
    fill: "#141414",
    stroke: "#444",
    strokeWidth: "2"
  }), /*#__PURE__*/React.createElement("g", {
    transform: `rotate(${rotation} 20 20)`,
    className: "knob-dial"
  }, /*#__PURE__*/React.createElement("line", {
    x1: "20",
    y1: "20",
    x2: "20",
    y2: "6",
    stroke: "#ef4444",
    strokeWidth: "3",
    strokeLinecap: "round"
  }))));
};
const MixerStrip = ({ track, index, onUpdateTrack, trackVuRefs }) => {
  const dbLabel = (track.volumeDb == null || track.volumeDb <= -50) ? '-inf' : (track.volumeDb > 0 ? '+' : '') + (track.volumeDb || 0).toFixed(1) + 'dB';
  const isMuted = track.muted;
  const isSoloed = track.solo;
  const isAudioBypassed = !!track.audioBypass;
  const isMidiBypassed = !!track.midiBypass;
  const vol = track.volumeDb != null ? track.volumeDb : 0;
  var pct = Math.max(0, Math.min(100, (vol + 60) / 72 * 100));
  var vuColor = pct >= 80 ? '#ef4444' : pct >= 50 ? '#eab308' : '#22c55e';
  var trackColor = track.color || '#06b6d4';
  return React.createElement("div", {
    className: "flex flex-col items-stretch w-[84px] shrink-0 bg-[#2b2b2b] border border-black/70 overflow-hidden rounded-sm"
  },
    React.createElement("div", {
      className: "flex items-center justify-between px-1 py-0.5 bg-[#222] border-b border-black/60 shrink-0"
    }, React.createElement("span", {
      className: "text-[9px] font-mono font-bold text-zinc-400"
    }, (index + 1))),
    React.createElement("div", {
      className: "flex items-center justify-center gap-1 py-0.5 shrink-0"
    }, React.createElement("button", {
   onClick: e => { e.stopPropagation(); const next = !track.muted; if (onUpdateTrack) onUpdateTrack(track.id, { muted: next }); if (window.__applyTrackMuteSolo) window.__applyTrackMuteSolo(track.id, { muted: next }); },
   title: "Mute",
   className: "w-5 h-5 flex items-center justify-center rounded-sm text-[10px] font-mono font-bold border transition " + (isMuted ? 'bg-orange-500 text-black border-orange-400' : 'bg-[#3a3a3a] text-zinc-400 border-black/60 hover:text-zinc-100')
 }, "M"), React.createElement("button", {
   onClick: e => { e.stopPropagation(); const next = !track.solo; if (onUpdateTrack) onUpdateTrack(track.id, { solo: next }); if (window.__applyTrackMuteSolo) window.__applyTrackMuteSolo(track.id, { solo: next }); },
   title: "Solo",
   className: "w-5 h-5 flex items-center justify-center rounded-sm text-[10px] font-mono font-bold border transition " + (isSoloed ? 'bg-yellow-400 text-black border-yellow-300' : 'bg-[#3a3a3a] text-zinc-400 border-black/60 hover:text-zinc-100')
 }, "S"), React.createElement("button", {
      onClick: e => {
        e.stopPropagation();
        const next = !(track.audioBypass ?? track.masteringBypass);
        if (onUpdateTrack) onUpdateTrack(track.id, { audioBypass: next });
        // Live audio re-route (applies immediately to playing tracks).
        if (window.__setTrackBypass) window.__setTrackBypass(track.id, 'audio', next);
        else if (window.__setTrackMasteringBypass) window.__setTrackMasteringBypass(track.id, next);
      },
      title: "A = Mastering FX Chain cho audio items (clips + sections): XÁM = bypass mastering (vẫn qua track FX Rack), SÁNG XANH = qua mastering",
      className: "w-5 h-5 flex items-center justify-center rounded-sm text-[10px] font-mono font-bold border transition " + (isAudioBypassed ? 'bg-[#3a3a3a] text-zinc-400 border-black/60 hover:text-zinc-100' : 'bg-sky-400 text-black border-sky-300')
    }, "A"), React.createElement("button", {
      onClick: e => {
        e.stopPropagation();
        const next = !(track.midiBypass ?? track.masteringBypass);
        if (onUpdateTrack) onUpdateTrack(track.id, { midiBypass: next });
        if (window.__setTrackBypass) window.__setTrackBypass(track.id, 'midi', next);
        else if (window.__setTrackMasteringBypass) window.__setTrackMasteringBypass(track.id, next);
      },
      title: "♪ = Mastering FX Chain cho MIDI (soundfont): XÁM = bypass mastering (vẫn qua track FX Rack), SÁNG TÍM = qua mastering",
      className: "w-5 h-5 flex items-center justify-center rounded-sm text-[10px] font-mono font-bold border transition " + (isMidiBypassed ? 'bg-[#3a3a3a] text-zinc-400 border-black/60 hover:text-zinc-100' : 'bg-fuchsia-400 text-black border-fuchsia-300')
    }, "\u266A")),
    React.createElement("div", {
      className: "flex-1 flex items-stretch justify-center gap-1 px-1 py-1 min-h-0"
    }, React.createElement("div", {
      className: "w-[30px] rounded-sm relative overflow-hidden bg-[#0d0d0d] border border-black/60 flex flex-col items-center cursor-pointer",
      onMouseDown: function(e) {
        e.preventDefault();
        var rect = e.currentTarget.getBoundingClientRect();
        var tid = track.id;
        function onMove(ev) {
          var pct = 1 - Math.max(0, Math.min(1, (ev.clientY - rect.top) / rect.height));
          var val = Math.round((pct * 72 - 60) * 2) / 2;
          if (onUpdateTrack) onUpdateTrack(tid, { volumeDb: val });
        }
        function onUp() { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); }
        document.addEventListener('mousemove', onMove);
        document.addEventListener('mouseup', onUp);
        onMove(e);
      }
    },
    /* 0dB reference line */
    React.createElement("div", {
      className: "absolute w-full h-px bg-amber-400/60 z-10 pointer-events-none",
      style: { bottom: '83.333%' }
    }),
    /* Background gradient */
    React.createElement("div", {
      className: "absolute inset-0",
      style: { background: 'linear-gradient(to top, #22c55e, #eab308, #ef4444)' }
    }),
    /* Level overlay - dark at TOP, gradient visible at bottom */
    React.createElement("div", {
      className: "absolute top-0 w-full transition-all duration-75 bg-[#0d0d0d]",
      style: { height: (100 - pct) + '%' }
    })), React.createElement("canvas", {
      ref: el => {
        if (el) trackVuRefs.current[track.id + '_mixer'] = el;
        else delete trackVuRefs.current[track.id + '_mixer'];
      },
      width: 15,
      height: 120,
      className: "w-[15px] rounded-sm bg-[#0d0d0d] border border-black/60 block h-full"
    })),
    React.createElement("div", {
      className: "text-center text-[9px] font-mono font-bold py-0.5 " + (vol > 0 ? 'text-orange-400' : 'text-zinc-300') + " bg-[#1c1c1c] border-t border-black/50 shrink-0"
    }, dbLabel),
    React.createElement("div", {
      className: "text-[8px] font-mono truncate w-full text-center px-1 py-0.5 bg-[#222] border-t border-black/60 shrink-0",
    style: { color: trackColor }
  }, track.name));
};

// ── Master Strip Console Component (from md/47_MASTER_STRIP_CONSOLE.md) ──
const MasterStripConsole = ({ masterVolume, setMasterVolume, showMasteringModal, setShowMasteringModal, masteringSettings, setMasteringSettings, isPlaying }) => {
  const [isFxActive, setIsFxActive] = React.useState(true);
  const [isTestPlaying, setIsTestPlaying] = React.useState(false);
  const [isMuted, setIsMuted] = React.useState(false);
  const [isMono, setIsMono] = React.useState(false);
  const [pan, setPan] = React.useState(0.0);
  const [panText, setPanText] = React.useState('center');

  const vuCanvasRef = React.useRef(null);
  const animFrameRef = React.useRef(null);
  const rmsValRef = React.useRef(null);
  const peakLRef = React.useRef(null);
  const peakRRef = React.useRef(null);
  const panPointerRef = React.useRef(null);
  const isMasterActive = masteringSettings ? masteringSettings.masterConnected : false;
  const panStartYRef = React.useRef(0);
  const startPanValRef = React.useRef(0);

  const ensureAudio = () => {
    getAudioContext();
  };

  const handleFaderChange = (val) => {
    setMasterVolume(val);
    ensureAudio();
    if (masterBus && masterBus.output) {
      const linear = val <= -50 ? 0 : Math.pow(10, val / 20);
      masterBus.output.gain.setTargetAtTime(linear, audioCtx.currentTime, 0.01);
    }
  };

  const handlePanPointerDown = (e) => {
    isPanDraggingRef.current = true;
    panStartYRef.current = e.clientY;
    startPanValRef.current = pan;
    e.currentTarget.setPointerCapture(e.pointerId);
  };

  const handlePanPointerMove = (e) => {
    if (!isPanDraggingRef.current) return;
    const deltaY = panStartYRef.current - e.clientY;
    let newPan = startPanValRef.current + (deltaY / 80);
    newPan = Math.min(1.0, Math.max(-1.0, newPan));
    setPan(newPan);
    const angle = newPan * 120;
    if (panPointerRef.current) panPointerRef.current.style.transform = `rotate(${angle}deg)`;
    if (newPan === 0) setPanText('center');
    else if (newPan < 0) setPanText('L' + Math.abs(Math.round(newPan * 100)));
    else setPanText('R' + Math.round(newPan * 100));
  };

  const handlePanPointerUp = (e) => {
    isPanDraggingRef.current = false;
    try { e.currentTarget.releasePointerCapture(e.pointerId); } catch(err) {}
  };

  React.useEffect(() => {
    const canvas = vuCanvasRef.current;
    if (!canvas) return;
    const ctx = canvas.getContext('2d');

    function render() {
      animFrameRef.current = requestAnimationFrame(render);
      canvas.width = canvas.clientWidth;
      canvas.height = canvas.clientHeight;
      const w = canvas.width;
      const h = canvas.height;
      ctx.clearRect(0, 0, w, h);

      let levelL = 0;
      let levelR = 0;

      if (masterBus && masterBus.leftAnalyser && masterBus.rightAnalyser) {
        const leftData = new Uint8Array(256);
        const rightData = new Uint8Array(256);
        masterBus.leftAnalyser.getByteTimeDomainData(leftData);
        masterBus.rightAnalyser.getByteTimeDomainData(rightData);
        let peakL = 0;
        let peakR = 0;
        for (let i = 0; i < leftData.length; i++) {
          const v = Math.abs(leftData[i] - 128) / 128;
          if (v > peakL) peakL = v;
        }
        for (let i = 0; i < rightData.length; i++) {
          const v = Math.abs(rightData[i] - 128) / 128;
          if (v > peakR) peakR = v;
        }
        levelL = peakL;
        levelR = peakR;
      }

      const padding = 4;
      const gap = 4;
      const barW = Math.max(4, (w - padding * 2 - gap) / 2);
      const grad = ctx.createLinearGradient(0, h, 0, 0);
      grad.addColorStop(0, '#10b981');
      grad.addColorStop(0.7, '#f59e0b');
      grad.addColorStop(0.95, '#ef4444');
      ctx.fillStyle = grad;
      ctx.fillRect(padding, h - levelL * h, barW, levelL * h);
      ctx.fillRect(padding + barW + gap, h - levelR * h, barW, levelR * h);

      const maxLevel = Math.max(levelL, levelR);
      if (rmsValRef.current) {
        rmsValRef.current.innerText = maxLevel > 0 ? (20 * Math.log10(maxLevel) - 3.2).toFixed(1) + ' dB' : '-inf';
      }
      if (peakLRef.current) { peakLRef.current.innerText = levelL > 0 ? (20 * Math.log10(levelL)).toFixed(1) + 'dB' : '-inf'; }
      if (peakRRef.current) { peakRRef.current.innerText = levelR > 0 ? (20 * Math.log10(levelR)).toFixed(1) + 'dB' : '-inf'; }
    }

    render();
    return () => {
      if (animFrameRef.current) cancelAnimationFrame(animFrameRef.current);
    };
  }, [masterVolume, isMuted, isMono]);

  return React.createElement("div", {
    className: "flex flex-col items-stretch w-[300px] shrink-0 strip-bg rounded-lg p-2 text-slate-300 select-none shadow-2xl relative overflow-hidden"
  },
    React.createElement("div", { className: "space-y-1.5 mb-2" },
      React.createElement("button", {
        onClick: () => setShowMasteringModal(true),
        className: "w-full bg-slate-800 hover:bg-slate-700 text-[10px] font-semibold py-1 rounded border border-slate-700 text-slate-300 tracking-tight"
      }, "MASTERING PANEL"),
      React.createElement("div", {
        className: "flex items-center justify-between bg-slate-950 border border-slate-800 rounded px-1.5 py-0.5 text-[10px] font-mono"
      },
        React.createElement("span", { className: "text-slate-400 truncate" }, "Output 1 / Output 2"),
        React.createElement("i", { className: "fa-solid fa-circle-notch text-[9px] text-slate-500" })
      )
    ),
    React.createElement("div", { className: "flex flex-col items-center my-0.5" },
      React.createElement("span", { className: "text-[9px] text-slate-400 font-mono" }, panText || 'center'),
      React.createElement("div", { className: "flex items-center gap-1" },
        React.createElement("span", { className: "text-[8px] font-mono text-slate-500 w-5 text-right" },
          pan < 0 ? 'L' + Math.abs(Math.round(pan * 100)) : ''
        ),
        React.createElement("div", {
          id: "panDial",
          className: "w-6 h-6 rounded-full bg-slate-800 border-2 border-slate-600 relative flex items-center justify-center shadow-inner cursor-pointer",
          title: "Kéo chuột để chỉnh Pan (Left/Right)",
          onPointerDown: handlePanPointerDown,
          onPointerMove: handlePanPointerMove,
          onPointerUp: handlePanPointerUp,
          onWheel: function(e) {
            e.preventDefault();
            var step = e.deltaY > 0 ? -0.05 : 0.05;
            var newPan = Math.max(-1, Math.min(1, pan + step));
            newPan = Math.round(newPan * 100) / 100;
            setPan(newPan);
            if (panPointerRef.current) panPointerRef.current.style.transform = 'rotate(' + (newPan * 120) + 'deg)';
            if (newPan === 0) setPanText('center');
            else if (newPan < 0) setPanText('L' + Math.abs(Math.round(newPan * 100)));
            else setPanText('R' + Math.round(newPan * 100));
          },
          onDoubleClick: function() { setPan(0); setPanText('center'); if (panPointerRef.current) panPointerRef.current.style.transform = 'rotate(0deg)'; }
        }, React.createElement("div", {
          ref: panPointerRef,
          id: "panPointer",
          className: "w-0.5 h-2 bg-slate-200 rounded absolute top-0.5 transition-transform",
          style: { transform: 'rotate(' + (pan * 120) + 'deg)' }
        })),
        React.createElement("span", { className: "text-[8px] font-mono text-slate-500 w-5 text-left" },
          pan > 0 ? 'R' + Math.round(pan * 100) : ''
        ),
        React.createElement("span", { ref: React.createRef ? null : null, className: "text-[10px] font-bold font-mono text-slate-200 ml-8", onDoubleClick: function() { handleFaderChange(0); } },
          masterVolume <= -50 ? '-inf' : (masterVolume > 0 ? '+' : '') + masterVolume.toFixed(1)
        )
      )
    ),
      React.createElement("div", { className: "flex gap-1 my-1 justify-between items-stretch min-h-0", style: {flex: '1 1 0%'} },
        React.createElement("div", { className: "flex-1 flex flex-col bg-black/80 p-1.5 rounded border border-slate-800/90 relative shadow-inner" },
          React.createElement("div", { className: "flex-1 flex items-stretch justify-between min-h-0" },
            React.createElement("div", { className: "flex flex-col h-full text-[8px] font-mono text-slate-400 select-none pr-1 text-right border-r border-slate-800/60 overflow-hidden" },
           React.createElement("span", { className: "flex-1 flex items-center justify-end" }, "+12"), React.createElement("span", { className: "flex-1 flex items-center justify-end" }, "+6"), React.createElement("span", { className: "flex-1 flex items-center justify-end" }, "0"), React.createElement("span", { className: "flex-1 flex items-center justify-end" }, "-6"),
           React.createElement("span", { className: "flex-1 flex items-center justify-end" }, "-12"), React.createElement("span", { className: "flex-1 flex items-center justify-end" }, "-18"), React.createElement("span", { className: "flex-1 flex items-center justify-end" }, "-24"), React.createElement("span", { className: "flex-1 flex items-center justify-end" }, "-30"),
           React.createElement("span", { className: "flex-1 flex items-center justify-end" }, "-36"), React.createElement("span", { className: "flex-1 flex items-center justify-end" }, "-42"), React.createElement("span", { className: "flex-1 flex items-center justify-end" }, "-54")
         ),
         React.createElement("div", { className: "flex-1 relative bg-slate-950 rounded overflow-hidden mx-1.5 border border-slate-900" },
          React.createElement("canvas", { ref: vuCanvasRef, className: "w-[100px] h-full block" }),
          React.createElement("div", { className: "absolute bottom-0.5 inset-x-0 flex justify-around text-[7px] font-mono text-slate-500 font-bold pointer-events-none bg-black/60 py-0.5" },
            React.createElement("span", null, "L"), React.createElement("span", null, "R")
          )
        ),
         React.createElement("div", { className: "flex flex-col h-full text-[8px] font-mono text-slate-400 select-none pl-1 text-left border-l border-slate-800/60 overflow-hidden" },
           React.createElement("span", { className: "flex-1 flex items-center justify-start" }, "+12"), React.createElement("span", { className: "flex-1 flex items-center justify-start" }, "+6"), React.createElement("span", { className: "flex-1 flex items-center justify-start" }, "0"), React.createElement("span", { className: "flex-1 flex items-center justify-start" }, "-6"),
           React.createElement("span", { className: "flex-1 flex items-center justify-start" }, "-12"), React.createElement("span", { className: "flex-1 flex items-center justify-start" }, "-18"), React.createElement("span", { className: "flex-1 flex items-center justify-start" }, "-24"), React.createElement("span", { className: "flex-1 flex items-center justify-start" }, "-30"),
            React.createElement("span", { className: "flex-1 flex items-center justify-start" }, "-36"), React.createElement("span", { className: "flex-1 flex items-center justify-start" }, "-42"), React.createElement("span", { className: "flex-1 flex items-center justify-start" }, "-54")
          )
        ),
        React.createElement("div", { className: "flex justify-between text-[9px] font-mono text-slate-400 mt-0.5" },
          React.createElement("span", { ref: peakLRef }, "-inf"),
          React.createElement("span", { ref: peakRRef }, "-inf")
        )
      ),
      React.createElement("div", { className: "w-16 flex items-stretch gap-1 bg-slate-900/60 p-1 rounded border border-slate-800" },
        React.createElement("div", {
          className: "flex-1 flex flex-col items-center justify-center relative fader-track rounded",
          onWheel: function(e) {
            e.preventDefault();
            var delta = e.deltaY > 0 ? -0.5 : 0.5;
            handleFaderChange(Math.max(-60, Math.min(12, masterVolume + delta)));
          }
        },
          React.createElement("div", { className: "w-0.5 h-full bg-slate-700 absolute left-1/2 -translate-x-1/2 top-0" }),
          React.createElement("input", {
            id: "masterFader",
            type: "range",
            min: "-60",
            max: "12",
            step: "0.5",
            value: masterVolume,
            className: "fader-slider w-full z-10",
            orient: "vertical",
            onChange: function(e) { handleFaderChange(parseFloat(e.target.value)); },
            onDoubleClick: function() { handleFaderChange(0); }
          })
        ),
        React.createElement("div", { className: "relative w-5 text-[7px] font-mono text-slate-500 select-none overflow-hidden" },
          React.createElement("span", { className: "absolute", style: {top: '0%', right: '2px'} }, "+12"),
          React.createElement("span", { className: "absolute", style: {top: '8.3%', right: '2px'} }, "+6"),
          React.createElement("span", { className: "absolute", style: {top: '16.7%', right: '2px'} }, "0"),
          React.createElement("span", { className: "absolute", style: {top: '25%', right: '2px'} }, "-6"),
          React.createElement("span", { className: "absolute", style: {top: '33.3%', right: '2px'} }, "-12"),
          React.createElement("span", { className: "absolute", style: {top: '50%', right: '2px'} }, "-24"),
          React.createElement("span", { className: "absolute", style: {top: '66.7%', right: '2px'} }, "-36"),
          React.createElement("span", { className: "absolute", style: {top: '91.7%', right: '2px'} }, "-54")
        )
       ),
      React.createElement("div", { className: "w-8 flex flex-col justify-between text-[10px] font-bold" },
        React.createElement("button", {
          id: "monoBtn",
          onClick: function() { setIsMono(function(p) { return !p; }); },
          className: "btn-daw h-[18px] rounded flex flex-col items-center justify-center text-[8px]" + (isMono ? " btn-mono-active" : ""),
          title: "Mono Switch"
        },
          React.createElement("i", { className: "fa-solid fa-circle-half-stroke text-[9px]" }),
          React.createElement("span", null, "MONO")
        ),
        React.createElement("button", {
          id: "muteBtn",
          onClick: function() { setIsMuted(function(p) { return !p; }); },
          className: "btn-daw h-[18px] rounded text-amber-500 font-bold hover:text-amber-400" + (isMuted ? " btn-mute-active" : ""),
          title: "Mute Master Output"
        }, "M"),
        React.createElement("button", {
          id: "soloBtn",
          className: "btn-daw h-[18px] rounded text-yellow-400 font-bold hover:text-yellow-300",
          title: "Solo Master"
        }, "S"),
        React.createElement("button", { className: "btn-daw h-[18px] rounded text-slate-400 hover:text-slate-200", title: "Route Matrix" },
          React.createElement("i", { className: "fa-solid fa-diagram-project text-[9px]" })
        ),
        React.createElement("button", {
          id: "fxBtn",
          className: "btn-daw h-[18px] rounded font-extrabold text-[10px] transition-all",
          onClick: function() { setShowMasteringModal(true); },
          title: "Mở MASTERING PANEL để chỉnh sửa"
        }, "FX"),
        React.createElement("button", {
          id: "powerBtn",
          className: "btn-daw h-[18px] rounded text-xs transition-all" + (isMasterActive ? " btn-teal-active" : ""),
          onClick: function() {
            if (setMasteringSettings) {
              setMasteringSettings(function(prev) { return Object.assign({}, prev, { masterConnected: !prev.masterConnected, isBypassed: false }); });
            }
          },
          title: "Bật/Tắt MASTERING PANEL Bypass"
        }, [React.createElement("i", { className: "fa-solid fa-power-off" + (isFxActive ? " text-emerald-400" : " text-slate-500"), key: "ico" }), React.createElement("span", { key: "lbl", className: "text-[7px] font-bold" + (isFxActive ? " text-emerald-300" : " text-slate-400") }, "PWR")]),
        React.createElement("button", { className: "btn-daw h-[18px] rounded text-slate-400 text-[8px]", title: "Trim Envelope" }, "TRIM"),
        React.createElement("button", { className: "btn-daw h-[18px] rounded text-slate-400 text-[9px]", title: "Session Info" },
          React.createElement("i", { className: "fa-solid fa-info" })
        )
      )
    ),
    React.createElement("div", { className: "text-center text-[10px] font-bold font-mono text-slate-200 shrink-0" },
      masterVolume <= -50 ? '-inf' : (masterVolume > 0 ? '+' : '') + masterVolume.toFixed(1)
    ),
    React.createElement("div", { className: "shrink-0 border-t border-slate-800 flex flex-col items-center" },
      React.createElement("div", { className: "flex justify-between w-full text-[9px] font-mono py-0.5" },
        React.createElement("span", { className: "text-emerald-400" }, "RMS"),
        React.createElement("span", { ref: rmsValRef, className: "text-emerald-400 font-bold" }, "-inf")
      ),
      React.createElement("div", {
        className: "w-full text-center bg-black/80 py-0.5 rounded border border-slate-800 text-[10px] font-extrabold tracking-widest text-slate-100 uppercase"
      },
        React.createElement("span", null, "MAIN OUT")
      )
    )
  );
};

// ── Track Strip Console Component (from md/48_TRACK_STRIP_COMPONENT.md) ──
const TrackStripConsole = ({ track, index, onUpdateTrack, trackVuRefs, style }) => {
  var vol = track.volumeDb != null ? track.volumeDb : 0;
  var trackColor = track.color || '#06b6d4';
  var isMuted = track.muted;
  var isSoloed = track.solo;
  var isBypassed = !!(track.audioBypass ?? track.masteringBypass);
  var isArmed = track.isArmed;
  var trackName = track.name || 'Track ' + (index + 1);
  var isMicActive = track.inputSource?.deviceType === 'MICROPHONE';

  const [pan, setPan] = React.useState(0.0);
  const [panLabel, setPanLabel] = React.useState('center');
  const [isPhaseInverted, setIsPhaseInverted] = React.useState(false);
  const panPointerRef = React.useRef(null);
  const setVuCanvas = React.useCallback(function(el) {
    if (el) trackVuRefs.current[track.id + '_mixer'] = el;
    else delete trackVuRefs.current[track.id + '_mixer'];
  }, [track.id, trackVuRefs]);

  // Track FX chain editor (mastering_expand.md §II.4): reuse the same module
  // types as the mastering suite inside this track's FX chain.
  var FX_MODULE_TYPES = ['compressor', 'limiter', 'exciter', 'rebalance', 'eq'];
  const updateFxChain = function(nextChain) {
    if (onUpdateTrack) onUpdateTrack(track.id, { fxChain: nextChain });
  };

  const handlePanPointerDown = (e) => {
    e.currentTarget._panStartY = e.clientY;
    e.currentTarget._startPan = pan;
    e.currentTarget.setPointerCapture(e.pointerId);
    function onMove(ev) {
      if (!e.currentTarget) return;
      var deltaY = e.currentTarget._panStartY - ev.clientY;
      var newPan = Math.min(1.0, Math.max(-1.0, e.currentTarget._startPan + (deltaY / 80)));
      setPan(newPan);
      var angle = newPan * 120;
      if (panPointerRef.current) panPointerRef.current.style.transform = 'rotate(' + angle + 'deg)';
      if (newPan === 0) setPanLabel('center');
      else if (newPan < 0) setPanLabel('L' + Math.abs(Math.round(newPan * 100)));
      else setPanLabel('R' + Math.round(newPan * 100));
    }
    function onUp() {
      document.removeEventListener('pointermove', onMove);
      document.removeEventListener('pointerup', onUp);
    }
    document.addEventListener('pointermove', onMove);
    document.addEventListener('pointerup', onUp);
  };

  return React.createElement("div", {
    style: style || undefined,
      className: "flex flex-col items-stretch w-[145px] shrink-0 strip-bg-track rounded-md text-slate-300 select-none shadow-2xl border border-slate-900 overflow-y-auto"
    },
    /* 1. Top Track Color Accent Bar */
    React.createElement("div", {
      className: "h-1.5 w-full shrink-0 transition-colors",
      style: { backgroundColor: trackColor }
    }),

    /* 2. Pan Rotary Dial Area */
    React.createElement("div", {
      className: "h-[46px] shrink-0 py-1 px-2 flex flex-col items-center justify-center border-b border-slate-700/40",
      style: { backgroundColor: trackColor + '15' }
    },
      React.createElement("div", {
        className: "w-6 h-6 rounded-full bg-slate-800 border-2 border-slate-400 relative flex items-center justify-center cursor-pointer shadow-md",
        title: "Kéo chuột lên/xuống để chỉnh Pan",
        onPointerDown: handlePanPointerDown,
        onDoubleClick: function() { setPan(0); setPanLabel('center'); if (panPointerRef.current) panPointerRef.current.style.transform = 'rotate(0deg)'; },
        onWheel: function(e) {
          e.preventDefault();
          var step = e.deltaY > 0 ? -0.05 : 0.05;
          var newPan = Math.max(-1, Math.min(1, pan + step));
          newPan = Math.round(newPan * 100) / 100;
          setPan(newPan);
          if (panPointerRef.current) panPointerRef.current.style.transform = 'rotate(' + (newPan * 120) + 'deg)';
          if (newPan === 0) setPanLabel('center');
          else if (newPan < 0) setPanLabel('L' + Math.abs(Math.round(newPan * 100)));
          else setPanLabel('R' + Math.round(newPan * 100));
        }
      },
        React.createElement("div", {
          ref: panPointerRef,
          className: "w-0.5 h-2 rounded absolute top-0.5 transition-transform",
          style: { backgroundColor: trackColor, transform: 'rotate(0deg)' }
        })
      ),
      React.createElement("span", {
        className: "text-[8px] font-mono mt-0.5 font-semibold",
        style: { color: trackColor }
      }, panLabel)
    ),

    /* 3. Center Area: Peak dB + Fader + VU + Button Stack */
    React.createElement("div", { className: "flex-1 p-1 flex gap-1 justify-between items-stretch min-h-[170px]" },
      /* Left Fader & VU Column */
      React.createElement("div", {
        className: "flex-1 flex flex-col justify-between items-center bg-black/60 p-1 rounded border border-slate-800/80"
      },
        React.createElement("div", { className: "w-full flex justify-center text-[8px] font-mono text-slate-400 h-4 items-center" },
          React.createElement("span", null, vol <= -50 ? '-inf' : (vol > 0 ? '+' : '') + vol.toFixed(1) + 'dB')
        ),
        React.createElement("div", { className: "flex items-stretch justify-around w-full flex-1 relative py-1" },
          /* Fader Rail */
          React.createElement("div", {
            className: "relative fader-track-bg w-3 flex-1 rounded flex items-center justify-center overflow-hidden",
          },
            React.createElement("div", { className: "w-0.5 h-full bg-slate-700 absolute" }),
            React.createElement("input", {
              type: "range", min: "-60", max: "12", step: "0.5",
              value: vol, className: "fader-slider w-full z-10",
              onChange: function(e) {
                var val = parseFloat(e.target.value);
                if (onUpdateTrack) onUpdateTrack(track.id, { volumeDb: val });
              },
              onWheel: function(e) {
                e.preventDefault();
                var step = e.deltaY > 0 ? -0.5 : 0.5;
                var newVol = Math.max(-60, Math.min(12, vol + step));
                if (onUpdateTrack) onUpdateTrack(track.id, { volumeDb: newVol });
              },
              onDoubleClick: function() {
                if (onUpdateTrack) onUpdateTrack(track.id, { volumeDb: 0 });
              }
            })
          ),
          /* VU Meter */
          React.createElement("div", {
            className: "w-2.5 flex-1 bg-slate-950 rounded border border-slate-900 overflow-hidden relative",
            title: "Peak VU Meter"
          },
            React.createElement("canvas", { ref: setVuCanvas, className: "w-full h-full block" })
          )
        )
      ),

      /* Right Button Stack */
      React.createElement("div", { className: "w-7 flex flex-col justify-between text-[8px] font-bold shrink-0" },
        React.createElement("button", {
          onClick: function(e) { e.stopPropagation(); var next = !track.muted; if (onUpdateTrack) onUpdateTrack(track.id, { muted: next }); if (window.__applyTrackMuteSolo) window.__applyTrackMuteSolo(track.id, { muted: next }); },
          className: "btn-daw h-[24px] rounded text-amber-500 font-extrabold flex items-center justify-center" + (isMuted ? " btn-mute-active" : ""),
          title: "Mute Track"
        }, "M"),
        React.createElement("button", {
          onClick: function(e) { e.stopPropagation(); var next = !track.solo; if (onUpdateTrack) onUpdateTrack(track.id, { solo: next }); if (window.__applyTrackMuteSolo) window.__applyTrackMuteSolo(track.id, { solo: next }); },
          className: "btn-daw h-[24px] rounded text-yellow-400 font-extrabold flex items-center justify-center" + (isSoloed ? " btn-solo-active" : ""),
          title: "Solo Track"
        }, "S"),
        React.createElement("button", {
          onClick: function(e) {
            e.stopPropagation();
            var next = !(track.audioBypass ?? track.masteringBypass);
            if (onUpdateTrack) onUpdateTrack(track.id, { audioBypass: next });
            // Live re-route: bypassed channel skips FX + mastering chain at Main out.
            if (window.__setTrackBypass) window.__setTrackBypass(track.id, 'audio', next);
            else if (window.__setTrackMasteringBypass) window.__setTrackMasteringBypass(track.id, next);
          },
          className: "btn-daw h-[24px] rounded flex items-center justify-center font-extrabold text-[9px] " + (track.audioBypass ? " text-slate-500" : " text-sky-300 bg-sky-500/30 border-sky-400/70"),
          title: "A = Mastering FX Chain cho audio items (clips + sections): XÁM = bypass mastering (vẫn qua track FX Rack), SÁNG XANH = qua mastering"
        }, "A"),
        React.createElement("button", {
          onClick: function(e) {
            e.stopPropagation();
            var next = !(track.midiBypass ?? track.masteringBypass);
            if (onUpdateTrack) onUpdateTrack(track.id, { midiBypass: next });
            if (window.__setTrackBypass) window.__setTrackBypass(track.id, 'midi', next);
            else if (window.__setTrackMasteringBypass) window.__setTrackMasteringBypass(track.id, next);
          },
          className: "btn-daw h-[24px] rounded flex items-center justify-center font-extrabold text-[10px] " + (track.midiBypass ? " text-slate-500" : " text-fuchsia-300 bg-fuchsia-500/30 border-fuchsia-400/70"),
          title: "♪ = Mastering FX Chain cho MIDI (soundfont): XÁM = bypass mastering (vẫn qua track FX Rack), SÁNG TÍM = qua mastering"
        }, "\u266A"),
        React.createElement("button", {
          onClick: function() { if (window.__openFxRack) window.__openFxRack(track.id, track.name); },
          className: "btn-daw h-[24px] rounded text-slate-300 font-semibold flex items-center justify-center text-[7px]" + ((track.fxChain || []).length > 0 ? " text-cyan-400" : ""),
          title: "Track FX Chain (mastering_expand.md §II.4)"
        }, "FX"),
        React.createElement("button", {
          onClick: function() {
            var next = track.fxActive === false;
            if (onUpdateTrack) onUpdateTrack(track.id, { fxActive: next });
            // Live re-route: rebuild both FX chains (audio + SF) so EVERY item
            // (midi/section/audioclip) immediately follows the FX power state.
            if (window.__rebuildTrackFxGraph) window.__rebuildTrackFxGraph(track.id);
          },
          className: "btn-daw h-[24px] rounded flex items-center justify-center text-[8px]" + (track.fxActive !== false ? " text-emerald-400" : " text-slate-500"),
          title: "Toggle FX Power: ON (sáng) = MỌI items (MIDI/section/audioclip) qua FX Rack Panel (nút A/♪ chỉ ảnh hưởng Mastering FX Chain); OFF (tối) = bỏ toàn bộ track FX"
        }, React.createElement("i", { className: "fa-solid fa-power-off" })),
        React.createElement("button", {
          className: "btn-daw h-[24px] rounded text-slate-400 flex items-center justify-center",
          title: "Automation Envelopes"
        }, React.createElement("i", { className: "fa-solid fa-chart-line text-[8px]" })),
        React.createElement("button", {
          onClick: function() { setIsPhaseInverted(function(p) { return !p; }); },
          className: "btn-daw h-[24px] rounded flex items-center justify-center text-[9px]" + (isPhaseInverted ? " bg-amber-600 text-white" : " text-slate-400"),
          title: "Phase Invert"
        }, "\u00D8")
      )
    ),

    /* 4. Record Arm Button Row */
    React.createElement("div", {
      className: "h-[28px] shrink-0 flex items-center justify-between mx-1.5 px-1.5 bg-black/40 rounded border border-slate-800/60"
    },
      React.createElement("button", {
        onClick: function(e) {
          e.stopPropagation();
          if (!onUpdateTrack) return;
          if (track.inputSource?.deviceType === 'MICROPHONE') {
            onUpdateTrack(track.id, { inputSource: { deviceType: 'NONE', deviceId: '' } });
          } else {
            onUpdateTrack(track.id, { inputSource: { deviceType: 'MICROPHONE', deviceId: 'default' } });
          }
        },
        className: "w-4 h-4 rounded flex items-center justify-center text-[7px] font-bold transition-all" + (isMicActive ? " bg-sky-600 text-white border border-sky-400 shadow-sm" : " bg-slate-800 text-slate-500 border border-slate-700"),
        title: isMicActive ? "Mic Input ON" : "Mic Input OFF"
      }, "MIC"),
      React.createElement("button", {
        onClick: function(e) { e.stopPropagation(); if (onUpdateTrack) onUpdateTrack(track.id, { isArmed: !track.isArmed }); },
        className: "w-5 h-5 rounded-full flex items-center justify-center transition-all shadow-inner" + (isArmed ? " btn-arm-active border-red-400" : " bg-red-950 border-2 border-red-800 text-red-500"),
        title: "Arm for Recording"
      }, React.createElement("i", { className: "fa-solid fa-circle text-[8px]" }))
    ),

    /* 5. Track Name Identifier */
    React.createElement("div", {
      className: "h-[26px] shrink-0 mx-1.5 flex items-center justify-center bg-slate-950/80 rounded border border-slate-800/80"
    },
      React.createElement("span", {
        className: "text-[11px] font-bold tracking-wider font-sans uppercase",
        style: { color: trackColor }
      }, trackName)
    ),

    /* 6. Footer Bar */
    React.createElement("div", {
      className: "h-[22px] shrink-0 w-full text-slate-950 flex items-center justify-center font-extrabold text-xs font-mono tracking-widest transition-colors",
      style: { backgroundColor: trackColor }
    }, index + 1)
  );
};

const WaveformLane = ({
  track,
  zoom,
  timelineWidth,
  viewportWidth,
  onSelectRange,
  onPlayheadSet,
  isSelected,
  onSelectTrack,
  markers,
  selectionMode,
  localSelectionTrackId,
  localSelectionStart,
  currentTime,
  getLocalAnchor,
  onClearLocalSelection,
  onDeselectItem,
  onAddToSelection,
  onSetPendingDrag,
  onSetPendingDragMove,
  onSetSelectionMode,
  onSetSelectionStart,
  onSetSelectionEnd,
  onSetCurrentTime,
  onSetLocalSelectionTrackId,
  onSetLocalSelectionStart,
  onSetLocalSelectionEnd,
  localSelLeft,
  localSelRight,
  onTrackLaneMouseDown,
  onContextMenu,
  onClipDragStart,
  onClipStretchStart,
  onSectionItemDragStart,
  onSectionItemResizeStart,
  onEditSectionInTab,
  onEditMidiInTab,
  onSelectionEdgeDragStart,
  setSelectedClipId,
  selectedClipId,
  activeTool,
  onSplitTrackAtTime,
  onEditClipInSubTab,
  selectedItemIds,
  onClearSelection,
  onSweepSelectStart,
  snapValue,
  bpm,
  scrollLeft,
  recordingState,
  recTempMidiNotes,
  recTempAudioBuffer,
  recStartTimelineTime,
  canvasRedrawCount
}) => {
  const canvasRef = useRef(null);
  const drawWidth = Math.min(timelineWidth, viewportWidth);
  const leadInMargin = 0;
  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const ctx = canvas.getContext('2d');
    const dpr = window.devicePixelRatio || 1;
    let scrollLeftVal = scrollLeft || 0;
    let el = canvas.parentElement;
    while (el) {
      if (el.scrollLeft !== undefined && (el.scrollWidth > el.clientWidth || el.scrollLeft > 0)) {
        scrollLeftVal = el.scrollLeft;
        break;
      }
      el = el.parentElement;
    }
    const vWidth = viewportWidth || 1200;
    const height = canvas.parentElement ? canvas.parentElement.clientHeight : 96;
    canvas.width = Math.min(Math.round(drawWidth * dpr), 32768);
    canvas.height = Math.min(Math.round(height * dpr), 32768);
    ctx.scale(dpr, dpr);
    ctx.imageSmoothingEnabled = false;
    canvas.style.width = `${drawWidth}px`;
    canvas.style.height = `${height}px`;
    ctx.fillStyle = isSelected ? '#2a2a2a' : track.id % 2 === 0 ? '#181818' : '#1d1d1d';
    ctx.fillRect(0, 0, drawWidth, height);

    // Grid lines based on Snap value
    ctx.strokeStyle = 'rgba(255, 255, 255, 0.03)';
    ctx.lineWidth = 1;
    const beatDuration = (60.0 / (parseFloat(bpm) || 120));
    const barDuration = beatDuration * 4;
    const leadIn = 0;
    const CLIP_BUFFER = Math.max(400, barDuration * zoom + 200);
    const PADDING_LEFT = 0;
    const tStart = (scrollLeftVal - leadIn * zoom) / zoom - PADDING_LEFT;
    const tEnd = (scrollLeftVal + drawWidth - leadIn * zoom) / zoom + CLIP_BUFFER / zoom;
    let snapDivisor = 1;
    if (snapValue && snapValue !== 'free') {
      if (snapValue === '4') snapDivisor = 4; else if (snapValue === '1') snapDivisor = 1; else if (snapValue === '1/2') snapDivisor = 0.5; else if (snapValue === '1/4') snapDivisor = 0.25; else if (snapValue === '1/8') snapDivisor = 0.125; else if (snapValue === '1/16') snapDivisor = 0.0625; else if (snapValue === '1/32') snapDivisor = 0.03125;
    }
    const snapInterval = beatDuration * snapDivisor;
    const firstSnap = Math.floor(tStart / snapInterval) * snapInterval;
    for (let t = firstSnap; t <= tEnd; t += snapInterval) {
      const localX = (t - tStart) * zoom;
      if (localX < -CLIP_BUFFER || localX > drawWidth + CLIP_BUFFER) continue;
      const barNum = Math.round(t / barDuration);
      const onBar = Math.abs(t - barNum * barDuration) < 0.001;
      ctx.strokeStyle = onBar ? 'rgba(255, 255, 255, 0.12)' : 'rgba(255, 255, 255, 0.04)';
      ctx.lineWidth = onBar ? 1.2 : 0.5;
      ctx.beginPath();
      ctx.moveTo(localX, 0);
      ctx.lineTo(localX, height);
      ctx.stroke();
      if (onBar && zoom >= 2) {
        ctx.fillStyle = 'rgba(255, 255, 255, 0.15)';
        ctx.font = 'bold 7px Inter, sans-serif';
        ctx.textAlign = 'left';
        ctx.fillText(`${barNum}`, localX + 2, 10);
      }
    }

    // Draw waveform lane
    const clips = [...(track.clips && track.clips.length > 0 ? track.clips : track.buffer ? [{
      id: 'default',
      buffer: track.buffer,
      startTime: track.startTime || 0,
      name: track.name,
      speed: track.speed || 1.0
    }] : [])];

    if (recordingState === 'RECORDING' && track.isArmed && track.inputSource?.deviceType === 'MICROPHONE' && recTempAudioBuffer) {
      clips.push({
        id: 'rec_temp_' + track.id,
        buffer: recTempAudioBuffer,
        startTime: recStartTimelineTime,
        name: '[GHI ÂM...]',
        speed: 1.0,
        isTemp: true
      });
    }

    if (clips.length > 0) {
      clips.forEach(clip => {
        const hasBuffer = !!clip.buffer;
        const clipSpeed = clip.speed || 1.0;
        const originalDuration = hasBuffer ? clip.buffer.duration / clipSpeed : 4.0;
        const duration = originalDuration;
        const clipStartTime = clip.startTime || 0;
        const clipEndTime = clipStartTime + duration;

        // Culling: Skip rendering if clip is outside visible viewport window
        if (clipEndTime < tStart || clipStartTime > tEnd) return;
        const xStartGlobal = clipStartTime * zoom;
        const wClip = duration * zoom;
        const xStartLocal = xStartGlobal - scrollLeftVal;
        const xEndLocal = xStartLocal + wClip;

        // 1. Draw Clip Layer Background & Border (always draw even without buffer)
        const clipIdentifier = clip.id === 'default' ? 'default_' + track.id : clip.id;
        const isClipSelected = (selectedClipId && selectedClipId.trackId === track.id && selectedClipId.clipId === clipIdentifier) || (selectedItemIds && selectedItemIds.has(clipIdentifier));
        ctx.fillStyle = clip.isTemp ? 'rgba(239, 68, 68, 0.25)' : isClipSelected ? track.color ? track.color + '44' : 'rgba(6, 182, 212, 0.30)' : track.color ? track.color + '22' : 'rgba(6, 182, 212, 0.12)';
        ctx.strokeStyle = clip.isTemp ? '#ef4444' : isClipSelected ? '#fbbf24' : track.color || '#06b6d4';
        ctx.lineWidth = isClipSelected ? 1 : 1.5;
        const clipTop = 4;
        const clipHeight = height - 8;
        ctx.beginPath();
        if (ctx.roundRect) {
          ctx.roundRect(xStartLocal, clipTop, wClip, clipHeight, 4);
        } else {
          ctx.rect(xStartLocal, clipTop, wClip, clipHeight);
        }
        ctx.fill();
        ctx.stroke();

        // 2. Draw Clip Label & Speed Label
        ctx.fillStyle = '#e4e4e7';
        ctx.font = 'bold 9px sans-serif';
        ctx.fillText(clip.name || 'Clip', Math.max(xStartLocal + 8, 8), clipTop + 12);
        if (clipSpeed !== 1.0) {
          ctx.fillStyle = '#fbbf24';
          ctx.font = 'bold 8px sans-serif';
          ctx.fillText(`Speed: ${(clipSpeed * 100).toFixed(1)}%`, Math.max(xStartLocal + 8, 8), clipTop + 22);
        }

        // Draw markers
        if (markers && markers.length > 0) {
          markers.forEach(m => {
            const mxLocal = m.time * zoom - scrollLeftVal;
            if (mxLocal >= 0 && mxLocal <= drawWidth) {
              ctx.fillStyle = '#fbbf24';
              ctx.fillRect(mxLocal - 1, 0, 2, height);
            }
          });
        }

        if (!hasBuffer) {
          ctx.fillStyle = 'rgba(255, 255, 255, 0.15)';
          ctx.font = 'italic 8px sans-serif';
          ctx.fillText('(audio chưa được tải)', Math.max(xStartLocal + 8, 8), clipTop + clipHeight - 6);
          return;
        }

        const numChannels = clip.buffer.numberOfChannels || 1;
        const dataL = clip.buffer.getChannelData(0);
        const dataR = numChannels >= 2 ? clip.buffer.getChannelData(1) : dataL;
        const sampleRate = clip.buffer.sampleRate;
        const totalSamples = dataL.length;

        // 3. Peak / Vector Waveform Drawing (Sound Forge Dual Stereo Channel Split Match)
        const drawXStartLocal = Math.max(0, Math.floor(xStartLocal));
        const drawXEndLocal = Math.min(drawWidth, Math.ceil(xEndLocal));
        const samplesPerPixel = sampleRate / zoom * clipSpeed;
        const isStereo = numChannels >= 2;
        const channelConfigs = isStereo ? [{
          data: dataL,
          mid: height / 4,
          chHeight: height / 2 - 8,
          label: '1'
        }, {
          data: dataR,
          mid: 3 * height / 4,
          chHeight: height / 2 - 8,
          label: '2'
        }] : [{
          data: dataL,
          mid: height / 2,
          chHeight: clipHeight - 12,
          label: 'MONO'
        }];
        if (isStereo) {
          ctx.strokeStyle = 'rgba(255, 255, 255, 0.12)';
          ctx.lineWidth = 1;
          ctx.beginPath();
          ctx.moveTo(xStartLocal, height / 2);
          ctx.lineTo(xStartLocal + wClip, height / 2);
          ctx.stroke();
        }
        channelConfigs.forEach(ch => {
          const data = ch.data;
          const mid = ch.mid;
          const peakRatio = ch.chHeight * 0.42;

          // Decibel Amplitude Grid Lines (+6.0dB, -Inf, -6.0dB)
          const yPlus6 = mid - peakRatio * 0.8;
          const yMinus6 = mid + peakRatio * 0.8;
          ctx.strokeStyle = 'rgba(255, 255, 255, 0.08)';
          ctx.lineWidth = 0.5;
          ctx.beginPath();
          ctx.moveTo(xStartLocal, yPlus6);
          ctx.lineTo(xStartLocal + wClip, yPlus6);
          ctx.stroke();
          ctx.beginPath();
          ctx.moveTo(xStartLocal, mid);
          ctx.lineTo(xStartLocal + wClip, mid);
          ctx.stroke();
          ctx.beginPath();
          ctx.moveTo(xStartLocal, yMinus6);
          ctx.lineTo(xStartLocal + wClip, yMinus6);
          ctx.stroke();

          // Decibel Text Labels (Sound Forge Monospace)
          ctx.fillStyle = '#71717a';
          ctx.font = '8px monospace';
          const labelX = Math.max(xStartLocal + 4, 4);
          ctx.fillText('+6.0', labelX, yPlus6 - 2);
          ctx.fillText('-Inf', labelX, mid - 2);
          ctx.fillText('-6.0', labelX, yMinus6 + 8);

          // Sound Forge Channel ID Badge (1 or 2)
          if (isStereo) {
            ctx.fillStyle = 'rgba(6, 182, 212, 0.85)';
            ctx.font = 'bold 9px monospace';
            ctx.fillText(ch.label, Math.min(xStartLocal + wClip - 12, drawWidth - 16), mid - ch.chHeight * 0.35);
          }
          ctx.strokeStyle = '#5bc0be'; // Cornflower Blue
          ctx.lineWidth = 1.2;
          if (samplesPerPixel < 4) {
            // High-zoom continuous vector line rendering (#5bc0be Cornflower Blue)
            const visibleTStart = (drawXStartLocal - xStartLocal) / zoom;
            const visibleTEnd = (drawXEndLocal - xStartLocal) / zoom;
            const startSample = Math.max(0, Math.floor(visibleTStart * clipSpeed * sampleRate));
            const endSample = Math.min(totalSamples, Math.ceil(visibleTEnd * clipSpeed * sampleRate));
            ctx.beginPath();
            let first = true;
            const maxSamples = 50000;
            const vectorStep = Math.max(1, Math.floor((endSample - startSample) / maxSamples));
            for (let i = startSample; i < endSample; i += vectorStep) {
              const sampleTime = i / sampleRate / clipSpeed;
              const pxLocal = xStartLocal + sampleTime * zoom;
              const y = mid - data[i] * peakRatio;
              if (first) {
                ctx.moveTo(pxLocal, y);
                first = false;
              } else {
                ctx.lineTo(pxLocal, y);
              }
            }
            ctx.stroke();

            // Draw granular sample nodes only at extreme zoom (samplesPerPixel < 0.3)
            if (samplesPerPixel < 0.3) {
              const maxNodes = 5000;
              const step = Math.max(1, Math.floor((endSample - startSample) / maxNodes));
              ctx.fillStyle = '#6ee7b7';
              let drawn = 0;
              for (let i = startSample; i < endSample && drawn < maxNodes; i += step) {
                const sampleTime = i / sampleRate / clipSpeed;
                const pxLocal = xStartLocal + sampleTime * zoom;
                const y = mid - data[i] * peakRatio;
                ctx.fillRect(pxLocal - 1, y - 1, 2, 2);
                drawn++;
              }
            }
          } else {
            // Coarse view vertical peak min/max bars per pixel
            for (let pxLocal = drawXStartLocal; pxLocal < drawXEndLocal; pxLocal++) {
              const timeInClip = (pxLocal - xStartLocal) / zoom * clipSpeed;
              const sampleIdx = Math.floor(timeInClip * sampleRate);
              const chunkSize = Math.max(1, Math.floor(samplesPerPixel));
              const chunkStart = Math.max(0, sampleIdx - Math.floor(chunkSize / 2));
              const chunkEnd = Math.min(totalSamples, chunkStart + chunkSize);
              let maxVal = 0;
              for (let i = chunkStart; i < chunkEnd; i++) {
                const abs = Math.abs(data[i]);
                if (abs > maxVal) maxVal = abs;
              }
              const peakHeight = maxVal * peakRatio;
              ctx.beginPath();
              ctx.moveTo(pxLocal, mid - peakHeight);
              ctx.lineTo(pxLocal, mid + peakHeight);
              ctx.stroke();
            }
          }
        });
      });
    } else {
      ctx.fillStyle = '#444';
      ctx.font = '12px Inter, sans-serif';
      ctx.textAlign = 'center';
      ctx.fillText('Kéo thả hoặc tải file âm thanh vào Track này', drawWidth / 2, height / 2);
    }

    // Draw sections
    const sections = track.sections || [];
    sections.forEach(sec => {
      const secStartLocal = sec.start * zoom - scrollLeftVal; // use secStartLocal NOT secStart
      const secWidth = sec.duration * zoom;
      if (secStartLocal + secWidth < 0 || secStartLocal > drawWidth) return;
      var isSecSelected = selectedItemIds && selectedItemIds.has(sec.id);
      ctx.fillStyle = isSecSelected ? 'rgba(245, 158, 11, 0.35)' : ((sec.color || track.color || '#06b6d4') + '44');
      ctx.fillRect(secStartLocal, 2, secWidth, height - 4);
      ctx.strokeStyle = isSecSelected ? '#f59e0b' : (sec.color || track.color || '#06b6d4');
      ctx.lineWidth = isSecSelected ? 2.5 : 1;
      ctx.setLineDash(isSecSelected ? [] : [4, 4]);
      ctx.strokeRect(secStartLocal, 2, secWidth, height - 4);
      ctx.setLineDash([]);
      ctx.fillStyle = '#e4e4e7';
      ctx.font = 'bold 9px sans-serif';
      ctx.fillText(sec.name || 'Section', Math.max(secStartLocal + 4, 4), 14);

      // Draw sub-tracks within section (khôi phục 19:45 — section item phải
      // vẽ lại các item chứa bên trong sau khi mở project; buffers đã được
      // nạp đầy đủ bởi loadAudioBuffersForTracks recursion 19:00)
      const subTracks = sec.tracks || [];
      const subTrackCount = Math.min(subTracks.length, 4);
      const subTrackHeight = (height - 20) / Math.max(1, subTrackCount);
      const subColors = ['#fbbf24', '#a78bfa', '#ec4899', '#10b981'];
      ctx.save();
      ctx.beginPath();
      ctx.rect(secStartLocal, 2, secWidth, height - 4);
      ctx.clip();
      for (let stIdx = 0; stIdx < subTrackCount; stIdx++) {
        const sub = subTracks[stIdx];
        if (!sub) continue;
        const subY = 20 + stIdx * subTrackHeight;
        ctx.fillStyle = sub.color ? sub.color + '22' : subColors[stIdx] + '22';
        ctx.fillRect(secStartLocal + 1, subY, secWidth - 2, subTrackHeight - 1);
        // Draw clips as waveform bars
        const subClips = sub.clips || [];
        subClips.forEach(cl => {
          if (!cl.buffer) return;
          const sr = cl.buffer.sampleRate;
          const bufData = cl.buffer.getChannelData(0);
          const bufLen = bufData.length;
          const clStartLocal = cl.startTime || 0;
          const clDurLocal = bufLen / sr / (cl.speed || 1.0);
          const clStartMain = secStartLocal + clStartLocal * zoom;
          const clW = Math.max(1, clDurLocal * zoom);
          const peakSamples = Math.max(10, Math.min(100, Math.floor(clW / 3)));
          const step = Math.max(1, Math.floor(bufLen / peakSamples));
          for (let p = 0; p < peakSamples; p++) {
            const sIdx = p * step;
            let maxVal = 0;
            for (let j = 0; j < step && sIdx + j < bufLen; j++) {
              const abs = Math.abs(bufData[sIdx + j]);
              if (abs > maxVal) maxVal = abs;
            }
            const bx = clStartMain + (p / peakSamples) * clW;
            const barH = Math.max(1, maxVal * (subTrackHeight * 0.7));
            const barY = subY + (subTrackHeight - barH) / 2;
            ctx.fillStyle = subColors[stIdx] + '99';
            ctx.fillRect(bx, barY, Math.max(1, clW / peakSamples), barH);
          }
        });
        // Draw MIDI items as colored note bars
        const subMidi = sub.midiItems || [];
        subMidi.forEach(item => {
          const itemStartLocal = secStartLocal + (item.startTime || 0) * zoom;
          const itemDurLocal = (item.duration || 1) * zoom;
          const notes = item.notes || [];
          const pitchMin = 36;
          const pitchMax = 84;
          notes.forEach(note => {
            const beatSec = 60.0 / (parseInt(bpm) || 120);
            const noteStartSec = (note.start_beat || 0) * beatSec;
            // CLIP theo item duration: item bị KÉO NGẮN (trim — duration 4 bars
            // nhưng notes vẫn còn 8 bars) → note ngoài duration KHÔNG vẽ —
            // canvas MAIN phải hiển thị đúng phần đã trim (user 08:20)
            if (noteStartSec >= (item.duration || 0) - 0.01) return;
            const noteDurSec = Math.max(0.02, (note.duration_beats || 0.25) * beatSec);
            const noteStartLocal = itemStartLocal + noteStartSec * zoom;
            const nw = noteDurSec * zoom;
            const pitchFrac = Math.max(0, Math.min(1, (note.pitch - pitchMin) / (pitchMax - pitchMin)));
            const ny = subY + 2 + (1.0 - pitchFrac) * (subTrackHeight - 6);
            const nh = Math.max(4, (subTrackHeight - 6) / (pitchMax - pitchMin) * 3);
            ctx.fillStyle = subColors[stIdx] + 'cc';
            ctx.fillRect(Math.max(noteStartLocal, secStartLocal + 2), ny, Math.max(2, nw), nh);
          });
        });
      }
      ctx.restore();
    });

    // Draw MIDI items
    const midiItems = track.midiItems || [];
    midiItems.forEach(midi => {
      const midiStartLocal = midi.startTime * zoom - scrollLeftVal;
      const midiWidth = midi.duration * zoom;
      if (midiStartLocal + midiWidth < 0 || midiStartLocal > drawWidth) return;

      const isRecordingItem = midi.name === 'Recording...';

      if (isRecordingItem && Math.random() < 0.05) {
        console.log(`[DevLog] [Canvas Draw] Rendering MIDI item: ID=${midi.id}, Name="${midi.name}", Start=${midiStartLocal.toFixed(1)}px, Width=${midiWidth.toFixed(1)}px, NotesCount=${midi.notes?.length || 0}`);
      }

      var isMidiSelected = selectedItemIds && selectedItemIds.has(midi.id);
      ctx.fillStyle = isMidiSelected ? 'rgba(245, 158, 11, 0.35)' : (isRecordingItem ? 'rgba(239, 68, 68, 0.2)' : (track.color || '#a78bfa') + '33');
      ctx.fillRect(midiStartLocal, 2, midiWidth, height - 4);
      ctx.strokeStyle = isMidiSelected ? '#f59e0b' : (isRecordingItem ? '#ef4444' : (track.color || '#a78bfa'));
      ctx.lineWidth = isMidiSelected ? 2.5 : 1.5;
      ctx.strokeRect(midiStartLocal, 2, midiWidth, height - 4);
      ctx.fillStyle = isRecordingItem ? '#fca5a5' : (track.color || '#a78bfa');
      ctx.font = 'bold 9px sans-serif';
      ctx.fillText(isRecordingItem ? '[Ghi MIDI...]' : (midi.name || 'MIDI'), Math.max(midiStartLocal + 4, 4), 14);

      const midiNotes = midi.notes || [];
      if (midiNotes.length > 0) {
        const secondsPerBeat = 60.0 / (parseInt(bpm) || 120);
        const pitchMin = 36;
        const pitchMax = 84;
        midiNotes.forEach(note => {
          const noteStartSec = (note.start_beat || 0) * secondsPerBeat;
          const noteDurSec = Math.max(0.02, (note.duration_beats || 0.25) * secondsPerBeat);
          const noteStartLocal = (midi.startTime + noteStartSec) * zoom - scrollLeftVal;
          const nw = noteDurSec * zoom;
          if (noteStartLocal + nw < midiStartLocal || noteStartLocal > midiStartLocal + midiWidth) return;

          const pitchFrac = Math.max(0, Math.min(1, (note.pitch - pitchMin) / (pitchMax - pitchMin)));
          const ny = 18 + (1.0 - pitchFrac) * (height - 26);
          const nh = Math.max(6, (height - 26) / (pitchMax - pitchMin) * 4);

          ctx.fillStyle = isRecordingItem ? '#10b981' : (track.color || '#a78bfa');
          ctx.fillRect(Math.max(noteStartLocal, midiStartLocal + 2), ny, Math.max(2, nw), nh);
        });
      }
    });

    // Selection highlight - local selection on this track
    if (selectionMode === 'local' && localSelectionTrackId === track.id && localSelLeft !== null && localSelRight !== null && localSelRight > localSelLeft) {
      const hlLeftLocal = localSelLeft * zoom - scrollLeftVal;
      const hlWidth = (localSelRight - localSelLeft) * zoom;
      ctx.fillStyle = 'rgba(245, 158, 11, 0.15)';
      ctx.fillRect(hlLeftLocal, 0, hlWidth, height);
      ctx.strokeStyle = '#f59e0b';
      ctx.lineWidth = 1;
      ctx.strokeRect(hlLeftLocal, 0, hlWidth, height);
    }
  }, [track, zoom, timelineWidth, viewportWidth, isSelected, markers, selectionMode, localSelectionTrackId, localSelLeft, localSelRight, snapValue, bpm, scrollLeft, recordingState, recTempMidiNotes, recStartTimelineTime, canvasRedrawCount, currentTime, selectedItemIds]);
  return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
    key: "virtual-spacer",
    style: {
      width: `${timelineWidth}px`,
      height: '1px',
      pointerEvents: 'none'
    }
  }), /*#__PURE__*/React.createElement("canvas", {
    ref: canvasRef,
    style: {
      position: 'sticky',
      left: 0,
      imageRendering: 'pixelated'
    },
    className: "cursor-crosshair",
    onMouseMove: e => {
      if (!canvasRef.current) return;
      const rect = canvasRef.current.getBoundingClientRect();
      const x = e.clientX - rect.left + (scrollLeft || 0);
      const time = Math.max(0, x / zoom - leadInMargin);
      const clips = track.clips && track.clips.length > 0 ? track.clips : track.buffer ? [{
        id: 'default',
        buffer: track.buffer,
        startTime: track.startTime || 0,
        name: track.name,
        speed: track.speed || 1.0
      }] : [];

      // 1. Shift+Drag (TOP PRIORITY): Range selection on track waveform
      if (e.shiftKey && e.buttons > 0) {
        e.preventDefault();
        e.stopPropagation();
        const currentAnchor = getLocalAnchor ? getLocalAnchor() : null;
        const anchor = currentAnchor !== null && currentAnchor !== undefined ? currentAnchor : localSelectionStart !== null ? localSelectionStart : currentTime;
        const selS = Math.min(anchor, time);
        const selE = Math.max(anchor, time);
        if (onSetSelectionMode) onSetSelectionMode('local');
        if (onSetLocalSelectionTrackId) onSetLocalSelectionTrackId(track.id);
        if (onSetLocalSelectionStart) onSetLocalSelectionStart(selS);
        if (onSetLocalSelectionEnd) onSetLocalSelectionEnd(selE);
        if (onSetSelectionStart) onSetSelectionStart(selS);
        if (onSetSelectionEnd) onSetSelectionEnd(selE);
        if (onSetCurrentTime) onSetCurrentTime(time);
        if (onSelectTrack) onSelectTrack(track.id);
        return;
      }

      // Check if hovering near local selection boundaries of this track
      const isLocal = selectionMode === 'local' && localSelectionTrackId === track.id;
      if (isLocal && localSelLeft !== null && localSelRight !== null) {
        const leftPx = localSelLeft * zoom;
        const rightPx = localSelRight * zoom;
        const distToLeft = Math.abs(x - leftPx);
        const distToRight = Math.abs(x - rightPx);
        if (distToLeft <= 5 || distToRight <= 5) {
          canvasRef.current.style.cursor = 'ew-resize';
          return;
        }
      }

      // Check if hovering near right edge of a clip for time-stretching (Alt key required)
      const toleranceSec = 8 / zoom;
      const rightEdgeClip = clips.find(c => {
        if (!c.buffer) return false;
        const duration = c.buffer.duration / (c.speed || 1.0);
        return Math.abs(time - (c.startTime + duration)) <= toleranceSec;
      });
      if (rightEdgeClip && e.altKey) {
        canvasRef.current.style.cursor = 'ew-resize';
        return;
      }

      // Check section/MIDI item hover for resize or drag
      const allSections = track.sections || [];
      const allMidiItems = track.midiItems || [];
      const sectionTolerance = 8 / zoom;
      let foundSectionItem = null;
      let sectionItemEdge = null;
      const checkEdge = (item, startTime, dur) => {
        const leftEdge = Math.abs(time - startTime) <= sectionTolerance;
        const rightEdge = Math.abs(time - (startTime + dur)) <= sectionTolerance;
        if (leftEdge || rightEdge) return leftEdge ? 'left' : 'right';
        return null;
      };
      for (const sec of allSections) {
        const edge = checkEdge(sec, sec.start, sec.duration);
        if (edge) { foundSectionItem = { type: 'section', item: sec }; sectionItemEdge = edge; break; }
      }
      if (!foundSectionItem) {
        for (const midi of allMidiItems) {
          const edge = checkEdge(midi, midi.startTime, midi.duration);
          if (edge) { foundSectionItem = { type: 'midiItem', item: midi }; sectionItemEdge = edge; break; }
        }
      }
      if (foundSectionItem && sectionItemEdge) {
        canvasRef.current.style.cursor = 'ew-resize';
        return;
      }
      // Check body hover for drag
      if (!foundSectionItem) {
        for (const sec of allSections) {
          if (time >= sec.start && time < sec.start + sec.duration) { foundSectionItem = { type: 'section', item: sec }; break; }
        }
      }
      if (!foundSectionItem) {
        for (const midi of allMidiItems) {
          if (time >= midi.startTime && time < midi.startTime + midi.duration) { foundSectionItem = { type: 'midiItem', item: midi }; break; }
        }
      }
      if (foundSectionItem) {
        canvasRef.current.style.cursor = 'grab';
        return;
      }

      const hoveredClip = clips.find(c => c.buffer && time >= c.startTime && time < c.startTime + c.buffer.duration / (c.speed || 1.0));
      const isOverClip = !!hoveredClip;
      if (activeTool === 'pen') {
        canvasRef.current.style.cursor = isOverClip ? 'copy' : 'not-allowed';
      } else if (activeTool === 'grab') {
        canvasRef.current.style.cursor = isOverClip ? 'grab' : 'default';
      } else if (activeTool === 'razor') {
        canvasRef.current.style.cursor = isOverClip ? 'cell' : 'not-allowed';
      } else {
        // select tool
        canvasRef.current.style.cursor = isOverClip && (e.altKey || e.ctrlKey) ? 'grab' : 'crosshair';
      }
    },
    onMouseDown: e => {
      // Ignore right-click for local selection drag (context menu handles it)
      if (e.button === 2) return;
      const rect = canvasRef.current.getBoundingClientRect();
      const x = e.clientX - rect.left + (scrollLeft || 0);
      const time = Math.max(0, x / zoom - leadInMargin);
      onSelectTrack(track.id);
      const clips = track.clips && track.clips.length > 0 ? track.clips : track.buffer ? [{
        id: 'default',
        buffer: track.buffer,
        startTime: track.startTime || 0,
        name: track.name,
        speed: track.speed || 1.0
      }] : [];

      // Check if Ctrl+Click to exit selection
      if (e.ctrlKey && selectionMode) {
        e.preventDefault();
        e.stopPropagation();
        if (onClearLocalSelection) onClearLocalSelection();
        if (onSetSelectionMode) onSetSelectionMode(null);
        if (onSetSelectionStart) onSetSelectionStart(null);
        if (onSetSelectionEnd) onSetSelectionEnd(null);
        return;
      }

      // 1. Shift+Click (TOP PRIORITY): Range selection on track waveform
      if (e.shiftKey) {
        e.preventDefault();
        e.stopPropagation();
        const currentAnchor = getLocalAnchor ? getLocalAnchor() : null;
        const anchor = currentAnchor !== null && currentAnchor !== undefined ? currentAnchor : localSelectionStart !== null ? localSelectionStart : currentTime;
        const selS = Math.min(anchor, time);
        const selE = Math.max(anchor, time);
        if (onSetSelectionMode) onSetSelectionMode('local');
        if (onSetLocalSelectionTrackId) onSetLocalSelectionTrackId(track.id);
        if (onSetLocalSelectionStart) onSetLocalSelectionStart(selS);
        if (onSetLocalSelectionEnd) onSetLocalSelectionEnd(selE);
        if (onSetSelectionStart) onSetSelectionStart(selS);
        if (onSetSelectionEnd) onSetSelectionEnd(selE);
        if (onSetCurrentTime) onSetCurrentTime(time);
        if (onSelectTrack) onSelectTrack(track.id);
        return;
      }

      // Check if dragging selection boundaries (local mode)
      const isLocal = selectionMode === 'local' && localSelectionTrackId === track.id;
      if (isLocal && localSelLeft !== null && localSelRight !== null) {
        const leftPx = localSelLeft * zoom;
        const rightPx = localSelRight * zoom;
        const distToLeft = Math.abs(x - leftPx);
        const distToRight = Math.abs(x - rightPx);
        if (distToLeft <= 5) {
          e.preventDefault();
          e.stopPropagation();
          if (onSelectionEdgeDragStart) onSelectionEdgeDragStart(e, track.id, 'left');
          return;
        } else if (distToRight <= 5) {
          e.preventDefault();
          e.stopPropagation();
          if (onSelectionEdgeDragStart) onSelectionEdgeDragStart(e, track.id, 'right');
          return;
        }
      }

      // Check if time-stretching (Alt + Right Edge)
      const toleranceSec = 8 / zoom;
      const rightEdgeClip = clips.find(c => {
        if (!c.buffer) return false;
        const duration = c.buffer.duration / (c.speed || 1.0);
        return Math.abs(time - (c.startTime + duration)) <= toleranceSec;
      });
      if (rightEdgeClip && e.altKey) {
        e.preventDefault();
        e.stopPropagation();
        if (onClipStretchStart) {
          onClipStretchStart(track.id, rightEdgeClip.id, time);
        }
        return;
      }

      // Check section/MIDI item edge for resize, then body for drag
      const secItems = track.sections || [];
      const midiItems = track.midiItems || [];
      const secTol = 8 / zoom;
      let hitItem = null;
      let hitEdge = null;
      for (const sec of secItems) {
        if (Math.abs(time - sec.start) <= secTol) { hitItem = { type: 'section', id: sec.id, start: sec.start, dur: sec.duration }; hitEdge = 'left'; break; }
        if (Math.abs(time - (sec.start + sec.duration)) <= secTol) { hitItem = { type: 'section', id: sec.id, start: sec.start, dur: sec.duration }; hitEdge = 'right'; break; }
      }
      if (!hitItem) {
        for (const midi of midiItems) {
          if (Math.abs(time - midi.startTime) <= secTol) { hitItem = { type: 'midiItem', id: midi.id, start: midi.startTime, dur: midi.duration }; hitEdge = 'left'; break; }
          if (Math.abs(time - (midi.startTime + midi.duration)) <= secTol) { hitItem = { type: 'midiItem', id: midi.id, start: midi.startTime, dur: midi.duration }; hitEdge = 'right'; break; }
        }
      }
      if (hitItem && hitEdge) {
        e.preventDefault();
        e.stopPropagation();
        if (onSectionItemResizeStart) onSectionItemResizeStart(track.id, hitItem.type, hitItem.id, hitEdge, time);
        return;
      }
      if (!hitItem) {
        for (const sec of secItems) {
          if (time >= sec.start && time < sec.start + sec.duration) { hitItem = { type: 'section', id: sec.id, start: sec.start }; break; }
        }
      }
      if (!hitItem) {
        for (const midi of midiItems) {
          if (time >= midi.startTime && time < midi.startTime + midi.duration) { hitItem = { type: 'midiItem', id: midi.id, start: midi.startTime }; break; }
        }
      }
      if (hitItem && !e.altKey && !e.shiftKey) {
        e.preventDefault();
        e.stopPropagation();
        // Chọn TRACK chứa item (user 07:55: Ctrl+V phải paste vào track đang
        // select — click item không qua onTrackLaneMouseDown → selectedTrackId
        // giữ track cũ → paste sai track)
        if (onSelectTrack) onSelectTrack(track.id);
        if (e.ctrlKey) {
          // Ctrl+Click: toggle selection (add/remove from group) + set pending copy-drag
          var preToggle = selectedItemIds ? new Set(selectedItemIds) : new Set();
          if (selectedItemIds && selectedItemIds.has(hitItem.id)) {
            if (onDeselectItem) onDeselectItem(hitItem.id);
          } else {
            if (onAddToSelection) onAddToSelection(hitItem.id);
          }
          if (onSetPendingDrag) onSetPendingDrag(track.id, hitItem.type, hitItem.id, time - hitItem.start, e.nativeEvent || e, preToggle);
        } else {
          // Click: select this item (clear others if not already selected).
          // Drag only starts after a small movement threshold (5px) — click alone
          // just selects; this unifies section/MIDI/clip behavior and stops
          // accidental moves from mouse jitter.
          if (!selectedItemIds || !selectedItemIds.has(hitItem.id)) {
            if (onClearSelection) onClearSelection();
            if (onAddToSelection) onAddToSelection(hitItem.id);
          }
          // Drag all currently selected items (or just this one)
          var dragIds = (selectedItemIds && selectedItemIds.has(hitItem.id) && selectedItemIds.size > 1)
            ? selectedItemIds
            : new Set([hitItem.id]);
          if (onSetPendingDragMove) onSetPendingDragMove(track.id, hitItem.type, hitItem.id, time - hitItem.start, e.nativeEvent || e, dragIds);
        }
        return;
      }

      const clickedClip = clips.find(c => c.buffer && time >= c.startTime && time < c.startTime + c.buffer.duration / (c.speed || 1.0));

      // Set selected clip ID
      if (clickedClip) {
        setSelectedClipId({
          trackId: track.id,
          clipId: clickedClip.id === 'default' ? 'default_' + track.id : clickedClip.id
        });
      } else {
        setSelectedClipId(null);
      }
      if (activeTool === 'pen' && !e.ctrlKey) {
        if (clickedClip) {
          e.preventDefault();
          e.stopPropagation();
          if (onEditClipInSubTab) {
            onEditClipInSubTab(track.id, clickedClip.id);
          }
        }
        return;
      }
      if (activeTool === 'razor' && !e.ctrlKey) {
        if (clickedClip) {
          e.preventDefault();
          e.stopPropagation();
          if (onSplitTrackAtTime) {
            onSplitTrackAtTime(track.id, clickedClip.id, time);
          }
        }
        return;
      }
      if (activeTool === 'grab') {
        if (onTrackLaneMouseDown) {
          onTrackLaneMouseDown(track.id, time, e);
        }
        if (clickedClip) {
          e.preventDefault();
          e.stopPropagation();
          if (onClipDragStart) {
            onClipDragStart(track.id, clickedClip.id, time - clickedClip.startTime, e.ctrlKey);
          }
        } else {
          onPlayheadSet(time);
        }
        return;
      }

      // Check for click drag clip (plain click = move, Alt = sweep-select duration, Ctrl = duplicate)
      if (clickedClip && e.ctrlKey) {
        e.preventDefault();
        e.stopPropagation();
        // Ctrl+Click: toggle selection, store pending drag w/ pre-toggle snapshot
        var clipCanonicalId = clickedClip.id === 'default' ? 'default_' + track.id : clickedClip.id;
        var clipPreToggleSnapshot = selectedItemIds ? new Set(selectedItemIds) : new Set();
        if (selectedItemIds && selectedItemIds.has(clipCanonicalId)) {
          if (onDeselectItem) onDeselectItem(clipCanonicalId);
        } else if (onAddToSelection) {
          onAddToSelection(clipCanonicalId);
        }
        if (onSetPendingDrag) onSetPendingDrag(track.id, 'clip', clipCanonicalId, time - clickedClip.startTime, e.nativeEvent || e, clipPreToggleSnapshot);
        return;
      }

      // Alt+Click on a clip: sweep-select duration (the OLD plain click+drag
      // behavior moved here). Plain click+drag now MOVES the clip like
      // section/MIDI items.
      if (clickedClip && e.altKey) {
        e.preventDefault();
        e.stopPropagation();
        onPlayheadSet(time);
        if (onTrackLaneMouseDown) {
          onTrackLaneMouseDown(track.id, time, e);
        }
        return;
      }

      // Plain click on a clip: select it; drag after a small movement
      // threshold moves it (unified with section/MIDI items).
      if (clickedClip) {
        e.preventDefault();
        e.stopPropagation();
        var clipCanonicalId2 = clickedClip.id === 'default' ? 'default_' + track.id : clickedClip.id;
        if (!selectedItemIds || !selectedItemIds.has(clipCanonicalId2)) {
          if (onClearSelection) onClearSelection();
          if (onAddToSelection) onAddToSelection(clipCanonicalId2);
        }
        var clipDragIds = (selectedItemIds && selectedItemIds.has(clipCanonicalId2) && selectedItemIds.size > 1)
          ? selectedItemIds
          : new Set([clipCanonicalId2]);
        if (onSetPendingDragMove) onSetPendingDragMove(track.id, 'clip', clipCanonicalId2, time - clickedClip.startTime, e.nativeEvent || e, clipDragIds);
        return;
      }

      // Ctrl+Click on empty space: deselect all on click, marquee on drag
      if (e.ctrlKey && !clickedClip && !hitItem) {
        e.preventDefault();
        e.stopPropagation();
        // Start a pending sweep: mouseup with no drag → deselect all; drag → marquee
        if (onSweepSelectStart) onSweepSelectStart(track.id, time, e.clientY);
        return;
      }
      onPlayheadSet(time);
      if (onTrackLaneMouseDown) {
        onTrackLaneMouseDown(track.id, time, e);
      }
      e.stopPropagation();
    },
    onDoubleClick: e => {
      const rect = canvasRef.current.getBoundingClientRect();
      const x = e.clientX - rect.left + (scrollLeft || 0);
      const time = Math.max(0, x / zoom - leadInMargin);
      const clips = track.clips && track.clips.length > 0 ? track.clips : track.buffer ? [{
        id: 'default',
        buffer: track.buffer,
        startTime: track.startTime || 0,
        name: track.name,
        speed: track.speed || 1.0
      }] : [];
      // Check double-click on section first
      const dblSecItems = track.sections || [];
      let dblSecHit = null;
      for (const sec of dblSecItems) {
        if (time >= sec.start && time < sec.start + sec.duration) { dblSecHit = sec; break; }
      }
      if (dblSecHit) {
        e.preventDefault();
        e.stopPropagation();
        if (onEditSectionInTab) onEditSectionInTab(track.id, dblSecHit.id);
        return;
      }
      // Check double-click on MIDI item next
      const dblMidiItems = track.midiItems || [];
      let dblMidiHit = null;
      for (const midi of dblMidiItems) {
        if (time >= midi.startTime && time < midi.startTime + midi.duration) { dblMidiHit = midi; break; }
      }
      if (dblMidiHit) {
        e.preventDefault();
        e.stopPropagation();
        if (onEditMidiInTab) onEditMidiInTab(track.id, dblMidiHit.id);
        return;
      }
      const clickedClip = clips.find(c => c.buffer && time >= c.startTime && time < c.startTime + c.buffer.duration / (c.speed || 1.0));
      if (clickedClip) {
        e.preventDefault();
        e.stopPropagation();
        if (onEditClipInSubTab) {
          onEditClipInSubTab(track.id, clickedClip.id);
        }
      }
    },
    onContextMenu: e => {
      e.preventDefault();
      e.stopPropagation();
      onSelectTrack(track.id);
      const rect = canvasRef.current.getBoundingClientRect();
      const x = e.clientX - rect.left + (scrollLeft || 0);
      const time = Math.max(0, x / zoom - leadInMargin);
      // Detect section under cursor
      const secList = track.sections || [];
      let hitSectionId = null;
      for (const sec of secList) {
        if (time >= sec.start && time < sec.start + sec.duration) { hitSectionId = sec.id; break; }
      }
      // Detect MIDI item dưới chuột (user 07:25 — Cut/Copy item phải copy
      // ĐÚNG item — bounds × secondsPerBar (m.duration tính BEATS — như
      // contextMenuDelete 17452) — thiếu × spb → detect miss → copy nhầm track)
      const _spb = (60.0 / (parseInt(bpm) || 120)) * 4;
      let hitMidiId = null;
      for (const m of (track.midiItems || [])) {
        if (time >= m.startTime && time < m.startTime + (m.duration || 4) * _spb) { hitMidiId = m.id; break; }
      }
      // Detect clip dưới chuột
      let hitClipId = null;
      for (const c of (track.clips || [])) {
        if (c.buffer && time >= c.startTime && time < c.startTime + c.buffer.duration / (c.speed || 1.0)) { hitClipId = c.id; break; }
      }
      if (onContextMenu) onContextMenu(e, track.id, time, hitSectionId, hitMidiId || hitClipId || null, hitMidiId ? 'midi' : (hitClipId ? 'clip' : null));
    }
  }));
};
const TimelineRuler = ({
  bpm, zoom, timelineWidth, viewportWidth,
  onPlayheadSet, snapValue, onRulerMouseDown, scrollLeft, canvasRedrawCount
}) => {
  const canvasRef = useRef(null);
  const RULER_HEIGHT = 40;
  const drawWidth = Math.min(timelineWidth, viewportWidth);
  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const ctx = canvas.getContext('2d');
    const dpr = window.devicePixelRatio || 1;
    let scrollLeftVal = scrollLeft || 0;
    const height = RULER_HEIGHT;
    canvas.width = Math.min(Math.round(drawWidth * dpr), 32768);
    canvas.height = Math.min(Math.round(height * dpr), 32768);
    ctx.scale(dpr, dpr);
    ctx.imageSmoothingEnabled = false;
    canvas.style.width = `${drawWidth}px`;
    canvas.style.height = `${height}px`;
    ctx.fillStyle = '#242424';
    ctx.fillRect(0, 0, drawWidth, height);
    ctx.strokeStyle = 'rgba(255,255,255,0.08)';
    ctx.lineWidth = 1;
    ctx.beginPath();
    ctx.moveTo(0, height - 0.5);
    ctx.lineTo(drawWidth, height - 0.5);
    ctx.stroke();

    const CLIP_BUFFER = 400;
    const PADDING_LEFT = 0;
    const tStart = scrollLeftVal / zoom - PADDING_LEFT;
    const tEnd = (scrollLeftVal + drawWidth) / zoom + CLIP_BUFFER / zoom;

    // Draw bar markers (aligned with TempoTrackLane)
    const beatDuration = 60 / bpm;
    const barDuration = beatDuration * 4;
    const firstBarNum = Math.floor(tStart / barDuration);
    const lastBarNum = Math.ceil(tEnd / barDuration);
    for (let bn = firstBarNum; bn <= lastBarNum; bn++) {
      const t = bn * barDuration;
      const localX = (t - tStart) * zoom;
      if (localX < -CLIP_BUFFER || localX > drawWidth + CLIP_BUFFER) continue;
      ctx.strokeStyle = 'rgba(255, 255, 255, 0.25)';
      ctx.lineWidth = 1.2;
      ctx.beginPath();
      ctx.moveTo(localX, 0);
      ctx.lineTo(localX, height);
      ctx.stroke();
      ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
      ctx.font = 'bold 10px Inter, sans-serif';
      ctx.textAlign = 'center';
      ctx.fillText(`${bn}`, localX, 32);
    }

    // Draw time duration labels with drag-selection markers
    const minTimePx = 60;
    const rawSecInt = Math.max(1, Math.ceil(minTimePx / zoom));
    const timePowers = [1, 2, 5, 10, 30, 60];
    let secInterval = timePowers.find(p => p >= rawSecInt) || 120;
    if (secInterval * zoom < minTimePx) secInterval = Math.ceil(minTimePx / zoom);
    const firstSec = Math.floor(tStart / secInterval) * secInterval;
    for (let t = firstSec; t <= tEnd; t += secInterval) {
      const localX = (t - tStart) * zoom;
      if (localX < -CLIP_BUFFER || localX > drawWidth + CLIP_BUFFER) continue;
      ctx.strokeStyle = 'rgba(255, 180, 100, 0.12)';
      ctx.lineWidth = 0.8;
      ctx.beginPath();
      ctx.moveTo(localX, 0);
      ctx.lineTo(localX, height);
      ctx.stroke();
      ctx.strokeStyle = 'rgba(255, 180, 100, 0.3)';
      ctx.lineWidth = 1.5;
      ctx.beginPath();
      ctx.moveTo(localX, 0);
      ctx.lineTo(localX, 10);
      ctx.stroke();
      ctx.fillStyle = 'rgba(255, 180, 100, 0.7)';
      ctx.font = 'bold 11px monospace';
      ctx.textAlign = 'center';
      ctx.fillText(formatTimeSimple(t), localX, 24);
    }
  }, [bpm, zoom, timelineWidth, viewportWidth, scrollLeft, canvasRedrawCount]);
  return React.createElement(React.Fragment, null, React.createElement("div", {
    key: "virtual-spacer-ruler",
    style: { width: `${timelineWidth}px`, height: '1px', pointerEvents: 'none' }
  }), React.createElement("canvas", {
    ref: canvasRef,
    style: { position: 'sticky', left: 0, imageRendering: 'pixelated' },
    className: "cursor-crosshair",
    onMouseDown: e => {
      const rect = canvasRef.current.getBoundingClientRect();
      const x = e.clientX - rect.left + (scrollLeft || 0);
      const time = Math.max(0, x / zoom);
      if (e.shiftKey) { e.preventDefault(); e.stopPropagation(); }
      if (onRulerMouseDown) onRulerMouseDown(e);
      else onPlayheadSet(time, e.shiftKey);
    }
  }));
};
const TempoTrackLane = ({
  bpm,
  zoom,
  timelineWidth,
  viewportWidth,
  onPlayheadSet,
  snapValue,
  onRulerMouseDown,
  scrollLeft,
  canvasRedrawCount,
  leadInMargin: propLeadIn
}) => {
  const canvasRef = useRef(null);
  const drawWidth = Math.min(timelineWidth, viewportWidth);
  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const ctx = canvas.getContext('2d');
    const dpr = window.devicePixelRatio || 1;
    // Use scrollLeft prop directly (DOM traversal broken by sticky wrapper)
    let scrollLeftVal = scrollLeft || 0;
    const height = 40;
    canvas.width = Math.min(Math.round(drawWidth * dpr), 32768);
    canvas.height = Math.min(Math.round(height * dpr), 32768);
    ctx.scale(dpr, dpr);
    ctx.imageSmoothingEnabled = false;
    canvas.style.width = `${drawWidth}px`;
    canvas.style.height = `${height}px`;
    ctx.fillStyle = '#1a1a2e';
    ctx.fillRect(0, 0, drawWidth, height);
    const beatDuration = 60 / bpm;
    const barDuration = beatDuration * 4;
    const leadIn = propLeadIn !== undefined ? propLeadIn : 0;
    const CLIP_BUFFER = Math.max(400, barDuration * zoom + 200);
    const PADDING_LEFT = 0;
    const tStart = (scrollLeftVal - leadIn * zoom) / zoom - PADDING_LEFT;
    const tEnd = (scrollLeftVal + drawWidth - leadIn * zoom) / zoom + CLIP_BUFFER / zoom;
    const firstBeatNum = Math.floor(tStart / beatDuration);
    const lastBeatNum = Math.ceil(tEnd / beatDuration);
    for (let bn = firstBeatNum; bn <= lastBeatNum; bn++) {
      const t = bn * beatDuration;
      const beatNum = bn + 1;
      const isBar = beatNum % 4 === 1;
      const localX = (t - tStart) * zoom;
      if (localX < -CLIP_BUFFER || localX > drawWidth + CLIP_BUFFER) continue;
      if (isBar) {
        ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)';
        ctx.lineWidth = 1.5;
        ctx.beginPath();
        ctx.moveTo(localX, 0);
        ctx.lineTo(localX, height);
        ctx.stroke();
        ctx.fillStyle = 'rgba(255, 255, 255, 0.7)';
        ctx.font = 'bold 9px Inter, sans-serif';
        ctx.textAlign = 'left';
        ctx.fillText(`${Math.floor((beatNum - 1) / 4)}`, localX + 3, 11);
      } else {
        ctx.strokeStyle = 'rgba(255, 255, 255, 0.08)';
        ctx.lineWidth = 1;
        ctx.beginPath();
        ctx.moveTo(localX, 0);
        ctx.lineTo(localX, height);
        ctx.stroke();
      }
      ctx.fillStyle = 'rgba(255, 255, 255, 0.35)';
      ctx.font = '7px Inter, sans-serif';
      const bar = Math.floor((beatNum - 1) / 4);
      const beat = ((beatNum - 1) % 4) + 1;
      ctx.fillText(`${bar}:${beat}`, localX + 2, height - 3);
    }

    // Draw snap sub-ticks at the bottom
    if (snapValue && snapValue !== 'free') {
      ctx.strokeStyle = 'rgba(255, 255, 255, 0.15)';
      ctx.lineWidth = 0.8;
      let divisor = 1;
      if (snapValue === '4') divisor = 4; else if (snapValue === '1') divisor = 1; else if (snapValue === '1/2') divisor = 0.5; else if (snapValue === '1/4') divisor = 0.25; else if (snapValue === '1/8') divisor = 0.125; else if (snapValue === '1/16') divisor = 0.0625; else if (snapValue === '1/32') divisor = 0.03125;
      const snapInterval = beatDuration * divisor;
      if (snapInterval * zoom >= 4) {
        const firstSnapNum = Math.floor(tStart / snapInterval);
        const lastSnapNum = Math.ceil(tEnd / snapInterval);
        for (let sn = firstSnapNum; sn <= lastSnapNum; sn++) {
          const t = sn * snapInterval;
          const onBeat = Math.abs(t / beatDuration - Math.round(t / beatDuration)) < 0.001;
          if (!onBeat) {
            const localX = (t - tStart) * zoom;
            ctx.beginPath();
            ctx.moveTo(localX, height - 6);
            ctx.lineTo(localX, height);
            ctx.stroke();
          }
        }
      }
    }
    // Draw time duration labels (restored from original time ruler)
    const minTimePx = 60;
    const rawSecInterval = Math.max(1, Math.ceil(minTimePx / zoom));
    const timePowers = [1, 2, 5, 10, 30, 60];
    let secInterval = timePowers.find(p => p >= rawSecInterval) || 120;
    if (secInterval * zoom < minTimePx) secInterval = Math.ceil(minTimePx / zoom);
    const firstSec = Math.floor(tStart / secInterval) * secInterval;
    for (let t = firstSec; t <= tEnd; t += secInterval) {
      const localX = (t - tStart) * zoom;
      if (localX < -CLIP_BUFFER || localX > drawWidth + CLIP_BUFFER) continue;
      ctx.strokeStyle = 'rgba(255, 180, 100, 0.15)';
      ctx.lineWidth = 0.8;
      ctx.beginPath();
      ctx.moveTo(localX, 16);
      ctx.lineTo(localX, height);
      ctx.stroke();
      ctx.fillStyle = 'rgba(255, 180, 100, 0.5)';
      ctx.font = '8px monospace';
      ctx.textAlign = 'center';
      ctx.fillText(formatTimeSimple(t), localX, 12);
    }

    ctx.fillStyle = 'rgba(255, 255, 255, 0.35)';
    ctx.font = 'bold 10px Inter, sans-serif';
    ctx.textAlign = 'right';
    ctx.fillText(`${bpm} BPM`, drawWidth - 6, 12);
  }, [bpm, zoom, timelineWidth, viewportWidth, snapValue, scrollLeft, canvasRedrawCount, propLeadIn]);
  return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
    key: "virtual-spacer-tempo",
    style: {
      width: `${timelineWidth}px`,
      height: '1px',
      pointerEvents: 'none'
    }
  }), /*#__PURE__*/React.createElement("canvas", {
    ref: canvasRef,
    style: {
      position: 'sticky',
      left: 0,
      imageRendering: 'pixelated'
    },
    className: "cursor-crosshair",
    onMouseDown: e => {
      const rect = canvasRef.current.getBoundingClientRect();
      const x = e.clientX - rect.left + (scrollLeft || 0);
      const time = Math.max(0, x / zoom);
      if (e.shiftKey) {
        e.preventDefault();
        e.stopPropagation();
      }
      if (onRulerMouseDown) {
        onRulerMouseDown(e);
      } else {
        onPlayheadSet(time, e.shiftKey);
      }
    }
  }));
};

// ── Sub-Tab Waveform Component (LOOP_EDITOR_2.md §1.2 & SUB_EDITOR.md) ──
const SubTabWaveform = ({
  buffer,
  subTabId,
  activeTab,
  currentTime,
  selectionStart,
  selectionEnd,
  onSelectRange,
  onPlayheadSet,
  onContextMenu,
  activeTool,
  zoom,
  timelineWidth,
  color,
  name,
  speed = 1.0,
  onSpeedChange,
  volumeNodes = [],
  panningNodes = [],
  fadeInLen = 0,
  fadeOutLen = 0,
  graphMode = null,
  onUpdateNodes,
  onUpdateFade,
  onModeToggle,
  selectedNodeTime,
  setSelectedNodeTime,
  channelInfo = null
}) => {
  const canvasRef = useRef(null);
  const isStretchingRef = useRef(false);
  const stretchStartRef = useRef({
    mouseX: 0,
    originalDuration: 0,
    originalSpeed: 1.0
  });
  const subTabAnchorRef = useRef(null);
  const isStereo = channelInfo ? channelInfo.isStereo : buffer && buffer.numberOfChannels >= 2;
  const channelLabel = channelInfo ? channelInfo.label : isStereo ? 'STEREO' : 'MONO';
  // Mono: force volume mode (panning not applicable)
  const effectiveGraphMode = !isStereo && graphMode === 'pan' ? null : graphMode;
  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas || !buffer) return;
    const ctx = canvas.getContext('2d');
    const dpr = window.devicePixelRatio || 1;
    const wrapper = canvas.parentElement ? canvas.parentElement.parentElement : null;
    const scrollLeft = wrapper ? wrapper.scrollLeft : 0;
    const vWidth = wrapper ? wrapper.clientWidth : 1200;
    const drawWidth = Math.min(timelineWidth, Math.max(vWidth, 1200));
    const h = canvas.parentElement ? canvas.parentElement.clientHeight : 200;
    canvas.width = Math.round(drawWidth * dpr);
    canvas.height = Math.round(h * dpr);
    ctx.scale(dpr, dpr);
    ctx.imageSmoothingEnabled = false;
    canvas.style.position = 'absolute';
    canvas.style.left = `${scrollLeft}px`;
    canvas.style.width = `${drawWidth}px`;
    canvas.style.height = `${h}px`;
    ctx.fillStyle = '#181818';
    ctx.fillRect(0, 0, drawWidth, h);
    const data = buffer.getChannelData(0);
    const len = data.length;
    if (len === 0) return;

    // Helper to compute volume gain at a specific time in clip using Monotone Cubic Hermite Spline
    const computeHermiteTangents = pts => {
      const n = pts.length;
      if (n < 2) return [];
      const m = new Array(n);
      for (let i = 1; i < n - 1; i++) {
        const hP = pts[i].time - pts[i - 1].time;
        const hN = pts[i + 1].time - pts[i].time;
        const sP = (pts[i].db - pts[i - 1].db) / hP;
        const sN = (pts[i + 1].db - pts[i].db) / hN;
        m[i] = (sP + sN) / 2;
      }
      m[0] = n > 1 ? (pts[1].db - pts[0].db) / (pts[1].time - pts[0].time) : 0;
      m[n - 1] = n > 1 ? (pts[n - 1].db - pts[n - 2].db) / (pts[n - 1].time - pts[n - 2].time) : 0;
      return m;
    };
    const getVolumeDbAtTime = t => {
      const volNodes = volumeNodes || [];
      if (volNodes.length === 0) return 0.0;
      const sortedNodes = [...volNodes].sort((a, b) => a.time - b.time);
      if (sortedNodes.length === 1) return sortedNodes[0].db;
      if (t <= sortedNodes[0].time) return sortedNodes[0].db;
      if (t >= sortedNodes[sortedNodes.length - 1].time) return sortedNodes[sortedNodes.length - 1].db;
      const tangents = computeHermiteTangents(sortedNodes);
      for (let i = 0; i < sortedNodes.length - 1; i++) {
        const n1 = sortedNodes[i];
        const n2 = sortedNodes[i + 1];
        if (t >= n1.time && t <= n2.time) {
          const h = n2.time - n1.time;
          if (h <= 0) return n1.db;
          const frac = (t - n1.time) / h;
          const frac2 = frac * frac,
            frac3 = frac2 * frac;
          return (2 * frac3 - 3 * frac2 + 1) * n1.db + (frac3 - 2 * frac2 + frac) * h * tangents[i] + (-2 * frac3 + 3 * frac2) * n2.db + (frac3 - frac2) * h * tangents[i + 1];
        }
      }
      return 0.0;
    };
    const getVolumeGainAtTime = t => {
      return Math.pow(10, getVolumeDbAtTime(t) / 20);
    };
    const computeHermiteTangentsForPan = pts => {
      const n = pts.length;
      if (n < 2) return [];
      const m = new Array(n);
      for (let i = 1; i < n - 1; i++) {
        const hP = pts[i].time - pts[i - 1].time;
        const hN = pts[i + 1].time - pts[i].time;
        const sP = (pts[i].pan - pts[i - 1].pan) / hP;
        const sN = (pts[i + 1].pan - pts[i].pan) / hN;
        m[i] = (sP + sN) / 2;
      }
      m[0] = n > 1 ? (pts[1].pan - pts[0].pan) / (pts[1].time - pts[0].time) : 0;
      m[n - 1] = n > 1 ? (pts[n - 1].pan - pts[n - 2].pan) / (pts[n - 1].time - pts[n - 2].time) : 0;
      return m;
    };
    const getPanningValueAtTime = t => {
      const panNodes = panningNodes || [];
      if (panNodes.length === 0) return 0;
      const sortedNodes = [...panNodes].sort((a, b) => a.time - b.time);
      if (sortedNodes.length === 1) return Math.round(sortedNodes[0].pan * 100);
      if (t <= sortedNodes[0].time) return Math.round(sortedNodes[0].pan * 100);
      if (t >= sortedNodes[sortedNodes.length - 1].time) return Math.round(sortedNodes[sortedNodes.length - 1].pan * 100);
      const tangents = computeHermiteTangentsForPan(sortedNodes);
      for (let i = 0; i < sortedNodes.length - 1; i++) {
        const n1 = sortedNodes[i];
        const n2 = sortedNodes[i + 1];
        if (t >= n1.time && t <= n2.time) {
          const h = n2.time - n1.time;
          if (h <= 0) return Math.round(n1.pan * 100);
          const frac = (t - n1.time) / h;
          const frac2 = frac * frac,
            frac3 = frac2 * frac;
          const val = (2 * frac3 - 3 * frac2 + 1) * n1.pan + (frac3 - 2 * frac2 + frac) * h * tangents[i] + (-2 * frac3 + 3 * frac2) * n2.pan + (frac3 - frac2) * h * tangents[i + 1];
          return Math.round(val * 100);
        }
      }
      return 0;
    };

    // Draw clip container (like main session clips)
    const xStart = 0;
    const wClip = buffer.duration / speed * zoom; // speed-adjusted width
    const xEnd = xStart + wClip;
    const clipTop = 8;
    const clipHeight = h - 16;
    const clipColor = color || '#06b6d4';
    ctx.fillStyle = clipColor + '22';
    ctx.strokeStyle = clipColor;
    ctx.lineWidth = 1.5;
    if (ctx.roundRect) {
      ctx.beginPath();
      ctx.roundRect(xStart, clipTop, wClip, clipHeight, 4);
      ctx.fill();
      ctx.stroke();
    } else {
      ctx.fillRect(xStart, clipTop, wClip, clipHeight);
      ctx.strokeRect(xStart, clipTop, wClip, clipHeight);
    }

    // Draw clip label with speed %
    ctx.fillStyle = '#e4e4e7';
    ctx.font = 'bold 10px sans-serif';
    let displayName = name || 'Audio Clip';
    if (speed !== 1.0) {
      displayName += ` (${Math.round(speed * 100)}%)`;
    }
    ctx.fillText(displayName, xStart + 8, clipTop + 14);

    // ── Graph Grid & Axes ──
    const drawGrid = true;
    if (drawGrid) {
      // Background fill
      ctx.fillStyle = '#181818';
      ctx.fillRect(xStart, clipTop, wClip, clipHeight);

      // Vertical grid lines (time markers)
      ctx.strokeStyle = '#2a2a2a';
      ctx.lineWidth = 0.5;
      ctx.setLineDash([]);
      const timeStep = Math.max(0.1, Math.ceil(buffer.duration / 20 * 10) / 10);
      for (let t = 0; t <= buffer.duration; t += timeStep) {
        const px = t * zoom;
        ctx.beginPath();
        ctx.moveTo(px, clipTop);
        ctx.lineTo(px, clipTop + clipHeight);
        ctx.stroke();
      }

      // Always draw the reference Volume 0dB Axis (White) and Panning Center Axis (Brown)
      const volZeroY = clipTop + 1 / 3 * clipHeight;
      const panZeroY = clipTop + 1 / 2 * clipHeight;

      // White line for volume 0dB
      ctx.strokeStyle = '#ffffff';
      ctx.lineWidth = 1.2;
      ctx.beginPath();
      ctx.moveTo(xStart, volZeroY);
      ctx.lineTo(xStart + wClip, volZeroY);
      ctx.stroke();

      // Brown line for panning center
      ctx.strokeStyle = '#854d0e';
      ctx.lineWidth = 1.2;
      ctx.beginPath();
      ctx.moveTo(xStart, panZeroY);
      ctx.lineTo(xStart + wClip, panZeroY);
      ctx.stroke();

      // Horizontal grid lines (other value markers)
      const isPanMode = effectiveGraphMode === 'pan';
      if (isPanMode) {
        for (let p = -100; p <= 100; p += 20) {
          if (p === 0) continue;
          const y = clipTop + clipHeight * (1 - (p / 100 + 1) / 2);
          ctx.strokeStyle = '#2a2a2a';
          ctx.lineWidth = 0.5;
          ctx.beginPath();
          ctx.moveTo(xStart, y);
          ctx.lineTo(xStart + wClip, y);
          ctx.stroke();
        }
      } else {
        for (let db = -30; db <= 3; db += 3) {
          if (db === 0) continue;
          const y = db >= 0 ? volZeroY - db / 3 * (2 / 3 * clipHeight) : volZeroY + -db / 30 * (1 / 3 * clipHeight);
          ctx.strokeStyle = '#2a2a2a';
          ctx.lineWidth = 0.5;
          ctx.beginPath();
          ctx.moveTo(xStart, y);
          ctx.lineTo(xStart + wClip, y);
          ctx.stroke();
        }
      }

      // Y-axis labels (left side)
      ctx.fillStyle = '#71717a';
      ctx.font = '7px monospace';
      ctx.textAlign = 'right';
      if (isPanMode) {
        ctx.fillText('L100', xStart - 2, clipTop + 8);
        ctx.fillText('R50', xStart - 2, clipTop + clipHeight * 0.25 + 2);
        ctx.fillStyle = '#854d0e'; // Brown label for active Panning Center
        ctx.fillText('C (Pan)', xStart - 2, clipTop + clipHeight * 0.5 + 2);
        ctx.fillStyle = '#71717a';
        ctx.fillText('L50', xStart - 2, clipTop + clipHeight * 0.75 + 2);
        ctx.fillText('R100', xStart - 2, clipTop + clipHeight - 2);
      } else {
        ctx.fillText('+3dB', xStart - 2, clipTop + 8);
        ctx.fillStyle = '#ffffff'; // White label for active Volume 0dB
        ctx.fillText('0dB (Vol)', xStart - 2, volZeroY + 2);
        ctx.fillStyle = '#71717a';
        ctx.fillText('-15dB', xStart - 2, volZeroY + clipHeight / 6 + 2);
        ctx.fillText('-30dB', xStart - 2, clipTop + clipHeight - 2);
      }
      ctx.textAlign = 'start';
    }

    // Drag hint labels for fade endpoints
    ctx.fillStyle = '#71717a';
    ctx.font = '8px sans-serif';
    ctx.fillText('Kéo FI/FO trên đường cong để điều chỉnh', 8, clipTop + clipHeight + 12);

    // Display speed percentage in the bottom left corner of the waveform area
    ctx.fillStyle = '#e4e4e7';
    ctx.font = 'bold 9px sans-serif';
    ctx.fillText(`Tốc độ: ${Math.round(speed * 100)}%`, xStart + 8, clipTop + clipHeight - 8);

    // Mode toggle button on waveform (bottom-right)
    const modeBtnW = 28;
    const modeBtnH = 14;
    const modeBtnX = wClip - modeBtnW - 4;
    const modeBtnY = clipTop + clipHeight - modeBtnH - 2;
    const isPanMode = effectiveGraphMode === 'pan';
    ctx.fillStyle = isPanMode ? 'rgba(168, 85, 247, 0.5)' : 'rgba(6, 182, 212, 0.5)';
    ctx.beginPath();
    ctx.roundRect(modeBtnX, modeBtnY, modeBtnW, modeBtnH, 3);
    ctx.fill();
    ctx.fillStyle = '#fff';
    ctx.font = 'bold 7px sans-serif';
    ctx.textAlign = 'center';
    ctx.fillText(isPanMode ? 'PAN' : 'VOL', modeBtnX + modeBtnW / 2, modeBtnY + 10);
    ctx.textAlign = 'start';

    // Channel label (L / R for stereo, M for mono)
    ctx.fillStyle = '#a1a1aa';
    ctx.font = 'bold 8px monospace';
    if (isStereo) {
      ctx.fillText('L', xStart + 2, clipTop + clipHeight * 0.28);
      ctx.fillText('R', xStart + 2, clipTop + clipHeight * 0.72);
    } else {
      ctx.fillText('M', xStart + 2, clipTop + clipHeight / 2);
    }

    // Draw waveform inside clip (speed-adjusted) with fade envelope applied
    const drawXStart = Math.max(0, Math.floor(xStart));
    const drawXEnd = Math.min(drawWidth, Math.ceil(xEnd));
    const samplesPerPixel = buffer.sampleRate / zoom * speed;
    const bufDur = buffer.duration;
    const mid = h / 2;
    const peakRatio = clipHeight * 0.45;
    const getFadeGainAtTime = t => {
      if (fadeInLen > 0 && t < fadeInLen) {
        return (1 - Math.cos(Math.PI * t / fadeInLen)) / 2;
      } else if (fadeOutLen > 0 && t > bufDur - fadeOutLen) {
        const ratio = (t - (bufDur - fadeOutLen)) / fadeOutLen;
        return (1 + Math.cos(Math.PI * ratio)) / 2;
      }
      return 1;
    };
    ctx.strokeStyle = '#5bc0be';
    ctx.lineWidth = 1.2;
    if (samplesPerPixel < 4) {
      // High-zoom continuous vector line rendering (#5bc0be Cornflower Blue)
      const startSample = Math.max(0, Math.floor((drawXStart - xStart) / zoom * speed * buffer.sampleRate));
      const endSample = Math.min(len, Math.ceil((drawXEnd - xStart) / zoom * speed * buffer.sampleRate));
      ctx.beginPath();
      let first = true;
      const maxSamples = 50000;
      const vectorStep = Math.max(1, Math.floor((endSample - startSample) / maxSamples));
      for (let i = startSample; i < endSample; i += vectorStep) {
        const timeInClip = i / buffer.sampleRate / speed;
        const px = xStart + timeInClip * zoom;
        const fadeGain = getFadeGainAtTime(timeInClip);
        const volGain = getVolumeGainAtTime(px / zoom);
        const totalGain = fadeGain * volGain;
        const y = mid - data[i] * totalGain * peakRatio;
        if (first) {
          ctx.moveTo(px, y);
          first = false;
        } else {
          ctx.lineTo(px, y);
        }
      }
      ctx.stroke();

      // Draw granular sample nodes only at extreme zoom (samplesPerPixel < 0.3)
      if (samplesPerPixel < 0.3) {
        const maxNodes = 5000;
        const step = Math.max(1, Math.floor((endSample - startSample) / maxNodes));
        ctx.fillStyle = '#6ee7b7';
        let drawn = 0;
        for (let i = startSample; i < endSample && drawn < maxNodes; i += step) {
          const timeInClip = i / buffer.sampleRate / speed;
          const px = xStart + timeInClip * zoom;
          const fadeGain = getFadeGainAtTime(timeInClip);
          const volGain = getVolumeGainAtTime(px / zoom);
          const totalGain = fadeGain * volGain;
          const y = mid - data[i] * totalGain * peakRatio;
          ctx.fillRect(px - 1, y - 1, 2, 2);
          drawn++;
        }
      }
    } else {
      // Coarse view vertical peak min/max bars
      for (let px = drawXStart; px < drawXEnd; px++) {
        const timeInClip = px / zoom * speed;
        const sampleIdx = Math.floor(timeInClip * buffer.sampleRate);
        const chunkSize = Math.max(1, Math.floor(samplesPerPixel));
        const chunkStart = Math.max(0, sampleIdx - Math.floor(chunkSize / 2));
        const chunkEnd = Math.min(len, chunkStart + chunkSize);
        let fadeGain = getFadeGainAtTime(timeInClip);
        const volGain = getVolumeGainAtTime(px / zoom);
        const totalGain = fadeGain * volGain;
        let maxVal = 0;
        let minVal = 0;
        for (let i = chunkStart; i < chunkEnd; i++) {
          const val = data[i];
          if (val > maxVal) maxVal = val;
          if (val < minVal) minVal = val;
        }
        maxVal *= totalGain;
        minVal *= totalGain;
        const yTop = mid + minVal * peakRatio;
        const yBottom = mid + maxVal * peakRatio;
        ctx.beginPath();
        ctx.moveTo(px, yTop);
        ctx.lineTo(px, yBottom);
        ctx.stroke();
      }
    }

    // Volume: 0dB at 1/3 from top
    const autoY = node => {
      const db = typeof node === 'number' ? node : node.db;
      const zeroY = clipTop + 1 / 3 * clipHeight;
      return db >= 0 ? zeroY - db / 3 * (1 / 3 * clipHeight) : zeroY + -db / 30 * (2 / 3 * clipHeight);
    };
    // Panning: 0 at center
    const autoPanY = node => {
      const pan = typeof node === 'number' ? node : node.pan;
      return clipTop + (1 - (pan + 1) / 2) * clipHeight;
    };

    // Helper: compute tangents for monotone Hermite spline
    const computeTangents = (pts, yFn) => {
      const n = pts.length;
      if (n < 2) return [];
      const m = new Array(n);
      for (let i = 1; i < n - 1; i++) {
        const hP = pts[i].time - pts[i - 1].time;
        const hN = pts[i + 1].time - pts[i].time;
        const sP = (yFn(pts[i]) - yFn(pts[i - 1])) / hP;
        const sN = (yFn(pts[i + 1]) - yFn(pts[i])) / hN;
        m[i] = (sP + sN) / 2;
      }
      m[0] = n > 1 ? (yFn(pts[1]) - yFn(pts[0])) / (pts[1].time - pts[0].time) : 0;
      m[n - 1] = n > 1 ? (yFn(pts[n - 1]) - yFn(pts[n - 2])) / (pts[n - 1].time - pts[n - 2].time) : 0;
      return m;
    };

    // Helper: evaluate Hermite at pixel position px
    const hermiteY = (px, x0, y0, m0, x1, y1, m1) => {
      const h = x1 - x0;
      if (h <= 0) return y0;
      const t = (px - x0) / h;
      const t2 = t * t,
        t3 = t2 * t;
      return (2 * t3 - 3 * t2 + 1) * y0 + (t3 - 2 * t2 + t) * h * m0 + (-2 * t3 + 3 * t2) * y1 + (t3 - t2) * h * m1;
    };

    // Draw automation curve with Hermite spline
    const drawSpline = (nodes, yFn, color, lineDash) => {
      if (nodes.length < 2) {
        if (nodes.length === 1) {
          const px = nodes[0].time * zoom;
          const y = yFn(nodes[0]);
          ctx.fillStyle = color;
          ctx.beginPath();
          ctx.arc(px, y, 4, 0, Math.PI * 2);
          ctx.fill();
        }
        return;
      }
      const tangents = computeTangents(nodes, yFn);
      ctx.strokeStyle = color;
      ctx.lineWidth = 2;
      ctx.setLineDash(lineDash || []);
      ctx.beginPath();
      for (let i = 0; i < nodes.length - 1; i++) {
        const x0 = nodes[i].time * zoom,
          y0 = yFn(nodes[i]);
        const x1 = nodes[i + 1].time * zoom,
          y1 = yFn(nodes[i + 1]);
        const t0 = nodes[i].time,
          t1 = nodes[i + 1].time;
        const m0 = tangents[i],
          m1 = tangents[i + 1];
        for (let px = Math.floor(x0); px < Math.ceil(x1); px++) {
          const t = px / zoom;
          const y = hermiteY(t, t0, y0, m0, t1, y1, m1);
          if (px === Math.floor(x0) && i === 0) ctx.moveTo(px, y); else ctx.lineTo(px, y);
        }
      }
      ctx.stroke();
      ctx.setLineDash([]);

      // Draw node handles
      nodes.forEach(n => {
        const px = n.time * zoom,
          y = yFn(n);
        const isSelected = n.time === selectedNodeTime;
        ctx.fillStyle = isSelected ? '#ffebb3' : '#fff';
        ctx.beginPath();
        ctx.arc(px, y, isSelected ? 6 : 4, 0, Math.PI * 2);
        ctx.fill();
        ctx.strokeStyle = isSelected ? '#fbbf24' : color;
        ctx.lineWidth = isSelected ? 2.5 : 1.5;
        ctx.beginPath();
        ctx.arc(px, y, isSelected ? 6 : 4, 0, Math.PI * 2);
        ctx.stroke();

        // Display value at node
        ctx.fillStyle = '#ffffff';
        ctx.font = 'bold 12px sans-serif';
        const labelText = isPanMode ? n.pan > 0 ? 'R' + Math.round(n.pan * 100) : n.pan < 0 ? 'L' + Math.round(Math.abs(n.pan) * 100) : 'C' : `${n.db >= 0 ? '+' : ''}${n.db.toFixed(1)}dB`;
        ctx.fillText(labelText, px + 8, y + 4);
      });
    };
    drawSpline(volumeNodes, autoY, '#f43f5e');
    drawSpline(panningNodes, autoPanY, '#a855f7', [4, 4]);

    // ── Fade Curves (transparent, only curve lines + endpoint handles) ──
    const FADE_COLOR = '#b91c1c';
    const HANDLE_RADIUS = 5;
    if (fadeInLen >= 0) {
      const fiPx = fadeInLen * zoom;
      if (fadeInLen > 0) {
        ctx.strokeStyle = FADE_COLOR;
        ctx.lineWidth = 1.8;
        ctx.setLineDash([]);
        ctx.beginPath();
        ctx.moveTo(0, clipTop + clipHeight);
        for (let px = 0; px <= fiPx; px++) {
          const ratio = px / fiPx;
          const amp = (1 - Math.cos(Math.PI * ratio)) / 2;
          const y = clipTop + clipHeight - amp * clipHeight;
          ctx.lineTo(px, y);
        }
        ctx.stroke();
      }

      // Endpoint handle (draggable)
      const endX = fiPx,
        endY = clipTop;
      ctx.fillStyle = '#fff';
      ctx.beginPath();
      ctx.arc(endX, endY, HANDLE_RADIUS, 0, Math.PI * 2);
      ctx.fill();
      ctx.strokeStyle = FADE_COLOR;
      ctx.lineWidth = 1.5;
      ctx.beginPath();
      ctx.arc(endX, endY, HANDLE_RADIUS, 0, Math.PI * 2);
      ctx.stroke();
      // Label
      ctx.fillStyle = '#b91c1c';
      ctx.font = 'bold 7px sans-serif';
      ctx.textAlign = 'center';
      ctx.fillText('FI', endX, endY - HANDLE_RADIUS - 3);
      ctx.textAlign = 'start';
    }
    if (fadeOutLen >= 0) {
      const foPx = fadeOutLen * zoom;
      const startX = wClip - foPx;
      if (fadeOutLen > 0) {
        ctx.strokeStyle = FADE_COLOR;
        ctx.lineWidth = 1.8;
        ctx.setLineDash([]);
        ctx.beginPath();
        ctx.moveTo(startX, clipTop);
        for (let px = 0; px <= foPx; px++) {
          const ratio = px / foPx;
          const amp = (1 + Math.cos(Math.PI * ratio)) / 2;
          const y = clipTop + clipHeight - amp * clipHeight;
          ctx.lineTo(startX + px, y);
        }
        ctx.stroke();
      }

      // Endpoint handle (draggable)
      const handleX = startX,
        handleY = clipTop;
      ctx.fillStyle = '#fff';
      ctx.beginPath();
      ctx.arc(handleX, handleY, HANDLE_RADIUS, 0, Math.PI * 2);
      ctx.fill();
      ctx.strokeStyle = FADE_COLOR;
      ctx.lineWidth = 1.5;
      ctx.beginPath();
      ctx.arc(handleX, handleY, HANDLE_RADIUS, 0, Math.PI * 2);
      ctx.stroke();
      ctx.fillStyle = '#b91c1c';
      ctx.font = 'bold 7px sans-serif';
      ctx.textAlign = 'center';
      ctx.fillText('FO', handleX, handleY - HANDLE_RADIUS - 3);
      ctx.textAlign = 'start';
    }

    // Draw right-edge stretch handle indicator
    if (wClip > 0 && wClip < drawWidth) {
      ctx.strokeStyle = clipColor;
      ctx.lineWidth = 1.5;
      ctx.setLineDash([3, 3]);
      ctx.beginPath();
      ctx.moveTo(wClip + xStart, clipTop);
      ctx.lineTo(wClip + xStart, clipTop + clipHeight);
      ctx.stroke();
      ctx.setLineDash([]);
    }

    // Highlight selection if active
    if (selectionStart !== null && selectionEnd !== null && selectionStart !== selectionEnd) {
      const left = Math.min(selectionStart, selectionEnd);
      const right = Math.max(selectionStart, selectionEnd);
      const leftPx = left * zoom;
      const rightPx = right * zoom;
      ctx.fillStyle = 'rgba(245, 158, 11, 0.15)';
      ctx.fillRect(leftPx, 0, rightPx - leftPx, h);
      ctx.strokeStyle = '#f59e0b';
      ctx.lineWidth = 1;
      ctx.beginPath();
      ctx.moveTo(leftPx, 0);
      ctx.lineTo(leftPx, h);
      ctx.moveTo(rightPx, 0);
      ctx.lineTo(rightPx, h);
      ctx.stroke();
    }

    // Draw playhead
    if (currentTime !== null && currentTime >= 0 && currentTime <= buffer.duration / speed) {
      const playheadPx = currentTime * zoom;
      ctx.strokeStyle = '#ef4444';
      ctx.lineWidth = 2;
      ctx.beginPath();
      ctx.moveTo(playheadPx, 0);
      ctx.lineTo(playheadPx, h);
      ctx.stroke();
    }

    // Update TCP slider values in real-time to match the curve at currentTime
    const volInput = document.getElementById(`tcp-vol-${subTabId}`);
    const volLabel = document.getElementById(`tcp-vol-label-${subTabId}`);
    if (volInput && volLabel) {
      const dbVal = getVolumeDbAtTime(currentTime || 0);
      volInput.value = dbVal.toFixed(1);
      volLabel.textContent = `${dbVal.toFixed(1)}dB`;
    }
    const panInput = document.getElementById(`tcp-pan-${subTabId}`);
    const panLabel = document.getElementById(`tcp-pan-label-${subTabId}`);
    if (panInput && panLabel) {
      const panVal = getPanningValueAtTime(currentTime || 0);
      panInput.value = panVal;
      panLabel.textContent = panVal > 0 ? 'R' : panVal < 0 ? 'L' : 'C';
      const panLabelDetailed = document.getElementById(`tcp-pan-label-detailed-${subTabId}`);
      if (panLabelDetailed) {
        panLabelDetailed.textContent = panVal > 0 ? 'R' + panVal : panVal < 0 ? 'L' + Math.abs(panVal) : 'C';
      }
    }
  }, [buffer, currentTime, selectionStart, selectionEnd, zoom, timelineWidth, color, name, speed, volumeNodes, panningNodes, fadeInLen, fadeOutLen, graphMode, selectedNodeTime, subTabId]);
  const handleMouseDown = e => {
    if (e.button === 2) return;
    const canvas = canvasRef.current;
    const rect = canvas.getBoundingClientRect();
    const parent = canvas.parentElement;
    const scrollContainer = parent ? parent.parentElement : null;
    const scrollLeft = scrollContainer ? scrollContainer.scrollLeft : 0;
    const bufDuration = buffer.duration;
    const wallDuration = bufDuration / (speed || 1.0);
    const mouseX = e.clientX - rect.left + scrollLeft;
    const startTime = Math.max(0, Math.min(wallDuration, mouseX / zoom));

    // Shift + Click range selection in SubTab Waveform
    if (e.shiftKey) {
      e.preventDefault();
      e.stopPropagation();
      const anchor = subTabAnchorRef.current !== null && subTabAnchorRef.current !== undefined ? subTabAnchorRef.current : selectionStart !== null && selectionStart !== undefined ? selectionStart : currentTime;
      const selS = Math.min(anchor, startTime);
      const selE = Math.max(anchor, startTime);
      onSelectRange(selS, selE);
      onPlayheadSet(startTime);
      return;
    }

    // Alt+Click near right edge → speed stretch
    if (e.altKey && onSpeedChange) {
      const clipRightEdge = bufDuration / (speed || 1.0) * zoom;
      const tolerance = 8;
      if (Math.abs(mouseX - clipRightEdge) <= tolerance) {
        isStretchingRef.current = true;
        stretchStartRef.current = {
          mouseX,
          originalDuration: bufDuration,
          originalSpeed: speed,
          finalSpeed: speed,
          clipName: name || 'clip'
        };
        canvas.style.cursor = 'ew-resize';
        const handleMouseMove = moveEvent => {
          const currentX = moveEvent.clientX - rect.left + scrollLeft;
          const wClipPx = stretchStartRef.current.originalDuration / stretchStartRef.current.originalSpeed * zoom;
          const deltaX = currentX - stretchStartRef.current.mouseX;
          const newWClip = Math.max(10, wClipPx + deltaX);
          const newSpeed = stretchStartRef.current.originalDuration / (newWClip / zoom);
          stretchStartRef.current.finalSpeed = Math.max(0.05, Math.min(10, newSpeed));
          if (onSpeedChange) onSpeedChange(stretchStartRef.current.finalSpeed);
        };
        const handleMouseUp = () => {
          isStretchingRef.current = false;
          canvas.style.cursor = 'default';
          document.removeEventListener('mousemove', handleMouseMove);
          document.removeEventListener('mouseup', handleMouseUp);
          // UNDO/REDO cho alt-click-drag speed stretch: entry SET_CLIP_SPEED —
          // undo về speed gốc, redo về speed cuối (onSpeedChange tự tính lại
          // volumeNodes/panningNodes/fade/label theo ratio nên khớp 2 chiều).
          const st = stretchStartRef.current;
          if (st && typeof st.originalSpeed === 'number' && window.UndoRedoEngine) {
            const finalSpeed = st.finalSpeed || st.originalSpeed;
            if (Math.abs(finalSpeed - st.originalSpeed) > 0.001) {
              window.UndoRedoEngine.execute({
                type: 'SET_CLIP_SPEED',
                scope: 'section_tab',
                label: `Speed ${Math.round(st.originalSpeed * 100)}% → ${Math.round(finalSpeed * 100)}% (${st.clipName})`,
                before: st.originalSpeed,
                after: finalSpeed,
                undo: e => { if (onSpeedChange) onSpeedChange(e.before); },
                redo: e => { if (onSpeedChange) onSpeedChange(e.after); }
              });
            }
          }
          stretchStartRef.current = null;
        };
        document.addEventListener('mousemove', handleMouseMove);
        document.addEventListener('mouseup', handleMouseUp);
        return;
      }
    }

    // Mode toggle button click (bottom-right VOL/PAN)
    const wClipPx = bufDuration / (speed || 1.0) * zoom;
    const cTop = 8;
    const cSize = 16;
    const cHeight = rect.height - 16;
    const modeBtnX = wClipPx - 28 - 4;
    const modeBtnY = cTop + cHeight - 14 - 2;
    if (mouseX >= modeBtnX && mouseX <= modeBtnX + 28 && e.clientY - rect.top >= modeBtnY && e.clientY - rect.top <= modeBtnY + 14) {
      if (onModeToggle) onModeToggle();
      return;
    }

    // Fade endpoint handle drag (click on FI/FO handle circles)
    const HANDLE_R = 5;
    const fiEndPx = fadeInLen * zoom;
    const foStartPx = wClipPx - fadeOutLen * zoom;
    const distToFiHandle = Math.abs(mouseX - fiEndPx) + Math.abs(e.clientY - rect.top - cTop);
    const distToFoHandle = Math.abs(mouseX - foStartPx) + Math.abs(e.clientY - rect.top - cTop);
    if (distToFiHandle <= HANDLE_R + 6) {
      const handleMouseMove = moveEvent => {
        const x = moveEvent.clientX - rect.left + scrollLeft;
        const t = Math.max(0, Math.min(wallDuration, x / zoom));
        if (onUpdateFade) onUpdateFade({
          fadeInLen: t
        });
      };
      const handleMouseUp = () => {
        document.removeEventListener('mousemove', handleMouseMove);
        document.removeEventListener('mouseup', handleMouseUp);
      };
      document.addEventListener('mousemove', handleMouseMove);
      document.addEventListener('mouseup', handleMouseUp);
      return;
    }
    if (distToFoHandle <= HANDLE_R + 6) {
      const handleMouseMove = moveEvent => {
        const x = moveEvent.clientX - rect.left + scrollLeft;
        const t = Math.max(0, Math.min(wallDuration, (wClipPx - x) / zoom));
        if (onUpdateFade) onUpdateFade({
          fadeOutLen: t
        });
      };
      const handleMouseUp = () => {
        document.removeEventListener('mousemove', handleMouseMove);
        document.removeEventListener('mouseup', handleMouseUp);
      };
      document.addEventListener('mousemove', handleMouseMove);
      document.addEventListener('mouseup', handleMouseUp);
      return;
    }

    // Tool-specific behavior
    if (activeTool === 'grab') {
      setSelectedNodeTime(null);
      onPlayheadSet(startTime);
      const handleMouseMove = moveEvent => {
        const currentX = moveEvent.clientX - rect.left + scrollLeft;
        const ct = Math.max(0, Math.min(wallDuration, currentX / zoom));
        onPlayheadSet(ct);
      };
      const handleMouseUp = () => {
        document.removeEventListener('mousemove', handleMouseMove);
        document.removeEventListener('mouseup', handleMouseUp);
      };
      document.addEventListener('mousemove', handleMouseMove);
      document.addEventListener('mouseup', handleMouseUp);
      return;
    }
    if (activeTool === 'razor') {
      setSelectedNodeTime(null);
      onPlayheadSet(startTime);
      showToast(`Cut point at ${formatTime(startTime)}`, 'info');
      return;
    }
    if (activeTool === 'pen') {
      // Deduplicate by time: keep last occurrence per time key
      const mergeNodes = (existing, incoming) => {
        const map = new Map();
        existing.forEach(n => map.set(n.time, n));
        incoming.forEach(n => map.set(n.time, n));
        return Array.from(map.values()).sort((a, b) => a.time - b.time);
      };
      onPlayheadSet(startTime);
      canvas.style.cursor = 'crosshair';
      const isPan = (graphMode || 'volume') === 'pan';
      const isCtrl = e.ctrlKey || e.metaKey;
      const cTop = 8;
      const cHeight = rect.height - 16;
      const curNodes = isPan ? panningNodes : volumeNodes;
      const valFromY = y => {
        if (isPan) return Math.max(-1, Math.min(1, -(y - cTop) / cHeight * 2 + 1));
        const yOff = (y - cTop) / cHeight;
        return yOff <= 1 / 3 ? Math.max(0, Math.min(3, 3 * (1 - yOff * 3))) : Math.max(-30, Math.min(0, -30 * (yOff - 1 / 3) * (3 / 2)));
      };
      const snapValue = v => isPan ? Math.round(v * 20) / 20 : Math.round(v * 2) / 2;
      const snapTime = t => Math.round(t * 10) / 10;

      // Check if clicking near existing node (any mode)
      const volNodeY = v => {
        const z = cTop + 1 / 3 * cHeight;
        return v >= 0 ? z - v / 3 * (1 / 3 * cHeight) : z + -v / 30 * (2 / 3 * cHeight);
      };
      const nearNode = curNodes.findIndex(n => {
        const t = Math.abs(n.time - startTime);
        const val = isPan ? n.pan : n.db;
        const ny = isPan ? cTop + (1 - (val + 1) / 2) * cHeight : volNodeY(val);
        const dy = Math.abs(e.clientY - rect.top - ny);
        return t < 0.1 / (speed || 1) && dy < 10;
      });
      if (nearNode >= 0) {
        // Drag existing node
        setSelectedNodeTime(curNodes[nearNode].time);
        let working = [...curNodes];
        let dragIdx = nearNode;
        const handleMouseMove = moveEvent => {
          const currentX = moveEvent.clientX - rect.left + scrollLeft;
          const ct = Math.max(0, Math.min(wallDuration, currentX / zoom));
          const val = snapValue(valFromY(moveEvent.clientY - rect.top));
          const updated = isPan ? {
            time: Math.min(snapTime(ct), wallDuration),
            pan: val
          } : {
            time: Math.min(snapTime(ct), wallDuration),
            db: val
          };
          const cleaned = mergeNodes(working.filter((_, i) => i !== dragIdx), [updated]);
          if (onUpdateNodes) onUpdateNodes(cleaned);
          working = [...cleaned];
          dragIdx = working.findIndex(n => n.time === updated.time && (isPan ? n.pan === updated.pan : n.db === updated.db));
          // Follow the selected node during dragging
          setSelectedNodeTime(updated.time);
        };
        const handleMouseUp = () => {
          document.removeEventListener('mousemove', handleMouseMove);
          document.removeEventListener('mouseup', handleMouseUp);
          canvas.style.cursor = 'crosshair';
        };
        document.addEventListener('mousemove', handleMouseMove);
        document.addEventListener('mouseup', handleMouseUp);
        return;
      }
      if (isCtrl) {
        setSelectedNodeTime(null);
        const pts = [];
        let lastKey = '';
        const handleMouseMove = moveEvent => {
          const currentX = moveEvent.clientX - rect.left + scrollLeft;
          const ct = Math.max(0, Math.min(wallDuration, currentX / zoom));
          const t = +snapTime(ct).toFixed(3);
          const v = isPan ? +snapValue(valFromY(moveEvent.clientY - rect.top)).toFixed(2) : +snapValue(valFromY(moveEvent.clientY - rect.top)).toFixed(1);
          const key = t + '|' + v;
          if (key !== lastKey) {
            pts.push({
              time: t,
              [isPan ? 'pan' : 'db']: v
            });
            lastKey = key;
          }
        };
        const handleMouseUp = () => {
          document.removeEventListener('mousemove', handleMouseMove);
          document.removeEventListener('mouseup', handleMouseUp);
          canvas.style.cursor = 'crosshair';
          const merged = mergeNodes(curNodes, pts);
          if (onUpdateNodes) onUpdateNodes(merged);
        };
        document.addEventListener('mousemove', handleMouseMove);
        document.addEventListener('mouseup', handleMouseUp);
      } else {
        const newNode = isPan ? {
          time: +snapTime(startTime).toFixed(3),
          pan: +snapValue(valFromY(e.clientY - rect.top)).toFixed(2)
        } : {
          time: +snapTime(startTime).toFixed(3),
          db: +snapValue(valFromY(e.clientY - rect.top)).toFixed(1)
        };
        setSelectedNodeTime(newNode.time);
        const merged = mergeNodes(curNodes, [newNode]);
        if (onUpdateNodes) onUpdateNodes(merged);

        // Now drag this newly created node
        let working = [...merged];
        let dragIdx = working.findIndex(n => n.time === newNode.time && (isPan ? n.pan === newNode.pan : n.db === newNode.db));
        const handleMouseMove = moveEvent => {
          const currentX = moveEvent.clientX - rect.left + scrollLeft;
          const ct = Math.max(0, Math.min(wallDuration, currentX / zoom));
          const val = snapValue(valFromY(moveEvent.clientY - rect.top));
          const updated = isPan ? {
            time: Math.min(snapTime(ct), wallDuration),
            pan: val
          } : {
            time: Math.min(snapTime(ct), wallDuration),
            db: val
          };
          const cleaned = mergeNodes(working.filter((_, i) => i !== dragIdx), [updated]);
          if (onUpdateNodes) onUpdateNodes(cleaned);
          working = [...cleaned];
          dragIdx = working.findIndex(n => n.time === updated.time && (isPan ? n.pan === updated.pan : n.db === updated.db));
          setSelectedNodeTime(updated.time);
        };
        const handleMouseUp = () => {
          document.removeEventListener('mousemove', handleMouseMove);
          document.removeEventListener('mouseup', handleMouseUp);
          canvas.style.cursor = 'crosshair';
        };
        document.addEventListener('mousemove', handleMouseMove);
        document.addEventListener('mouseup', handleMouseUp);
      }
      return;
    }

    // Select tool (default): drag to select range
    subTabAnchorRef.current = startTime;
    setSelectedNodeTime(null);
    onSelectRange(startTime, startTime);
    onPlayheadSet(startTime);
    const handleMouseMove = moveEvent => {
      const currentX = moveEvent.clientX - rect.left + scrollLeft;
      const ct = Math.max(0, Math.min(wallDuration, currentX / zoom));
      const anchor = subTabAnchorRef.current ?? startTime;
      onSelectRange(Math.min(anchor, ct), Math.max(anchor, ct));
    };
    const handleMouseUp = () => {
      document.removeEventListener('mousemove', handleMouseMove);
      document.removeEventListener('mouseup', handleMouseUp);
    };
    document.addEventListener('mousemove', handleMouseMove);
    document.addEventListener('mouseup', handleMouseUp);
  };
  const handleContextMenuInternal = e => {
    e.preventDefault();
    e.stopPropagation();
    const canvas = canvasRef.current;
    const rect = canvas.getBoundingClientRect();
    const parent = canvas.parentElement;
    const scrollContainer = parent ? parent.parentElement : null;
    const scrollLeft = scrollContainer ? scrollContainer.scrollLeft : 0;
    const x = e.clientX - rect.left + scrollLeft;
    const clickTime = Math.max(0, Math.min(buffer.duration / (speed || 1.0), x / zoom));
    onContextMenu(e, clickTime);
  };

  // Double-click on automation curve to create a new node
  const handleDoubleClick = e => {
    const canvas = canvasRef.current;
    if (!canvas || !buffer) return;
    if (activeTool !== 'select' && activeTool !== 'pen') return;
    const rect = canvas.getBoundingClientRect();
    const parent = canvas.parentElement;
    const scrollContainer = parent ? parent.parentElement : null;
    const scrollLeft = scrollContainer ? scrollContainer.scrollLeft : 0;
    const x = e.clientX - rect.left + scrollLeft;
    const clickTime = Math.max(0, Math.min(buffer.duration / (speed || 1.0), x / zoom));
    const isPan = (graphMode || 'volume') === 'pan';
    const curNodes = isPan ? panningNodes : volumeNodes;
    const cTop = 8;
    const cHeight = rect.height - 16;
    const valFromY = y => {
      if (isPan) return Math.max(-1, Math.min(1, -(y - cTop) / cHeight * 2 + 1));
      const yOff = (y - cTop) / cHeight;
      return yOff <= 2 / 3 ? Math.max(0, Math.min(3, 3 * (1 - yOff * 3 / 2))) : Math.max(-30, Math.min(0, -30 * (yOff - 2 / 3) * 3));
    };

    // Interpolate value at click position from existing curve
    let interpolatedVal = valFromY(e.clientY - rect.top);
    if (curNodes.length >= 2) {
      const sorted = [...curNodes].sort((a, b) => a.time - b.time);
      for (let i = 0; i < sorted.length - 1; i++) {
        if (clickTime >= sorted[i].time && clickTime <= sorted[i + 1].time) {
          const t = (clickTime - sorted[i].time) / (sorted[i + 1].time - sorted[i].time);
          const v1 = isPan ? sorted[i].pan : sorted[i].db;
          const v2 = isPan ? sorted[i + 1].pan : sorted[i + 1].db;
          interpolatedVal = v1 + t * (v2 - v1);
          break;
        }
      }
    }
    const newNode = isPan ? {
      time: +Math.round(clickTime * 10) / 10,
      pan: +Math.round(interpolatedVal * 20) / 20
    } : {
      time: +Math.round(clickTime * 10) / 10,
      db: +Math.round(interpolatedVal * 2) / 2
    };
    const merged = (() => {
      const map = new Map();
      curNodes.forEach(n => map.set(n.time, n));
      map.set(newNode.time, newNode);
      return Array.from(map.values()).sort((a, b) => a.time - b.time);
    })();
    if (onUpdateNodes) onUpdateNodes(merged);
  };
  return /*#__PURE__*/React.createElement("div", {
    style: {
      width: `${timelineWidth}px`,
      height: '100%',
      position: 'relative',
      overflow: 'hidden'
    }
  }, /*#__PURE__*/React.createElement("canvas", {
    ref: canvasRef,
    style: {
      position: 'absolute',
      top: 0,
      left: 0,
      imageRendering: 'pixelated'
    },
    className: "cursor-crosshair rounded border border-zinc-800",
    onMouseDown: handleMouseDown,
    onDoubleClick: handleDoubleClick,
    onContextMenu: handleContextMenuInternal,
    onMouseMove: e => {
      if (canvasRef.current && e.altKey && onSpeedChange) {
        const rect = canvasRef.current.getBoundingClientRect();
        const parent = canvasRef.current.parentElement;
        const scrollContainer = parent ? parent.parentElement : null;
        const scrollLeft = scrollContainer ? scrollContainer.scrollLeft : 0;
        const mx = e.clientX - rect.left + scrollLeft;
        const wClip = buffer.duration / speed * zoom;
        const tolerance = 8;
        canvasRef.current.style.cursor = Math.abs(mx - wClip) <= tolerance && !isStretchingRef.current ? 'ew-resize' : 'crosshair';
      } else if (canvasRef.current && !isStretchingRef.current) {
        const rect = canvasRef.current.getBoundingClientRect();
        const parent = canvasRef.current.parentElement;
        const scrollContainer = parent ? parent.parentElement : null;
        const scrollLeft = scrollContainer ? scrollContainer.scrollLeft : 0;
        const mx = e.clientX - rect.left + scrollLeft;
        const wClipPx = buffer.duration / (speed || 1.0) * zoom;
        const cH = canvasRef.current.height / (window.devicePixelRatio || 1) - 16;
        const btnX = wClipPx - 28 - 4;
        const btnY = 8 + cH - 14 - 2;
        const overBtn = mx >= btnX && mx <= btnX + 28 && e.clientY - rect.top >= btnY && e.clientY - rect.top <= btnY + 14;
        canvasRef.current.style.cursor = overBtn ? 'pointer' : 'crosshair';
      }
    }
  }));
};
const SubTabToolbar = ({
  st,
  activeTool,
  setActiveTool,
  handleSubTabNormalizeWithValue,
  handleSubTabGainWithValue,
  handleSubTabPitch,
  handleSubTabStretch,
  handleSubTabFade,
  onPlayPause,
  onStop,
  onRewind,
  onForward,
  onLoop,
  onRecord,
  onRateChange,
  onCut,
  onCopy,
  onPaste,
  onGlue,
  snapValue,
  onSnapChange
}) => {
  const isPlaying = st?.isPlaying || false;
  const isLooping = st?.isLooping || false;
  const isRecording = st?.isRecording || false;
  return /*#__PURE__*/React.createElement("div", {
    className: "daw-header flex h-16 items-center px-4 border-b daw-border gap-3 bg-zinc-800"
  }, /*#__PURE__*/React.createElement("div", {
    className: "flex items-center space-x-1 border-r border-zinc-700 pr-3"
  }, /*#__PURE__*/React.createElement("button", {
    className: `p-1 rounded ${activeTool === 'select' ? 'bg-cyan-700' : 'bg-zinc-700'}`,
    onClick: () => setActiveTool('select'),
    title: "Select Tool"
  }, /*#__PURE__*/React.createElement("span", {
    className: "inline-flex items-center shrink-0"
  }, /*#__PURE__*/React.createElement("i", {
    "data-lucide": "mouse-pointer",
    className: "w-4 h-4"
  }))), /*#__PURE__*/React.createElement("button", {
    className: `p-1 rounded ${activeTool === 'grab' ? 'bg-cyan-700' : 'bg-zinc-700'}`,
    onClick: () => setActiveTool('grab'),
    title: "Grab Tool"
  }, /*#__PURE__*/React.createElement("span", {
    className: "inline-flex items-center shrink-0"
  }, /*#__PURE__*/React.createElement("i", {
    "data-lucide": "hand",
    className: "w-4 h-4"
  }))), /*#__PURE__*/React.createElement("button", {
    className: `p-1 rounded ${activeTool === 'razor' ? 'bg-cyan-700' : 'bg-zinc-700'}`,
    onClick: () => setActiveTool('razor'),
    title: "Razor Tool"
  }, /*#__PURE__*/React.createElement("svg", {
    className: "w-4 h-4 text-orange-400",
    viewBox: "0 0 24 24",
    fill: "none",
    stroke: "currentColor",
    strokeWidth: "2.5",
    strokeLinecap: "round",
    strokeLinejoin: "round"
  }, /*#__PURE__*/React.createElement("path", {
    d: "M6 3h12a2 2 0 0 1 2 2v2a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z"
  }), /*#__PURE__*/React.createElement("path", {
    d: "M4 9h16l-3 9H7z"
  }), /*#__PURE__*/React.createElement("circle", {
    cx: "12",
    cy: "6",
    r: "1"
  }))), /*#__PURE__*/React.createElement("button", {
    className: `p-1 rounded ${activeTool === 'pen' ? 'bg-cyan-700' : 'bg-zinc-700'}`,
    onClick: () => setActiveTool('pen'),
    title: "Pen Tool"
  }, /*#__PURE__*/React.createElement("span", {
    className: "inline-flex items-center shrink-0"
  }, /*#__PURE__*/React.createElement("i", {
    "data-lucide": "pen-tool",
    className: "w-4 h-4"
  }))), /*#__PURE__*/React.createElement("div", {
    className: "w-[1px] h-4 bg-zinc-800 mx-0.5"
  }), /*#__PURE__*/React.createElement("button", {
    onClick: onGlue,
    className: "p-0.5 rounded text-zinc-400 hover:bg-zinc-800 hover:text-purple-400 transition",
    title: "Glue Clips"
  }, /*#__PURE__*/React.createElement("span", {
    className: "inline-flex items-center shrink-0"
  }, /*#__PURE__*/React.createElement("i", {
    "data-lucide": "link",
    className: "w-3.5 h-3.5"
  }))), /*#__PURE__*/React.createElement("button", {
    onClick: onCut,
    className: "p-0.5 rounded text-zinc-400 hover:bg-zinc-800 hover:text-red-400 transition",
    title: "Cut (Ctrl+X)"
  }, /*#__PURE__*/React.createElement("span", {
    className: "inline-flex items-center shrink-0"
  }, /*#__PURE__*/React.createElement("i", {
    "data-lucide": "scissors",
    className: "w-3.5 h-3.5"
  }))), /*#__PURE__*/React.createElement("button", {
    onClick: onCopy,
    className: "p-0.5 rounded text-zinc-400 hover:bg-zinc-800 hover:text-blue-400 transition",
    title: "Copy (Ctrl+C)"
  }, /*#__PURE__*/React.createElement("span", {
    className: "inline-flex items-center shrink-0"
  }, /*#__PURE__*/React.createElement("i", {
    "data-lucide": "copy",
    className: "w-3.5 h-3.5"
  }))), /*#__PURE__*/React.createElement("button", {
    onClick: onPaste,
    className: "p-0.5 rounded text-zinc-400 hover:bg-zinc-800 hover:text-emerald-400 transition",
    title: "Paste (Ctrl+V)"
  }, /*#__PURE__*/React.createElement("span", {
    className: "inline-flex items-center shrink-0"
  }, /*#__PURE__*/React.createElement("i", {
    "data-lucide": "clipboard",
    className: "w-3.5 h-3.5"
  })))), /*#__PURE__*/React.createElement("div", {
    className: "flex items-center gap-1 border-r border-zinc-700 pr-3"
  }, /*#__PURE__*/React.createElement("span", {
    className: "text-xs text-zinc-500 font-bold uppercase"
  }, "Snap"), /*#__PURE__*/React.createElement("select", {
    value: snapValue || 'free',
    onChange: e => onSnapChange(e.target.value),
    className: "bg-zinc-850 text-zinc-300 text-xs px-1 py-0.5 rounded border border-zinc-800 focus:outline-none font-mono"
  }, /*#__PURE__*/React.createElement("option", {
    value: "free"
  }, "Free"), /*#__PURE__*/React.createElement("option", {
    value: "4"
  }, "4"), /*#__PURE__*/React.createElement("option", {
    value: "1"
  }, "1"), /*#__PURE__*/React.createElement("option", {
    value: "1/2"
  }, "1/2"), /*#__PURE__*/React.createElement("option", {
    value: "1/4"
  }, "1/4"), /*#__PURE__*/React.createElement("option", {
    value: "1/8"
  }, "1/8"), /*#__PURE__*/React.createElement("option", {
    value: "1/16"
  }, "1/16"), /*#__PURE__*/React.createElement("option", {
    value: "1/32"
  }, "1/32"))), /*#__PURE__*/React.createElement("div", {
    className: "flex items-center space-x-2 flex-1 overflow-x-auto no-scrollbar"
  }, /*#__PURE__*/React.createElement("div", {
    className: "flex items-center gap-1"
  }, /*#__PURE__*/React.createElement("span", {
    className: "text-xs text-zinc-400"
  }, "Normalize:"), /*#__PURE__*/React.createElement("input", {
    type: "range",
    min: "-12",
    max: "0",
    value: st.effects?.normalizeDb || 0,
    step: "0.1",
    onChange: e => handleSubTabNormalizeWithValue(st.id, parseFloat(e.target.value)),
    className: "w-20 h-1.5"
  }), /*#__PURE__*/React.createElement("span", {
    className: "text-xs text-zinc-500 w-10 text-right"
  }, st.effects?.normalizeDb || 0, " dB")), /*#__PURE__*/React.createElement("div", {
    className: "flex items-center gap-1"
  }, /*#__PURE__*/React.createElement("span", {
    className: "text-xs text-zinc-400"
  }, "Gain:"), /*#__PURE__*/React.createElement("input", {
    type: "range",
    min: "-40",
    max: "24",
    value: st.effects?.gainDb || 0,
    step: "0.1",
    onChange: e => handleSubTabGainWithValue(st.id, parseFloat(e.target.value)),
    className: "w-20 h-1.5"
  }), /*#__PURE__*/React.createElement("span", {
    className: "text-xs text-zinc-500 w-10 text-right"
  }, st.effects?.gainDb || 0, " dB")), /*#__PURE__*/React.createElement("div", {
    className: "flex items-center gap-1"
  }, /*#__PURE__*/React.createElement("span", {
    className: "text-xs text-zinc-400"
  }, "Pitch:"), /*#__PURE__*/React.createElement("input", {
    type: "range",
    min: "-12",
    max: "12",
    value: st.effects?.pitch || 0,
    step: "0.1",
    onChange: e => handleSubTabPitch(st.id, parseFloat(e.target.value)),
    className: "w-20 h-1.5"
  }), /*#__PURE__*/React.createElement("span", {
    className: "text-xs text-zinc-500 w-10 text-right"
  }, st.effects?.pitch || 0, " st")), /*#__PURE__*/React.createElement("div", {
    className: "flex items-center gap-1"
  }, /*#__PURE__*/React.createElement("span", {
    className: "text-xs text-zinc-400"
  }, "Stretch:"), /*#__PURE__*/React.createElement("input", {
    type: "range",
    min: "50",
    max: "200",
    value: st.effects?.speedStretch || 100,
    step: "1",
    onChange: e => handleSubTabStretch(st.id, parseInt(e.target.value)),
    className: "w-20 h-1.5"
  }), /*#__PURE__*/React.createElement("span", {
    className: "text-xs text-zinc-500 w-10 text-right"
  }, st.effects?.speedStretch || 100, "%"))), /*#__PURE__*/React.createElement("div", {
    className: "flex items-center space-x-1 border-r border-zinc-700 pr-3"
  }, /*#__PURE__*/React.createElement("button", {
    onClick: () => handleSubTabFade(st.id, 'in'),
    className: "px-2 py-1 text-xs rounded hover:bg-zinc-700"
  }, "Fade In"), /*#__PURE__*/React.createElement("button", {
    onClick: () => handleSubTabFade(st.id, 'out'),
    className: "px-2 py-1 text-xs rounded hover:bg-zinc-700"
  }, "Fade Out")), /*#__PURE__*/React.createElement("div", {
    className: "flex items-center space-x-1 border-r border-zinc-700 pr-3"
  }, /*#__PURE__*/React.createElement("button", {
    onClick: onRewind,
    className: "w-8 h-8 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700 transition",
    title: "Rewind"
  }, /*#__PURE__*/React.createElement("span", {
    className: "inline-flex items-center shrink-0"
  }, /*#__PURE__*/React.createElement("i", {
    "data-lucide": "skip-back",
    className: "w-4 h-4"
  }))), /*#__PURE__*/React.createElement("button", {
    onClick: onPlayPause,
    className: `w-8 h-8 flex items-center justify-center rounded border transition ${isPlaying ? 'bg-emerald-600 text-black border-emerald-500 hover:bg-emerald-500' : 'bg-cyan-600 text-white border-cyan-500 hover:bg-cyan-500'}`,
    title: isPlaying ? "Pause" : "Play"
  }, /*#__PURE__*/React.createElement("span", {
    className: "inline-flex items-center shrink-0"
  }, /*#__PURE__*/React.createElement("i", {
    "data-lucide": isPlaying ? 'pause' : 'play',
    className: "w-4 h-4 fill-current"
  }))), /*#__PURE__*/React.createElement("button", {
    onClick: onStop,
    className: "w-8 h-8 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700 transition",
    title: "Stop"
  }, /*#__PURE__*/React.createElement("span", {
    className: "inline-flex items-center shrink-0"
  }, /*#__PURE__*/React.createElement("i", {
    "data-lucide": "square",
    className: "w-4 h-4 fill-current"
  }))), /*#__PURE__*/React.createElement("button", {
    onClick: onForward,
    className: "w-8 h-8 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700 transition",
    title: "Forward"
  }, /*#__PURE__*/React.createElement("span", {
    className: "inline-flex items-center shrink-0"
  }, /*#__PURE__*/React.createElement("i", {
    "data-lucide": "skip-forward",
    className: "w-4 h-4"
  }))), /*#__PURE__*/React.createElement("button", {
    onClick: onLoop,
    className: `w-8 h-8 flex items-center justify-center rounded border transition ${isLooping ? 'bg-amber-600 text-black border-amber-500 hover:bg-amber-500' : 'bg-zinc-800 text-zinc-200 border-zinc-700 hover:bg-zinc-700'}`,
    title: isLooping ? "Loop On" : "Loop Off"
  }, /*#__PURE__*/React.createElement("span", {
    className: "inline-flex items-center shrink-0"
  }, /*#__PURE__*/React.createElement("i", {
    "data-lucide": "repeat",
    className: "w-4 h-4"
  }))), /*#__PURE__*/React.createElement("button", {
    onClick: onRecord,
    className: `w-8 h-8 flex items-center justify-center rounded border transition ${isRecording ? 'bg-red-600 text-white border-red-500' : 'bg-zinc-800 text-zinc-200 border-zinc-700 hover:bg-zinc-700'}`,
    title: isRecording ? "Recording" : "Record"
  }, /*#__PURE__*/React.createElement("span", {
    className: "inline-flex items-center shrink-0"
  }, /*#__PURE__*/React.createElement("i", {
    "data-lucide": "circle",
    className: "w-4 h-4"
  })))), /*#__PURE__*/React.createElement("div", {
    className: "flex items-center space-x-1"
  }, /*#__PURE__*/React.createElement("span", {
    className: "text-xs text-zinc-400"
  }, "Rate:"), /*#__PURE__*/React.createElement("select", {
    value: st.playbackRate || 1,
    onChange: e => onRateChange(parseFloat(e.target.value)),
    className: "bg-zinc-700 text-zinc-100 text-xs px-1 py-0.5 rounded border border-zinc-600"
  }, /*#__PURE__*/React.createElement("option", {
    value: "0.5"
  }, "0.5x"), /*#__PURE__*/React.createElement("option", {
    value: "0.75"
  }, "0.75x"), /*#__PURE__*/React.createElement("option", {
    value: "1"
  }, "1x"), /*#__PURE__*/React.createElement("option", {
    value: "1.25"
  }, "1.25x"), /*#__PURE__*/React.createElement("option", {
    value: "1.5"
  }, "1.5x"), /*#__PURE__*/React.createElement("option", {
    value: "2"
  }, "2x"))));
};

// ── Graph Editor Canvas for Volume/Pan/Fade Automation ──
const GraphEditorCanvas = ({
  buffer,
  zoom,
  timelineWidth,
  volumeNodes,
  panningNodes,
  fadeInLen,
  fadeOutLen,
  onUpdateNodes,
  graphMode
}) => {
  const canvasRef = useRef(null);
  const isDraggingNode = useRef(false);
  const dragNodeIdx = useRef(-1);
  const isCreatingNode = useRef(false);
  const getNodes = () => graphMode === 'pan' ? panningNodes : volumeNodes;
  const nodeLabel = n => graphMode === 'pan' ? `${n.pan.toFixed(2)}` : `${n.db.toFixed(1)}dB`;
  const nodeY = (n, h) => {
    if (graphMode === 'pan') return (1 - (n.pan + 1) / 2) * h;
    const zeroY = 2 / 3 * h;
    return n.db >= 0 ? zeroY - n.db / 3 * (2 / 3 * h) : zeroY + -n.db / 30 * (1 / 3 * h);
  };
  const nodeValFromY = (y, h) => {
    if (graphMode === 'pan') return -(y / h * 2 - 1);
    const yOff = y / h;
    return yOff <= 2 / 3 ? 3 * (1 - yOff * 3 / 2) : -30 * (yOff - 2 / 3) * 3;
  };
  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas || !buffer) return;
    const ctx = canvas.getContext('2d');
    const dpr = window.devicePixelRatio || 1;
    const wrapper = canvas.parentElement ? canvas.parentElement.parentElement : null;
    const scrollLeft = wrapper ? wrapper.scrollLeft : 0;
    const vWidth = wrapper ? wrapper.clientWidth : 1200;
    const drawWidth = Math.min(timelineWidth, Math.max(vWidth, 1200));
    const h = canvas.parentElement ? canvas.parentElement.clientHeight : 200;
    canvas.width = Math.round(drawWidth * dpr);
    canvas.height = Math.round(h * dpr);
    ctx.scale(dpr, dpr);
    ctx.imageSmoothingEnabled = false;
    canvas.style.position = 'absolute';
    canvas.style.left = `${scrollLeft}px`;
    canvas.style.width = `${drawWidth}px`;
    canvas.style.height = `${h}px`;
    ctx.fillStyle = '#1a1a2e';
    ctx.fillRect(0, 0, drawWidth, h);
    ctx.strokeStyle = '#2a2a4e';
    ctx.lineWidth = 0.5;
    for (let t = 0; t <= buffer.duration; t += 0.5) {
      const x = t / buffer.duration * drawWidth;
      ctx.beginPath();
      ctx.moveTo(x, 0);
      ctx.lineTo(x, h);
      ctx.stroke();
    }
    for (let i = 0; i <= 10; i++) {
      const y = i / 10 * h;
      ctx.beginPath();
      ctx.moveTo(0, y);
      ctx.lineTo(drawWidth, y);
      ctx.stroke();
    }
    if (fadeInLen > 0) {
      const fadeX = fadeInLen / buffer.duration * drawWidth;
      ctx.fillStyle = 'rgba(16, 185, 129, 0.12)';
      ctx.fillRect(0, 0, fadeX, h);
    }
    if (fadeOutLen > 0) {
      const fadeX = (buffer.duration - fadeOutLen) / buffer.duration * drawWidth;
      const fadeW = fadeOutLen / buffer.duration * drawWidth;
      ctx.fillStyle = 'rgba(239, 68, 68, 0.12)';
      ctx.fillRect(fadeX, 0, fadeW, h);
    }
    const nodes = getNodes();
    if (nodes.length > 0) {
      ctx.strokeStyle = graphMode === 'pan' ? '#a855f7' : '#06b6d4';
      ctx.lineWidth = 2;
      ctx.beginPath();
      nodes.forEach((n, i) => {
        const x = n.time / buffer.duration * drawWidth;
        const y = nodeY(n, h);
        if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
      });
      ctx.stroke();
      nodes.forEach((n, i) => {
        const x = n.time / buffer.duration * drawWidth;
        const y = nodeY(n, h);
        ctx.fillStyle = graphMode === 'pan' ? '#a855f7' : '#06b6d4';
        ctx.beginPath();
        ctx.arc(x, y, 5, 0, Math.PI * 2);
        ctx.fill();
        ctx.fillStyle = '#e4e4e7';
        ctx.font = '9px monospace';
        ctx.fillText(nodeLabel(n), x + 8, y + 3);
      });
    } else {
      ctx.fillStyle = '#52525b';
      ctx.font = '11px sans-serif';
      ctx.textAlign = 'center';
      ctx.fillText(graphMode === 'pan' ? 'Click to add Pan points' : 'Click to add Volume points', drawWidth / 2, h / 2);
      ctx.textAlign = 'start';
    }
    const zeroY = graphMode === 'pan' ? h / 2 : nodeY({
      db: 0
    }, h);
    ctx.strokeStyle = graphMode === 'pan' ? '#a855f744' : '#06b6d444';
    ctx.lineWidth = 1;
    ctx.setLineDash([4, 4]);
    ctx.beginPath();
    ctx.moveTo(0, zeroY);
    ctx.lineTo(drawWidth, zeroY);
    ctx.stroke();
    ctx.setLineDash([]);
  }, [buffer, zoom, timelineWidth, volumeNodes, panningNodes, fadeInLen, fadeOutLen, graphMode]);
  const handleMouseDown = e => {
    const canvas = canvasRef.current;
    if (!canvas || !buffer) return;
    const rect = canvas.getBoundingClientRect();
    const x = e.clientX - rect.left;
    const y = e.clientY - rect.top;
    const time = x / rect.width * buffer.duration;
    const val = nodeValFromY(y, rect.height);
    const nodes = getNodes();
    const snapped = Math.max(-30, Math.min(3, val));
    const snappedPan = Math.max(-1, Math.min(1, val));
    const threshold = 12 / rect.width * buffer.duration;
    const nearIdx = nodes.findIndex(n => Math.abs(n.time - time) < threshold);
    if (nearIdx >= 0) {
      isDraggingNode.current = true;
      dragNodeIdx.current = nearIdx;
      return;
    }
    const newNode = graphMode === 'pan' ? {
      time: +time.toFixed(3),
      pan: +snappedPan.toFixed(2)
    } : {
      time: +time.toFixed(3),
      db: +snapped.toFixed(1)
    };
    const sorted = [...nodes, newNode].sort((a, b) => a.time - b.time);
    onUpdateNodes(sorted);
    const rearrangeNewIdx = sorted.findIndex(n => n.time === newNode.time && (graphMode === 'pan' ? n.pan : n.db) === (graphMode === 'pan' ? newNode.pan : newNode.db));
    isDraggingNode.current = true;
    dragNodeIdx.current = rearrangeNewIdx;
    isCreatingNode.current = true;
  };
  const handleMouseMove = e => {
    if (!isDraggingNode.current || dragNodeIdx.current < 0) return;
    const canvas = canvasRef.current;
    if (!canvas) return;
    const rect = canvas.getBoundingClientRect();
    const x = e.clientX - rect.left;
    const y = e.clientY - rect.top;
    const time = Math.max(0, Math.min(buffer.duration, x / rect.width * buffer.duration));
    const val = nodeValFromY(y, rect.height);
    const nodes = [...getNodes()];
    nodes[dragNodeIdx.current] = graphMode === 'pan' ? {
      time: +time.toFixed(3),
      pan: +Math.max(-1, Math.min(1, val)).toFixed(2)
    } : {
      time: +time.toFixed(3),
      db: +Math.max(-30, Math.min(3, val)).toFixed(1)
    };
    onUpdateNodes(nodes.sort((a, b) => a.time - b.time));
  };
  const handleMouseUp = () => {
    isDraggingNode.current = false;
    dragNodeIdx.current = -1;
    isCreatingNode.current = false;
  };
  const handleContextMenu = e => {
    e.preventDefault();
    const canvas = canvasRef.current;
    if (!canvas || !buffer) return;
    const rect = canvas.getBoundingClientRect();
    const x = e.clientX - rect.left;
    const time = x / rect.width * buffer.duration;
    const threshold = 12 / rect.width * buffer.duration;
    const nodes = getNodes();
    const nearIdx = nodes.findIndex(n => Math.abs(n.time - time) < threshold);
    if (nearIdx >= 0) onUpdateNodes(nodes.filter((_, i) => i !== nearIdx));
  };
  return /*#__PURE__*/React.createElement("div", {
    style: {
      width: `${timelineWidth}px`,
      height: '100%',
      position: 'relative',
      overflow: 'hidden'
    }
  }, /*#__PURE__*/React.createElement("canvas", {
    ref: canvasRef,
    style: {
      position: 'absolute',
      top: 0,
      left: 0,
      imageRendering: 'pixelated'
    },
    className: "cursor-crosshair rounded border border-zinc-700",
    onMouseDown: handleMouseDown,
    onMouseMove: handleMouseMove,
    onMouseUp: handleMouseUp,
    onMouseLeave: handleMouseUp,
    onContextMenu: handleContextMenu
  }));
};
const AuthModal = ({
  isOpen,
  mode,
  forceMandatory,
  onClose,
  onSuccess
}) => {
  if (!isOpen) return null;
  const [activeTab, setActiveTab] = useState(mode || 'login');
  const [username, setUsername] = useState('admin');
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [oldPassword, setOldPassword] = useState('');
  const [newPassword, setNewPassword] = useState('');
  const [error, setError] = useState('');
  const [loading, setLoading] = useState(false);
  const [firstTime, setFirstTime] = useState(true);
  useEffect(() => {
    if (mode) setActiveTab(mode);
    if (mode === 'force_change' && !oldPassword) {
      setOldPassword('admin123');
    }
  }, [mode]);
  // Lần đăng nhập ĐẦU (admin còn mật khẩu mặc định) → hiện gợi ý
  // username/mật khẩu; sau khi đã đổi → ẩn vĩnh viễn.
  useEffect(() => {
    if (!isOpen) return;
    if (window.SonicAPI && window.SonicAPI.apiRequest) {
      window.SonicAPI.apiRequest('/api/v1/auth/first-time', { method: 'GET' })
        .then(function (d) { setFirstTime(!!(d && d.first_time)); })
        // Fail-closed: endpoint lỗi/404 (backend chưa restart) → ẨN gợi ý
        // (không hiện — user đã yêu cầu bỏ gợi ý sau lần đầu).
        .catch(function () { setFirstTime(false); });
    } else {
      setFirstTime(false);
    }
  }, [isOpen]);
  const handleSubmit = async e => {
    e.preventDefault();
    setError('');
    setLoading(true);
    try {
      if (activeTab === 'login') {
        const targetUsername = username.trim() || 'admin';
        const targetPwd = password.trim() || 'admin123';
        const res = await window.SonicAPI.login(targetUsername, targetPwd);
        localStorage.setItem('sonic_token', res.access_token);
        localStorage.setItem('sonic_user', JSON.stringify(res.user));
        if (res.user && res.user.must_change_password) {
          setOldPassword(targetPwd);
        }
        onSuccess(res.user, res.access_token);
      } else if (activeTab === 'register') {
        const res = await window.SonicAPI.register(username.trim(), email.trim(), password.trim());
        localStorage.setItem('sonic_token', res.access_token);
        localStorage.setItem('sonic_user', JSON.stringify(res.user));
        onSuccess(res.user, res.access_token);
      } else if (activeTab === 'force_change') {
        const res = await window.SonicAPI.changePassword(oldPassword.trim(), newPassword.trim());
        localStorage.setItem('sonic_token', res.access_token);
        const user = JSON.parse(localStorage.getItem('sonic_user') || '{}');
        user.must_change_password = false;
        localStorage.setItem('sonic_user', JSON.stringify(user));
        onSuccess(user, res.access_token);
      }
    } catch (err) {
      setError(err.message || (activeTab === 'login' ? 'Tài khoản hoặc mật khẩu không chính xác. (Nếu bạn đã đổi mật khẩu trước đó, vui lòng nhập mật khẩu mới mà bạn đã tạo)' : 'Thao tác không thành công'));
    } finally {
      setLoading(false);
    }
  };
  const isForceMode = activeTab === 'force_change';
  const canClose = !forceMandatory && !isForceMode;
  return /*#__PURE__*/React.createElement("div", {
    className: "fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-md"
  }, /*#__PURE__*/React.createElement("div", {
    className: "bg-[#262626] border border-[#383838] rounded-xl shadow-2xl w-full max-w-md p-6 text-slate-200"
  }, /*#__PURE__*/React.createElement("div", {
    className: "flex justify-between items-center pb-4 border-b border-[#383838]"
  }, /*#__PURE__*/React.createElement("h3", {
    className: "text-lg font-bold text-teal-400"
  }, isForceMode ? '⚠️ Bắt Buộc Đổi Mật Khẩu Khởi Tạo' : activeTab === 'login' ? '🔐 Đăng Nhập Hệ Thống' : '📝 Đăng Ký Tài Khoản'), canClose && /*#__PURE__*/React.createElement("button", {
    onClick: onClose,
    className: "text-slate-400 hover:text-slate-200"
  }, "✕")), error && /*#__PURE__*/React.createElement("div", {
    className: "mt-4 p-3 bg-red-900/40 border border-red-700 rounded-lg text-red-200 text-sm"
  }, error), /*#__PURE__*/React.createElement("form", {
    onSubmit: handleSubmit,
    className: "mt-4 space-y-4"
  }, isForceMode ? /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("p", {
    className: "text-xs text-amber-400 bg-amber-950/60 p-2.5 border border-amber-800/80 rounded leading-relaxed"
  }, "🔒 Tài khoản của bạn đang dùng mật khẩu khởi tạo mặc định. Để bảo mật hệ thống, bạn phải đổi mật khẩu mới trước khi tiếp tục."), /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("label", {
    className: "block text-xs font-semibold mb-1 text-slate-400"
  }, "Mật khẩu hiện tại (Mặc định: admin123)"), /*#__PURE__*/React.createElement("input", {
    type: "password",
    autoComplete: "current-password",
    required: true,
    value: oldPassword,
    onChange: e => setOldPassword(e.target.value),
    className: "w-full bg-[#1e1e1e] border border-[#383838] rounded px-3 py-2 text-sm focus:outline-none focus:border-teal-500"
  })), /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("label", {
    className: "block text-xs font-semibold mb-1 text-slate-400"
  }, "Mật khẩu mới"), /*#__PURE__*/React.createElement("input", {
    type: "password",
    autoComplete: "new-password",
    required: true,
    value: newPassword,
    onChange: e => setNewPassword(e.target.value),
    className: "w-full bg-[#1e1e1e] border border-[#383838] rounded px-3 py-2 text-sm focus:outline-none focus:border-teal-500"
  }))) : /*#__PURE__*/React.createElement(React.Fragment, null, activeTab === 'login' ? /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("label", {
    className: "block text-xs font-semibold mb-1 text-slate-400"
  }, "Tên đăng nhập ", firstTime && /*#__PURE__*/React.createElement("span", {
    className: "text-teal-400 font-normal"
  }, "(Tùy chọn - Admin có thể bỏ trống)")), /*#__PURE__*/React.createElement("input", {
    type: "text",
    placeholder: "Mặc định: admin",
    value: username,
    onChange: e => setUsername(e.target.value),
    className: "w-full bg-[#1e1e1e] border border-[#383838] rounded px-3 py-2 text-sm focus:outline-none focus:border-teal-500"
  })) : /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("label", {
    className: "block text-xs font-semibold mb-1 text-slate-400"
  }, "Tên đăng nhập"), /*#__PURE__*/React.createElement("input", {
    type: "text",
    required: true,
    value: username,
    onChange: e => setUsername(e.target.value),
    className: "w-full bg-[#1e1e1e] border border-[#383838] rounded px-3 py-2 text-sm focus:outline-none focus:border-teal-500"
  })), activeTab === 'register' && /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("label", {
    className: "block text-xs font-semibold mb-1 text-slate-400"
  }, "Email"), /*#__PURE__*/React.createElement("input", {
    type: "email",
    required: true,
    value: email,
    onChange: e => setEmail(e.target.value),
    className: "w-full bg-[#1e1e1e] border border-[#383838] rounded px-3 py-2 text-sm focus:outline-none focus:border-teal-500"
  })), /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("label", {
    className: "block text-xs font-semibold mb-1 text-slate-400"
  }, "Mật khẩu ", activeTab === 'login' && firstTime && /*#__PURE__*/React.createElement("span", {
    className: "text-amber-400 font-normal"
  }, "(Lần đầu: admin123)")), /*#__PURE__*/React.createElement("input", {
    type: "password",
    autoComplete: activeTab === 'login' ? 'current-password' : 'new-password',
    required: true,
    value: password,
    onChange: e => setPassword(e.target.value),
    className: "w-full bg-[#1e1e1e] border border-[#383838] rounded px-3 py-2 text-sm focus:outline-none focus:border-teal-500"
  }))), /*#__PURE__*/React.createElement("button", {
    type: "submit",
    disabled: loading,
    className: "w-full py-2 bg-teal-600 hover:bg-teal-500 text-white font-semibold rounded-lg shadow transition duration-150"
  }, loading ? 'Đang xác thực...' : isForceMode ? 'Đổi Mật Khẩu Ngay' : activeTab === 'login' ? 'Đăng Nhập System' : 'Tạo Tài Khoản Mới'), activeTab === 'login' && !isForceMode && firstTime && /*#__PURE__*/React.createElement("button", {
    type: "button",
    onClick: () => {
      setUsername('admin');
      setPassword('admin123');
      setError('');
    },
    className: "w-full mt-2 py-1.5 bg-amber-950/60 hover:bg-amber-900/80 text-amber-300 border border-amber-800/60 text-xs font-semibold rounded-lg transition flex items-center justify-center gap-1.5"
  }, "🔑 Điền Nhanh Tài Khoản Admin (admin / admin123)")), !isForceMode && /*#__PURE__*/React.createElement("div", {
    className: "mt-4 pt-4 border-t border-[#383838] text-center text-xs text-slate-400"
  }, activeTab === 'login' ? /*#__PURE__*/React.createElement("span", null, "Chưa có tài khoản? ", /*#__PURE__*/React.createElement("button", {
    onClick: () => setActiveTab('register'),
    className: "text-teal-400 hover:underline"
  }, "Đăng ký ngay")) : /*#__PURE__*/React.createElement("span", null, "Đã có tài khoản? ", /*#__PURE__*/React.createElement("button", {
    onClick: () => setActiveTab('login'),
    className: "text-teal-400 hover:underline"
  }, "Đăng nhập")))));
};
const AIConfigModal = ({
  isOpen,
  onClose,
  onConfigSaved
}) => {
  if (!isOpen) return null;
  const defaultProvidersList = [{
    id: 'openai_default',
    name: 'OpenAI Official',
    provider_type: 'openai',
    api_base_url: 'https://api.openai.com/v1',
    api_key: '',
    model_name: 'gpt-4o',
    temperature: 0.7,
    is_active: true
  }, {
    id: 'openai_compat_default',
    name: 'OpenAI Compatible (Ollama/LocalAI/DeepSeek)',
    provider_type: 'openai_compatible',
    api_base_url: 'http://localhost:11434/v1',
    api_key: 'ollama',
    model_name: 'deepseek-r1',
    temperature: 0.7,
    is_active: false
  }, {
    id: 'anthropic_default',
    name: 'Anthropic Claude',
    provider_type: 'anthropic',
    api_base_url: 'https://api.anthropic.com/v1',
    api_key: '',
    model_name: 'claude-3-5-sonnet',
    temperature: 0.7,
    is_active: false
  }, {
    id: 'gemini_default',
    name: 'Google Gemini',
    provider_type: 'gemini',
    api_base_url: 'https://generativelanguage.googleapis.com',
    api_key: '',
    model_name: 'gemini-1.5-pro',
    temperature: 0.7,
    is_active: false
  }];
  const [providers, setProviders] = useState(defaultProvidersList);
  const [selectedId, setSelectedId] = useState('openai_default');
  const [msg, setMsg] = useState('');
  const [error, setError] = useState('');
  const [loading, setLoading] = useState(false);
  useEffect(() => {
    if (isOpen) loadConfigs();
  }, [isOpen]);
  const loadConfigs = async () => {
    setLoading(true);
    setError('');
    try {
      const data = await window.SonicAPI.getAIConfigs();
      if (data && data.providers) {
        setProviders(data.providers);
        if (data.providers.length > 0) setSelectedId(data.providers[0].id);
      }
    } catch (err) {
      setError(err.message || 'Lỗi nạp cấu hình AI');
    } finally {
      setLoading(false);
    }
  };
  const handleSave = async e => {
    e.preventDefault();
    setMsg('');
    setError('');
    setLoading(true);
    try {
      const res = await window.SonicAPI.saveAIConfigs(providers);
      setMsg(res.message || 'Đã lưu cấu hình AI Providers thành công!');
      if (onConfigSaved) onConfigSaved(providers);
    } catch (err) {
      setError(err.message || 'Lỗi khi lưu cấu hình AI');
    } finally {
      setLoading(false);
    }
  };
  const updateProviderField = (id, field, value) => {
    setProviders(prev => prev.map(p => p.id === id ? {
      ...p,
      [field]: value
    } : p));
  };
  const activeProvider = providers.find(p => p.id === selectedId) || providers[0];
  return /*#__PURE__*/React.createElement("div", {
    className: "fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm select-none"
  }, /*#__PURE__*/React.createElement("div", {
    className: "bg-[#262626] border border-[#383838] rounded-xl shadow-2xl w-full max-w-2xl p-6 text-slate-200"
  }, /*#__PURE__*/React.createElement("div", {
    className: "flex justify-between items-center pb-4 border-b border-[#383838]"
  }, /*#__PURE__*/React.createElement("h3", {
    className: "text-lg font-bold text-cyan-400 flex items-center gap-2"
  }, "🤖 Quản Lý & Cấu Hình AI Providers"), /*#__PURE__*/React.createElement("button", {
    onClick: onClose,
    className: "text-slate-400 hover:text-slate-200"
  }, "✕")), msg && /*#__PURE__*/React.createElement("div", {
    className: "mt-3 p-2 bg-emerald-950/60 border border-emerald-700 text-emerald-300 rounded text-xs"
  }, msg), error && /*#__PURE__*/React.createElement("div", {
    className: "mt-3 p-2 bg-red-950/60 border border-red-700 text-red-300 rounded text-xs"
  }, error), /*#__PURE__*/React.createElement("div", {
    className: "mt-4 grid grid-cols-3 gap-4"
  }, /*#__PURE__*/React.createElement("div", {
    className: "space-y-1.5 border-r border-[#383838] pr-3"
  }, /*#__PURE__*/React.createElement("span", {
    className: "text-xs uppercase font-bold text-slate-400 block mb-2"
  }, "Providers"), providers.map(p => /*#__PURE__*/React.createElement("button", {
    key: p.id,
    tabIndex: 0,
    onClick: () => setSelectedId(p.id),
    onKeyDown: e => { if (e.key === ' ') { e.preventDefault(); updateProviderField(p.id, 'is_active', !p.is_active); } },
    className: `w-full text-left px-3 py-2 rounded-lg text-xs font-semibold flex items-center justify-between transition ${selectedId === p.id ? 'bg-cyan-600 text-white shadow' : 'bg-[#1e1e1e] text-slate-300 hover:bg-[#2e2e2e]'}`
  }, /*#__PURE__*/React.createElement("span", {
    className: "truncate"
  }, p.name), p.is_active && /*#__PURE__*/React.createElement("span", {
    className: "w-2 h-2 rounded-full bg-emerald-400"
  }))), /*#__PURE__*/React.createElement("div", {
    className: "flex gap-1 mt-2"
  }, /*#__PURE__*/React.createElement("button", {
    onClick: e => {
      e.stopPropagation();
      const rearrangeNewId = 'provider_' + Date.now();
      setProviders(prev => [...prev, {
        id: rearrangeNewId,
        name: 'New Provider',
        provider_type: 'openai_compatible',
        api_base_url: 'https://api.openai.com/v1',
        api_key: '',
        model_name: 'gpt-4o-mini',
        temperature: 0.7,
        is_active: false
      }]);
      setSelectedId(rearrangeNewId);
    },
    className: "flex-1 px-2 py-1 bg-emerald-700 hover:bg-emerald-600 text-white text-xs font-bold rounded"
  }, "+ Thêm"), providers.length > 1 && /*#__PURE__*/React.createElement("button", {
    onClick: e => {
      e.stopPropagation();
      if (confirm(`Xóa provider "${providers.find(p => p.id === selectedId)?.name}"?`)) {
        setProviders(prev => {
          const filtered = prev.filter(p => p.id !== selectedId);
          if (filtered.length > 0) setSelectedId(filtered[0].id);
          return filtered;
        });
      }
    },
    className: "px-2 py-1 bg-red-800 hover:bg-red-700 text-white text-xs font-bold rounded"
  }, "Xóa"), providers.length > 1 && /*#__PURE__*/React.createElement("button", {
    onClick: function (e) { e.stopPropagation(); var provs = providers.slice(); var pos = -1; for (var pi = 0; pi < provs.length; pi++) { if (provs[pi].id === selectedId) { pos = pi; break; } } if (pos > 0) { var tmp = provs[pos]; provs[pos] = provs[pos - 1]; provs[pos - 1] = tmp; setProviders(provs); } },
    className: "px-2 py-1 bg-zinc-700 hover:bg-zinc-600 text-white text-xs font-bold rounded",
    title: "Di chuyển lên"
  }, "▲"), providers.length > 1 && /*#__PURE__*/React.createElement("button", {
    onClick: function (e) { e.stopPropagation(); var provs = providers.slice(); var pos = -1; for (var pi = 0; pi < provs.length; pi++) { if (provs[pi].id === selectedId) { pos = pi; break; } } if (pos < provs.length - 1) { var tmp = provs[pos]; provs[pos] = provs[pos + 1]; provs[pos + 1] = tmp; setProviders(provs); } },
    className: "px-2 py-1 bg-zinc-700 hover:bg-zinc-600 text-white text-xs font-bold rounded",
    title: "Di chuyển xuống"
  }, "▼"))), activeProvider && /*#__PURE__*/React.createElement("form", {
    onSubmit: handleSave,
    className: "col-span-2 space-y-3.5"
  }, /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("label", {
    className: "block text-xs font-semibold text-slate-400 mb-1"
  }, "Tên Provider"), /*#__PURE__*/React.createElement("input", {
    type: "text",
    value: activeProvider.name,
    onChange: e => updateProviderField(activeProvider.id, 'name', e.target.value),
    className: "w-full bg-[#1e1e1e] border border-[#383838] rounded px-3 py-1.5 text-xs text-slate-200 focus:outline-none focus:border-cyan-500"
  })), /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("label", {
    className: "block text-xs font-semibold text-slate-400 mb-1"
  }, "API Base URL (Endpoint)"), /*#__PURE__*/React.createElement("input", {
    type: "text",
    value: activeProvider.api_base_url || '',
    onChange: e => updateProviderField(activeProvider.id, 'api_base_url', e.target.value),
    className: "w-full bg-[#1e1e1e] border border-[#383838] rounded px-3 py-1.5 text-xs font-mono text-cyan-300 focus:outline-none focus:border-cyan-500",
    placeholder: "https://api.openai.com/v1"
  })), /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("label", {
    className: "block text-xs font-semibold text-slate-400 mb-1"
  }, "API Key Cá Nhân"), /*#__PURE__*/React.createElement("input", {
    type: "password",
    value: activeProvider.api_key || '',
    onChange: e => updateProviderField(activeProvider.id, 'api_key', e.target.value),
    className: "w-full bg-[#1e1e1e] border border-[#383838] rounded px-3 py-1.5 text-xs font-mono text-slate-200 focus:outline-none focus:border-cyan-500",
    placeholder: "sk-..."
  })), /*#__PURE__*/React.createElement("div", {
    className: "grid grid-cols-2 gap-3"
  }, /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("label", {
    className: "block text-xs font-semibold text-slate-400 mb-1"
  }, "Model Name"), /*#__PURE__*/React.createElement("input", {
    type: "text",
    value: activeProvider.model_name || '',
    onChange: e => updateProviderField(activeProvider.id, 'model_name', e.target.value),
    className: "w-full bg-[#1e1e1e] border border-[#383838] rounded px-3 py-1.5 text-xs font-mono text-amber-300 focus:outline-none focus:border-cyan-500"
  })), /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("label", {
    className: "block text-xs font-semibold text-slate-400 mb-1"
  }, "Temperature"), /*#__PURE__*/React.createElement("input", {
    type: "number",
    step: "0.1",
    min: "0",
    max: "2",
    value: activeProvider.temperature ?? 0.7,
    onChange: e => updateProviderField(activeProvider.id, 'temperature', parseFloat(e.target.value)),
    className: "w-full bg-[#1e1e1e] border border-[#383838] rounded px-3 py-1.5 text-xs text-slate-200 focus:outline-none focus:border-cyan-500"
  }))), /*#__PURE__*/React.createElement("div", {
    className: "pt-2 flex items-center justify-between"
  }, /*#__PURE__*/React.createElement("label", {
    className: "flex items-center gap-2 cursor-pointer text-xs text-slate-300"
  }, /*#__PURE__*/React.createElement("input", {
    type: "checkbox",
    checked: activeProvider.is_active,
    onChange: e => updateProviderField(activeProvider.id, 'is_active', e.target.checked),
    className: "rounded accent-cyan-500"
  }), "Kích hoạt Provider này"), /*#__PURE__*/React.createElement("button", {
    type: "submit",
    disabled: loading,
    className: "px-4 py-1.5 bg-cyan-600 hover:bg-cyan-500 text-white font-semibold text-xs rounded-lg shadow transition"
  }, loading ? 'Đang lưu...' : 'Lưu Cấu Hình AI'))))));
};

const PluginManagerModal = ({ isOpen, onClose, pluginsData, onInsertInstrument }) => {
  if (!isOpen) return null;
  const [localData, setLocalData] = React.useState(pluginsData);
  const [sfUploadStatus, setSfUploadStatus] = React.useState('');
  const [sfToDelete, setSfToDelete] = React.useState(null);
  const [pmDirs, setPmDirs] = React.useState([]);
  const [pmScanning, setPmScanning] = React.useState(false);
  const [pmScanResult, setPmScanResult] = React.useState('');
  const [pmScanData, setPmScanData] = React.useState(null); // { vst_found, soundfonts }
  // Instrument bên trong mỗi soundfont (expand) — "Chèn vào Synth" qua onInsertInstrument
  const [pmSfExpanded, setPmSfExpanded] = React.useState({});
  const [pmSfInstruments, setPmSfInstruments] = React.useState({});
  const [pmSfLoading, setPmSfLoading] = React.useState({});
  // Force re-render sau khi định vị Carla (capabilities đổi)
  const [pmCarlaVersion, setPmCarlaVersion] = React.useState(0);
  // Khai báo trực tiếp thư mục chứa carla.exe (nhập tay, không cần picker)
  const [pmCarlaPathInput, setPmCarlaPathInput] = React.useState('');
  // D7: trạng thái Native Bridge (query khi mở modal) — badge header.
  const [bridgeStatus, setBridgeStatus] = React.useState(null);
  React.useEffect(() => {
    if (isOpen) {
      window.SonicAPI.bridgeStatus().then(s => setBridgeStatus(s)).catch(() => setBridgeStatus(null));
    }
  }, [isOpen]);
  React.useEffect(() => {
    if (isOpen) {
      window.SonicAPI.listPlugins()
        .then(data => setLocalData(data))
        .catch(() => setLocalData({ vst_instruments: [], soundfonts: [] }));
      window.SonicAPI.getPluginDirs()
        .then(d => setPmDirs(d.plugin_dirs || []))
        .catch(() => {});
      setTimeout(() => { try { window.lucide.createIcons(); } catch(e) {} }, 50);
    }
  }, [isOpen]);
  // Folder picker:
  // 1) Neu page duoc Tauri serve (__TAURI__ co) -> dialog plugin invoke.
  // 2) Binh thuong UI chay tren http://127.0.0.1:8000 (engine) -> __TAURI__
  //    KHONG co (Tauri chi inject vao trang no serve; window.prompt cung
  //    khong hoat dong trong WebView2) -> mo TRINH DUYET THU MUC in-app
  //    (backend /media/computer + /media/browse) — hoat dong moi OS.
  // 3) Cuoi cung: prompt nhap path (browser thuan).
  const [pmPicker, setPmPicker] = React.useState(null); // {path, dirs, parent, roots, loading}
  const openPluginPicker = async () => {
    setPmPicker({ path: null, dirs: null, parent: null, roots: null, loading: true });
    try {
      const data = await window.SonicAPI.browseComputer();
      setPmPicker({ path: null, dirs: null, parent: null, roots: data.roots || [], loading: false });
    } catch (e) {
      setPmPicker(null);
      showToast('Không mở được trình duyệt thư mục: ' + (e.message || e), 'error');
    }
  };
  const browsePluginDir = async (path) => {
    setPmPicker(prev => ({ ...prev, loading: true }));
    try {
      const data = await window.SonicAPI.browseDir(path);
      setPmPicker({ path: data.path, parent: data.parent, dirs: data.dirs || [], roots: null, loading: false });
    } catch (e) {
      setPmPicker(prev => ({ ...prev, loading: false }));
      showToast('Không đọc được thư mục: ' + (e.message || e), 'error');
    }
  };
  const confirmPluginDir = () => {
    const p = pmPicker && pmPicker.path;
    if (p && !pmDirs.includes(p)) {
      setPmDirs(prev => [...prev, p]);
      // TỰ ĐỘNG lưu + scan → nút Synth có ngay danh sách instrument
      clearTimeout(window.__pmAutoScanTimer);
      window.__pmAutoScanTimer = setTimeout(() => { saveAndScanDirs(); }, 600);
    }
    setPmPicker(null);
  };
  const pickPluginFolder = async () => {
    const addDir = (p) => { if (p && !pmDirs.includes(p)) { setPmDirs(prev => [...prev, p]); clearTimeout(window.__pmAutoScanTimer); window.__pmAutoScanTimer = setTimeout(() => { saveAndScanDirs(); }, 600); } };
    try {
      // 1) NATIVE dialog qua engine (Tauri bridge → IFileDialog/Explorer;
      //    fallback PowerShell/zenity/osascript) — UI chạy localhost:8000 nên
      //    __TAURI__ không có, window.prompt vô hiệu trong WebView2.
      try {
        const d = await window.SonicAPI.pickPluginDir();
        if (d && typeof d.path === 'string' && d.path) { addDir(d.path); return; }
      } catch (e) { /* fallthrough */ }
      // 2) Tauri dialog trực tiếp (chỉ khi page được Tauri serve)
      if (window.__TAURI__ && window.__TAURI__.core && window.__TAURI__.core.invoke) {
        const sel = await window.__TAURI__.core.invoke('plugin:dialog|open', {
          options: { directory: true, multiple: false }
        });
        if (typeof sel === 'string' && sel) { addDir(sel); return; }
      }
      // 3) Trình duyệt thư mục in-app (backend) — fallback mọi OS
      await openPluginPicker();
      return;
    } catch (e) {
      // 4) Cuối cùng: prompt nhập tay (browser thuần, không phải WebView2)
      try {
        const manual = window.prompt('Nhập đường dẫn thư mục plugin (VST / SoundFont):');
        if (manual && manual.trim()) addDir(manual.trim());
      } catch (e2) {
        showToast('Browse failed: ' + (e.message || e), 'error');
      }
    }
  };
  const removePluginDir = (dir) => {
    setPmDirs(prev => prev.filter(d => d !== dir));
    // Tự động lưu + scan lại sau khi xóa thư mục
    clearTimeout(window.__pmAutoScanTimer);
    window.__pmAutoScanTimer = setTimeout(() => { saveAndScanDirs(); }, 600);
  };
  // Expand 1 soundfont → đọc danh sách instrument (bank/program/name) bên trong
  // qua API soundfont-instruments/{id} → hiển thị nút "Chèn vào Synth".
  const toggleSfInstruments = async (sf) => {
    const baseId = String(sf.id || '').replace('sf_', '');
    setPmSfExpanded(prev => ({ ...prev, [baseId]: !prev[baseId] }));
    if (!pmSfInstruments[baseId] && !pmSfLoading[baseId]) {
      setPmSfLoading(prev => ({ ...prev, [baseId]: true }));
      try {
        const r = await window.SonicAPI.listSoundfontInstruments(baseId);
        setPmSfInstruments(prev => ({ ...prev, [baseId]: (r && r.presets) || [] }));
      } catch (e) {
        setPmSfInstruments(prev => ({ ...prev, [baseId]: [] }));
      } finally {
        setPmSfLoading(prev => ({ ...prev, [baseId]: false }));
      }
    }
  };
  // Định vị Carla.exe — bản Windows là zip portable: KHÔNG cài đặt, KHÔNG dùng
  // biến môi trường PATH nên heuristic không tìm thấy → user tự chọn thư mục
  // chứa carla.exe (folder picker native) → lưu config phía server.
  const locateCarla = async () => {
    try {
      let picked = null;
      try {
        const d = await window.SonicAPI.pickPluginDir();
        if (d && typeof d.path === 'string' && d.path) picked = d.path;
      } catch (e) { /* fallthrough */ }
      if (!picked && window.__TAURI__ && window.__TAURI__.core && window.__TAURI__.core.invoke) {
        try {
          const sel = await window.__TAURI__.core.invoke('plugin:dialog|open', { options: { directory: true, multiple: false } });
          if (typeof sel === 'string' && sel) picked = sel;
        } catch (e) { /* fallthrough */ }
      }
      if (!picked) { showToast('Không mở được hộp thoại chọn thư mục', 'error'); return; }
      const r = await window.SonicAPI.setCarlaPath(picked);
      if (r && r.success && r.carla_path) {
        window.SonicRuntime.capabilities = r;
        document.documentElement.dataset.carla = r.features && r.features.carla_local ? '1' : '0';
        setPmCarlaVersion(v => v + 1);
        showToast('Đã định vị Carla: ' + r.carla_path, 'success');
      } else {
        showToast('Không tìm thấy carla.exe trong thư mục đã chọn', 'error');
      }
    } catch (err) { showToast('Lỗi định vị Carla: ' + (err.message || err), 'error'); }
  };
  // Khai báo thư mục Carla trực tiếp (nhập tay) — tương tự Định vị nhưng
  // không cần hộp thoại chọn thư mục.
  const saveCarlaInput = async () => {
    const p = (pmCarlaPathInput || '').trim();
    if (!p) { showToast('Nhập đường dẫn thư mục chứa carla.exe', 'warning'); return; }
    try {
      const r = await window.SonicAPI.setCarlaPath(p);
      if (r && r.success && r.carla_path) {
        window.SonicRuntime.capabilities = r;
        document.documentElement.dataset.carla = r.features && r.features.carla_local ? '1' : '0';
        setPmCarlaVersion(v => v + 1);
        setPmCarlaPathInput('');
        showToast('Đã lưu Carla: ' + r.carla_path, 'success');
      } else {
        showToast('Không tìm thấy carla.exe trong đường dẫn đã nhập', 'error');
      }
    } catch (err) { showToast('Lỗi: ' + (err.message || err), 'error'); }
  };
  // Lưu dirs (user override) + scan cả 2 thư mục → refresh list + catalog.
  const saveAndScanDirs = async () => {
    setPmScanning(true);
    setPmScanResult('');
    setPmScanData(null);
    try {
      await window.SonicAPI.savePluginDirs({ plugin_dirs: pmDirs });
      const scan = await window.SonicAPI.scanPluginDirs();
      setPmScanData({ vst_found: scan.vst_found || [], soundfonts: scan.soundfonts || [] });
      const data = await window.SonicAPI.listPlugins();
      setLocalData(data);
      try {
        const cat = await window.SonicAPI.getSoundfontCatalog();
        window.__soundfontCatalog = cat;
      } catch (_) {}
      setPmScanResult(`VST: ${scan.vst_count || 0} | SoundFonts: ${scan.soundfont_count || 0}`);
      showToast(`Scan xong: ${scan.vst_count || 0} VST, ${scan.soundfont_count || 0} SoundFonts.`, 'success');
    } catch (err) {
      setPmScanResult('Scan failed: ' + (err.message || err));
      showToast('Scan failed: ' + (err.message || err), 'error');
    } finally {
      setPmScanning(false);
    }
  };
  const handleUploadSF = async (e) => {
    const file = e.target.files?.[0];
    if (!file) return;
    setSfUploadStatus('Uploading...');
    try {
      const result = await window.SonicAPI.uploadSoundFont(file);
      setSfUploadStatus('Uploaded: ' + result.name);
      // Refresh plugin list and catalog
      const data = await window.SonicAPI.listPlugins();
      setLocalData(data);
      try {
        const cat = await window.SonicAPI.getSoundfontCatalog();
        window.__soundfontCatalog = cat;
      } catch (_) {}
    } catch (err) {
      setSfUploadStatus('Error: ' + err.message);
    }
  };
  // D7: Load asset vào Native Bridge — backend resolve path thật (E2) → JS
  // invoke load_native_instrument (C2) → bridge C++ load vào channel 0.
  const loadToBridge = async (name, path, type) => {
    try {
      const r = await window.SonicAPI.bridgeLoad({ name: name, path: path || null, instrumentType: type, channel: 0 });
      if (!r || !r.path) { window.showToast && window.showToast('Không resolve được asset: ' + name, 'error'); return false; }
      const ok = await window.NativeBridgeService.loadInstrument(r.path, type, 0);
      window.showToast && window.showToast(ok ? ('Đã load vào Native Bridge: ' + r.path) : 'Bridge không khả dụng (dev mode?)', ok ? 'success' : 'warning');
      if (ok) window.SonicAPI.bridgeStatus().then(s => setBridgeStatus(s)).catch(() => {});
      // Requirement 2: VST3 load → mở native GUI ngay (C++ attach editor vào
      // cửa sổ bridge tự tạo — control type=4, hwnd=0).
      if (ok && type === 'VST3' && window.NativeBridgeService.openNativeGUI) {
        try { await openVstGuiRetry(name, 0, 20); } catch (e) {}
      }
      return ok;
    } catch (err) {
      window.showToast && window.showToast('Lỗi load bridge: ' + (err.message || err), 'error');
      return false;
    }
  };
  const [pmTab, setPmTab] = React.useState('soundfont');
  return React.createElement('div', {
    className: 'fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm',
    onClick: onClose
  }, React.createElement('div', {
    className: 'bg-[#1e1e1e] border border-[#383838] rounded-xl shadow-2xl w-full max-w-3xl p-0 text-slate-200 overflow-hidden flex flex-col relative',
    style: { maxHeight: '80vh' },
    onClick: e => e.stopPropagation()
  },
    // ── In-app folder picker (Plugin Directories) ─────────────────────
    pmPicker && React.createElement('div', {
      className: 'absolute inset-0 z-10 bg-[#171717]/97 flex flex-col',
      style: { padding: 16 }
    },
      React.createElement('div', { className: 'flex items-center justify-between mb-2' },
        React.createElement('div', { className: 'text-xs font-bold text-violet-300 uppercase' }, 'Chọn thư mục plugin'),
        React.createElement('button', {
          onClick: () => setPmPicker(null),
          className: 'text-zinc-500 hover:text-zinc-200 transition'
        }, React.createElement('i', { 'data-lucide': 'x', className: 'w-4 h-4' }))
      ),
      React.createElement('div', { className: 'flex items-center gap-2 mb-2' },
        React.createElement('button', {
          onClick: () => { if (pmPicker.parent) browsePluginDir(pmPicker.parent); },
          disabled: !pmPicker.parent,
          className: 'px-2 py-1 bg-zinc-800 hover:bg-zinc-700 rounded text-[11px] text-zinc-300 disabled:opacity-30 shrink-0'
        }, 'Lên'),
        React.createElement('div', {
          className: 'flex-1 text-[11px] text-zinc-400 font-mono truncate',
          title: pmPicker.path || ''
        }, pmPicker.path || (pmPicker.loading ? 'Đang tải...' : 'My Computer'))
      ),
      pmPicker.loading ? React.createElement('div', { className: 'flex-1 flex items-center justify-center text-zinc-500 text-xs' }, 'Đang tải...') :
      (pmPicker.dirs ? (
        pmPicker.dirs.length === 0 ? React.createElement('div', { className: 'flex-1 flex items-center justify-center text-zinc-600 text-xs italic' }, 'Thư mục trống') :
        React.createElement('div', { className: 'flex-1 overflow-y-auto space-y-1' },
          pmPicker.dirs.map((d, i) =>
            React.createElement('div', {
              key: 'pd_' + i,
              className: 'flex items-center gap-2 px-2 py-1.5 rounded hover:bg-violet-900/30 cursor-pointer group',
              onClick: () => browsePluginDir(d.path)
            },
              React.createElement('i', { 'data-lucide': 'folder', className: 'w-3.5 h-3.5 text-amber-500/80 shrink-0' }),
              React.createElement('span', { className: 'flex-1 text-[11px] text-zinc-300 truncate' }, d.name),
              React.createElement('i', { 'data-lucide': 'chevron-right', className: 'w-3 h-3 text-zinc-600 group-hover:text-violet-400 shrink-0' })
            )
          )
        )
      ) : (
        pmPicker.roots ? (
          pmPicker.roots.length === 0 ? React.createElement('div', { className: 'flex-1 flex items-center justify-center text-zinc-600 text-xs' }, 'Không tìm thấy ổ đĩa') :
          React.createElement('div', { className: 'flex-1 overflow-y-auto space-y-1' },
            pmPicker.roots.map((r, i) =>
              React.createElement('div', {
                key: 'pr_' + i,
                className: 'flex items-center gap-2 px-2 py-1.5 rounded hover:bg-violet-900/30 cursor-pointer',
                onClick: () => browsePluginDir(r.path)
              },
                React.createElement('i', { 'data-lucide': 'hard-drive', className: 'w-3.5 h-3.5 text-cyan-500/80 shrink-0' }),
                React.createElement('span', { className: 'flex-1 text-[11px] text-zinc-300' }, r.name || r.path)
              )
            )
          )
        ) : null
      )),
      React.createElement('div', { className: 'flex items-center justify-end gap-2 mt-2 pt-2 border-t border-[#383838]' },
        React.createElement('button', {
          onClick: () => setPmPicker(null),
          className: 'px-3 py-1.5 bg-zinc-800 hover:bg-zinc-700 text-white text-xs rounded'
        }, 'Hủy'),
        React.createElement('button', {
          onClick: confirmPluginDir,
          disabled: !pmPicker.path,
          className: 'px-3 py-1.5 bg-violet-700 hover:bg-violet-600 text-white text-xs font-semibold rounded disabled:opacity-30'
        }, 'Chọn thư mục này')
      )
    ),
    // Header
    React.createElement('div', {
      className: 'flex items-center justify-between px-5 py-3 bg-[#252525] border-b border-[#383838]'
    },
      React.createElement('h3', {
        className: 'text-base font-bold text-cyan-400 flex items-center gap-2'
      }, React.createElement('i', { 'data-lucide': 'zap', className: 'w-4 h-4' }), 'Plugin Manager (SoundFont / VSTi)'),
      React.createElement('div', { className: 'flex items-center gap-2' },
        React.createElement('span', {
          className: `text-[9px] font-bold uppercase tracking-wider px-2 py-0.5 rounded border ${bridgeStatus && bridgeStatus.connected ? 'text-emerald-400 border-emerald-700 bg-emerald-900/30' : 'text-zinc-500 border-zinc-700 bg-zinc-800'}`,
          title: 'Native Bridge (C++ engine) — ' + (bridgeStatus ? JSON.stringify(bridgeStatus).slice(0, 120) : 'chưa query')
        }, bridgeStatus && bridgeStatus.connected ? '● Bridge ON' : '● Bridge OFF'),
        React.createElement('button', {
          onClick: onClose,
          className: 'text-zinc-500 hover:text-zinc-200 transition'
        }, React.createElement('i', { 'data-lucide': 'x', className: 'w-4 h-4' }))
      )),
    // Left-right body
    React.createElement('div', {
      className: 'flex flex-1 overflow-hidden',
      style: { minHeight: '300px' }
    },
      // Left sidebar
      React.createElement('div', {
        className: 'w-40 shrink-0 border-r border-[#383838] bg-[#1a1a1a] p-3 flex flex-col gap-2'
      },
        ['vst', 'soundfont'].map(tab => 
          React.createElement('button', {
            key: tab,
            onClick: () => setPmTab(tab),
            className: `w-full py-2 text-xs font-bold rounded transition border ${pmTab === tab ? (tab === 'vst' ? 'bg-violet-900 border-violet-700 text-violet-200' : 'bg-amber-900 border-amber-700 text-amber-200') : 'bg-zinc-800 border-transparent text-zinc-400 hover:text-zinc-200 hover:bg-zinc-700'} flex items-center gap-2 px-3`
          }, React.createElement('i', {
            'data-lucide': tab === 'vst' ? 'cpu' : 'music',
            className: 'w-3.5 h-3.5'
          }), tab === 'vst' ? 'VST Instruments' : 'SoundFonts')
        )
      ),
      // Right content
      React.createElement('div', {
        className: 'flex-1 overflow-y-auto p-4 bg-[#1e1e1e]'
      }, !localData ? 
        React.createElement('div', { className: 'flex items-center justify-center h-full text-zinc-500 text-xs' }, 'Loading...') :
        React.createElement('div', { className: 'space-y-2' },
          pmTab === 'vst' ?
            (localData.vst_instruments?.length === 0 ?
              React.createElement('div', { className: 'flex items-center justify-center h-32 text-zinc-500 text-xs' }, 'No VST instruments found on server.') :
              localData.vst_instruments.map((v, i) =>
                React.createElement('div', {
                  key: i,
                  className: 'flex items-center justify-between bg-[#252525] px-4 py-3 rounded-lg border border-[#333] hover:border-violet-800/50 transition'
                },
                  React.createElement('div', { className: 'flex items-center gap-3' },
                    React.createElement('div', { className: 'w-8 h-8 rounded bg-violet-900/30 flex items-center justify-center' },
                      React.createElement('i', { 'data-lucide': 'cpu', className: 'w-4 h-4 text-violet-400' })
                    ),
                    React.createElement('div', null,
                      React.createElement('div', { className: 'text-xs font-semibold text-slate-200' }, v.name || v.id),
                      React.createElement('div', { className: 'text-[10px] text-zinc-500' }, v.type || 'VST3')
                    )
                  ),
                  React.createElement('div', { className: 'flex items-center gap-2' },
                                        React.createElement('button', {
                      onClick: (e) => { e.stopPropagation(); loadToBridge(v.name || v.id, v.path, v.type || 'VST3'); },
                      className: 'text-[10px] bg-cyan-800 hover:bg-cyan-700 text-white px-2 py-1 rounded transition shrink-0',
                      title: 'Load vào Native Bridge (C++ engine)'
                    }, 'Load Bridge'),
                    React.createElement('span', { className: 'text-[10px] bg-violet-950/40 text-violet-400 px-2 py-0.5 rounded-full border border-violet-800/30' }, v.type || 'VST3')
                  )
                )
              )
            ) :
            (localData.soundfonts?.length === 0 ?
              React.createElement('div', { className: 'flex items-center justify-center h-32 text-zinc-500 text-xs' }, 'No SoundFonts found. Upload one below.') :
              localData.soundfonts.map((sf, i) => {
                const baseId = String(sf.id || '').replace('sf_', '');
                const expanded = !!pmSfExpanded[baseId];
                const insts = pmSfInstruments[baseId] || [];
                const loading = !!pmSfLoading[baseId];
                return React.createElement('div', {
                  key: i,
                  className: 'bg-[#252525] rounded-lg border border-[#333] hover:border-amber-800/50 transition group'
                },
                  React.createElement('div', {
                    className: 'flex items-center justify-between px-4 py-3 cursor-pointer',
                    onClick: () => toggleSfInstruments(sf)
                  },
                    React.createElement('div', { className: 'flex items-center gap-3' },
                      React.createElement('div', { className: 'w-8 h-8 rounded bg-amber-900/30 flex items-center justify-center' },
                        React.createElement('i', { 'data-lucide': 'music', className: 'w-4 h-4 text-amber-400' })
                      ),
                      React.createElement('div', null,
                        React.createElement('div', { className: 'text-xs font-semibold text-slate-200' }, sf.display || sf.name || sf.id),
                        React.createElement('div', { className: 'text-[10px] text-zinc-500' }, (sf.file || sf.name) + (insts.length ? ' — ' + insts.length + ' instruments' : ''))
                      )
                    ),
                    React.createElement('div', { className: 'flex items-center gap-2' },
                      React.createElement('button', {
                        onClick: (e) => { e.stopPropagation(); loadToBridge(sf.id, sf.file || sf.path, 'SF2'); },
                        className: 'text-[10px] bg-cyan-800 hover:bg-cyan-700 text-white px-2 py-1 rounded transition shrink-0',
                        title: 'Load vào Native Bridge (C++ engine)'
                      }, 'Load Bridge'),
                      React.createElement('span', { className: 'text-[10px] text-zinc-500' }, expanded ? '▾' : '▸'),
                      React.createElement('button', {
                        onClick: (e) => { e.stopPropagation(); setSfToDelete(sf); },
                        className: 'text-[10px] text-zinc-500 hover:text-red-400 opacity-0 group-hover:opacity-100 transition px-2 py-1'
                      }, 'Delete')
                    )
                  ),
                  expanded && React.createElement('div', { className: 'border-t border-[#333] px-3 py-1 max-h-40 overflow-y-auto' },
                    loading ?
                      React.createElement('p', { className: 'text-[10px] text-zinc-500 italic py-1' }, 'Đang đọc instruments...') :
                      insts.length === 0 ?
                        React.createElement('p', { className: 'text-[10px] text-zinc-500 italic py-1' }, 'Không có instrument (SF3 cần chuyển đổi trước)') :
                        insts.map((p, pi) => React.createElement('div', { key: 'si_' + pi, className: 'flex items-center gap-2 py-1 text-[11px]' },
                          React.createElement('span', { className: 'text-zinc-500 font-mono w-24 shrink-0 text-[9px]' }, 'B' + (p.bank || 0) + ' P' + (p.program || 0)),
                          React.createElement('span', { className: 'flex-1 truncate text-zinc-300' }, p.name || ('Program ' + p.program)),
                          React.createElement('button', {
                            onClick: () => onInsertInstrument && onInsertInstrument({
                              instrumentId: 'sf_' + baseId,
                              bank: p.bank || 0,
                              program: p.program || 0,
                              name: p.name || ('Program ' + p.program),
                              displayName: (sf.display || sf.name || sf.id) + ' — ' + (p.name || ('Program ' + p.program))
                            }),
                            className: 'text-[10px] bg-amber-800 hover:bg-amber-700 text-white px-2 py-0.5 rounded transition shrink-0'
                          }, 'Chèn vào Synth')
                        ))
                  )
                );
              })
            )
          ),
          // Plugin directories section (folder picker + save + scan)
          React.createElement('div', {
            className: 'pt-4 mt-4 border-t border-[#383838]'
          },
            React.createElement('div', { className: 'flex items-center justify-between mb-2' },
              React.createElement('h4', { className: 'text-xs font-bold text-zinc-400 uppercase' },
                'Plugin Directories'),
              React.createElement('button', {
                className: 'px-3 py-1.5 bg-violet-800 hover:bg-violet-700 text-white text-xs font-semibold rounded transition flex items-center gap-1 shrink-0',
                title: 'Add plugin directory (VST / SoundFont)',
                onClick: pickPluginFolder
              },
                React.createElement('i', { 'data-lucide': 'plus', className: 'w-3 h-3' }), 'Add Directory')
            ),
            pmDirs.length === 0 &&
              React.createElement('p', { className: 'text-[10px] text-zinc-600 mb-2 italic' },
                'Chưa có thư mục nào. Nhấn Add Directory để chọn thư mục chứa VST / SoundFont.'),
            pmDirs.map((dir, idx) =>
              React.createElement('div', {
                key: 'pdir_' + idx,
                className: 'flex items-center gap-2 mb-1.5 bg-zinc-800/70 border border-zinc-700 rounded px-2 py-1.5'
              },
                React.createElement('button', {
                  className: 'text-zinc-500 hover:text-red-400 transition shrink-0',
                  title: 'Remove directory',
                  onClick: () => removePluginDir(dir)
                },
                  React.createElement('i', { 'data-lucide': 'x', className: 'w-3.5 h-3.5' })),
                React.createElement('span', {
                  className: 'flex-1 text-[11px] text-zinc-300 font-mono truncate',
                  title: dir
                }, dir),
                React.createElement('i', {
                  'data-lucide': 'folder',
                  className: 'w-3 h-3 text-zinc-600 shrink-0'
                })
              )
            ),
            React.createElement('div', { className: 'flex gap-2 items-center mt-2' },
              React.createElement('button', {
                className: 'px-4 py-2 bg-emerald-800 hover:bg-emerald-700 text-white text-xs font-semibold rounded transition flex items-center gap-1',
                onClick: saveAndScanDirs
              },
                React.createElement('i', { 'data-lucide': 'search', className: 'w-3 h-3' }), 'Scan'),
              pmScanning && React.createElement('span', { className: 'text-[10px] text-emerald-400' }, 'Scanning...'),
              pmScanResult && React.createElement('span', { className: 'text-[10px] text-zinc-400' }, pmScanResult)
            ),
            pmScanData && React.createElement('div', { className: 'mt-3 space-y-2 max-h-40 overflow-y-auto' },
              React.createElement('div', { className: 'text-[10px] font-bold text-violet-300 uppercase flex items-center gap-1' },
                React.createElement('i', { 'data-lucide': 'cpu', className: 'w-3 h-3' }),
                'VST Instruments (' + pmScanData.vst_found.length + ')'),
              pmScanData.vst_found.length === 0 ?
                React.createElement('p', { className: 'text-[10px] text-zinc-600 italic' }, 'Không tìm thấy VST.') :
                pmScanData.vst_found.map((v, i) =>
                  React.createElement('div', { key: 'sv_' + i, className: 'flex items-center gap-2 text-[11px] text-zinc-300' },
                    React.createElement('span', { className: 'w-16 shrink-0 text-zinc-500 font-mono text-[9px] truncate' }, v.type || 'VST'),
                    React.createElement('span', { className: 'truncate' }, v.name),
                    React.createElement('span', { className: 'text-[9px] text-zinc-600 font-mono truncate ml-auto' }, v.dir),
                  )
                ),
              React.createElement('div', { className: 'text-[10px] font-bold text-amber-300 uppercase flex items-center gap-1 mt-2' },
                React.createElement('i', { 'data-lucide': 'music', className: 'w-3 h-3' }),
                'SoundFonts (' + pmScanData.soundfonts.length + ')'),
              pmScanData.soundfonts.length === 0 ?
                React.createElement('p', { className: 'text-[10px] text-zinc-600 italic' }, 'Không tìm thấy SoundFont.') :
                pmScanData.soundfonts.map((s, i) =>
                  React.createElement('div', { key: 'ss_' + i, className: 'flex items-center gap-2 text-[11px] text-zinc-300' },
                    React.createElement('span', { className: 'truncate' }, s.name),
                    React.createElement('span', { className: 'text-[9px] text-zinc-600 font-mono truncate ml-auto' }, s.dir)
                  )
                )
            )
          ),
          // Upload section (bottom of right panel)
          React.createElement('div', {
            className: 'pt-4 mt-4 border-t border-[#383838]'
          },
            React.createElement('h4', { className: 'text-xs font-bold text-zinc-400 mb-3 uppercase' },
              pmTab === 'vst' ? 'Add VST Directory' : 'Upload SoundFont'
            ),
            pmTab === 'vst' ? 
              React.createElement('div', { className: 'flex gap-2' },
                React.createElement('input', {
                  type: 'text',
                  placeholder: '/opt/daw_engine/vst3',
                  className: 'flex-1 bg-zinc-800 border border-zinc-700 rounded px-3 py-2 text-xs text-zinc-300 focus:outline-none focus:border-violet-600'
                }),
                React.createElement('button', {
                  className: 'px-4 py-2 bg-violet-800 hover:bg-violet-700 text-white text-xs font-semibold rounded transition',
                  onClick: async () => {
                    try {
                      const data = await window.SonicAPI.listPlugins();
                      setLocalData(data);
                      showToast('Scanned VST directory.', 'info');
                    } catch (err) {
                      showToast('Scan failed: ' + err.message, 'error');
                    }
                  }
                }, 'Scan')
              ) :
              React.createElement('div', { className: 'space-y-2' },
                React.createElement('label', {
                  className: 'flex items-center gap-3 px-4 py-3 border-2 border-dashed border-zinc-700 rounded-lg cursor-pointer hover:border-amber-600/50 bg-zinc-800/40 transition'
                },
                  React.createElement('i', { 'data-lucide': 'upload', className: 'w-5 h-5 text-zinc-400' }),
                  React.createElement('span', { className: 'text-xs text-zinc-400' }, 'Click to upload .sf2 / .sf3 file'),
                  React.createElement('input', {
                    type: 'file',
                    accept: '.sf2,.sf3',
                    onChange: async (e) => {
                      const file = e.target.files?.[0];
                      if (!file) return;
                      setSfUploadStatus('Uploading...');
                      try {
                        await window.SonicAPI.uploadSoundFont(file);
                        setSfUploadStatus('Uploaded: ' + file.name);
                        const data = await window.SonicAPI.listPlugins();
                        setLocalData(data);
                      } catch (err) {
                        setSfUploadStatus('Error: ' + err.message);
                      }
                    },
                    className: 'hidden'
                  })
                ),
                sfUploadStatus && React.createElement('p', { className: 'text-[10px] text-zinc-500' }, sfUploadStatus)
              )
          )
        )
      )
    ),
    // Status bar at bottom
    React.createElement('div', {
      className: 'px-5 py-2 bg-[#1a1a1a] border-t border-[#383838] flex items-center justify-between text-[10px] text-zinc-500'
    },
      React.createElement('span', null, 'VST: ', localData?.vst_instruments?.length || 0, ' | SoundFonts: ', localData?.soundfonts?.length || 0),
      React.createElement('span', null, 'Last scanned: ', new Date().toLocaleTimeString())
    ),
    // Delete confirmation modal
    sfToDelete && React.createElement('div', {
      className: 'fixed inset-0 z-[60] flex items-center justify-center bg-black/70',
      onClick: () => setSfToDelete(null)
    },
      React.createElement('div', {
        className: 'bg-[#262626] border border-[#383838] rounded-xl shadow-2xl w-full max-w-sm p-5 text-slate-200',
        onClick: e => e.stopPropagation()
      },
        React.createElement('h3', { className: 'text-sm font-bold text-red-400 mb-3' }, 'Delete SoundFont?'),
        React.createElement('p', { className: 'text-xs text-zinc-400 mb-1' }, 'Are you sure you want to delete:'),
        React.createElement('p', { className: 'text-sm font-semibold text-slate-200 mb-4' }, sfToDelete.display || sfToDelete.name || sfToDelete.id),
        React.createElement('div', { className: 'flex justify-end gap-2' },
          React.createElement('button', {
            onClick: () => setSfToDelete(null),
            className: 'px-4 py-2 text-xs rounded bg-zinc-700 hover:bg-zinc-600 text-zinc-300 transition'
          }, 'Cancel'),
          React.createElement('button', {
            onClick: async () => {
              try {
                if (window.SonicAPI.deleteSoundFont) {
                  await window.SonicAPI.deleteSoundFont(sfToDelete.id);
                }
                const data = await window.SonicAPI.listPlugins();
                setLocalData(data);
                setSfToDelete(null);
                window.showToast && window.showToast('SoundFont deleted.', 'info');
              } catch (err) {
                window.showToast && window.showToast('Delete failed: ' + err.message, 'error');
                setSfToDelete(null);
              }
            },
            className: 'px-4 py-2 text-xs rounded bg-red-700 hover:bg-red-600 text-white font-semibold transition'
          }, 'Delete')
        )
      )
    )
  );
};

// ── Bridge VST3 auto-load: dùng chung cho preload project + bridge
// reconnect + nút GUI (ensure-load rồi mới open — tránh nền trắng khi bridge
// restart làm mất instrument). __bridgeLoadedChannels (ch→true) đánh dấu đã
// load trong session; reset khi bridge down/restart → load lại đúng lúc.
const loadVstToBridge = async (track) => {
  if (!window.NativeBridgeService || !window.SonicMidiRouter || !window.SonicMidiRouter.isBridgeActive()) return false;
  const instrumentId = track.instrumentId || (track.synth_engine && track.synth_engine.plugin_id);
  if (!instrumentId || (typeof instrumentId === 'string' && instrumentId.startsWith('sf_'))) return false;
  if (!window.__bridgeLoadedChannels) window.__bridgeLoadedChannels = {};
  const bch = window.SonicMidiRouter.allocateChannel(track.id, false);
  if (window.__bridgeLoadedChannels[bch]) return true;
  try {
    // V8 bug 5: engine _resolve_bridge_asset chi match FILE theo filename —
    // KHONG walk folder VST3 ("X.vst3/X.vst3.dll") → 404. Dung path da luu
    // (synth_engine.plugin_path) hoac cache tu listPlugins.
    let knownPath = track.synth_engine && track.synth_engine.plugin_path;
    if (!knownPath && window.__bridgePluginPaths) knownPath = window.__bridgePluginPaths[instrumentId];
    const r = await window.SonicAPI.bridgeLoad({ name: instrumentId, path: knownPath || null, instrumentType: 'VST3', channel: bch });
    let p = r && r.path ? r.path : null;
    if (!p && knownPath) p = knownPath;
    if (!p && window.SonicAPI.listPlugins) {
      try {
        const pl = await window.SonicAPI.listPlugins();
        const v = ((pl && pl.vst_instruments) || []).find(x => x.id === instrumentId || x.name === instrumentId);
        if (v && v.path) {
          p = v.path;
          if (!window.__bridgePluginPaths) window.__bridgePluginPaths = {};
          window.__bridgePluginPaths[instrumentId] = v.path;
        }
      } catch (e2) { console.warn('[Bridge] listPlugins fallback fail:', e2); }
    }
    if (!p) return false;
    const ok = await window.NativeBridgeService.loadInstrument(p, 'VST3', bch);
    if (ok) window.__bridgeLoadedChannels[bch] = true;
    console.log('[Bridge] auto-load track', track.id, '-> VST3 ch', bch, ok ? 'OK' : 'FAIL', p);
    return !!ok;
  } catch (e) { console.warn('[Bridge] loadVstToBridge fail:', track.id, e); return false; }
};
const ensureAndOpenVstGui = async (trackId, instrumentId) => {
  if (!window.SonicMidiRouter || !window.SonicMidiRouter.isBridgeActive() || !window.NativeBridgeService) return;
  if (!instrumentId || (typeof instrumentId === 'string' && instrumentId.startsWith('sf_'))) return;
  if (!window.__bridgeLoadedChannels) window.__bridgeLoadedChannels = {};
  const bch = window.SonicMidiRouter.allocateChannel(trackId, false);
  if (!window.__bridgeLoadedChannels[bch]) {
    const ok = await loadVstToBridge({ id: trackId, instrumentId });
    if (!ok) return;
  }
  try { await openVstGuiRetry(instrumentId, bch, 20); } catch (e) { console.warn('[Bridge] openNativeGUI fail:', e); }
};
// V8 bug 4 + V9 bug 3/6/7: C++ load instrument ASYNC (worker thread) — OPEN_GUI
// som → C++ defer (chi log stderr, invoke van tra Ok) → retry PHAI chay DU so
// lan, khong `return true` som. Mo GUI co the can cho den khi assign() xong
// (Nexus ~1-2s) — 20 lan x 500ms, window reuse khi load xong.
const openVstGuiRetry = async (instrumentId, bch, attempts) => {
  const n = (attempts && attempts > 0) ? attempts : 20;
  for (let i = 0; i < n; i++) {
    try { await window.NativeBridgeService.openNativeGUI(instrumentId, bch); }
    catch (e) { console.warn('[Bridge] openNativeGUI retry', i + 1, e); }
    await new Promise(r => setTimeout(r, 500));
  }
  return true;
};
// Module-level so both ProfileModal (open project) and App (auto-restore) can
// warm the FluidSynth font cache for each track's instrument. This only loads
// the soundfonts; the per-track channel re-selection happens at note time.
const preloadTrackInstruments = async (tracks) => {
  const list = tracks || [];
  if (window.SonicSF && window.SonicSF.selectInstrument) {
    for (let i = 0; i < list.length; i++) {
      const t = list[i];
      const se = t.synth_engine;
      const sfId = (se && se.soundfont_id) || t.soundfont_id;
      if (!sfId) continue;
      const bank = se ? (se.soundfont_bank !== undefined ? se.soundfont_bank : 0) : (t.soundfont_bank !== undefined ? t.soundfont_bank : 0);
      const prog = se ? (se.soundfont_program !== undefined ? se.soundfont_program : 0) : (t.soundfont_program !== undefined ? t.soundfont_program : 0);
      const ch = t.midiChannel !== undefined ? t.midiChannel : (i % 16);
      try {
        await window.SonicSF.selectInstrument(ch, bank, prog, sfId);
      } catch (e) {}
    }
  }
  // Lỗi 3: mở lại project → VST3 đã lưu phải load lại vào bridge (không mở GUI).
  if (window.NativeBridgeService && window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) {
    for (let i = 0; i < list.length; i++) {
      const t = list[i];
      const inst = t.instrumentId || (t.synth_engine && t.synth_engine.plugin_id);
      if (!inst || (typeof inst === 'string' && inst.startsWith('sf_'))) continue;
      try { await loadVstToBridge(t); } catch (e) {}
    }
  }
};

const ProfileModal = ({
  isOpen,
  onClose,
  tracks,
  setTracks,
  setSelectedTrackId,
  projectName,
  setProjectName,
  currentProjectId,
  setCurrentProjectId,
  showToast,
  loadAudioBuffersForTracks,
  setAppWarningModal,
  bpm,
  setBpm,
  setMasteringSettings,
  setSessionTabs,
  setSubTabs,
  trackMidiChannelsRef
}) => {
  if (!isOpen) return null;
  const [activeTab, setActiveTab] = useState('account');
  const [profile, setProfile] = useState(null);
  const [oldPassword, setOldPassword] = useState('');
  const [newPassword, setNewPassword] = useState('');
  const [msg, setMsg] = useState('');
  const [error, setError] = useState('');
  const [loading, setLoading] = useState(false);
  const [dragOfs, setDragOfs] = useState({ x: 0, y: 0 });
  const dragRef = useRef({ active: false, startX: 0, startY: 0, ofsX: 0, ofsY: 0 });

  // Projects list state
  const [projectsList, setProjectsList] = useState([]);
  const [loadingProjects, setLoadingProjects] = useState(false);

  // Files list state
  const [filesList, setFilesList] = useState([]);
  const [loadingFiles, setLoadingFiles] = useState(false);

  // Confirmation modal state
  const [confirmModal, setConfirmModal] = useState(null);

  // Backup state
  const [expandedBackupId, setExpandedBackupId] = useState(null);
  const [backupsMap, setBackupsMap] = useState({}); // project_id -> [backups]
  const [loadingBackups, setLoadingBackups] = useState({}); // project_id -> bool
  const [backupMaxCount, setBackupMaxCount] = useState(() => parseInt(localStorage.getItem('sonic_backup_max_count') || '10'));
  const [showBackupConfig, setShowBackupConfig] = useState(false);

  const handleDragStart = (e) => {
    const r = dragRef.current;
    r.active = true;
    r.startX = e.clientX;
    r.startY = e.clientY;
    r.ofsX = dragOfs.x;
    r.ofsY = dragOfs.y;
    const onMove = (ev) => {
      if (!r.active) return;
      setDragOfs({ x: r.ofsX + ev.clientX - r.startX, y: r.ofsY + ev.clientY - r.startY });
    };
    const onUp = () => {
      r.active = false;
      document.removeEventListener('mousemove', onMove);
      document.removeEventListener('mouseup', onUp);
    };
    document.addEventListener('mousemove', onMove);
    document.addEventListener('mouseup', onUp);
  };

  useEffect(() => {
    if (isOpen) {
      fetchProfile();
      if (activeTab === 'projects') fetchProjects();
      if (activeTab === 'files') fetchFiles();
    }
  }, [isOpen, activeTab]);

  const fetchProfile = async () => {
    try {
      const data = await window.SonicAPI.getProfile();
      setProfile(data);
    } catch (e) {
      setError(e.message || 'Không thể tải thông tin profile');
    }
  };

  const fetchProjects = async () => {
    setLoadingProjects(true);
    try {
      const data = await window.SonicAPI.listCloudProjects();
      setProjectsList(data || []);
    } catch (e) {
      showToast(e.message || 'Không thể tải danh sách dự án', 'error');
    } finally {
      setLoadingProjects(false);
    }
  };

  const fetchFiles = async () => {
    setLoadingFiles(true);
    try {
      const activeFileIds = tracks.map(t => t.serverFileId).filter(Boolean);
      const data = await window.SonicAPI.listMyFiles(activeFileIds);
      setFilesList(data || []);
    } catch (e) {
      showToast(e.message || 'Không thể tải danh sách tệp tin', 'error');
    } finally {
      setLoadingFiles(false);
    }
  };

  const fetchBackups = async (projectId) => {
    setLoadingBackups(prev => ({ ...prev, [projectId]: true }));
    try {
      const data = await window.SonicAPI.listBackups(projectId);
      setBackupsMap(prev => ({ ...prev, [projectId]: data || [] }));
    } catch (e) {
      showToast(e.message || 'Không thể tải danh sách backup', 'error');
    } finally {
      setLoadingBackups(prev => ({ ...prev, [projectId]: false }));
    }
  };

  const handleDeleteBackup = async (backupId) => {
    try {
      await window.SonicAPI.deleteBackup(backupId);
      setBackupsMap(prev => {
        const next = { ...prev };
        Object.keys(next).forEach(pid => {
          next[pid] = next[pid].filter(b => b.id !== backupId);
        });
        return next;
      });
      fetchProjects();
      showToast('Đã xóa bản backup', 'info');
    } catch (e) {
      showToast(e.message || 'Lỗi xóa backup', 'error');
    }
  };

  const handleCleanupBackups = async () => {
    try {
      const res = await window.SonicAPI.cleanupBackups(backupMaxCount);
      setBackupsMap({});
      fetchProjects();
      showToast(`Đã dọn dẹp ${res.deleted} bản backup cũ (giữ lại ${res.keep})`, 'info');
    } catch (e) {
      showToast(e.message || 'Lỗi dọn dẹp backup', 'error');
    }
  };

  const handleOpenProject = async (projectId, projectName) => {
    setAppWarningModal({
      title: "Mở dự án",
      message: "Bạn có muốn mở dự án này? Các thay đổi chưa lưu trên workspace hiện tại sẽ bị mất.",
      isAlert: false,
      onConfirm: async () => {
        try {
          const proj = await window.SonicAPI.getCloudProject(projectId);
          const parsed = JSON.parse(proj.data_json);
          let restoredTracks = [];
          let restoredBpm = bpm;
          let restoredSessionTabs = [];
          let restoredSubTabs = [];
          if (parsed.main_session) {
            const result = deserializeProjectFromSchema(parsed);
            restoredTracks = result.tracks;
            restoredBpm = result.bpm;
            restoredSessionTabs = result.sessionTabs;
            restoredSubTabs = result.subTabs;
            if (result.masteringSettings) {
              setMasteringSettings(result.masteringSettings);
            } else {
              setMasteringSettings({
                masterConnected: false,
                activeModule: 'eq',
                eqActive: true,
                imagerActive: true,
                maximizerActive: true,
                eqLowGain: 1.5,
                eqMid1Gain: -1.0,
                eqMid2Gain: 2.0,
                eqHighGain: 1.8,
                w1: 0,
                w2: 115,
                w3: 135,
                w4: 150,
                imagerScale: 'v2',
                maxGain: 5.4,
                maxUpward: 2.0,
                maxSoftClip: 15,
                maxTransient: 25,
                ceiling: -0.1,
                isBypassed: false,
                chain: DEFAULT_MASTER_CHAIN.map(m => ({ ...m })),
                compActive: false, compThreshold: -16, compRatio: 3, compMakeup: 0,
                limActive: false, limThreshold: -1.0,
                excActive: false, excDrive: 40,
                rebalActive: false, rebalMid: 0, rebalSide: 0,
              });
            }
          } else {
            restoredTracks = (parsed.tracks || []).map(t => {
              const { height: _h, ...rest } = t;
              return {
                ...rest,
                buffer: null,
                channelInfo: t.channelInfo || null,
                clips: t.clips || [],
                serverFileId: t.serverFileId || null
              };
            });
          }
          setTracks(restoredTracks);
          loadAudioBuffersForTracks(restoredTracks).catch(function(err) { console.warn('loadAudioBuffersForTracks error:', err); });
          trackMidiChannelsRef.current = {};
          preloadTrackInstruments(restoredTracks).catch(function(err) { console.warn('preloadTrackInstruments error:', err); });
          setBpm(restoredBpm.toString());
          setSelectedTrackId(restoredTracks[0]?.id || '1');
          setProjectName(proj.name);
          setCurrentProjectId(proj.id);
          if (restoredSessionTabs.length > 0) {
            setSessionTabs(restoredSessionTabs);
          }
          if (restoredSubTabs.length > 0) {
            setSubTabs(restoredSubTabs);
          }
          localStorage.setItem('sonic_project_name', proj.name);
          localStorage.setItem('sonic_project_id', proj.id);
          showToast(`Đã nạp dự án "${proj.name}" thành công!`, "success");
        } catch (e) {
          showToast(e.message || "Lỗi khi nạp dự án", "error");
        }
      }
    });
  };

  const handleDeleteProject = async (projectId, e) => {
    e.stopPropagation();
    setConfirmModal({
      title: "Xóa dự án Cloud",
      message: "Bạn có chắc chắn muốn xóa dự án này khỏi Cloud? Hành động này không thể hoàn tác.",
      onConfirm: async () => {
        try {
          await window.SonicAPI.deleteCloudProject(projectId);
          showToast("Đã xóa dự án thành công!", "success");
          fetchProjects();
          fetchProfile();
        } catch (err) {
          showToast(err.message || "Lỗi khi xóa dự án", "error");
        }
      }
    });
  };

  const handleDeleteFile = async (fileId) => {
    if (!confirm(`Bạn có chắc chắn muốn xóa tệp tin ${fileId}?`)) return;
    try {
      await window.SonicAPI.deleteMyFile(fileId);
      showToast("Đã xóa tệp tin thành công!", "success");
      fetchFiles();
      fetchProfile();
    } catch (err) {
      showToast(err.message || "Lỗi khi xóa tệp tin", "error");
    }
  };

  const handleCleanUnusedFiles = async () => {
    const unusedFiles = filesList.filter(f => !f.is_in_use);
    if (unusedFiles.length === 0) {
      showToast("Không có tập tin rác nào để dọn dẹp.", "info");
      return;
    }
    if (!confirm(`Bạn có chắc chắn muốn xóa tất cả ${unusedFiles.length} tập tin rác để giải phóng dung lượng?`)) return;
    let successCount = 0;
    for (const file of unusedFiles) {
      try {
        await window.SonicAPI.deleteMyFile(file.file_id);
        successCount++;
      } catch (e) {
        console.error("Lỗi xóa file rác: ", file.file_id, e);
      }
    }
    showToast(`Đã dọn dẹp thành công ${successCount}/${unusedFiles.length} tập tin rác!`, "success");
    fetchFiles();
    fetchProfile();
  };

  const handleChangePassword = async e => {
    e.preventDefault();
    setMsg('');
    setError('');
    setLoading(true);
    try {
      const res = await window.SonicAPI.changePassword(oldPassword, newPassword);
      setMsg(res.message || 'Đổi mật khẩu thành công!');
      setOldPassword('');
      setNewPassword('');
    } catch (err) {
      setError(err.message || 'Lỗi khi đổi mật khẩu');
    } finally {
      setLoading(false);
    }
  };

  const modalStyle = {
    left: `calc(50% + ${dragOfs.x}px)`,
    top: `calc(50% + ${dragOfs.y}px)`,
    transform: 'translate(-50%, -50%)'
  };
  return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
    key: "backdrop",
    className: "fixed inset-0 z-40 bg-black/70 backdrop-blur-sm",
    onClick: onClose
  }), confirmModal && /*#__PURE__*/React.createElement("div", {
    key: "confirm-overlay",
    className: "fixed inset-0 z-[60] flex items-center justify-center bg-black/50"
  }, /*#__PURE__*/React.createElement("div", {
    className: "bg-[#262626] border border-[#383838] rounded-lg shadow-2xl p-5 max-w-sm w-full text-slate-200"
  }, /*#__PURE__*/React.createElement("h4", {
    className: "text-sm font-bold text-rose-400 mb-2"
  }, confirmModal.title), /*#__PURE__*/React.createElement("p", {
    className: "text-xs text-slate-300 mb-4"
  }, confirmModal.message), /*#__PURE__*/React.createElement("div", {
    className: "flex justify-end gap-2"
  }, /*#__PURE__*/React.createElement("button", {
    onClick: () => setConfirmModal(null),
    className: "px-3 py-1.5 bg-zinc-700 hover:bg-zinc-600 text-slate-300 rounded text-xs font-semibold"
  }, confirmModal.cancelText || "Hủy"), /*#__PURE__*/React.createElement("button", {
    onClick: () => { const fn = confirmModal.onConfirm; setConfirmModal(null); fn(); },
    className: "px-3 py-1.5 bg-rose-700 hover:bg-rose-600 text-white rounded text-xs font-semibold"
  }, confirmModal.confirmText || "Xác nhận xóa")))), /*#__PURE__*/React.createElement("div", {
    key: "dialog",
    className: "fixed z-50 bg-[#262626] border border-[#383838] rounded-xl shadow-2xl w-full max-w-2xl p-6 text-slate-200 flex flex-col max-h-[85vh]",
    style: modalStyle
  }, /*#__PURE__*/React.createElement("div", {
    className: "flex justify-between items-center pb-3 border-b border-[#383838] shrink-0 cursor-grab active:cursor-grabbing select-none",
    onMouseDown: handleDragStart
  }, /*#__PURE__*/React.createElement("h3", {
    className: "text-md font-bold text-teal-400 flex items-center gap-1.5"
  }, "👤 Hồ Sơ Cá Nhân & Quản Lý Dự Án"), /*#__PURE__*/React.createElement("button", {
    onClick: onClose,
    className: "text-slate-400 hover:text-slate-200"
  }, "✕")), /*#__PURE__*/React.createElement("div", {
    className: "flex gap-2 border-b border-[#383838] py-2 shrink-0 text-xs font-semibold"
  }, [
    /*#__PURE__*/React.createElement("button", {
    key: "tab-acc",
    onClick: () => setActiveTab('account'),
    className: `px-3 py-1.5 rounded transition ${activeTab === 'account' ? 'bg-teal-950/60 text-teal-300 border border-teal-800' : 'text-slate-400 hover:text-slate-200'}`
  }, "Tài Khoản"),
    /*#__PURE__*/React.createElement("button", {
    key: "tab-proj",
    onClick: () => setActiveTab('projects'),
    className: `px-3 py-1.5 rounded transition ${activeTab === 'projects' ? 'bg-teal-950/60 text-teal-300 border border-teal-800' : 'text-slate-400 hover:text-slate-200'}`
  }, "Dự Án Cloud"),
    /*#__PURE__*/React.createElement("button", {
    key: "tab-files",
    onClick: () => setActiveTab('files'),
    className: `px-3 py-1.5 rounded transition ${activeTab === 'files' ? 'bg-teal-950/60 text-teal-300 border border-teal-800' : 'text-slate-400 hover:text-slate-200'}`
  }, "Tập Tin Của Tôi")
  ]), /*#__PURE__*/React.createElement("div", {
    className: "flex-1 overflow-y-auto pt-4 space-y-4 pr-1 min-h-[300px]"
  }, activeTab === 'account' && profile ? [
    /*#__PURE__*/React.createElement("div", {
    key: "quota-info",
    className: "bg-[#1e1e1e] p-4 rounded-lg border border-[#333] grid grid-cols-2 gap-4 text-xs"
  }, [
      /*#__PURE__*/React.createElement("div", { key: "username" }, [
        /*#__PURE__*/React.createElement("span", { className: "text-slate-500 block" }, "Tên người dùng"),
        /*#__PURE__*/React.createElement("span", { className: "font-bold text-teal-300 text-sm" }, profile.username)
  ]),
      /*#__PURE__*/React.createElement("div", { key: "role" }, [
        /*#__PURE__*/React.createElement("span", { className: "text-slate-500 block" }, "Vai trò"),
        /*#__PURE__*/React.createElement("span", { className: "uppercase font-semibold text-amber-400" }, profile.role)
  ]),
      /*#__PURE__*/React.createElement("div", { key: "email" }, [
        /*#__PURE__*/React.createElement("span", { className: "text-slate-500 block" }, "Email"),
        /*#__PURE__*/React.createElement("span", null, profile.email)
  ]),
      /*#__PURE__*/React.createElement("div", { key: "quota" }, [
        /*#__PURE__*/React.createElement("span", { className: "text-slate-500 block" }, "Dung lượng Quota"),
        /*#__PURE__*/React.createElement("span", { className: "font-semibold text-slate-200" }, `${profile.quota.used_mb} MB / ${profile.quota.storage_limit_mb} MB`)
  ])
  ]),
    /*#__PURE__*/React.createElement("div", { key: "progress" }, [
      /*#__PURE__*/React.createElement("div", { className: "flex justify-between text-xs mb-1" }, [
        /*#__PURE__*/React.createElement("span", { className: "text-slate-400" }, "Tiến trình sử dụng bộ nhớ Server"),
        /*#__PURE__*/React.createElement("span", { className: "font-bold text-teal-400" }, `${(profile.quota.used_mb / profile.quota.storage_limit_mb * 100).toFixed(1)}%`)
  ]),
      /*#__PURE__*/React.createElement("div", { className: "w-full h-2 bg-slate-800 rounded-full overflow-hidden" }, [
        /*#__PURE__*/React.createElement("div", {
    className: "h-full bg-teal-500 rounded-full transition-all duration-300",
    style: { width: `${Math.min(100, profile.quota.used_mb / profile.quota.storage_limit_mb * 100)}%` }
  })
  ])
  ]),
    /*#__PURE__*/React.createElement("form", {
    key: "pwd-form",
    onSubmit: handleChangePassword,
    className: "pt-4 border-t border-[#383838] space-y-3"
  }, [
      /*#__PURE__*/React.createElement("h4", { className: "text-xs font-bold text-slate-300 uppercase" }, "Thay Đổi Mật Khẩu"),
    msg && /*#__PURE__*/React.createElement("div", { className: "p-2 bg-emerald-950/60 border border-emerald-700 text-emerald-300 rounded text-xs" }, msg),
    error && /*#__PURE__*/React.createElement("div", { className: "p-2 bg-red-950/60 border border-red-700 text-red-300 rounded text-xs" }, error),
      /*#__PURE__*/React.createElement("div", { key: "old" }, [
        /*#__PURE__*/React.createElement("label", { className: "block text-xs text-slate-400 mb-1" }, "Mật khẩu cũ"),
        /*#__PURE__*/React.createElement("input", {
      type: "password",
      autoComplete: "current-password",
      required: true,
      value: oldPassword,
      onChange: e => setOldPassword(e.target.value),
      className: "w-full bg-[#1e1e1e] border border-[#383838] rounded px-3 py-1.5 text-xs focus:outline-none focus:border-teal-500"
    })
    ]),
      /*#__PURE__*/React.createElement("div", { key: "new" }, [
        /*#__PURE__*/React.createElement("label", { className: "block text-xs text-slate-400 mb-1" }, "Mật khẩu mới"),
        /*#__PURE__*/React.createElement("input", {
      type: "password",
      autoComplete: "new-password",
      required: true,
      value: newPassword,
      onChange: e => setNewPassword(e.target.value),
      className: "w-full bg-[#1e1e1e] border border-[#383838] rounded px-3 py-1.5 text-xs focus:outline-none focus:border-teal-500"
    })
    ]),
      /*#__PURE__*/React.createElement("button", {
      type: "submit",
      disabled: loading,
      className: "w-full py-1.5 bg-teal-600 hover:bg-teal-500 text-white font-semibold text-xs rounded transition"
    }, loading ? 'Đang cập nhật...' : 'Cập Nhật Mật Khẩu')
  ])
  ] : activeTab === 'projects' ? [
    loadingProjects ? /*#__PURE__*/React.createElement("div", { key: "loading", className: "text-center py-8 text-xs text-zinc-500" }, "Đang tải danh sách dự án...") :
      projectsList.length === 0 ? /*#__PURE__*/React.createElement("div", { key: "empty", className: "text-center py-8 text-xs text-zinc-500" }, "Bạn chưa có dự án nào lưu trên Cloud.") :
    /*#__PURE__*/React.createElement(React.Fragment, { key: "list" }, [
      /*#__PURE__*/React.createElement("div", { key: "backup-config-bar", className: "flex items-center justify-between bg-zinc-900/60 p-2 rounded border border-zinc-800 mb-2 text-xs" }, [
        /*#__PURE__*/React.createElement("div", { className: "flex items-center gap-2" }, [
          /*#__PURE__*/React.createElement("span", { className: "text-zinc-400 text-[10px]" }, "⚙️ Tự động lưu 5 phút / Backup 30 phút"),
          /*#__PURE__*/React.createElement("button", {
            onClick: (e) => { e.stopPropagation(); setShowBackupConfig(!showBackupConfig); },
            className: "px-2 py-0.5 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 rounded text-[10px] border border-zinc-700"
          }, showBackupConfig ? "ẨN" : "CẤU HÌNH")
        ]),
        /*#__PURE__*/React.createElement("button", {
          onClick: (e) => { e.stopPropagation(); handleCleanupBackups(); },
          className: "px-2 py-0.5 bg-amber-800 hover:bg-amber-700 text-amber-200 rounded text-[10px] border border-amber-700"
        }, "🧹 DỌN BACKUP")
      ]),
      showBackupConfig && /*#__PURE__*/React.createElement("div", { key: "backup-config-detail", className: "bg-[#18181b] border border-zinc-800 rounded p-3 mb-2 text-xs" }, [
        /*#__PURE__*/React.createElement("div", { className: "flex items-center justify-between mb-2" }, [
          /*#__PURE__*/React.createElement("label", { className: "text-zinc-300 font-semibold" }, "Số bản backup tối đa:"),
          /*#__PURE__*/React.createElement("div", { className: "flex items-center gap-2" }, [
            /*#__PURE__*/React.createElement("input", {
              type: "range",
              min: 5, max: 20,
              value: backupMaxCount,
              onChange: (e) => { const v = parseInt(e.target.value); setBackupMaxCount(v); localStorage.setItem('sonic_backup_max_count', v.toString()); },
              className: "w-24 accent-amber-500"
            }),
            /*#__PURE__*/React.createElement("span", { className: "text-amber-400 font-bold w-6 text-center" }, backupMaxCount)
          ])
        ]),
        /*#__PURE__*/React.createElement("p", { className: "text-[10px] text-zinc-500" }, "Mỗi dự án sẽ giữ tối đa số bản backup này. Backup cũ nhất sẽ tự động bị xóa khi vượt quá giới hạn.")
      ]),
      /*#__PURE__*/React.createElement("div", { className: "space-y-1.5" }, projectsList.map(proj => /*#__PURE__*/React.createElement("div", { key: proj.id }, [
        /*#__PURE__*/React.createElement("div", {
          onClick: () => handleOpenProject(proj.id),
          className: "flex items-center justify-between p-3 bg-[#1e1e1e] hover:bg-zinc-800 rounded border border-zinc-800 transition cursor-pointer text-xs group"
        }, [
          /*#__PURE__*/React.createElement("div", { key: "meta" }, [
            /*#__PURE__*/React.createElement("div", { className: "font-bold text-slate-200 group-hover:text-teal-400" }, proj.name),
            /*#__PURE__*/React.createElement("div", { className: "text-[10px] text-zinc-500 mt-0.5" }, [
              `Dung lượng: ${proj.size_mb} MB | Cập nhật: ${new Date(proj.updated_at * 1000).toLocaleString()}`,
              proj.backup_count > 0 && ` | Backup: ${proj.backup_count}`
            ])
          ]),
          /*#__PURE__*/React.createElement("div", { key: "actions", className: "flex items-center gap-1.5" }, [
            /*#__PURE__*/React.createElement("button", {
              onClick: (e) => { e.stopPropagation(); if (expandedBackupId === proj.id) { setExpandedBackupId(null); } else { setExpandedBackupId(proj.id); fetchBackups(proj.id); } },
              className: `px-2 py-1 rounded font-semibold text-[10px] border ${expandedBackupId === proj.id ? 'bg-amber-800/80 text-amber-200 border-amber-700' : 'bg-zinc-800 hover:bg-zinc-700 text-zinc-400 border-zinc-700'}`
            }, `Backup (${proj.backup_count})`),
            /*#__PURE__*/React.createElement("button", {
              onClick: (e) => { e.stopPropagation(); handleOpenProject(proj.id); },
              className: "px-2.5 py-1 bg-teal-800/80 hover:bg-teal-700 text-teal-200 rounded font-semibold text-[10px]"
            }, "MỞ"),
            /*#__PURE__*/React.createElement("button", {
              onClick: (e) => handleDeleteProject(proj.id, e),
              className: "px-2 py-1 bg-rose-950/60 hover:bg-rose-900/80 text-rose-300 rounded font-semibold text-[10px]"
            }, "XÓA")
          ])
        ]),
        expandedBackupId === proj.id && /*#__PURE__*/React.createElement("div", { key: "backup-list", className: "ml-4 pl-3 border-l-2 border-amber-800/50 bg-[#161618] rounded-b p-2 mb-1" }, [
          loadingBackups[proj.id] ? /*#__PURE__*/React.createElement("div", { className: "text-[10px] text-zinc-500 py-2 text-center" }, "Đang tải...") :
            (!backupsMap[proj.id] || backupsMap[proj.id].length === 0) ? /*#__PURE__*/React.createElement("div", { className: "text-[10px] text-zinc-500 py-2 text-center" }, "Chưa có bản backup nào.") :
            /*#__PURE__*/React.createElement("div", { className: "space-y-1 max-h-48 overflow-y-auto" }, backupsMap[proj.id].map(b => /*#__PURE__*/React.createElement("div", {
              key: b.id,
              className: "flex items-center justify-between py-1.5 px-2 bg-[#1e1e22] rounded border border-zinc-800"
            }, [
              /*#__PURE__*/React.createElement("div", { key: "info", className: "flex-1 min-w-0" }, [
                /*#__PURE__*/React.createElement("div", { className: "text-[10px] text-zinc-300 truncate" }, b.name),
                /*#__PURE__*/React.createElement("div", { className: "text-[9px] text-zinc-500 mt-0.5" }, `${b.size_mb} MB | ${new Date(b.created_at * 1000).toLocaleString()}`)
              ]),
              /*#__PURE__*/React.createElement("button", {
                onClick: (e) => { e.stopPropagation(); handleDeleteBackup(b.id); },
                className: "px-1.5 py-0.5 bg-rose-950 hover:bg-rose-900 text-rose-300 rounded text-[9px] border border-rose-900 ml-2 shrink-0"
              }, "XÓA")
            ])))
        ])
      ])))
    ])
  ] : activeTab === 'files' ? [
    /*#__PURE__*/React.createElement("div", { key: "cleanup-header", className: "flex items-center justify-between bg-zinc-900/60 p-2.5 rounded border border-zinc-800 text-xs shrink-0 mb-3" }, [
      /*#__PURE__*/React.createElement("span", { className: "text-zinc-400 text-[10px]" }, "💡 Các tập tin không dùng trong dự án nào sẽ được đánh dấu 'Không dùng' và có thể xóa để giải phóng dung lượng."),
      /*#__PURE__*/React.createElement("button", {
    onClick: handleCleanUnusedFiles,
    className: "px-2.5 py-1 bg-rose-700 hover:bg-rose-600 text-white rounded font-bold text-[10px] uppercase flex items-center gap-1"
  }, "🧹 Dọn dẹp tệp rác")
  ]),
    loadingFiles ? /*#__PURE__*/React.createElement("div", { key: "loading", className: "text-center py-8 text-xs text-zinc-500" }, "Đang tải danh sách tập tin...") :
      filesList.length === 0 ? /*#__PURE__*/React.createElement("div", { key: "empty", className: "text-center py-8 text-xs text-zinc-500" }, "Chưa có tập tin nào tải lên hoặc tạo ra.") :
    /*#__PURE__*/React.createElement("div", { key: "list", className: "space-y-1.5" }, [
        filesList.map(file => /*#__PURE__*/React.createElement("div", {
          key: file.file_id,
          className: "flex items-center justify-between p-3 bg-[#1e1e1e] rounded border border-zinc-800 text-xs"
        }, [
        /*#__PURE__*/React.createElement("div", { key: "meta", className: "max-w-[70%]" }, [
          /*#__PURE__*/React.createElement("div", { className: "font-semibold text-slate-300 truncate" }, file.original_name || file.file_id),
          /*#__PURE__*/React.createElement("div", { className: "text-[10px] text-zinc-500 mt-0.5" }, `Loại: ${file.type} | Dung lượng: ${file.size_mb} MB | Tạo lúc: ${new Date(file.created_at * 1000).toLocaleString()}`)
        ]),
        /*#__PURE__*/React.createElement("div", { key: "actions", className: "flex items-center gap-2" }, [
          file.is_in_use ?
            /*#__PURE__*/React.createElement("span", { className: "px-2 py-0.5 bg-emerald-950 text-emerald-400 border border-emerald-900 rounded text-[9px] uppercase font-bold font-mono" }, "Đang dùng") :
            /*#__PURE__*/React.createElement("span", { className: "px-2 py-0.5 bg-zinc-850 text-zinc-400 border border-zinc-700 rounded text-[9px] uppercase font-bold font-mono" }, "Không dùng"),
          !file.is_in_use && /*#__PURE__*/React.createElement("button", {
            onClick: () => handleDeleteFile(file.file_id),
            className: "px-2 py-0.5 bg-rose-950 hover:bg-rose-900 text-rose-300 rounded font-semibold text-[10px] border border-rose-900"
          }, "Xóa")
        ])
        ]))
      ])
  ] : null)));
};
const SaveProjectModal = ({
  isOpen,
  onClose,
  onSaveCloud,
  onSaveLocal,
  projectName
}) => {
  if (!isOpen) return null;
  const [name, setName] = useState(projectName || '');
  const [saveType, setSaveType] = useState(localStorage.getItem('sonic_token') ? 'cloud' : 'local');
  const [cloudProjects, setCloudProjects] = useState([]);
  const [loading, setLoading] = useState(false);
  const [selectedExisting, setSelectedExisting] = useState(null);
  const [confirmOverwriteProject, setConfirmOverwriteProject] = useState(null);
  React.useEffect(function() {
    if (!isOpen) return;
    if (saveType === 'cloud' && window.SonicAPI && window.SonicAPI.listCloudProjects) {
      setLoading(true);
      window.SonicAPI.listCloudProjects().then(function(data) { setCloudProjects(data || []); }).catch(function() { setCloudProjects([]); }).finally(function() { setLoading(false); });
    }
  }, [isOpen, saveType]);
  const handleSubmit = (e) => {
    e.preventDefault();
    if (!name.trim()) return;
    var matched = null;
    for (var i = 0; i < cloudProjects.length; i++) { if (cloudProjects[i].name === name.trim()) { matched = cloudProjects[i]; break; } }
    if (matched) {
      setConfirmOverwriteProject(matched);
    } else {
      if (saveType === 'cloud') { onSaveCloud(name.trim(), null); onClose(); }
      else { onSaveLocal(name.trim()); onClose(); }
    }
  };
  if (confirmOverwriteProject) {
    return React.createElement("div", {
      className: "fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm"
    }, React.createElement("div", {
      className: "bg-[#262626] border border-[#383838] rounded-lg shadow-2xl p-5 max-w-sm w-full text-slate-200"
    }, React.createElement("h4", {
      className: "text-sm font-bold text-amber-400 mb-2"
    }, "Ghi \u0111\u00E8 d\u1EF1 \u00E1n"), React.createElement("p", {
      className: "text-xs text-slate-300 mb-4"
    }, "D\u1EF1 \u00E1n \"", confirmOverwriteProject.name, "\" \u0111\u00E3 t\u1ED3n t\u1EA1i tr\u00EAn Cloud. B\u1EA1n c\u00F3 ch\u1EAFc ch\u1EAFn mu\u1ED1n ghi \u0111\u00E8 n\u00F3 kh\u00F4ng?"), React.createElement("div", {
      className: "flex justify-end gap-2"
    }, React.createElement("button", {
      type: "button", onClick: function() { setConfirmOverwriteProject(null); },
      className: "px-3 py-1.5 bg-zinc-700 hover:bg-zinc-600 text-slate-300 rounded text-xs font-semibold"
    }, "Quay l\u1EA1i"), React.createElement("button", {
      type: "button", onClick: function() {
        setConfirmOverwriteProject(null);
        onSaveCloud(name.trim(), confirmOverwriteProject.id);
        onClose();
      },
      className: "px-3 py-1.5 bg-rose-700 hover:bg-rose-600 text-white rounded text-xs font-semibold"
    }, "X\u00E1c nh\u1EADn ghi \u0111\u00E8"))));
  }
  var filtered = cloudProjects.filter(function(p) { return p.name.toLowerCase().includes(name.toLowerCase()); });
  return /*#__PURE__*/React.createElement("div", {
    className: "fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm"
  }, /*#__PURE__*/React.createElement("div", {
    className: "bg-[#262626] border border-[#383838] rounded-xl shadow-2xl w-full max-w-lg p-6 text-slate-200"
  }, /*#__PURE__*/React.createElement("h3", {
    className: "text-sm font-bold text-teal-400 mb-4 uppercase"
  }, "L\u01B0u d\u1EF1 \u00E1n"), /*#__PURE__*/React.createElement("form", {
    onSubmit: handleSubmit,
    className: "space-y-4"
  }, [
    /*#__PURE__*/React.createElement("div", { key: "type-block" }, [
      /*#__PURE__*/React.createElement("div", { className: "grid grid-cols-2 gap-2 text-xs" }, [
        /*#__PURE__*/React.createElement("button", {
          key: "btn-cloud", type: "button", onClick: function() { setSaveType('cloud'); setSelectedExisting(null); },
          className: "py-2 rounded border flex flex-col items-center gap-1 font-semibold transition " + (saveType === 'cloud' ? 'bg-teal-950/60 border-teal-500 text-teal-300' : 'bg-zinc-800 border-zinc-700 hover:bg-zinc-750 text-zinc-400')
        }, [/*#__PURE__*/React.createElement("span", { key: "title" }, "\u2601\uFE0F L\u01B0u Cloud"), /*#__PURE__*/React.createElement("span", { key: "desc", className: "text-[9px] font-normal text-zinc-500" }, "L\u01B0u l\u00EAn server")]),
        /*#__PURE__*/React.createElement("button", {
          key: "btn-local", type: "button", onClick: function() { setSaveType('local'); setSelectedExisting(null); },
          className: "py-2 rounded border flex flex-col items-center gap-1 font-semibold transition " + (saveType === 'local' ? 'bg-teal-950/60 border-teal-500 text-teal-300' : 'bg-zinc-800 border-zinc-700 hover:bg-zinc-750 text-zinc-400')
        }, [/*#__PURE__*/React.createElement("span", { key: "title" }, "\U0001F5C2\uFE0F L\u01B0u Local"), /*#__PURE__*/React.createElement("span", { key: "desc", className: "text-[9px] font-normal text-zinc-500" }, "L\u01B0u xu\u1ED1ng t\u1EC7p .sfs")])
      ])
    ]),
    /*#__PURE__*/React.createElement("div", { key: "name-block" }, [
      /*#__PURE__*/React.createElement("label", { className: "block text-[10px] text-zinc-400 uppercase font-bold mb-1" }, "T\u00EAn d\u1EF1 \u00E1n"),
      /*#__PURE__*/React.createElement("input", {
        key: "name-input", type: "text", placeholder: "Nh\u1EADp t\u00EAn d\u1EF1 \u00E1n...", required: true,
        value: name,
        onChange: function(e) { setName(e.target.value); setSelectedExisting(null); },
        className: "w-full bg-[#1e1e1e] border border-[#383838] rounded px-3 py-1.5 text-xs text-slate-200 focus:outline-none focus:border-teal-500 font-bold", autoFocus: true
      })
    ]),
    saveType === 'cloud' && /*#__PURE__*/React.createElement("div", { key: "cloud-list", className: "max-h-40 overflow-y-auto -mx-1" },
      loading ? [/*#__PURE__*/React.createElement("div", { key: "l", className: "text-center py-3 text-xs text-zinc-500" }, "\u0110ang t\u1EA3i danh s\u00E1ch d\u1EF1 \u00E1n...")]
      : filtered.length === 0 ? [/*#__PURE__*/React.createElement("div", { key: "e", className: "text-center py-3 text-xs text-zinc-500" }, "Ch\u01B0a c\u00F3 d\u1EF1 \u00E1n n\u00E0o.")]
      : filtered.map(function(p) {
          var isSelected = selectedExisting && selectedExisting.id === p.id;
          var isExactMatch = p.name === name.trim();
          return /*#__PURE__*/React.createElement("div", {
            key: p.id,
            onClick: function() { setName(p.name); setSelectedExisting(p); },
            className: "flex items-center justify-between px-2 py-1.5 rounded cursor-pointer text-xs border transition " + (isSelected ? 'bg-amber-950/60 border-amber-600 text-amber-300' : 'bg-[#1e1e1e] hover:bg-zinc-800 border-transparent text-slate-300')
          }, [/*#__PURE__*/React.createElement("span", { key: "n", className: "font-semibold truncate" }, p.name), /*#__PURE__*/React.createElement("span", { key: "t", className: "text-[9px] text-zinc-500 shrink-0 ml-2" }, isExactMatch ? "S\u1EBD ghi \u0111\u00E8" : "Ch\u1ECDn")]);
        })
    ),
    /*#__PURE__*/React.createElement("div", { key: "actions", className: "flex justify-end gap-2 text-xs" }, [
      /*#__PURE__*/React.createElement("button", { key: "cancel", type: "button", onClick: onClose, className: "px-3 py-1.5 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 rounded" }, "H\u1EE7y"),
      selectedExisting ? /*#__PURE__*/React.createElement("button", { key: "back", type: "button", onClick: function() { setName(''); setSelectedExisting(null); }, className: "px-3 py-1.5 bg-zinc-700 hover:bg-zinc-600 text-slate-300 rounded text-xs font-semibold" }, "Quay l\u1EA1i") : null,
      /*#__PURE__*/React.createElement("button", { key: "save", type: "submit", className: "px-3 py-1.5 bg-teal-600 hover:bg-teal-500 text-white rounded font-semibold" }, selectedExisting ? "Ghi \u0111\u00E8" : "L\u01B0u")
    ])
  ])));
};

const OpenProjectModal = ({ isOpen, onClose, onOpenCloud, onOpenLocal }) => {
  if (!isOpen) return null;
  const [tab, setTab] = useState('cloud');
  const [projects, setProjects] = useState([]);
  const [loading, setLoading] = useState(false);
  React.useEffect(function() {
    if (!isOpen) return;
    if (tab === 'cloud') {
      setLoading(true);
      var api = window.SonicAPI;
      if (api && api.listCloudProjects) {
        api.listCloudProjects().then(function(data) { setProjects(data || []); }).catch(function() { setProjects([]); }).finally(function() { setLoading(false); });
      } else { setLoading(false); }
    }
  }, [isOpen, tab]);
  return React.createElement("div", {
    className: "fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm"
  }, React.createElement("div", {
    className: "bg-[#262626] border border-[#383838] rounded-xl shadow-2xl w-full max-w-lg p-6 text-slate-200"
  }, React.createElement("h3", {
    className: "text-sm font-bold text-teal-400 mb-4 uppercase"
  }, "Mở dự án"), React.createElement("div", {
    className: "grid grid-cols-2 gap-2 text-xs mb-4"
  }, [
    React.createElement("button", {
      key: "cloud-tab", type: "button", onClick: function() { setTab('cloud'); },
      className: "py-2 rounded border flex flex-col items-center gap-1 font-semibold transition " + (tab === 'cloud' ? 'bg-teal-950/60 border-teal-500 text-teal-300' : 'bg-zinc-800 border-zinc-700 hover:bg-zinc-750 text-zinc-400')
    }, [React.createElement("span", { key: "t" }, "☁️ Cloud"), React.createElement("span", { key: "d", className: "text-[9px] font-normal text-zinc-500" }, "Dự án trên server")]),
    React.createElement("button", {
      key: "local-tab", type: "button", onClick: function() { setTab('local'); },
      className: "py-2 rounded border flex flex-col items-center gap-1 font-semibold transition " + (tab === 'local' ? 'bg-teal-950/60 border-teal-500 text-teal-300' : 'bg-zinc-800 border-zinc-700 hover:bg-zinc-750 text-zinc-400')
    }, [React.createElement("span", { key: "t" }, "💾 Local"), React.createElement("span", { key: "d", className: "text-[9px] font-normal text-zinc-500" }, "Tập tin .sfs trên máy")])
  ]),
  tab === 'cloud' ?
    React.createElement("div", { className: "space-y-1.5 max-h-64 overflow-y-auto" },
      loading ? [React.createElement("div", { key: "l", className: "text-center py-8 text-xs text-zinc-500" }, "Đang tải danh sách dự án...")]
        : projects.length === 0 ? [React.createElement("div", { key: "e", className: "text-center py-8 text-xs text-zinc-500" }, "Bạn chưa có dự án nào trên Cloud.")]
        : projects.map(function(p) {
            return React.createElement("div", {
              key: p.id,
              onClick: function() { onOpenCloud(p.id, p.name); },
              className: "flex items-center justify-between p-3 bg-[#1e1e1e] hover:bg-zinc-800 rounded border border-zinc-800 transition cursor-pointer text-xs group"
            }, [
              React.createElement("div", { key: "meta" }, [
                React.createElement("div", { className: "font-bold text-slate-200 group-hover:text-teal-400" }, p.name),
                React.createElement("div", { className: "text-[10px] text-zinc-500 mt-0.5" }, "Dung lượng: " + (p.size_mb || 0) + " MB | Cập nhật: " + new Date((p.updated_at || 0) * 1000).toLocaleString())
              ]),
              React.createElement("button", {
                onClick: function(e) { e.stopPropagation(); onOpenCloud(p.id, p.name); },
                className: "px-2.5 py-1 bg-teal-800/80 hover:bg-teal-700 text-teal-200 rounded font-semibold text-[10px]"
              }, "MỞ")
            ]);
          })
    )
    : React.createElement("div", { className: "py-4 text-center text-xs text-zinc-400 space-y-3" },
      [React.createElement("div", { key: "d", className: "text-zinc-500" }, "Chọn tệp .sfs để mở dự án từ Local."),
      React.createElement("button", {
        key: "b", onClick: function() { onOpenLocal(); },
        className: "px-4 py-2 bg-teal-600 hover:bg-teal-500 text-white rounded font-semibold text-xs transition"
      }, "Chọn tệp .sfs ...")]
    ),
  React.createElement("div", { className: "flex justify-end gap-2 text-xs mt-4 pt-3 border-t border-zinc-800" },
    React.createElement("button", {
      type: "button", onClick: onClose,
      className: "px-3 py-1.5 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 rounded"
    }, "Hủy"))));
};

const SaveAsModal = ({
  isOpen,
  onClose,
  projectName,
  onSaveCloud,
  onSaveLocal
}) => {
  if (!isOpen) return null;
  const [name, setName] = useState(projectName || '');
  const [saveType, setSaveType] = useState('cloud');
  const handleSubmit = (e) => {
    e.preventDefault();
    if (!name.trim()) return;
    if (saveType === 'cloud') {
      onSaveCloud(name.trim());
    } else {
      onSaveLocal(name.trim());
    }
    onClose();
  };
  return /*#__PURE__*/React.createElement("div", {
    className: "fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm"
  }, /*#__PURE__*/React.createElement("div", {
    className: "bg-[#262626] border border-[#383838] rounded-xl shadow-2xl w-full max-w-md p-6 text-slate-200"
  }, /*#__PURE__*/React.createElement("h3", {
    className: "text-sm font-bold text-teal-400 mb-4 uppercase"
  }, "Lưu dưới tên khác (Save As...)"), /*#__PURE__*/React.createElement("form", {
    onSubmit: handleSubmit,
    className: "space-y-4"
  }, [
    /*#__PURE__*/React.createElement("div", { key: "name-block" }, [
      /*#__PURE__*/React.createElement("label", { className: "block text-[10px] text-zinc-400 uppercase font-bold mb-1" }, "Tên dự án mới"),
      /*#__PURE__*/React.createElement("input", {
    type: "text",
    placeholder: "Nhập tên mới...",
    required: true,
    value: name,
    onChange: e => setName(e.target.value),
    className: "w-full bg-[#1e1e1e] border border-[#383838] rounded px-3 py-1.5 text-xs text-slate-200 focus:outline-none focus:border-teal-500 font-bold",
    autoFocus: true
  })
  ]),
    /*#__PURE__*/React.createElement("div", { key: "type-block" }, [
      /*#__PURE__*/React.createElement("label", { className: "block text-[10px] text-zinc-400 uppercase font-bold mb-1.5" }, "Phương thức lưu trữ"),
      /*#__PURE__*/React.createElement("div", { className: "grid grid-cols-2 gap-2 text-xs" }, [
        /*#__PURE__*/React.createElement("button", {
    key: "btn-cloud",
    type: "button",
    onClick: () => setSaveType('cloud'),
    className: `py-2 rounded border flex flex-col items-center gap-1 font-semibold transition ${saveType === 'cloud' ? 'bg-teal-950/60 border-teal-500 text-teal-300' : 'bg-zinc-800 border-zinc-700 hover:bg-zinc-750 text-zinc-400'}`
  }, [
          /*#__PURE__*/React.createElement("span", { key: "title" }, "☁️ Lưu Cloud"),
          /*#__PURE__*/React.createElement("span", { key: "desc", className: "text-[9px] font-normal text-zinc-500" }, "Lưu lên server cá nhân")
  ]),
        /*#__PURE__*/React.createElement("button", {
    key: "btn-local",
    type: "button",
    onClick: () => setSaveType('local'),
    className: `py-2 rounded border flex flex-col items-center gap-1 font-semibold transition ${saveType === 'local' ? 'bg-teal-950/60 border-teal-500 text-teal-300' : 'bg-zinc-800 border-zinc-700 hover:bg-zinc-750 text-zinc-400'}`
  }, [
          /*#__PURE__*/React.createElement("span", { key: "title" }, "💾 Tải về máy (.sfs)"),
          /*#__PURE__*/React.createElement("span", { key: "desc", className: "text-[9px] font-normal text-zinc-500" }, "Tải tệp JSON dự án về máy")
  ])
  ])
  ]),
    /*#__PURE__*/React.createElement("div", {
    key: "actions",
    className: "flex justify-end gap-2 text-xs pt-2"
  }, [
      /*#__PURE__*/React.createElement("button", {
    key: "cancel",
    type: "button",
    onClick: onClose,
    className: "px-3 py-1.5 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 rounded"
  }, "Hủy"),
      /*#__PURE__*/React.createElement("button", {
    key: "save",
    type: "submit",
    className: "px-3 py-1.5 bg-teal-600 hover:bg-teal-500 text-white rounded font-semibold"
  }, "Thực hiện lưu")
  ])
  ])));
};
const SystemManagerModal = ({
  isOpen,
  onClose
}) => {
  if (!isOpen) return null;
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [msg, setMsg] = useState('');
  const [error, setError] = useState('');
  const [editingQuotaUser, setEditingQuotaUser] = useState(null);
  const [newQuotaMb, setNewQuotaMb] = useState(500);
  useEffect(() => {
    if (isOpen) loadUsers();
  }, [isOpen]);
  const loadUsers = async () => {
    setLoading(true);
    setError('');
    try {
      const data = await window.SonicAPI.listUsers();
      setUsers(data);
    } catch (err) {
      setError(err.message || 'Không thể tải danh sách người dùng hệ thống');
    } finally {
      setLoading(false);
    }
  };
  const handleSaveQuota = async userId => {
    try {
      await window.SonicAPI.updateUserQuota(userId, parseInt(newQuotaMb));
      setMsg('Đã cập nhật hạn mức Quota thành công!');
      setEditingQuotaUser(null);
      loadUsers();
    } catch (err) {
      setError(err.message || 'Lỗi cập nhật Quota');
    }
  };
  const handleToggleRole = async user => {
    const nextRole = user.role === 'admin' ? 'standard' : 'admin';
    try {
      await window.SonicAPI.updateUserRole(user.id, nextRole, user.is_active);
      setMsg(`Đã đổi vai trò người dùng ${user.username} thành ${nextRole}`);
      loadUsers();
    } catch (err) {
      setError(err.message || 'Lỗi cập nhật vai trò');
    }
  };
  const handleDeleteUser = async userId => {
    if (!confirm('Bạn có chắc chắn muốn xóa người dùng này khỏi hệ thống?')) return;
    try {
      await window.SonicAPI.deleteUser(userId);
      setMsg('Đã xóa người dùng thành công');
      loadUsers();
    } catch (err) {
      setError(err.message || 'Lỗi khi xóa người dùng');
    }
  };
  return /*#__PURE__*/React.createElement("div", {
    className: "fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm"
  }, /*#__PURE__*/React.createElement("div", {
    className: "bg-[#262626] border border-[#383838] rounded-xl shadow-2xl w-full max-w-4xl p-6 text-slate-200"
  }, /*#__PURE__*/React.createElement("div", {
    className: "flex justify-between items-center pb-4 border-b border-[#383838]"
  }, /*#__PURE__*/React.createElement("h3", {
    className: "text-lg font-bold text-amber-400"
  }, "⚙️ Quản Lý Hệ Thống & Phân Quyền Admin"), /*#__PURE__*/React.createElement("button", {
    onClick: onClose,
    className: "text-slate-400 hover:text-slate-200"
  }, "✕")), msg && /*#__PURE__*/React.createElement("div", {
    className: "mt-3 p-2 bg-emerald-950/60 border border-emerald-700 text-emerald-300 rounded text-xs"
  }, msg), error && /*#__PURE__*/React.createElement("div", {
    className: "mt-3 p-2 bg-red-950/60 border border-red-700 text-red-300 rounded text-xs"
  }, error), /*#__PURE__*/React.createElement("div", {
    className: "mt-4 overflow-x-auto max-h-96 no-scrollbar"
  }, loading ? /*#__PURE__*/React.createElement("div", {
    className: "py-8 text-center text-slate-400 text-xs"
  }, "Đang tải thông tin hệ thống...") : /*#__PURE__*/React.createElement("table", {
    className: "w-full text-left text-xs border-collapse"
  }, /*#__PURE__*/React.createElement("thead", null, /*#__PURE__*/React.createElement("tr", {
    className: "border-b border-[#383838] text-slate-400 bg-[#1e1e1e]"
  }, /*#__PURE__*/React.createElement("th", {
    className: "p-3"
  }, "Tên Người Dùng"), /*#__PURE__*/React.createElement("th", {
    className: "p-3"
  }, "Email"), /*#__PURE__*/React.createElement("th", {
    className: "p-3"
  }, "Vai Trò"), /*#__PURE__*/React.createElement("th", {
    className: "p-3"
  }, "Dung Lượng Sử Dụng"), /*#__PURE__*/React.createElement("th", {
    className: "p-3"
  }, "Hạn Mức Quota"), /*#__PURE__*/React.createElement("th", {
    className: "p-3 text-right"
  }, "Thao Tác"))), /*#__PURE__*/React.createElement("tbody", {
    className: "divide-y divide-[#333]"
  }, users.map(u => /*#__PURE__*/React.createElement("tr", {
    key: u.id,
    className: "hover:bg-[#2e2e2e]"
  }, /*#__PURE__*/React.createElement("td", {
    className: "p-3 font-semibold text-teal-300"
  }, u.username, u.must_change_password && /*#__PURE__*/React.createElement("span", {
    className: "ml-2 text-xs bg-amber-900/60 text-amber-300 px-1.5 py-0.5 rounded"
  }, "Mật khẩu gốc")), /*#__PURE__*/React.createElement("td", {
    className: "p-3 text-slate-300"
  }, u.email), /*#__PURE__*/React.createElement("td", {
    className: "p-3 uppercase font-bold text-amber-400"
  }, u.role), /*#__PURE__*/React.createElement("td", {
    className: "p-3"
  }, u.used_mb, " MB"), /*#__PURE__*/React.createElement("td", {
    className: "p-3"
  }, editingQuotaUser === u.id ? /*#__PURE__*/React.createElement("div", {
    className: "flex items-center space-x-1"
  }, /*#__PURE__*/React.createElement("input", {
    type: "number",
    value: newQuotaMb,
    onChange: e => setNewQuotaMb(e.target.value),
    className: "w-16 bg-[#1e1e1e] border border-[#444] rounded px-1 py-0.5 text-xs text-slate-200"
  }), /*#__PURE__*/React.createElement("span", null, "MB"), /*#__PURE__*/React.createElement("button", {
    onClick: () => handleSaveQuota(u.id),
    className: "px-2 py-0.5 bg-teal-600 rounded text-xs"
  }, "Lưu")) : /*#__PURE__*/React.createElement("span", {
    className: "font-semibold"
  }, u.quota_mb, " MB")), /*#__PURE__*/React.createElement("td", {
    className: "p-3 text-right space-x-2"
  }, /*#__PURE__*/React.createElement("button", {
    onClick: () => {
      setEditingQuotaUser(u.id);
      setNewQuotaMb(u.quota_mb);
    },
    className: "px-2 py-1 bg-slate-700 hover:bg-slate-600 rounded text-xs"
  }, "Sửa Quota"), /*#__PURE__*/React.createElement("button", {
    onClick: () => handleToggleRole(u),
    className: "px-2 py-1 bg-amber-700/60 hover:bg-amber-600 rounded text-xs"
  }, "Đổi Role"), u.role !== 'admin' && /*#__PURE__*/React.createElement("button", {
    onClick: () => handleDeleteUser(u.id),
    className: "px-2 py-1 bg-red-700/60 hover:bg-red-600 rounded text-xs"
  }, "Xóa")))))))));
};

/* ═══════════════════════════════════════════════════════════════════
   ABOUT / HELP / PREFERENCES MODALS (menu Help + Tools → Preferences)
   ═══════════════════════════════════════════════════════════════════ */
const APP_VERSION = '1.0.0'; // khớp src-tauri/tauri.conf.json

const AboutModal = ({ isOpen, onClose }) => {
  if (!isOpen) return null;
  return (
    <div className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/70 backdrop-blur-sm" onClick={onClose}>
      <div className="bg-[#1e1e24] border border-zinc-700 rounded-xl shadow-2xl w-full max-w-md p-6 text-slate-200" onClick={e => e.stopPropagation()}>
        <div className="flex items-center gap-3 mb-4">
          <img src="/favicon.svg" alt="SonicForge Studio" className="w-12 h-12 rounded-lg bg-zinc-900 border border-zinc-700 shadow-lg object-contain p-0.5" />
          <div>
            <h2 className="text-lg font-bold text-white">SonicForge Studio</h2>
            <p className="text-xs text-zinc-400">Professional DAW — v{APP_VERSION}</p>
          </div>
        </div>
        <div className="space-y-2 text-sm">
          <p className="text-zinc-300 leading-relaxed">Phần mềm sản xuất âm nhạc (DAW) — soạn nhạc, ghi âm, chỉnh sửa MIDI/Audio, SoundFont &amp; VST, trộn và master.</p>
          <div className="pt-2 border-t border-zinc-800 space-y-1 text-xs">
            <div className="flex items-center gap-2"><span className="text-zinc-500 w-20 shrink-0">Developer</span><span className="text-cyan-400 font-semibold">Lộc Phạm</span></div>
            <div className="flex items-center gap-2"><span className="text-zinc-500 w-20 shrink-0">Email</span><a href="mailto:tranloclqd@gmail.com" className="text-cyan-400 hover:underline">tranloclqd@gmail.com</a></div>
            <div className="flex items-center gap-2"><span className="text-zinc-500 w-20 shrink-0">Version</span><span className="text-zinc-300">{APP_VERSION}</span></div>
            <div className="flex items-center gap-2"><span className="text-zinc-500 w-20 shrink-0">Build</span><span className="text-zinc-300">Standalone (Tauri v2 + PyInstaller)</span></div>
          </div>
        </div>
        <div className="mt-5 flex justify-end">
          <button onClick={onClose} className="px-4 py-1.5 bg-cyan-700 hover:bg-cyan-600 rounded text-xs font-bold text-white transition">Đóng</button>
        </div>
      </div>
    </div>
  );
};

const HelpModal = ({ isOpen, onClose, lang }) => {
  if (!isOpen) return null;
  const vi = lang !== 'en';
  const sections = vi ? [
    { title: '🚀 Bắt đầu nhanh', body: 'Nhấn phím Cách (Space) để Play/Pause. Nhấn phím / để dừng. Dùng nút Record (●) để ghi âm/MIDI. Tạo track mới từ menu Track hoặc nút "+ Add Track".' },
    { title: '🎹 MIDI & ARM', body: 'Bật nút ARM đỏ trên track để nhận phím từ MIDI keyboard. Vào menu Tools → MIDI Devices để chọn thiết bị. Khi ARM + bấm phím, âm preview phát và VU meter nhảy theo trường độ.' },
    { title: '🎼 Piano Roll', body: 'Nhấp đúp vào MIDI item để mở Piano Roll. Dùng Ctrl+scroll hoặc nút +/− để zoom. Nhấp để vẽ note, kéo để di chuyển, Ctrl+kéo để copy. Phím tắt: C (vẽ), E (tẩy), Space (nghe).' },
    { title: '🎛️ FX & Master', body: 'Chọn track → nút FX để mở FX Rack (thêm EQ, compressor...). Bấm nút PWR ở Master để mở Mastering Panel: EQ 4 băng, compressor, limiter. Mọi thay đổi áp dụng realtime.' },
    { title: '🧪 SoundFont & VST', body: 'Vào Tools → Plugin Manager để quét SoundFont (.sf2/.sf3) và VSTi. Track MIDI dùng SoundFont làm nhạc cụ — chọn instrument từ nút Synth trên track.' },
    { title: '🤖 AI', body: 'Tools → Config AI Providers để cấu hình API (OpenAI, Gemini, Ollama...). Dùng AI Prompt Generator (nút ✨) để sinh MIDI/ý tưởng; AI MIDI Preset Manager để lưu preset.' },
    { title: '💾 Lưu & Xuất', body: 'Ctrl+S lưu project (cloud/temp), Ctrl+Shift+S Save As. Export WAV qua nút Export — chọn vùng, format, bitrate rồi Render.' },
    { title: '⌨️ Phím tắt', body: 'Space: Play/Pause · /: Stop · Ctrl+Z/Y: Undo/Redo · Ctrl+C/X/V: Copy/Cut/Paste · Ctrl+S: Save · Ctrl+N: New · S: Split · Ctrl+E: Edit in new tab · Ctrl+wheel: zoom timeline' },
  ] : [
    { title: '🚀 Quick start', body: 'Press Space to Play/Pause. Press / to stop. Use Record (●) to capture audio/MIDI. Add tracks from the Track menu or the "+ Add Track" button.' },
    { title: '🎹 MIDI & ARM', body: 'Enable the red ARM button on a track to receive keys from a MIDI keyboard. Go to Tools → MIDI Devices to pick your device. When ARM + key press, preview audio plays and the VU meter animates for the note length.' },
    { title: '🎼 Piano Roll', body: 'Double-click a MIDI item to open the Piano Roll. Use Ctrl+scroll or +/− buttons to zoom. Click to draw notes, drag to move, Ctrl+drag to copy. Shortcuts: C (draw), E (erase), Space (listen).' },
    { title: '🎛️ FX & Master', body: 'Select a track → FX button to open the FX Rack (EQ, compressor...). Press the PWR button on the Master to open the Mastering Panel: 4-band EQ, compressor, limiter — all realtime.' },
    { title: '🧪 SoundFont & VST', body: 'Tools → Plugin Manager scans SoundFonts (.sf2/.sf3) and VSTi. MIDI tracks use SoundFonts as instruments — pick one from the Synth button on the track.' },
    { title: '🤖 AI', body: 'Tools → Config AI Providers to set up APIs (OpenAI, Gemini, Ollama...). Use the AI Prompt Generator (✨) to create MIDI/ideas; AI MIDI Preset Manager stores presets.' },
    { title: '💾 Save & Export', body: 'Ctrl+S saves the project (cloud/temp), Ctrl+Shift+S Save As. Export WAV via the Export button — pick range, format, bitrate, then Render.' },
    { title: '⌨️ Shortcuts', body: 'Space: Play/Pause · /: Stop · Ctrl+Z/Y: Undo/Redo · Ctrl+C/X/V: Copy/Cut/Paste · Ctrl+S: Save · Ctrl+N: New · S: Split · Ctrl+E: Edit in new tab · Ctrl+wheel: zoom timeline' },
  ];
  return (
    <div className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/70 backdrop-blur-sm" onClick={onClose}>
      <div className="bg-[#1e1e24] border border-zinc-700 rounded-xl shadow-2xl w-full max-w-2xl max-h-[85vh] flex flex-col text-slate-200" onClick={e => e.stopPropagation()}>
        <div className="flex items-center justify-between px-5 py-3 border-b border-zinc-800 shrink-0">
          <h2 className="text-base font-bold text-white">{vi ? 'Hướng dẫn sử dụng SonicForge Studio' : 'SonicForge Studio User Guide'}</h2>
          <button onClick={onClose} className="w-6 h-6 rounded hover:bg-zinc-700 text-zinc-400 hover:text-white text-sm">✕</button>
        </div>
        <div className="overflow-y-auto px-5 py-4 space-y-3">
          {sections.map((s, i) => (
            <div key={i} className="bg-zinc-900/60 border border-zinc-800 rounded-lg p-3">
              <h3 className="text-sm font-bold text-cyan-400 mb-1">{s.title}</h3>
              <p className="text-xs text-zinc-300 leading-relaxed">{s.body}</p>
            </div>
          ))}
        </div>
        <div className="px-5 py-3 border-t border-zinc-800 flex justify-end shrink-0">
          <button onClick={onClose} className="px-4 py-1.5 bg-cyan-700 hover:bg-cyan-600 rounded text-xs font-bold text-white transition">{vi ? 'Đóng' : 'Close'}</button>
        </div>
      </div>
    </div>
  );
};

const PreferencesModal = ({ isOpen, onClose, prefs, onPrefsChange }) => {
  if (!isOpen) return null;
  const vi = prefs.language !== 'en';
  const themes = [
    { id: 'dark', name: vi ? 'Tối (mặc định)' : 'Dark (default)', swatch: 'from-zinc-700 to-zinc-900', ring: 'ring-cyan-400' },
    { id: 'midnight', name: vi ? 'Đêm xanh' : 'Midnight', swatch: 'from-sky-800 to-slate-950', ring: 'ring-sky-400' },
    { id: 'forest', name: vi ? 'Rừng xanh' : 'Forest', swatch: 'from-emerald-700 to-green-950', ring: 'ring-emerald-400' },
    { id: 'violet', name: vi ? 'Tím' : 'Violet', swatch: 'from-violet-700 to-purple-950', ring: 'ring-violet-400' },
    { id: 'graphite', name: vi ? 'Than chì' : 'Graphite', swatch: 'from-zinc-600 to-neutral-900', ring: 'ring-zinc-300' },
  ];
  const set = (k, v) => onPrefsChange({ ...prefs, [k]: v });
  return (
    <div className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/70 backdrop-blur-sm" onClick={onClose}>
      <div className="bg-[#1e1e24] border border-zinc-700 rounded-xl shadow-2xl w-full max-w-md max-h-[85vh] overflow-y-auto p-5 text-slate-200" onClick={e => e.stopPropagation()}>
        <div className="flex items-center justify-between mb-4">
          <h2 className="text-base font-bold text-white">{vi ? 'Tùy chọn (Preferences)' : 'Preferences'}</h2>
          <button onClick={onClose} className="w-6 h-6 rounded hover:bg-zinc-700 text-zinc-400 hover:text-white text-sm">✕</button>
        </div>

        {/* Theme */}
        <div className="mb-5">
          <h3 className="text-xs font-bold text-zinc-400 uppercase tracking-wider mb-2">{vi ? '🎨 Chủ đề màu' : '🎨 Theme'}</h3>
          <div className="grid grid-cols-1 gap-1.5">
            {themes.map(th => (
              <button key={th.id} onClick={() => set('theme', th.id)}
                className={`flex items-center gap-2.5 px-2.5 py-2 rounded-lg border text-xs text-left transition ${prefs.theme === th.id ? 'border-cyan-500 bg-zinc-800' : 'border-zinc-700 bg-zinc-900 hover:bg-zinc-800'}`}>
                <span className={`w-6 h-6 rounded-md bg-gradient-to-br ${th.swatch} ring-1 ring-black/40 shrink-0 ${prefs.theme === th.id ? `ring-2 ${th.ring}` : ''}`}></span>
                <span className="text-zinc-200">{th.name}</span>
                {prefs.theme === th.id && <span className="ml-auto text-cyan-400 text-[10px] font-bold">✓</span>}
              </button>
            ))}
          </div>
        </div>

        {/* Language */}
        <div className="mb-5">
          <h3 className="text-xs font-bold text-zinc-400 uppercase tracking-wider mb-2">{vi ? '🌐 Ngôn ngữ' : '🌐 Language'}</h3>
          <div className="flex gap-1.5">
            {[{ id: 'vi', label: 'Tiếng Việt' }, { id: 'en', label: 'English' }].map(l => (
              <button key={l.id} onClick={() => set('language', l.id)}
                className={`flex-1 px-2 py-1.5 rounded-lg border text-xs font-semibold transition ${prefs.language === l.id ? 'border-cyan-500 bg-cyan-900/40 text-cyan-300' : 'border-zinc-700 bg-zinc-900 text-zinc-400 hover:bg-zinc-800'}`}>
                {l.label}
              </button>
            ))}
          </div>
          <p className="text-[10px] text-zinc-500 mt-1">{vi ? 'Áp dụng ngay cho menu & hướng dẫn.' : 'Applies immediately to menus & guide.'}</p>
        </div>

        {/* Button font size */}
        <div className="mb-5">
          <h3 className="text-xs font-bold text-zinc-400 uppercase tracking-wider mb-2">{vi ? '🔠 Cỡ chữ nút bấm' : '🔠 Button font size'}</h3>
          <div className="flex gap-1.5">
            {[{ id: 'sm', label: vi ? 'Nhỏ' : 'Small' }, { id: 'md', label: vi ? 'Vừa' : 'Medium' }, { id: 'lg', label: vi ? 'Lớn' : 'Large' }].map(f => (
              <button key={f.id} onClick={() => set('buttonFontSize', f.id)}
                className={`flex-1 px-2 py-1.5 rounded-lg border text-xs font-semibold transition ${prefs.buttonFontSize === f.id ? 'border-cyan-500 bg-cyan-900/40 text-cyan-300' : 'border-zinc-700 bg-zinc-900 text-zinc-400 hover:bg-zinc-800'}`}>
                {f.label}
              </button>
            ))}
          </div>
        </div>

        <div className="flex justify-end gap-2 pt-2 border-t border-zinc-800">
          <button onClick={onClose} className="px-3 py-1.5 bg-zinc-700 hover:bg-zinc-600 rounded text-xs font-bold text-white transition">{vi ? 'Hủy' : 'Cancel'}</button>
          <button onClick={onClose} className="px-4 py-1.5 bg-cyan-700 hover:bg-cyan-600 rounded text-xs font-bold text-white transition">{vi ? 'Lưu & Đóng' : 'Save & Close'}</button>
        </div>
      </div>
    </div>
  );
};

const AIPresetModal = ({ isOpen, onClose }) => {
  if (!isOpen) return null;

  const mgrRef = React.useRef(null);
  if (!mgrRef.current) mgrRef.current = new window.PromptTemplateManager();
  const mgr = mgrRef.current;

  const [presets, setPresets] = React.useState(() => [...mgr.getPresets()]);
  const [search, setSearch] = React.useState('');
  const [filterCategory, setFilterCategory] = React.useState('ALL');
  const [showFavoritesOnly, setShowFavoritesOnly] = React.useState(false);
  const [editingPreset, setEditingPreset] = React.useState(null);
  const [syncing, setSyncing] = React.useState(false);

  const [formName, setFormName] = React.useState('');
  const [formKeywords, setFormKeywords] = React.useState('');
  const [formCategory, setFormCategory] = React.useState('Orchestral / Film Score');
  const [formBars, setFormBars] = React.useState(8);
  const [formBpm, setFormBpm] = React.useState(120);
  const [formScale, setFormScale] = React.useState('C Minor');
  const [formTemplate, setFormTemplate] = React.useState('');
  const [showGeneratorModal, setShowGeneratorModal] = React.useState(false);
  const [selectedCategory, setSelectedCategory] = React.useState('Orchestral / Film Score');
  const [isAddingCategory, setIsAddingCategory] = React.useState(false);
  const [newCategoryValue, setNewCategoryValue] = React.useState('');
  const [categoriesVersion, setCategoriesVersion] = React.useState(0);

  const presetCategories = React.useMemo(() => {
    const fromPresets = [...new Set(presets.map(p => p.category).filter(Boolean))];
    let saved = [];
    try {
      const raw = localStorage.getItem('midi_prompt_categories');
      saved = raw ? JSON.parse(raw) : [];
    } catch (e) { saved = []; }
    return [...new Set([...fromPresets, ...saved])].sort();
  }, [presets, categoriesVersion]);

  const addNewCategory = (cat) => {
    const val = (cat || '').trim();
    if (!val) return;
    setFormCategory(val);
    setSelectedCategory(val);
    try {
      const raw = localStorage.getItem('midi_prompt_categories');
      const list = raw ? JSON.parse(raw) : [];
      if (!list.includes(val)) {
        list.push(val);
        localStorage.setItem('midi_prompt_categories', JSON.stringify(list));
        setCategoriesVersion(v => v + 1);
      }
    } catch (e) {}
  };

  const refreshPresets = () => {
    setPresets([...mgr.getPresets()]);
  };

  // Sync from backend on mount — merge into local presets, never overwrite
  React.useEffect(() => {
    if (!window.SonicAPI) return;
    setSyncing(true);
    window.SonicAPI.getAIPresets()
      .then(data => {
        if (!data || !data.presets || data.presets.length === 0) return;
        var existing = mgr.presets;
        var existingIds = new Set(existing.map(function(p) { return p.id; }));
        var merged = existing.slice();
        data.presets.forEach(function(bp) {
          if (!existingIds.has(bp.id)) {
            merged.push(bp);
            existingIds.add(bp.id);
          }
        });
        mgr.presets = merged;
        setPresets(merged);
      })
      .catch(function() {})
      .finally(function() { setSyncing(false); });
  }, []);

  // Listen for GENERATOR_PRESET_DATA from the iframe generator modal
  React.useEffect(() => {
    const handleMessage = (event) => {
      if (event.data && event.data.type === 'GENERATOR_PRESET_DATA') {
        const d = event.data;
        setFormName(d.name || '');
        setFormCategory(d.category || 'Orchestral / Film Score');
        setSelectedCategory(d.category || 'Orchestral / Film Score');
        setFormKeywords(d.keywords || '');
        setFormBars(parseInt(d.default_bars) || 8);
        setFormBpm(parseInt(d.default_bpm) || 120);
        setFormScale(d.default_scale || 'C Minor');
        setFormTemplate(d.template || '');
        setEditingPreset('new');
        setShowGeneratorModal(false);
        setIsAddingCategory(false);
        setNewCategoryValue('');
        refreshPresets();
      }
    };
    window.addEventListener('message', handleMessage);
    return () => window.removeEventListener('message', handleMessage);
  }, []);

  const savePresets = (newPresets) => {
    setPresets(newPresets);
    mgr.presets = newPresets;
    mgr.savePresets();
    // Sync to backend if available
    const userDefined = newPresets.filter(p => p.is_user_defined);
    if (window.SonicAPI && userDefined.length > 0) {
      userDefined.forEach(p => {
        window.SonicAPI.saveAIPreset(p).catch(() => {});
      });
    }
  };

  const handleEdit = (p) => {
    setEditingPreset(p);
    setFormName(p.name);
    setFormKeywords(p.keywords.join(', '));
    setFormCategory(p.category);
    setSelectedCategory(p.category);
    setFormBars(p.default_bars);
    setFormBpm(p.default_bpm);
    setFormScale(p.default_scale);
    setFormTemplate(p.system_instruction_template);
    setIsAddingCategory(false);
    setNewCategoryValue('');
  };

  const handleNew = () => {
    setEditingPreset('new');
    setFormName('');
    setFormKeywords('');
    setFormCategory('Orchestral / Film Score');
    setSelectedCategory('Orchestral / Film Score');
    setFormBars(8);
    setFormBpm(120);
    setFormScale('C Minor');
    setFormTemplate('');
    setIsAddingCategory(false);
    setNewCategoryValue('');
  };

  const handleNewStructured = () => {
    refreshPresets();
    setShowGeneratorModal(true);
  };

  const handleToggleFav = (id) => {
    mgr.toggleFavorite(id);
    setPresets([...mgr.getPresets()]);
    const p = mgr.presets.find(x => x.id === id);
    if (p && p.is_user_defined && window.SonicAPI) {
      window.SonicAPI.saveAIPreset(p).catch(() => {});
    }
  };

  const handleDelete = (id) => {
    const p = presets.find(x => x.id === id);
    if (p && p.is_user_defined && window.SonicAPI) {
      window.SonicAPI.deleteAIPreset(id).catch(() => {});
    }
    mgr.deletePreset(id);
    setPresets([...mgr.getPresets()]);
    showToast('Đã xóa preset.', 'info');
  };

  const handleSaveForm = (e) => {
    e.preventDefault();
    if (!formName.trim() || !formTemplate.trim()) {
      showToast('Vui lòng điền đầy đủ tên và mẫu gợi ý.', 'warning');
      return;
    }

    const keywordsArray = formKeywords.split(',').map(k => k.trim()).filter(Boolean);
    const presetObj = {
      id: editingPreset === 'new' ? 'preset_' + Date.now() : editingPreset.id,
      name: formName.trim(),
      keywords: keywordsArray,
      category: formCategory,
      default_bars: parseInt(formBars) || 8,
      default_bpm: parseInt(formBpm) || 120,
      default_scale: formScale,
      system_instruction_template: formTemplate.trim(),
      is_user_defined: true,
      is_favorite: editingPreset === 'new' ? false : (editingPreset.is_favorite || false),
      created_at: editingPreset === 'new' ? new Date().toISOString() : editingPreset.created_at
    };

    mgr.saveUserPreset(presetObj);
    setPresets([...mgr.getPresets()]);
    setEditingPreset(null);

    if (window.SonicAPI) {
      window.SonicAPI.saveAIPreset(presetObj).catch(() => {});
    }
    showToast('Đã lưu preset thành công!', 'success');
  };

  const categories = ['ALL', '★ Yêu thích', 'Người dùng', ...new Set(presets.map(p => p.category))];

  const filtered = presets.filter(p => {
    const matchesSearch = p.name.toLowerCase().includes(search.toLowerCase()) || 
                          p.keywords.some(k => k.toLowerCase().includes(search.toLowerCase()));
    let matchesCategory;
    if (filterCategory === 'ALL') {
      matchesCategory = true;
    } else if (filterCategory === '★ Yêu thích') {
      matchesCategory = p.is_favorite;
    } else if (filterCategory === 'Người dùng') {
      matchesCategory = p.is_user_defined;
    } else {
      matchesCategory = p.category === filterCategory;
    }
    if (showFavoritesOnly) matchesCategory = matchesCategory && p.is_favorite;
    return matchesSearch && matchesCategory;
  });

  return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
    className: "fixed inset-0 z-50 flex items-center justify-center bg-black/75 backdrop-blur-sm p-4 animate-fade-in"
  }, /*#__PURE__*/React.createElement("div", {
    className: "bg-[#18181b] border border-zinc-800 rounded-xl shadow-2xl w-full max-w-4xl max-h-[85vh] flex flex-col overflow-hidden text-zinc-100"
  }, /*#__PURE__*/React.createElement("div", {
    className: "p-4 border-b border-zinc-800 flex items-center justify-between shrink-0 bg-[#202024]"
  }, /*#__PURE__*/React.createElement("h2", {
    className: "text-sm font-bold tracking-wider uppercase text-purple-400 flex items-center gap-1.5"
  }, /*#__PURE__*/React.createElement("i", {
    "data-lucide": "sliders",
    className: "w-4 h-4"
  }), "AI Prompt Preset Manager", syncing && /*#__PURE__*/React.createElement("span", {
    className: "text-[10px] text-zinc-500 ml-2"
  }, "đang đồng bộ...")), /*#__PURE__*/React.createElement("button", {
    onClick: () => { setEditingPreset(null); onClose(); },
    className: "text-zinc-400 hover:text-zinc-200 transition"
  }, /*#__PURE__*/React.createElement("i", {
    "data-lucide": "x",
    className: "w-4 h-4"
  }))), /*#__PURE__*/React.createElement("div", {
    className: "flex-1 overflow-y-auto p-4 flex gap-4 min-h-0"
  }, !editingPreset ? /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
    className: "flex-1 flex flex-col min-w-0"
  }, /*#__PURE__*/React.createElement("div", {
    className: "flex items-center gap-2 mb-3 shrink-0"
  }, /*#__PURE__*/React.createElement("input", {
    type: "text",
    placeholder: "Tìm kiếm preset hoặc từ khóa...",
    value: search,
    onChange: e => setSearch(e.target.value),
    className: "flex-1 bg-zinc-900 border border-zinc-700 rounded px-2.5 py-1 text-xs outline-none focus:border-purple-600"
  }), /*#__PURE__*/React.createElement("select", {
    value: filterCategory,
    onChange: e => setFilterCategory(e.target.value),
    className: "bg-zinc-900 border border-zinc-700 rounded px-2.5 py-1 text-xs outline-none focus:border-purple-600"
  }, categories.map(c => /*#__PURE__*/React.createElement("option", {
    key: c,
    value: c
  }, c === 'ALL' ? 'Tất cả danh mục' : c))), /*#__PURE__*/React.createElement("button", {
    onClick: () => setShowFavoritesOnly(!showFavoritesOnly),
    className: `px-2.5 py-1 rounded text-xs font-bold transition shrink-0 ${showFavoritesOnly ? 'bg-yellow-700 text-yellow-300' : 'bg-zinc-800 text-zinc-400 hover:bg-zinc-700'}`,
    title: "Chỉ hiện yêu thích"
  }, /*#__PURE__*/React.createElement("i", {
    "data-lucide": "star",
    className: "w-3.5 h-3.5 inline-block mr-1"
  }), "★"), /*#__PURE__*/React.createElement("button", {
    onClick: handleNew,
    className: "px-3 py-1 bg-purple-700 hover:bg-purple-600 text-white rounded text-xs font-bold flex items-center gap-1.5 shadow transition shrink-0"
  }, /*#__PURE__*/React.createElement("i", {
    "data-lucide": "plus",
    className: "w-3.5 h-3.5"
  }), "Tạo mới")), /*#__PURE__*/React.createElement("div", {
    className: "flex-1 border border-zinc-800 rounded bg-[#0f0f12] overflow-y-auto"
  }, filtered.length === 0 ? /*#__PURE__*/React.createElement("div", {
    className: "p-8 text-center text-zinc-500 text-xs italic"
  }, "Không tìm thấy preset nào.") : /*#__PURE__*/React.createElement("table", {
    className: "w-full text-left text-xs border-collapse"
  }, /*#__PURE__*/React.createElement("thead", {
    className: "bg-[#1f1f23] text-zinc-400 font-bold border-b border-zinc-800"
  }, /*#__PURE__*/React.createElement("tr", null, /*#__PURE__*/React.createElement("th", {
    className: "p-2.5 w-8"
  }, ""), /*#__PURE__*/React.createElement("th", {
    className: "p-2.5 w-1/4"
  }, "Tên Preset"), /*#__PURE__*/React.createElement("th", {
    className: "p-2.5 w-1/4"
  }, "Từ khóa kích hoạt"), /*#__PURE__*/React.createElement("th", {
    className: "p-2.5 w-1/6"
  }, "Số Bar"), /*#__PURE__*/React.createElement("th", {
    className: "p-2.5 w-1/6"
  }, "BPM"), /*#__PURE__*/React.createElement("th", {
    className: "p-2.5 w-1/6 text-right"
  }, "Hành động"))), /*#__PURE__*/React.createElement("tbody", null, filtered.map(p => /*#__PURE__*/React.createElement("tr", {
    key: p.id,
    className: "border-b border-zinc-800/50 hover:bg-zinc-850"
  }, /*#__PURE__*/React.createElement("td", {
    className: "p-2.5 text-center"
  }, /*#__PURE__*/React.createElement("button", {
    onClick: () => handleToggleFav(p.id),
    className: `transition ${p.is_favorite ? 'text-yellow-400' : 'text-zinc-600 hover:text-zinc-400'}`,
    title: p.is_favorite ? 'Bỏ yêu thích' : 'Đánh dấu yêu thích'
  }, p.is_favorite ? "★" : "☆")), /*#__PURE__*/React.createElement("td", {
    className: "p-2.5 font-semibold text-purple-300"
  }, p.name), /*#__PURE__*/React.createElement("td", {
    className: "p-2.5 text-zinc-400 font-mono text-[11px] truncate max-w-[150px]"
  }, p.keywords.join(', ')), /*#__PURE__*/React.createElement("td", {
    className: "p-2.5 text-zinc-400"
  }, p.default_bars, " Bars"), /*#__PURE__*/React.createElement("td", {
    className: "p-2.5 text-zinc-400"
  }, p.default_bpm, " BPM"), /*#__PURE__*/React.createElement("td", {
    className: "p-2.5 text-right flex items-center justify-end gap-1.5 h-full"
  }, /*#__PURE__*/React.createElement("button", {
    type: "button",
    onClick: () => handleEdit(p),
    className: "px-2 py-0.5 bg-zinc-850 hover:bg-zinc-700 text-zinc-300 rounded border border-zinc-700 text-[10px]"
  }, "Sửa"), p.is_user_defined && /*#__PURE__*/React.createElement("button", {
    type: "button",
    onClick: () => handleDelete(p.id),
    className: "px-2 py-0.5 bg-red-950/40 hover:bg-red-800 text-red-400 rounded border border-red-900 text-[10px]"
  }, "Xóa"))))))))) : /*#__PURE__*/React.createElement("form", {
    onSubmit: handleSaveForm,
    className: "flex-1 flex flex-col gap-3 min-w-0"
  }, /*#__PURE__*/React.createElement("div", {
    className: "text-xs font-bold text-zinc-400 shrink-0 border-b border-zinc-800 pb-1"
  }, editingPreset === 'new' ? "TẠO PRESET MỚI" : `SỬA PRESET: ${editingPreset.name}`), /*#__PURE__*/React.createElement("div", {
    className: "grid grid-cols-2 gap-3"
  }, /*#__PURE__*/React.createElement("div", {
    className: "flex flex-col gap-1"
  }, /*#__PURE__*/React.createElement("label", {
    className: "text-[10px] uppercase font-bold text-zinc-500"
  }, "Tên Preset"), /*#__PURE__*/React.createElement("input", {
    type: "text",
    value: formName,
    onChange: e => setFormName(e.target.value),
    className: "bg-zinc-900 border border-zinc-700 rounded px-2.5 py-1 text-xs outline-none focus:border-purple-600 text-zinc-200"
  })), /*#__PURE__*/React.createElement("div", {
    className: "flex flex-col gap-1"
  }, /*#__PURE__*/React.createElement("label", {
    className: "text-[10px] uppercase font-bold text-zinc-500"
  }, "Danh mục"), isAddingCategory ? /*#__PURE__*/React.createElement("div", {
    className: "flex gap-2"
  }, /*#__PURE__*/React.createElement("input", {
    type: "text",
    value: newCategoryValue,
    onChange: e => setNewCategoryValue(e.target.value),
    onBlur: () => {
      if (newCategoryValue.trim()) {
        addNewCategory(newCategoryValue.trim());
      }
      setIsAddingCategory(false);
    },
    onKeyDown: e => {
      if (e.key === 'Enter') {
        e.preventDefault();
        if (newCategoryValue.trim()) {
          addNewCategory(newCategoryValue.trim());
        }
        setIsAddingCategory(false);
      } else if (e.key === 'Escape') {
        setIsAddingCategory(false);
      }
    },
    autoFocus: true,
    className: "flex-1 bg-zinc-900 border border-zinc-700 rounded px-2.5 py-1 text-xs outline-none focus:border-purple-600 text-zinc-200",
    placeholder: "Nhập danh mục mới..."
  })) : /*#__PURE__*/React.createElement("select", {
    value: selectedCategory,
    onChange: e => {
      if (e.target.value === '__add_new__') {
        setIsAddingCategory(true);
        setNewCategoryValue('');
      } else {
        setSelectedCategory(e.target.value);
        setFormCategory(e.target.value);
      }
    },
    className: "bg-zinc-900 border border-zinc-700 rounded px-2.5 py-1 text-xs outline-none focus:border-purple-600 text-zinc-200"
  }, presetCategories.map(c => /*#__PURE__*/React.createElement("option", {
    key: c,
    value: c
  }, c)), /*#__PURE__*/React.createElement("option", {
    value: '__add_new__'
  }, "+ Nhập danh mục mới...")))), /*#__PURE__*/React.createElement("div", {
    className: "flex flex-col gap-1"
  }, /*#__PURE__*/React.createElement("label", {
    className: "text-[10px] uppercase font-bold text-zinc-500"
  }, "Từ khóa kích hoạt (ngăn cách bằng dấu phẩy)"), /*#__PURE__*/React.createElement("input", {
    type: "text",
    value: formKeywords,
    onChange: e => setFormKeywords(e.target.value),
    placeholder: "Ví dụ: epic orchestra, hoành tráng, nhạc phim epic",
    className: "bg-zinc-900 border border-zinc-700 rounded px-2.5 py-1 text-xs outline-none focus:border-purple-600 text-zinc-200"
  })), /*#__PURE__*/React.createElement("div", {
    className: "grid grid-cols-3 gap-3"
  }, /*#__PURE__*/React.createElement("div", {
    className: "flex flex-col gap-1"
  }, /*#__PURE__*/React.createElement("label", {
    className: "text-[10px] uppercase font-bold text-zinc-500"
  }, "Số Bars mặc định"), /*#__PURE__*/React.createElement("input", {
    type: "number",
    value: formBars,
    onChange: e => setFormBars(e.target.value),
    className: "bg-zinc-900 border border-zinc-700 rounded px-2.5 py-1 text-xs outline-none focus:border-purple-600 text-zinc-200"
  })), /*#__PURE__*/React.createElement("div", {
    className: "flex flex-col gap-1"
  }, /*#__PURE__*/React.createElement("label", {
    className: "text-[10px] uppercase font-bold text-zinc-500"
  }, "BPM mặc định"), /*#__PURE__*/React.createElement("input", {
    type: "number",
    value: formBpm,
    onChange: e => setFormBpm(e.target.value),
    className: "bg-zinc-900 border border-zinc-700 rounded px-2.5 py-1 text-xs outline-none focus:border-purple-600 text-zinc-200"
  })), /*#__PURE__*/React.createElement("div", {
    className: "flex flex-col gap-1"
  }, /*#__PURE__*/React.createElement("label", {
    className: "text-[10px] uppercase font-bold text-zinc-500"
  }, "Âm giai (Scale) mặc định"), /*#__PURE__*/React.createElement("input", {
    type: "text",
    value: formScale,
    onChange: e => setFormScale(e.target.value),
    className: "bg-zinc-900 border border-zinc-700 rounded px-2.5 py-1 text-xs outline-none focus:border-purple-600 text-zinc-200"
  }))), /*#__PURE__*/React.createElement("div", {
    className: "flex flex-col gap-1 flex-1 min-h-0"
  }, /*#__PURE__*/React.createElement("label", {
    className: "text-[10px] uppercase font-bold text-zinc-500"
  }, "System Prompt Template / Luật soạn nhạc"), /*#__PURE__*/React.createElement("textarea", {
    value: formTemplate,
    onChange: e => setFormTemplate(e.target.value),
    rows: 6,
    className: "flex-1 bg-zinc-900 border border-zinc-700 rounded p-2.5 text-xs outline-none focus:border-purple-600 text-zinc-200 font-mono resize-none"
  })), /*#__PURE__*/React.createElement("div", {
    className: "flex items-center gap-2 shrink-0 pt-2 border-t border-zinc-800"
  }, /*#__PURE__*/React.createElement("button", {
    type: "button",
    onClick: handleNewStructured,
    className: "px-3 py-1.5 bg-emerald-800 hover:bg-emerald-700 text-emerald-300 border border-emerald-700 rounded text-xs transition"
  }, "Tạo preset với cấu trúc"), /*#__PURE__*/React.createElement("div", {
    className: "flex-1"
  }), /*#__PURE__*/React.createElement("button", {
    type: "button",
    onClick: () => setEditingPreset(null),
    className: "px-3 py-1.5 bg-zinc-800 hover:bg-zinc-700 text-zinc-400 border border-zinc-700 rounded text-xs transition"
  }, "Quay lại"), /*#__PURE__*/React.createElement("button", {
    type: "submit",
    className: "px-3 py-1.5 bg-purple-700 hover:bg-purple-600 text-white rounded text-xs font-bold shadow transition"
  }, "Lưu Preset")))), /*#__PURE__*/React.createElement("div", {
    className: "p-4 border-t border-zinc-800 flex justify-end shrink-0 bg-[#202024]"
  }, /*#__PURE__*/React.createElement("button", {
    onClick: () => { setEditingPreset(null); onClose(); },
    className: "px-4 py-1.5 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 border border-zinc-700 rounded text-xs font-bold shadow transition"
  }, "Đóng")))), showGeneratorModal ? /*#__PURE__*/React.createElement("div", {
    className: "fixed inset-0 z-[60] flex items-center justify-center bg-black/80 p-4",
    onClick: e => { if (e.target === e.currentTarget) { refreshPresets(); setShowGeneratorModal(false); } }
  }, /*#__PURE__*/React.createElement("div", {
    className: "w-full h-full max-w-6xl max-h-[90vh] bg-[#13141a] border border-zinc-800 rounded-2xl shadow-2xl overflow-hidden flex flex-col"
  }, /*#__PURE__*/React.createElement("div", {
    className: "flex items-center justify-between p-3 border-b border-zinc-800 shrink-0"
  }, /*#__PURE__*/React.createElement("h3", {
    className: "text-sm font-bold text-purple-400"
  }, "AI Prompt Generator"), /*#__PURE__*/React.createElement("button", {
    onClick: () => { refreshPresets(); setShowGeneratorModal(false); },
    className: "text-zinc-400 hover:text-white"
  }, /*#__PURE__*/React.createElement("i", {
    "data-lucide": "x",
    className: "w-4 h-4"
  }))), /*#__PURE__*/React.createElement("iframe", {
    src: "/ai-prompt-generator",
    className: "flex-1 w-full border-0 bg-white",
    title: "AI Prompt Generator"
  }))) : null);
};

const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClose, onUpdateNotes, onSaveNotes, setSubTabs, onPlayPause, onStop, isPlaying, playPreviewNote, showToast, midiDevices, recordingState, recTempMidiNotes, onRecord, selectedMidiInputId, onMidiInputSelect, activeMidiPitches, onInstrumentSelect, onRescheduleMidi, onSeekPlayhead, snapValue, onSnapChange, onRealtimePlay, onCopyNotes, clipboardNotes, onExportMidiAudio }) => {
  const [activeRollTool, setActiveRollTool] = React.useState('select');
  const [renderTick, setRenderTick] = React.useState(0);
  const [ccMode, setCcMode] = React.useState('velocity');
  const [rollZoom, setRollZoom] = React.useState(() => {
    // Persist zoom piano roll qua phiên (localStorage) — user yêu cầu giữ kích
    // thước zoom ở phiên làm việc trước.
    try {
      const v = parseFloat(localStorage.getItem('sf_pr_zoom'));
      if (isFinite(v) && v >= 15 && v <= 250) return v;
    } catch (e) { }
    return 60;
  }); // local horizontal zoom factor
  React.useEffect(() => {
    try { localStorage.setItem('sf_pr_zoom', String(rollZoom)); } catch (e) { }
  }, [rollZoom]);
  const [aiBarStart, setAiBarStart] = React.useState(0);
  const [aiBarEnd, setAiBarEnd] = React.useState(4);

  const canvasRef = React.useRef(null);
  const ccCanvasRef = React.useRef(null);
  const ccWrapperRef = React.useRef(null);
  const gridScrollRef = React.useRef(null);
  const keybedRef = React.useRef(null);
  const keybedMouseDownRef = React.useRef(false);
  const rulerScrollRef = React.useRef(null);
  // Audio element phát preview VSTi render — dừng cái cũ khi play cái mới
  const vstiPreviewAudioRef = React.useRef(null);
  React.useEffect(() => {
    const up = () => { keybedMouseDownRef.current = false; };
    window.addEventListener('mouseup', up);
    return () => window.removeEventListener('mouseup', up);
  }, []);

  const NoteHeight = 18;
  const PITCH_START = 0; // C0 (render all 128 keys)
  const KeybedPixelHeight = (128 - PITCH_START) * NoteHeight;
  const pixelsPerBeat = rollZoom;
  const timeSigNum = 4;
  const noteMaxBeat = (st.notes || []).reduce((max, n) => Math.max(max, (n.start_beat || 0) + (n.duration_beats || 1)), 0);
  const [selectionMarquee, setSelectionMarquee] = React.useState(null); // { startBeat, startPitch, currentBeat, currentPitch }
  const [draggedNote, setDraggedNote] = React.useState(null); // { mode: 'move'|'resize', idx, startOffsetBeat, originalStart }
  const draggedNoteRef = React.useRef(draggedNote);
  draggedNoteRef.current = draggedNote;
  const [hoveredResizeIdx, setHoveredResizeIdx] = React.useState(-1);
  const [rollBeats, setRollBeats] = React.useState(Math.max(noteMaxBeat + 16, 64));
  const rollBeatsRef = React.useRef(rollBeats);
  rollBeatsRef.current = rollBeats;

  const [showGhostNotes, setShowGhostNotes] = React.useState(true);
  const [sessionSyncMode, setSessionSyncMode] = React.useState(true);
  const [activePlayTrackIds, setActivePlayTrackIds] = React.useState([]);
  const [focusItemId, setFocusItemId] = React.useState(st.target_id);

  const allMidiItems = React.useMemo(() => {
    const result = [];
    (activeTracks || []).forEach(t => {
      if (!t.midiItems || !t.midiItems.length) return;
      t.midiItems.forEach(m => {
        var extended = Object.assign({}, m, { _trackId: t.id, _trackName: t.name });
        result.push(extended);
      });
    });
    return result;
  }, [activeTracks]);

  const ghostLayers = React.useMemo(function() {
    // Đang play (main/section) → BỎ ghost: extractGhostLayers duyệt TOÀN BỘ
    // project (tracks + notes) — block main thread 100-500ms → noteon
    // FluidSynth (scheduled qua setTimeout) TRỄ → âm bị lag 1-2 lần khi mở
    // piano roll tab lúc đang play (user bug 06:30). Ghost chỉ là hiển thị
    // tham chiếu — không cần khi âm đang chạy — tính lại khi dừng.
    if (isPlaying) return [];
    if (!activeTracks || !st || !st.target_id) return [];
    var fn = window.SonicGhost && window.SonicGhost.extractGhostLayers;
    return fn ? fn(activeTracks, st.trackId, st.target_id, parseInt(bpm) || 120) : [];
  }, [activeTracks, st.trackId, st.target_id, bpm, isPlaying]);

  const secondsPerBar = (60.0 / (parseInt(bpm) || 120)) * 4;
  const activeTargetItem = React.useMemo(function() {
    if (!activeTracks || !st) return null;
    var trk = activeTracks.find(function(t) { return t.id === st.trackId; });
    return trk ? (trk.midiItems || []).find(function(m) { return m.id === st.target_id; }) : null;
  }, [activeTracks, st.trackId, st.target_id]);
  var activeParentTrackName = '';
  if (st.target_id && activeTracks) {
    var aptTrk = window.SonicPianoRoll ? window.SonicPianoRoll.getParentTrackByItemId(st.target_id, activeTracks) : null;
    if (!aptTrk) aptTrk = activeTracks.find(function(t) { return t.id === st.trackId; });
    if (!aptTrk && activeTargetItem) aptTrk = activeTracks.find(function(t) { return (t.midiItems || []).some(function(m) { return m.id === st.target_id; }); });
    if (aptTrk) activeParentTrackName = aptTrk.name || aptTrk.id;
  }
  const sessionStartBar = 0;
  const renderBeatOffset = sessionSyncMode && activeTargetItem
    ? (activeTargetItem.startTime / secondsPerBar) * timeSigNum : 0;

  const sessionLengthBars = React.useMemo(function() {
    var maxSec = 0;
    (activeTracks || []).forEach(function(tr) {
      (tr.midiItems || []).forEach(function(m) {
        var end = (m.startTime || 0) + (m.duration || 4);
        if (end > maxSec) maxSec = end;
      });
    });
    return Math.ceil((maxSec || 4) / secondsPerBar);
  }, [activeTracks, secondsPerBar]);

  const handleSwitchMidiItem = function(itemId) {
    if (itemId === st.target_id) return;
    var match = allMidiItems.find(function(m) { return m.id === itemId; });
    if (!match) return;
    var scope = window.SonicPianoRoll ? window.SonicPianoRoll.buildActiveScope(itemId, activeTracks) : null;
    var trk = scope ? null : (activeTracks || []).find(function(t) { return t.id === match._trackId; });
    var newBeatOff = (match.startTime / secondsPerBar) * timeSigNum;
    var spb = 60.0 / (parseInt(bpm) || 120);
    var newTime = 0;
    setSubTabs(function(prev) {
      return prev.map(function(s) {
        if (s.id !== st.id) return s;
        return Object.assign({}, s, {
          trackId: scope ? scope.parent_track_id : (match._trackId || trk?.id),
          target_id: match.id,
          label: 'Piano Roll: ' + (match.name || 'MIDI'),
          notes: match.notes || [],
          duration: match.duration || 4,
          instrumentProgram: scope ? scope.instrument_program : (trk ? trk.instrumentProgram : undefined),
          instrumentName: scope ? scope.instrument_name : (trk ? trk.instrumentName : undefined),
          active_scope: scope || null,
          note_selection: [],
          currentTime: newTime
        });
      });
    });
    setSelectedNoteIds([]);
  };

  const rawTotalBeats = Math.max(rollBeats, noteMaxBeat + 16, 64);
  const drawWidth = rawTotalBeats * pixelsPerBeat;
  const [rollViewWidth, setRollViewWidth] = React.useState(800);
  const viewWidth = Math.max(drawWidth, rollViewWidth);
  const viewBeats = Math.ceil(viewWidth / pixelsPerBeat) + 4;
  const totalBeats = Math.max(rawTotalBeats, viewBeats + 32);

  const [notes, setNotes] = React.useState(st.notes || []);
  const notesRef = React.useRef(notes);
  notesRef.current = notes;
  React.useEffect(() => { if (!draggedNoteRef.current) setNotes(st.notes || []); }, [st.notes]);
  const brushVelocityRef = React.useRef(0.8);
  const lastNoteDurationRef = React.useRef(null);
  const previewPitchRef = React.useRef(null);
  const previewNodesRef = React.useRef(null);
  var stopPreviewNote = function() {
    var pn = previewNodesRef.current;
    if (pn) {
      try { pn.osc.stop(); } catch(e) {}
      try { pn.osc.disconnect(); } catch(e) {}
      try { pn.gain.disconnect(); } catch(e) {}
      previewNodesRef.current = null;
    }
  };
  const [selectedNoteIds, setSelectedNoteIds] = React.useState([]);
  const [loopStartBeat, setLoopStartBeat] = React.useState(null);
  const [loopEndBeat, setLoopEndBeat] = React.useState(null);
  const [isLooping, setIsLooping] = React.useState(false);
  const rulerDragRef = React.useRef(null);
  React.useEffect(() => {
    const handler = (e) => {
      if ((e.ctrlKey || e.metaKey) && e.key === 'a') {
        const target = e.target;
        if (target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable)) return;
        e.preventDefault();
        setSelectedNoteIds(notes.map(n => n.id));
        return;
      }
      // Spec 20:12 shortcuts: Alt+A Arp, Alt+S Strum, Alt+R Humanize,
      // Shift+C chord stamp, Shift+S lock-scale toggle
      const inField = e.target && (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.isContentEditable);
      // Ctrl+C/X/V — copy/paste/cut NOTES (thay 2 nút Copy/Paste đã bỏ —
      // user 08:40) — CHỈ tác động NOTES ĐƯỢC CHỌN (user 09:10)
      if ((e.ctrlKey || e.metaKey) && !e.altKey && !inField) {
        if (e.key === 'c') {
          e.preventDefault();
          const sel = notes.filter(n => selectedNoteIds.includes(n.id));
          if (sel.length) {
            if (onCopyNotes) onCopyNotes(sel);
            showToast('Đã copy ' + sel.length + ' nốt được chọn.', 'success');
          } else { showToast('Chọn notes trước khi copy (Ctrl+C).', 'warning'); }
          return;
        }
        if (e.key === 'x') {
          e.preventDefault();
          const sel = notes.filter(n => selectedNoteIds.includes(n.id));
          if (sel.length) {
            if (onCopyNotes) onCopyNotes(sel);
            pushToUndo(notes);
            setNotes(prev => (prev || []).filter(n => !selectedNoteIds.includes(n.id)));
            showToast('Đã cắt ' + sel.length + ' nốt được chọn.', 'success');
          } else { showToast('Chọn notes trước khi cắt (Ctrl+X).', 'warning'); }
          return;
        }
        if (e.key === 'v') {
          e.preventDefault();
          if (!clipboardNotes || !clipboardNotes.length) { showToast('Clipboard trống — bấm Ctrl+C trước.', 'warning'); return; }
          pushToUndo(notes);
          // Dán bắt đầu tại vị trí CON TRỎ PLAYHEAD (user 09:25) — bù offset
          // sao cho note đầu tiên của clipboard nằm đúng playhead (beat).
          const pasteBeat = (st.currentTime || 0) / (60.0 / (parseInt(bpm) || 120));
          const minStart = Math.min(...clipboardNotes.map(n => n.start_beat));
          const offset = pasteBeat - minStart;
          setNotes(prev => [...(prev || []), ...clipboardNotes.map(function(n) {
            return { ...n, id: 'note_cp_' + Date.now() + '_' + Math.floor(Math.random() * 100000), start_beat: Math.max(0, n.start_beat + offset) };
          })]);
          showToast('Đã paste ' + clipboardNotes.length + ' nốt tại playhead.', 'success');
          return;
        }
      }
      if (e.altKey && !e.ctrlKey && !inField && e.key === 'a') { e.preventDefault(); setArpModal({ pattern: 'UP', rate: '1/16', octaves: 2, gate: 80, triplet: false, dotted: false }); return; }
      if (e.altKey && !e.ctrlKey && !inField && e.key === 's') { e.preventDefault(); setStrumModal({ ms: 30, direction: 'DOWN' }); return; }
      if (e.altKey && !e.ctrlKey && !inField && e.key === 'r') { e.preventDefault(); setHumanizeModal({ timingMs: 12, velRange: 15, durRange: 10 }); return; }
      if (e.shiftKey && !e.ctrlKey && !e.altKey && !inField && e.key === 'c') { e.preventDefault(); setChordStampMode(m => !m); return; }
      if (e.shiftKey && !e.ctrlKey && !e.altKey && !inField && e.key === 's') {
        e.preventDefault();
        setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, snapToScale: !(s.snapToScale !== undefined ? s.snapToScale : true) } : s));
        return;
      }
    };
    window.addEventListener('keydown', handler);
    return () => window.removeEventListener('keydown', handler);
  }, [notes, setSelectedNoteIds, clipboardNotes, selectedNoteIds]);

  // Undo/redo stacks
  const undoStackRef = React.useRef([]);
  const redoStackRef = React.useRef([]);
  const notesBeforeDragRef = React.useRef(null);

  const pushToUndo = React.useCallback((prevNotes) => {
    undoStackRef.current.push(JSON.parse(JSON.stringify(prevNotes)));
    redoStackRef.current = [];
    if (undoStackRef.current.length > 50) undoStackRef.current.shift();
  }, []);

  // ── Humanize: ngẫu nhiên hóa velocity + timing theo cường độ ──
  const [humanizeStrength, setHumanizeStrength] = React.useState(0.10); // 0.05 nhẹ / 0.10 vừa / 0.18 mạnh
  const applyHumanize = React.useCallback(() => {
    if (!notes || !notes.length) { showToast('Không có nốt nào để humanize.', 'warning'); return; }
    const velAmt = humanizeStrength;
    const timeAmt = humanizeStrength * 0.15; // ±0.015 beat @ vừa (~12ms @120bpm)
    pushToUndo(notes);
    setNotes(prev => (prev || []).map(n => ({
      ...n,
      velocity: Math.max(0.05, Math.min(1.0, (n.velocity || 0.8) + (Math.random() * 2 - 1) * velAmt)),
      start_beat: Math.max(0, (n.start_beat || 0) + (Math.random() * 2 - 1) * timeAmt)
    })));
    showToast('Đã humanize ' + notes.length + ' nốt (velocity ±' + Math.round(velAmt * 100) + '%, timing ±' + Math.round(timeAmt * 1000) + 'ms).', 'success');
  }, [notes, pushToUndo, setNotes, showToast, humanizeStrength]);

  // ── Transpose semitone: dịch pitch tất cả nốt (clamp 0-127) ──
  const applyTranspose = React.useCallback((semi) => {
    const s = parseInt(semi);
    if (isNaN(s) || s === 0) { showToast('Nhập số semitone khác 0.', 'warning'); return; }
    if (!notes || !notes.length) { showToast('Không có nốt nào để transpose.', 'warning'); return; }
    pushToUndo(notes);
    setNotes(prev => (prev || []).map(n => ({
      ...n,
      pitch: Math.max(0, Math.min(127, (n.pitch || 60) + s))
    })));
    showToast('Đã transpose ' + notes.length + ' nốt ' + (s > 0 ? '+' : '') + s + ' semitone.', 'success');
  }, [notes, pushToUndo, setNotes, showToast]);

  // ── Transpose theo SCALE (chuyển giọng): detect key hiện tại → map degree ──
  const SCALE_PATTERNS = { major: [0, 2, 4, 5, 7, 9, 11], minor: [0, 2, 3, 5, 7, 8, 10] };
  const SCALE_ROOTS = { C: 0, 'C#': 1, D: 2, 'D#': 3, E: 4, F: 5, 'F#': 6, G: 7, 'G#': 8, A: 9, 'A#': 10, B: 11 };
  const detectKey = React.useCallback((noteList) => {
    const roots = Object.keys(SCALE_ROOTS);
    let best = null, bestScore = -1;
    for (let ri = 0; ri < roots.length; ri++) {
      for (const st of ['major', 'minor']) {
        const tones = new Set(SCALE_PATTERNS[st].map(s => (SCALE_ROOTS[roots[ri]] + s) % 12));
        let score = 0;
        (noteList || []).forEach(n => { const pc = (((n.pitch || 60) % 12) + 12) % 12; if (tones.has(pc)) score++; });
        if (score > bestScore) { bestScore = score; best = { root: roots[ri], scale: st }; }
      }
    }
    return best || { root: 'C', scale: 'major' };
  }, []);
  const [keyTargetRoot, setKeyTargetRoot] = React.useState('C');
  const [keyTargetScale, setKeyTargetScale] = React.useState('major');
  // Auto-detect scale khi MỞ midi item → hiển thị ở dropdown chuyển giọng.
  // Key theo st.target_id (item id) — sửa note cùng item KHÔNG reset lựa chọn
  // của user; mở item khác → detect lại.
  React.useEffect(() => {
    const itemNotes = st.notes || [];
    if (itemNotes.length) {
      const k = detectKey(itemNotes);
      setKeyTargetRoot(k.root);
      setKeyTargetScale(k.scale);
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [st.target_id]);
  const applyTransposeToKey = React.useCallback(() => {
    if (!notes || !notes.length) { showToast('Không có nốt nào để chuyển giọng.', 'warning'); return; }
    const srcKey = detectKey(notes);
    const dstKey = { root: keyTargetRoot, scale: keyTargetScale };
    if (srcKey.root === dstKey.root && srcKey.scale === dstKey.scale) {
      showToast('Đã ở giọng ' + dstKey.root + ' ' + dstKey.scale + ' rồi.', 'info');
      return;
    }
    const srcTones = SCALE_PATTERNS[srcKey.scale].map(s => (SCALE_ROOTS[srcKey.root] + s) % 12);
    const dstTones = SCALE_PATTERNS[dstKey.scale].map(s => (SCALE_ROOTS[dstKey.root] + s) % 12);
    pushToUndo(notes);
    setNotes(prev => (prev || []).map(n => {
      const p = n.pitch || 60;
      const pc = ((p % 12) + 12) % 12;
      // Degree gần nhất trong scale nguồn (7 bậc)
      let bestIdx = 0, bestDist = 99;
      for (let i = 0; i < 7; i++) {
        let d = Math.abs(pc - srcTones[i]); if (d > 6) d = 12 - d;
        if (d < bestDist) { bestDist = d; bestIdx = i; }
      }
      let shift = dstTones[bestIdx] - srcTones[bestIdx];
      if (shift > 6) shift -= 12; else if (shift < -6) shift += 12;
      return { ...n, pitch: Math.max(0, Math.min(127, p + shift)) };
    }));
    showToast('Chuyển giọng ' + srcKey.root + ' ' + srcKey.scale + ' → ' + dstKey.root + ' ' + dstKey.scale + ' (' + notes.length + ' nốt).', 'success');
  }, [notes, pushToUndo, setNotes, showToast, detectKey, keyTargetRoot, keyTargetScale]);

  const [transposeSemis, setTransposeSemis] = React.useState(0);

  const handleUndo = React.useCallback(() => {
    const prev = undoStackRef.current.pop();
    if (!prev) return;
    redoStackRef.current.push(JSON.parse(JSON.stringify(notes)));
    setNotes(prev);
    setSelectedNoteIds([]);
  }, [notes]);

  const handleRedo = React.useCallback(() => {
    const next = redoStackRef.current.pop();
    if (!next) return;
    undoStackRef.current.push(JSON.parse(JSON.stringify(notes)));
    setNotes(next);
    setSelectedNoteIds([]);
  }, [notes]);

  React.useEffect(() => {
    const handler = e => {
      if ((e.ctrlKey || e.metaKey) && e.key === 'z' && !e.shiftKey) {
        e.preventDefault();
        handleUndo();
      } else if ((e.ctrlKey || e.metaKey) && (e.key === 'y' || (e.key === 'z' && e.shiftKey))) {
        e.preventDefault();
        handleRedo();
      } else if ((e.ctrlKey || e.metaKey) && e.key === 's') {
        e.preventDefault();
        onSaveNotes(st.id, st.trackId, st.target_id, notes);
        showToast('Đã lưu MIDI notes', 'info');
      } else if (e.key === 'Delete' || e.key === 'Backspace') {
        if (selectedNoteIds.length > 0 && e.target.tagName !== 'INPUT' && e.target.tagName !== 'TEXTAREA') {
          e.preventDefault();
          pushToUndo(notes);
          setNotes(prev => prev.filter(n => !selectedNoteIds.includes(n.id)));
          setSelectedNoteIds([]);
          showToast(`Đã xóa ${selectedNoteIds.length} nốt!`, 'info');
        }
      } else if (e.key === 'F7') {
        e.preventDefault();
        e.stopPropagation();
        var toggleMixer = window.__toggleMixerRef;
        if (toggleMixer) toggleMixer();
      } else if (e.key === 'F6') {
        e.preventDefault();
        e.stopPropagation();
        var toggleMediaExplorer = window.__toggleMediaExplorerRef;
        if (toggleMediaExplorer) toggleMediaExplorer();
      }
    };
    window.addEventListener('keydown', handler);
    return () => window.removeEventListener('keydown', handler);
  }, [handleUndo, handleRedo, notes, selectedNoteIds, onSaveNotes, showToast]);

  React.useEffect(() => {
    onUpdateNotes(st.id, notes);
  }, [notes]);

  const getSnapBeat = (beat, mode) => {
    let q = 0.25;
    if (mode === 'free') return beat;
    if (mode === '1') q = 1.0;
    else if (mode === '1/2') q = 0.5;
    else if (mode === '1/4') q = 0.25;
    else if (mode === '1/8') q = 0.125;
    else if (mode === '1/16') q = 0.0625;
    else if (mode === '4') q = 4.0;
    else if (mode === '1/32') q = 0.03125;
    return Math.round(beat / q) * q;
  };

  const getSnapDuration = (mode) => {
    if (mode === 'free') return 0.25;
    if (mode === '1') return 1.0;
    if (mode === '1/2') return 0.5;
    if (mode === '1/4') return 0.25;
    if (mode === '1/8') return 0.125;
    if (mode === '1/16') return 0.0625;
    if (mode === '4') return 4.0;
    if (mode === '1/32') return 0.03125;
    return 0.25;
  };

  // Local Zoom Wheel Event handler to block browser page zoom
  React.useEffect(() => {
    const handleWheelRaw = (e) => {
      if (e.ctrlKey) {
        e.preventDefault();
        const zoomFactor = e.deltaY < 0 ? 1.15 : 0.85;
        setRollZoom(prev => Math.max(15, Math.min(250, prev * zoomFactor)));
      }
    };
    const container = gridScrollRef.current;
    if (container) {
      container.addEventListener('wheel', handleWheelRaw, { passive: false });
    }
    return () => {
      if (container) {
        container.removeEventListener('wheel', handleWheelRaw);
      }
    };
  }, []);

  // Alt + Scroll event listener: fast‑forward playhead + play notes
  React.useEffect(() => {
    const handleCanvasWheel = (e) => {
      const canvas = canvasRef.current;
      if (!canvas) return;
      const rect = canvas.getBoundingClientRect();
      const mx = e.clientX - rect.left;
      const my = e.clientY - rect.top;
      const pitch = 127 - Math.floor(my / NoteHeight);

      if (e.shiftKey) {
        e.preventDefault();
        // Shift+scroll on note → change velocity of single note or all selected
        const scrollBeat = (mx / pixelsPerBeat) - renderBeatOffset;
        const clickedNote = notes.find(n => pitch === n.pitch && scrollBeat >= n.start_beat && scrollBeat < (n.start_beat + n.duration_beats));
        if (clickedNote) {
          const delta = e.deltaY < 0 ? 0.05 : -0.05;
          if (selectedNoteIds.length > 0) {
            setNotes(prev => prev.map(n => selectedNoteIds.includes(n.id) ? { ...n, velocity: Math.max(0.05, Math.min(1, (n.velocity || 0.8) + delta)) } : n));
          } else {
            setNotes(prev => prev.map(n => n.id === clickedNote.id ? { ...n, velocity: Math.max(0.05, Math.min(1, (n.velocity || 0.8) + delta)) } : n));
          }
        } else {
          // Shift+scroll on empty space → horizontal scroll
          const container = gridScrollRef.current;
          if (container) container.scrollLeft += e.deltaY;
        }
        return;
      }

      if (e.altKey) {
        e.preventDefault();
        const scrollDelta = e.deltaY;
        const beatSec = 60.0 / (parseInt(bpm) || 120);
        const step = scrollDelta < 0 ? -0.25 : 0.25;
        const currentBeat = (st.currentTime || 0) / beatSec;
        const maxBeats = totalBeats;
        const newBeat = Math.max(0, Math.min(maxBeats, currentBeat + step));
        const newTime = newBeat * beatSec;
        setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, currentTime: newTime } : s));
        if (window.SonicSF) {
          const ctx = getAudioContext();
          const playing = notes.filter(n =>
            currentBeat < n.start_beat && newBeat >= n.start_beat
          );
          var pvTrk = activeTracks.find(function(t) { return t.id === st.trackId; });
          var pvCtx = resolveTrackInstrumentCtx(pvTrk, activeTracks);
          ensureSonicInstrument(pvCtx);
          playing.forEach(n => {
            if (isStandaloneSf() && isSfTrackEngine(pvCtx.synthEngine) && !shouldRouteCarla(pvCtx.synthEngine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive())) {
              playNativeSfNote(pvTrk, n.pitch, n.velocity || 0.8, 200, undefined, 'pv_' + st.trackId);
              return;
            }
            scheduleMidiNoteDispatch(pvTrk, n.pitch, n.velocity || 0.8, 200, ctx.currentTime, pvCtx.program, null, pvCtx.ch, pvCtx.synthEngine);
          });
        }
      }
    };

    const canvas = canvasRef.current;
    if (canvas) {
      canvas.addEventListener('wheel', handleCanvasWheel, { passive: false });
    }
    return () => {
      if (canvas) {
        canvas.removeEventListener('wheel', handleCanvasWheel);
      }
    };
  }, [notes, st.currentTime, pixelsPerBeat, st.id, totalBeats, bpm]);

    React.useLayoutEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const ctx = canvas.getContext('2d');
    const dpr = window.devicePixelRatio || 1;
    const h = 128 * NoteHeight;
    canvas.width = viewWidth * dpr;
    canvas.height = h * dpr;
    ctx.scale(dpr, dpr);

    // Scale highlight: rows thuộc scale (root+scale) sáng vàng; ngoài scale
    // dim hơn (spec 20:12 — Scale Highlight & Lock)
    const scHighlight = scaleWithRoot();
    // Draw background rows
    for (let pitch = 0; pitch < 128; pitch++) {
      const y = (127 - pitch) * NoteHeight;
      const isBlack = [1, 3, 6, 8, 10].includes(pitch % 12);
      const inScale = scHighlight ? scHighlight.includes(pitch % 12) : null;
      ctx.fillStyle = isBlack ? (inScale === false ? '#141419' : '#1f1f25') : (inScale === false ? '#1b1b20' : (inScale === true ? '#2c2a1e' : '#25252a'));
      ctx.fillRect(0, y, viewWidth, NoteHeight);

      ctx.strokeStyle = '#2d2d35';
      ctx.lineWidth = 0.5;
      ctx.beginPath();
      ctx.moveTo(0, y + NoteHeight);
      ctx.lineTo(viewWidth, y + NoteHeight);
      ctx.stroke();
    }

    // Draw snap lines
    let snapBeats = 0.25;
    if (snapValue === '1') snapBeats = 1.0;
    else if (snapValue === '1/2') snapBeats = 0.5;
    else if (snapValue === '1/4') snapBeats = 0.25;
    else if (snapValue === '1/8') snapBeats = 0.125;
    else if (snapValue === '1/16') snapBeats = 0.0625;
    else if (snapValue === '4') snapBeats = 4.0;
    else if (snapValue === '1/32') snapBeats = 0.03125;

    for (let beat = 0; beat <= viewBeats; beat += snapBeats) {
      const x = beat * pixelsPerBeat;
      if (x > viewWidth) break;
      const isBar = beat % timeSigNum === 0;
      ctx.strokeStyle = isBar ? '#444450' : '#2d2d35';
      ctx.lineWidth = isBar ? 1.2 : 0.6;
      ctx.beginPath();
      ctx.moveTo(x, 0);
      ctx.lineTo(x, h);
      ctx.stroke();
    }

    // Determine which MIDI item is focused (clicked/selected note → its item; playing → item under playhead)
    var focusedItemId = focusItemId || st.target_id;
    if (selectedNoteIds && selectedNoteIds.length > 0) {
      // Most recently selected note wins: main note → opened item; dim same-track note → its item
      focusedItemId = st.target_id;
      for (var s2 = selectedNoteIds.length - 1; s2 >= 0; s2--) {
        var sid = selectedNoteIds[s2];
        if (notes.some(function(n) { return n.id === sid; })) { focusedItemId = st.target_id; break; }
        var foundGhost2 = false;
        for (var sg2 = 0; sg2 < ghostLayers.length; sg2++) {
          var sl2 = ghostLayers[sg2];
          if (!sl2.isSameTrack || !sl2.notes) continue;
          var gnote2 = sl2.notes.find(function(g) { return g.id === sid; });
          if (gnote2) { focusedItemId = gnote2.item_id || st.target_id; foundGhost2 = true; break; }
        }
        if (foundGhost2) break;
      }
    } else if (st.isPlaying && st.currentTime != null && activeTracks) {
      var curSec = st.currentTime || 0;
      var fidxTrk = activeTracks.find(function(t) { return t.id === st.trackId; });
      var itemsArr = fidxTrk ? (fidxTrk.midiItems || []) : [];
      for (var fi = 0; fi < itemsArr.length; fi++) {
        var fit = itemsArr[fi];
        var fStart = fit.startTime || 0;
        var fEnd = fStart + (fit.duration || 4);
        if (curSec >= fStart && curSec < fEnd) { focusedItemId = fit.id; break; }
      }
    }

    // Layer 2: Ghost Notes (same-track: focused item bright, siblings dim; other tracks faint)
    if (showGhostNotes && sessionSyncMode && ghostLayers.length > 0) {
      ghostLayers.forEach(function(layer) {
        ctx.save();
        var isSameTrackLayer = layer.isSameTrack;
        layer.notes.forEach(function(note) {
          var isFocusedNote = isSameTrackLayer && note.item_id === focusedItemId;
          var isGhostSelected = selectedNoteIds.indexOf(note.id) !== -1;
          if (isSameTrackLayer) {
            ctx.globalAlpha = isFocusedNote ? 0.7 : 0.3;
            ctx.fillStyle = isGhostSelected ? 'rgba(96, 165, 250, 0.65)' : (isFocusedNote ? 'rgba(253, 224, 71, 0.7)' : 'rgba(251, 191, 36, 0.3)');
            ctx.strokeStyle = isGhostSelected ? '#60a5fa' : (isFocusedNote ? '#fde047' : 'rgba(245, 158, 11, 0.6)');
          } else {
            ctx.globalAlpha = 0.25;
            ctx.fillStyle = layer.track_color || '#888';
            ctx.strokeStyle = layer.track_color || '#888';
          }
          var snapStart = snapValue !== 'free' ? getSnapBeat(note.relative_start_beat, snapValue) : note.relative_start_beat;
          var rawEnd = note.relative_start_beat + note.duration_beats;
          var snapEnd = snapValue !== 'free' ? getSnapBeat(rawEnd, snapValue) : rawEnd;
          var x = (renderBeatOffset + snapStart) * pixelsPerBeat;
          var y = (127 - note.pitch) * NoteHeight;
          var w = Math.max(2, (snapEnd - snapStart) * pixelsPerBeat);
          var h = NoteHeight - 1;
          ctx.fillRect(x, y, w, h);
          if (isSameTrackLayer && (isFocusedNote || isGhostSelected)) ctx.strokeRect(x + 0.5, y + 0.5, w - 1, h - 1);
        });
        ctx.restore();
      });
    }

    // Layer 3: Active notes with velocity layer representation
    notes.forEach((note) => {
      const snapStart = snapValue !== 'free' ? getSnapBeat(note.start_beat, snapValue) : note.start_beat;
      const rawEnd = note.start_beat + note.duration_beats;
      const snapEnd = snapValue !== 'free' ? getSnapBeat(rawEnd, snapValue) : rawEnd;
      const x = (renderBeatOffset + snapStart) * pixelsPerBeat;
      const y = (127 - note.pitch) * NoteHeight;
      const w = Math.max(2, (snapEnd - snapStart) * pixelsPerBeat);
      const isSelected = selectedNoteIds.includes(note.id);

      // Draw background of note (focused item = brighter, siblings = dim)
      const playingNow = st.isPlaying;
      const mainFocused = focusedItemId === st.target_id;
      ctx.fillStyle = isSelected ? 'rgba(96, 165, 250, 0.6)' : (mainFocused ? (playingNow ? 'rgba(254, 240, 138, 0.75)' : 'rgba(253, 224, 71, 0.6)') : 'rgba(120, 100, 40, 0.35)');
      ctx.strokeStyle = isSelected ? '#60a5fa' : (mainFocused ? (playingNow ? '#fef08a' : '#fde047') : '#8a7504');
      ctx.lineWidth = isSelected ? 1.5 : (mainFocused ? 1.2 : 0.8);
      ctx.fillRect(x + 1, y + 1, w - 2, NoteHeight - 2);
      ctx.strokeRect(x + 1, y + 1, w - 2, NoteHeight - 2);

      // Draw velocity layer (solid yellow/blue bar inside, proportional to velocity)
      const vel = note.velocity !== undefined ? note.velocity : 0.8;
      const velW = Math.max(2, (w - 2) * vel);
      ctx.fillStyle = isSelected ? '#60a5fa' : (mainFocused ? (playingNow ? '#fde047' : '#facc15') : '#7a6a10');
      ctx.fillRect(x + 1, y + 1, velW, NoteHeight - 2);
    });

    // Draw real-time recording notes
    if (recordingState === 'RECORDING' && recTempMidiNotes && recTempMidiNotes.length > 0) {
      recTempMidiNotes.forEach(note => {
        const snapStart = snapValue !== 'free' ? getSnapBeat(note.start_beat, snapValue) : note.start_beat;
        const rawEnd = note.start_beat + (note.duration_beats || 0.25);
        const snapEnd = snapValue !== 'free' ? getSnapBeat(rawEnd, snapValue) : rawEnd;
        const x = (renderBeatOffset + snapStart) * pixelsPerBeat;
        const y = (127 - note.pitch) * NoteHeight;
        const w = Math.max(2, (snapEnd - snapStart) * pixelsPerBeat);
        ctx.fillStyle = 'rgba(255, 100, 100, 0.35)';
        ctx.strokeStyle = '#ff6464';
        ctx.lineWidth = 1;
        ctx.fillRect(x + 1, y + 1, Math.max(2, w - 2), NoteHeight - 2);
        ctx.strokeRect(x + 1, y + 1, Math.max(2, w - 2), NoteHeight - 2);
        const vel = Math.min(1, note.velocity || 0.8);
        ctx.fillStyle = '#ff6464';
        ctx.fillRect(x + 1, y + 1, Math.max(2, (w - 2) * vel), NoteHeight - 2);
      });
    }

    // Draw selection marquee if active
    if (selectionMarquee) {
      const minBeat = Math.min(selectionMarquee.startBeat, selectionMarquee.currentBeat);
      const maxBeat = Math.max(selectionMarquee.startBeat, selectionMarquee.currentBeat);
      const minPitch = Math.min(selectionMarquee.startPitch, selectionMarquee.currentPitch);
      const maxPitch = Math.max(selectionMarquee.startPitch, selectionMarquee.currentPitch);

      const mx = minBeat * pixelsPerBeat;
      const my = (127 - maxPitch) * NoteHeight;
      const mw = (maxBeat - minBeat) * pixelsPerBeat;
      const mh = (maxPitch - minPitch + 1) * NoteHeight;

      ctx.fillStyle = 'rgba(59, 130, 246, 0.15)';
      ctx.strokeStyle = '#3b82f6';
      ctx.lineWidth = 1;
      ctx.setLineDash([4, 4]);
      ctx.fillRect(mx, my, mw, mh);
      ctx.strokeRect(mx, my, mw, mh);
      ctx.setLineDash([]);
    }

    // Draw playhead
    if (st.currentTime !== undefined && st.currentTime !== null) {
      const phBeat = st.currentTime / (60.0 / (parseInt(bpm) || 120));
      const phX = phBeat * pixelsPerBeat;
      if (phX >= 0 && phX <= viewWidth) {
        ctx.strokeStyle = '#f59e0b';
        ctx.lineWidth = 1.5;
        ctx.beginPath();
        ctx.moveTo(phX, 0);
        ctx.lineTo(phX, (128 - PITCH_START) * NoteHeight);
        ctx.stroke();
      }
    }
  }, [notes, snapValue, rollZoom, selectedNoteIds, selectionMarquee, st.currentTime, bpm, viewWidth, viewBeats, recordingState, recTempMidiNotes, showGhostNotes, sessionSyncMode, ghostLayers, renderBeatOffset, renderTick, activeTracks, focusItemId]);

  // showCC/ccHeight khai báo TRƯỚC useLayoutEffect vẽ CC (deps tham chiếu —
  // khai báo sau → TDZ error — user 08:50)
  const [showCC, setShowCC] = React.useState(true);
  const [ccHeight, setCcHeight] = React.useState(80);

  React.useLayoutEffect(() => {
    const canvas = ccCanvasRef.current;
    if (!canvas) return;
    const ctx = canvas.getContext('2d');
    const dpr = window.devicePixelRatio || 1;
    const h = ccHeight;
    canvas.width = viewWidth * dpr;
    canvas.height = h * dpr;
    ctx.scale(dpr, dpr);

    ctx.fillStyle = '#161616';
    ctx.fillRect(0, 0, viewWidth, h);

    ctx.strokeStyle = '#252525';
    ctx.lineWidth = 1;
    ctx.beginPath();
    ctx.moveTo(0, h / 2);
    ctx.lineTo(viewWidth, h / 2);
    ctx.stroke();

    notes.forEach((note) => {
      const x = (renderBeatOffset + note.start_beat) * pixelsPerBeat;
      const isSelected = selectedNoteIds.includes(note.id);
      let val = note.velocity !== undefined ? note.velocity : 0.8;
      if (ccMode === 'pan') {
        val = (note.pan !== undefined ? note.pan : 0.0) * 0.5 + 0.5;
      }

      const stemH = val * (h - 20) + 10;
      const y = h - stemH;

      ctx.strokeStyle = ccMode === 'pan' ? (isSelected ? '#60a5fa' : '#a78bfa') : (isSelected ? '#3b82f6' : '#fbbf24');
      ctx.lineWidth = 2.5;
      ctx.beginPath();
      ctx.moveTo(x, h);
      ctx.lineTo(x, y);
      ctx.stroke();

      ctx.fillStyle = ccMode === 'pan' ? (isSelected ? '#3b82f6' : '#c084fc') : (isSelected ? '#3b82f6' : '#fbbf24');
      ctx.beginPath();
      ctx.arc(x, y, 3.5, 0, 2 * Math.PI);
      ctx.fill();
    });
  }, [notes, ccMode, rollZoom, viewWidth, selectedNoteIds, renderBeatOffset, ccHeight, showCC]);

  // Follow playhead: khi PLAY — playhead luôn ở GIỮA view, notes trôi sang
  // trái (scroll theo st.currentTime); khi STOP — scroll về đầu (playhead ở
  // vị trí đầu piano roll) — user 09:40. Dùng CẢ isPlaying (main) LẪN
  // st.isPlaying (piano roll play — main isPlaying=false khi tab play — bug
  // 09:50: effect tưởng đang stop → luôn về đầu).
  React.useEffect(function() {
    const wrapper = gridScrollRef.current;
    if (!wrapper) return;
    const playing = isPlaying || !!st.isPlaying;
    if (!playing) {
      if (wrapper.scrollLeft !== 0) { wrapper.scrollLeft = 0; setRenderTick(t => t + 1); }
      return;
    }
    const beatSec = 60.0 / (parseInt(bpm) || 120);
    const phBeat = (st.currentTime || 0) / beatSec;
    const midX = Math.max(0, wrapper.clientWidth / 2);
    const targetLeft = Math.max(0, phBeat * pixelsPerBeat - midX);
    if (Math.abs(wrapper.scrollLeft - targetLeft) > 1) {
      wrapper.scrollLeft = targetLeft;
    }
    setRenderTick(t => t + 1);
  }, [isPlaying, st.isPlaying, st.currentTime]);

  React.useEffect(() => {
    const scrollToC3 = () => {
      if (gridScrollRef.current) {
        const ch = gridScrollRef.current.clientHeight || 400;
        gridScrollRef.current.scrollTop = Math.max(0, (127 - 48) * NoteHeight + NoteHeight - ch);
      }
    };
    scrollToC3();
    const timer = setTimeout(scrollToC3, 100);
    return () => clearTimeout(timer);
  }, []);

  // Sync ghost play data to subTab state for playback integration
  React.useEffect(function() {
    if (!sessionSyncMode || !showGhostNotes || !ghostLayers.length) {
      setSubTabs(function(prev) { return prev.map(function(s) { if (s.id !== st.id) return s; return Object.assign({}, s, { ghostPlayLayers: [] }); }); });
      return;
    }
    var layers = [];
    ghostLayers.forEach(function(layer) {
      var layerIsSameTrack = layer.isSameTrack;
      if (!layerIsSameTrack && (!activePlayTrackIds || activePlayTrackIds.indexOf(layer.track_id) === -1)) return;
      var trk = (activeTracks || []).find(function(t) { return t.id === layer.track_id; });
      layers.push({
        trackId: layer.track_id,
        notes: layer.notes.map(function(n) { return { pitch: n.pitch, start_beat: n.relative_start_beat, duration_beats: n.duration_beats, velocity: n.velocity || 0.8 }; }),
        instrumentProgram: trk ? trk.instrumentProgram : undefined,
        instrumentName: trk ? trk.instrumentName : undefined,
        synthEngine: trk ? trk.synth_engine : undefined
      });
    });
    setSubTabs(function(prev) { return prev.map(function(s) { if (s.id !== st.id) return s; return Object.assign({}, s, { ghostPlayLayers: layers }); }); });
  }, [ghostLayers, activePlayTrackIds, sessionSyncMode, showGhostNotes, st.id, activeTracks]);

  // Reset item focus when the opened MIDI item changes
  React.useEffect(function() {
    setFocusItemId(st.target_id);
  }, [st.id, st.target_id]);

  const handleGridMouseDown = (e) => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const rect = canvas.getBoundingClientRect();
    const x = e.clientX - rect.left;
    const y = e.clientY - rect.top;

    const beat = x / pixelsPerBeat - renderBeatOffset;
    const pitch = 127 - Math.floor(y / NoteHeight);

    if (scaleMenuPos) setScaleMenuPos(null);

    // Right click -> delete note (if on note) or prepare for sweep-drag
    if (e.button === 2) {
      e.preventDefault();
      const clickedNote = notes.find(n => {
        return pitch === n.pitch && beat >= n.start_beat && beat < n.start_beat + n.duration_beats;
      });
      if (clickedNote) {
        pushToUndo(notes);
        setNotes(prev => prev.filter(n => n.id !== clickedNote.id));
        setSelectedNoteIds(prev => prev.filter(id => id !== clickedNote.id));
        swallowContextMenuRef.current = true;
        showToast('Đã xóa nốt!', 'info');
      } else {
        rightClickDragRef.current = { active: true, startX: e.clientX, startY: e.clientY };
      }
      return;
    }

    if (e.button !== 0) return; // Only handle left click

    // Check if clicking on an existing note
    const clickedNoteIdx = notes.findIndex(n => {
      return pitch === n.pitch && beat >= n.start_beat && beat < n.start_beat + n.duration_beats;
    });

    // Click on note → play note with SoundFont
    if (clickedNoteIdx !== -1 && !e.ctrlKey && !e.shiftKey && !e.altKey) {
      if (window.SonicSF) {
        const ctx = getAudioContext();
        var clTrk = activeTracks.find(function(t) { return t.id === st.trackId; });
        var clCtx = resolveTrackInstrumentCtx(clTrk, activeTracks);
        ensureSonicInstrument(clCtx);
        if (isStandaloneSf() && isSfTrackEngine(clCtx.synthEngine) && !shouldRouteCarla(clCtx.synthEngine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive())) {
          playNativeSfNote(clTrk, notes[clickedNoteIdx].pitch, notes[clickedNoteIdx].velocity || 0.8, 300, undefined, 'pv_' + st.trackId);
        } else {
          scheduleMidiNoteDispatch(clTrk, notes[clickedNoteIdx].pitch, notes[clickedNoteIdx].velocity || 0.8, 300, ctx.currentTime, clCtx.program, null, clCtx.ch, clCtx.synthEngine);
        }
      }
    }

    // Shift+click: toggle selection (multi-select) — user 09:30 (trước đây là
    // Ctrl+click — Ctrl giờ dành cho COPY-drag)
    if (e.shiftKey && !e.altKey && !e.ctrlKey) {
      if (clickedNoteIdx !== -1) {
        const clickedNote = notes[clickedNoteIdx];
        if (selectedNoteIds.includes(clickedNote.id)) {
          setSelectedNoteIds(prev => prev.filter(id => id !== clickedNote.id));
        } else {
          setSelectedNoteIds(prev => [...prev, clickedNote.id]);
        }
        return;
      }
    }

    // Ctrl+click NOTE + drag → COPY nhanh nhóm notes/note đến vị trí mới
    // (cùng pitch ban đầu; drag đổi vị trí + pitch) — user 09:30
    if (e.ctrlKey && !e.altKey && !e.shiftKey) {
      if (clickedNoteIdx !== -1) {
        const clickedNote = notes[clickedNoteIdx];
        const groupIds = (selectedNoteIds.includes(clickedNote.id) && selectedNoteIds.length > 1)
          ? selectedNoteIds : [clickedNote.id];
        const src = notes.filter(n => groupIds.includes(n.id));
        if (!src.length) return;
        pushToUndo(notes);
        notesBeforeDragRef.current = JSON.parse(JSON.stringify(notes));
        const clones = src.map(n => ({ ...JSON.parse(JSON.stringify(n)), id: 'note_' + Date.now() + Math.random().toString(36).substr(2, 8) }));
        setNotes(prev => [...prev, ...clones]);
        const cloneIds = clones.map(c => c.id);
        setSelectedNoteIds(cloneIds);
        const cloneOffsets = clones.map(n => ({ id: n.id, originalStartBeat: n.start_beat, originalPitch: n.pitch }));
        setDraggedNote({
          mode: 'move',
          idx: -1,
          startOffsetBeat: beat - clickedNote.start_beat,
          startOffsetPitch: pitch,
          selectedNotesOffset: cloneOffsets,
          clickedOriginalStartBeat: clickedNote.start_beat
        });
        showToast('Kéo để copy ' + clones.length + ' nốt.', 'info');
        return;
      } else {
        // Ctrl+click on a dim (same-track) MIDI note → select it and focus its MIDI item
        var ctrlGhostHit = null;
        for (var cgi = 0; cgi < ghostLayers.length; cgi++) {
          var cgl = ghostLayers[cgi];
          if (!cgl.isSameTrack || !cgl.notes) continue;
          for (var cgn = 0; cgn < cgl.notes.length; cgn++) {
            var cgnote = cgl.notes[cgn];
            if (pitch === cgnote.pitch && beat >= cgnote.relative_start_beat && beat < cgnote.relative_start_beat + (cgnote.duration_beats || 1)) {
              ctrlGhostHit = cgnote;
              break;
            }
          }
          if (ctrlGhostHit) break;
        }
        if (ctrlGhostHit) {
          setFocusItemId(ctrlGhostHit.item_id || st.target_id);
          if (selectedNoteIds.includes(ctrlGhostHit.id)) {
            setSelectedNoteIds(prev => prev.filter(id => id !== ctrlGhostHit.id));
          } else {
            setSelectedNoteIds(prev => [...prev, ctrlGhostHit.id]);
          }
          return;
        }
        // Ctrl+click on empty space: start selection marquee
        setSelectedNoteIds([]);
        const snapStart = getSnapBeat(beat, snapValue);
        setSelectionMarquee({
          startBeat: snapStart, startPitch: pitch,
          currentBeat: snapStart, currentPitch: pitch
        });
        return;
      }
    }

    // Ctrl+Shift+click on note → split at click position
    if (e.ctrlKey && e.shiftKey) {
      if (clickedNoteIdx !== -1) {
        const target = notes[clickedNoteIdx];
        const splitBeat = getSnapBeat(beat, snapValue);
        if (splitBeat > target.start_beat + 0.03125 && splitBeat < target.start_beat + target.duration_beats - 0.03125) {
          pushToUndo(notes);
          const noteA = {
            ...JSON.parse(JSON.stringify(target)),
            id: 'note_' + Date.now() + Math.random().toString(36).substr(2, 8),
            duration_beats: splitBeat - target.start_beat
          };
          const noteB = {
            ...JSON.parse(JSON.stringify(target)),
            id: 'note_' + Date.now() + Math.random().toString(36).substr(2, 8),
            start_beat: splitBeat,
            duration_beats: target.start_beat + target.duration_beats - splitBeat
          };
          setNotes(prev => {
            const idx = prev.findIndex(n => n.id === target.id);
            if (idx === -1) return prev;
            const result = [...prev];
            result.splice(idx, 1, noteA);
            result.splice(idx + 1, 0, noteB);
            return result;
          });
          setSelectedNoteIds([noteA.id, noteB.id]);
          showToast('Đã tách nốt!', 'info');
        }
      } else {
        // Ctrl+Shift+click on empty space → duplicate selected + clicked notes
        pushToUndo(notes);
        const clones = notes.filter(n => selectedNoteIds.includes(n.id)).map(n => ({
          ...JSON.parse(JSON.stringify(n)),
          id: 'note_' + Date.now() + Math.random().toString(36).substr(2, 8)
        }));
        if (clones.length > 0) {
          setNotes(prev => [...prev, ...clones]);
          const cloneIds = clones.map(c => c.id);
          setSelectedNoteIds(cloneIds);
          notesBeforeDragRef.current = JSON.parse(JSON.stringify(notes));
          const cloneOffsets = clones.map(n => ({ id: n.id, originalStartBeat: n.start_beat, originalPitch: n.pitch }));
          setDraggedNote({
            mode: 'move',
            idx: -1,
            startOffsetBeat: beat,
            startOffsetPitch: pitch,
            selectedNotesOffset: cloneOffsets
          });
          showToast('Đã nhân bản ' + clones.length + ' nốt!', 'info');
        }
      }
      return;
    }

    // Hovered resize edge (Alt+resize for scaling)
    if (hoveredResizeIdx !== -1 && e.altKey) {
      pushToUndo(notes);
      notesBeforeDragRef.current = JSON.parse(JSON.stringify(notes));
      const allSelected = [...new Set(selectedNoteIds.length > 0 ? selectedNoteIds : [notes[hoveredResizeIdx].id])];
      const selectedNotes = notes.filter(n => allSelected.includes(n.id));
      const firstStart = Math.min(...selectedNotes.map(n => n.start_beat));
      const draggedNote = notes[hoveredResizeIdx];
      setDraggedNote({
        mode: 'scale',
        idx: hoveredResizeIdx,
        originalEnd: draggedNote.start_beat + draggedNote.duration_beats,
        firstStart: firstStart,
        selectedNoteIds: allSelected
      });
      return;
    }

    // Hovered resize edge (normal resize)
    if (hoveredResizeIdx !== -1) {
      pushToUndo(notes);
      notesBeforeDragRef.current = JSON.parse(JSON.stringify(notes));
      setDraggedNote({
        mode: 'resize',
        idx: hoveredResizeIdx,
        originalStart: notes[hoveredResizeIdx].start_beat
      });
      return;
    }

    if (clickedNoteIdx !== -1) {
      // Click on existing note: drag-move → focus its MIDI item
      setFocusItemId(st.target_id);
      const clickedNote = notes[clickedNoteIdx];
      let nextSelectedIds;
      if (!selectedNoteIds.includes(clickedNote.id)) {
        nextSelectedIds = [clickedNote.id];
        setSelectedNoteIds(nextSelectedIds);
      } else {
        nextSelectedIds = selectedNoteIds;
      }
      pushToUndo(notes);
      notesBeforeDragRef.current = JSON.parse(JSON.stringify(notes));
      const selectedNotesOffset = notes
        .filter(n => nextSelectedIds.includes(n.id))
        .map(n => ({
          id: n.id,
          originalStartBeat: n.start_beat,
          originalPitch: n.pitch
        }));
      setDraggedNote({
        mode: 'move',
        idx: clickedNoteIdx,
        startOffsetBeat: beat - clickedNote.start_beat,
        startOffsetPitch: pitch,
        selectedNotesOffset: selectedNotesOffset,
        clickedOriginalStartBeat: clickedNote.start_beat
      });
    } else {
      // Click on a same-track ghost note → focus that MIDI item (no drawing)
      if (!e.ctrlKey && !e.shiftKey && !e.altKey) {
        var ghostHit = null;
        for (var gi = 0; gi < ghostLayers.length; gi++) {
          var gl = ghostLayers[gi];
          if (!gl.isSameTrack || !gl.notes) continue;
          for (var gn = 0; gn < gl.notes.length; gn++) {
            var gnote = gl.notes[gn];
            if (pitch === gnote.pitch && beat >= gnote.relative_start_beat && beat < gnote.relative_start_beat + (gnote.duration_beats || 1)) {
              ghostHit = gnote;
              break;
            }
          }
          if (ghostHit) break;
        }
        if (ghostHit) {
          setFocusItemId(ghostHit.item_id || st.target_id);
          if (ghostHit.item_id && ghostHit.item_id !== st.target_id) {
            handleSwitchMidiItem(ghostHit.item_id);
          }
          return;
        }
      }
      // Chord stamp mode: click canvas → stamp full chord shape (spec 20:12)
      if (chordStampMode) {
        const start = getSnapBeat(beat, snapValue);
        applyChordStamp(start, snapToScaleRef.current ? snapPitchToScale(pitch, scaleWithRoot()) : pitch);
        return;
      }
      // Click on empty space with pen tool: DRAW a new note (brush mode with visitedPitches)
      pushToUndo(notes);
      const start = getSnapBeat(beat, snapValue);
      const initialDur = lastNoteDurationRef.current || getSnapDuration(snapValue);
      const noteId = 'note_' + Date.now() + Math.random().toString(36).substr(2, 5);
      const newNote = {
        id: noteId,
        pitch: snapToScaleRef.current ? snapPitchToScale(pitch, selectedScaleRef.current) : pitch,
        start_beat: start,
        duration_beats: initialDur,
        velocity: brushVelocityRef.current,
        pan: 0.0
      };
      setNotes(prev => [...prev, newNote]);
      setSelectedNoteIds([noteId]);
      setDraggedNote({
        mode: 'draw',
        idx: -1,
        startOffsetBeat: start,
        startOffsetPitch: snapToScaleRef.current ? snapPitchToScale(pitch, selectedScaleRef.current) : pitch,
        drawNoteId: noteId,
        drawDuration: initialDur,
        initialBeat: start,
        initialPitch: snapToScaleRef.current ? snapPitchToScale(pitch, selectedScaleRef.current) : pitch,
        noteStartBeats: [start],
        lastDrawnPitch: snapToScaleRef.current ? snapPitchToScale(pitch, selectedScaleRef.current) : pitch
      });
      // Play the note with SoundFont - stop previous preview first
      if (previewNodesRef.current) {
        try { previewNodesRef.current.osc.stop(); } catch(e) {}
        try { previewNodesRef.current.osc.disconnect(); } catch(e) {}
        try { previewNodesRef.current.gain.disconnect(); } catch(e) {}
        previewNodesRef.current = null;
      }
      var dwTrk = activeTracks.find(function(t) { return t.id === st.trackId; });
      var dwCh = dwTrk ? assignTrackMidiChannel(dwTrk, activeTracks) : 0;
      var dwPitch = snapToScaleRef.current ? snapPitchToScale(pitch, selectedScaleRef.current) : pitch;
      var dwDurMs = Math.max(100, Math.round(initialDur * (60 / bpm) * 1000));
      if (isStandaloneSf() && isSfTrackEngine(dwTrk && dwTrk.synth_engine) && !shouldRouteCarla(dwTrk && dwTrk.synth_engine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive())) {
        playNativeSfNote(dwTrk, dwPitch, brushVelocityRef.current || 0.8, dwDurMs, undefined, 'pvdraw_' + st.trackId);
      } else if (window.SonicSF && window.SonicSF.playNote) {
        const ctx = getAudioContext();
        // playNote (FluidSynth — nhạc cụ THẬT của track). _playNoteFallback chỉ
        // là oscillator beep (sai âm với percussion/soundfont — user: note vẽ
        // mới nghe nhạc cụ track trước).
        scheduleMidiNoteDispatch(dwTrk, dwPitch, brushVelocityRef.current || 0.8, dwDurMs, ctx.currentTime, dwTrk ? dwTrk.instrumentProgram : undefined, null, dwCh, dwTrk ? dwTrk.synth_engine : undefined);
      }
    }
  };

  const handleGridMouseMove = (e) => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const rect = canvas.getBoundingClientRect();
    const x = e.clientX - rect.left;
    const y = e.clientY - rect.top;

    const beat = x / pixelsPerBeat - renderBeatOffset;
    const pitch = 127 - Math.floor(y / NoteHeight);

    if (selectionMarquee) {
      const snappedBeat = getSnapBeat(beat, snapValue);
      const marquee = {
        ...selectionMarquee,
        currentBeat: snappedBeat,
        currentPitch: pitch
      };
      setSelectionMarquee(marquee);

      const minBeat = Math.min(marquee.startBeat, marquee.currentBeat);
      const maxBeat = Math.max(marquee.startBeat, marquee.currentBeat);
      const minPitch = Math.min(marquee.startPitch, marquee.currentPitch);
      const maxPitch = Math.max(marquee.startPitch, marquee.currentPitch);

      const insideIds = notes
        .filter(n => {
          const withinPitch = n.pitch >= minPitch && n.pitch <= maxPitch;
          if (!withinPitch) return false;
          const noteEnd = n.start_beat + n.duration_beats;
          if (marquee.startBeat <= marquee.currentBeat) {
            // Left to right: select if any overlap
            return n.start_beat <= maxBeat && noteEnd >= minBeat;
          } else {
            // Right to left: select only if fully covered
            return n.start_beat >= minBeat && noteEnd <= maxBeat;
          }
        })
        .map(n => n.id);

      setSelectedNoteIds(insideIds);
      return;
    }

    // Right-click drag → erase sweep
    const rc = rightClickDragRef.current;
    if (rc.active && (Math.abs(e.clientX - rc.startX) > 5 || Math.abs(e.clientY - rc.startY) > 5)) {
      rc.active = false;
      swallowContextMenuRef.current = true;
      notesBeforeDragRef.current = JSON.parse(JSON.stringify(notes));
      setDraggedNote({ mode: 'erase_sweep', visitedPitches: [] });
      return;
    }

    if (!draggedNote) {
      let foundIdx = -1;
      for (let i = 0; i < notes.length; i++) {
        const n = notes[i];
        if (pitch === n.pitch) {
          const noteRightBeat = n.start_beat + n.duration_beats;
          const pixelDist = Math.abs((noteRightBeat - beat) * pixelsPerBeat);
          if (pixelDist < 6 && beat >= n.start_beat) {
            foundIdx = i;
            break;
          }
        }
      }
      if (foundIdx !== -1) {
        canvas.style.cursor = 'ew-resize';
        setHoveredResizeIdx(foundIdx);
      } else {
        canvas.style.cursor = activeRollTool === 'eraser' ? 'pointer' : 'crosshair';
        setHoveredResizeIdx(-1);
      }
      return;
    }
    if (draggedNote.mode === 'draw') {
      const snappedPitch = snapToScaleRef.current ? snapPitchToScale(pitch, selectedScaleRef.current) : pitch;
      const lastPitch = draggedNote.lastDrawnPitch !== undefined ? draggedNote.lastDrawnPitch : draggedNote.startOffsetPitch;
      const pitchChanged = snappedPitch !== lastPitch;
      const noteBeats = draggedNote.noteStartBeats || [];
      const defaultDur = getSnapDuration(snapValue);

      function playDrawPreview(p, durMs) {
        stopPreviewNote();
        // Đồng bộ mastering + routing SF trước khi preview draw (âm qua
        // mastering FX của main out khi chain bật)
        try { if (window.__ensureMasteringRouting) window.__ensureMasteringRouting(); } catch (er) {}
        var pvTrk = activeTracks.find(function(t) { return t.id === st.trackId; });
        var pvCtxInst = resolveTrackInstrumentCtx(pvTrk, activeTracks);
        if (isStandaloneSf() && isSfTrackEngine(pvCtxInst.synthEngine) && !shouldRouteCarla(pvCtxInst.synthEngine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive())) {
          playNativeSfNote(pvTrk, p, brushVelocityRef.current || 0.8, durMs, undefined, 'pvdraw_' + st.trackId);
        } else if (window.SonicSF && window.SonicSF.playNote) {
          var pvCtx = getAudioContext();
          // playNote (FluidSynth — nhạc cụ THẬT). _playNoteFallback = oscillator
          // beep sai âm (percussion/soundfont).
          scheduleMidiNoteDispatch(pvTrk, p, brushVelocityRef.current || 0.8, durMs, pvCtx.currentTime, pvCtxInst.program, null, pvCtxInst.ch, pvCtxInst.synthEngine);
          previewPitchRef.current = p;
        }
        // MIDI → Carla (track VSTi + ARM + Carla local): preview realtime
        if (window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoute(pvTrk && pvTrk.synth_engine, st.isArmed)) {
          var pvVel2 = Math.round((brushVelocityRef.current || 0.8) * 127);
          window.SonicCarlaMidi.playNote(pvTrk && pvTrk.synth_engine ? (pvTrk.synth_engine.midi_channel || 0) : 0, p, pvVel2, durMs);
        }
      }

      if (pitchChanged) {
        const brushIds = draggedNote.brushIds || [];
        if (brushIds.length > 0 && noteBeats.length > 0) {
          const prevNoteId = brushIds[brushIds.length - 1];
          const prevNoteBeat = noteBeats[noteBeats.length - 1];
          const prevDur = Math.max(0.125, beat - prevNoteBeat);
          setNotes(prev => prev.map(n => {
            if (n.id !== prevNoteId) return n;
            return { ...n, duration_beats: prevDur };
          }));
        }
        const newNote = {
          id: 'note_' + Date.now() + Math.random().toString(36).substr(2, 8) + '_' + (noteBeats.length + 1),
          pitch: snappedPitch,
          start_beat: beat,
          duration_beats: defaultDur,
          velocity: brushVelocityRef.current,
          pan: 0.0
        };
        setNotes(prev => [...prev, newNote]);
        setSelectedNoteIds(prev => [...prev, newNote.id]);
        draggedNote.brushIds = [...brushIds, newNote.id];
        draggedNote.lastDrawnPitch = snappedPitch;
        draggedNote.noteStartBeats = [...noteBeats, beat];
        playDrawPreview(snappedPitch, Math.max(100, Math.round(defaultDur * (60 / bpm) * 1000)));
      } else {
        const brushIds = draggedNote.brushIds || [];
        const lastBrushId = brushIds.length > 0 ? brushIds[brushIds.length - 1] : draggedNote.drawNoteId;
        const lastNoteBeat = noteBeats.length > 0 ? noteBeats[noteBeats.length - 1] : draggedNote.startOffsetBeat;
        if (lastBrushId) {
          const extDur = Math.max(0.125, beat - lastNoteBeat);
          setNotes(prev => prev.map(n => {
            if (n.id !== lastBrushId) return n;
            return { ...n, duration_beats: extDur };
          }));
          playDrawPreview(snappedPitch, Math.max(100, Math.round(extDur * (60 / bpm) * 1000)));
        }
      }
      const container = gridScrollRef.current;
      if (container) {
        const cr = container.getBoundingClientRect();
        const visTop = container.scrollTop;
        const visBot = visTop + container.clientHeight;
        const pitchPixel = (127 - snappedPitch) * NoteHeight;
        const safeMargin = NoteHeight * 2;
        if (pitchPixel < visTop + safeMargin) {
          const target = Math.max(0, pitchPixel - safeMargin);
          if (container.scrollTop !== target) container.scrollTop = target;
          if (!brushAutoScrollRef.current || brushAutoScrollRef.current.direction !== 'up') {
            if (brushAutoScrollRef.current) clearInterval(brushAutoScrollRef.current.id);
            brushAutoScrollRef.current = {
              direction: 'up',
              id: setInterval(() => {
                if (gridScrollRef.current) gridScrollRef.current.scrollTop = Math.max(0, gridScrollRef.current.scrollTop - Math.max(1, Math.floor(NoteHeight * 0.5)));
              }, 16)
            };
          }
        } else if (pitchPixel + NoteHeight > visBot - safeMargin) {
          const target = Math.min(container.scrollHeight - container.clientHeight, pitchPixel - container.clientHeight + safeMargin + NoteHeight);
          if (container.scrollTop !== target) container.scrollTop = target;
          if (!brushAutoScrollRef.current || brushAutoScrollRef.current.direction !== 'down') {
            if (brushAutoScrollRef.current) clearInterval(brushAutoScrollRef.current.id);
            brushAutoScrollRef.current = {
              direction: 'down',
              id: setInterval(() => {
                if (gridScrollRef.current) gridScrollRef.current.scrollTop = Math.min(gridScrollRef.current.scrollHeight - gridScrollRef.current.clientHeight, gridScrollRef.current.scrollTop + Math.max(1, Math.floor(NoteHeight * 0.5)));
              }, 16)
            };
          }
        } else {
          if (brushAutoScrollRef.current) {
            clearInterval(brushAutoScrollRef.current.id);
            brushAutoScrollRef.current = null;
          }
        }
      }
      return;
    }
    if (draggedNote.mode === 'erase_sweep') {
      const erased = draggedNote.erasedIds || [];
      const target = notes.find(n =>
        n.pitch === pitch && beat >= n.start_beat && beat < n.start_beat + n.duration_beats
      );
      if (target && !erased.includes(target.id)) {
        draggedNote.erasedIds = [...erased, target.id];
        setNotes(prev => prev.filter(n => n.id !== target.id));
        setSelectedNoteIds(prev => prev.filter(id => id !== target.id));
      }
      return;
    }
    if (draggedNote.mode === 'scale') {
      const newEnd = getSnapBeat(Math.max(draggedNote.firstStart + 0.125, beat), snapValue);
      const range = draggedNote.originalEnd - draggedNote.firstStart;
      if (range <= 0) return;
      const scaleFactor = Math.max(0.01, (newEnd - draggedNote.firstStart) / range);
      const ids = draggedNote.selectedNoteIds || [];
      const firstStart = draggedNote.firstStart;
      const notesBefore = notesBeforeDragRef.current;
      if (!notesBefore) return;
      setNotes(prev => prev.map(n => {
        if (!ids.includes(n.id)) return n;
        const orig = notesBefore ? notesBefore.find(o => o.id === n.id) : null;
        const origStart = orig ? orig.start_beat : n.start_beat;
        const origDur = orig ? orig.duration_beats : n.duration_beats;
        const relStart = origStart - firstStart;
        const relEnd = relStart + origDur;
        return {
          ...n,
          start_beat: firstStart + relStart * scaleFactor,
          duration_beats: Math.max(0.125, relEnd * scaleFactor - relStart * scaleFactor)
        };
      }));
      return;
    }
    if (draggedNote.mode === 'resize') {
      const newDuration = getSnapBeat(Math.max(0.125, beat - draggedNote.originalStart), snapValue);
      setNotes(prev => prev.map((n, idx) => {
        if (idx !== draggedNote.idx) return n;
        return {
          ...n,
          duration_beats: newDuration
        };
      }));
    } else if (draggedNote.mode === 'move') {
      const refOrigStart = draggedNote.clickedOriginalStartBeat;
      if (refOrigStart === undefined) return;
      const deltaBeat = getSnapBeat(beat - draggedNote.startOffsetBeat, snapValue) - refOrigStart;
      // Clamp so no note goes past beat 0
      const minOrigStart = Math.min(...draggedNote.selectedNotesOffset.map(o => o.originalStartBeat));
      const clampedDeltaBeat = minOrigStart + deltaBeat < 0 ? -minOrigStart : deltaBeat;
      const deltaPitch = Math.round(pitch - draggedNote.startOffsetPitch);

      setNotes(prev => prev.map(n => {
        const offset = draggedNote.selectedNotesOffset.find(o => o.id === n.id);
        if (!offset) return n;
        return {
          ...n,
          start_beat: getSnapBeat(Math.max(0, offset.originalStartBeat + clampedDeltaBeat), snapValue),
          pitch: Math.max(0, Math.min(127, offset.originalPitch + deltaPitch))
        };
      }));
    }
  };

  const handleGridMouseUp = () => {
    if (draggedNote && draggedNote.mode === 'draw') {
      const dn = draggedNote;
      const brushIds = dn.brushIds || [];
      const lastBrushId = brushIds.length > 0 ? brushIds[brushIds.length - 1] : dn.drawNoteId;
      if (lastBrushId) {
        const lastNote = notes.find(n => n.id === lastBrushId);
        if (lastNote) lastNoteDurationRef.current = lastNote.duration_beats;
      }
    }
    setDraggedNote(null);
    // Marquee (Ctrl+drag): CHỌN notes nằm trong vùng (user 09:15 — trước đây
    // chỉ vẽ highlight, không set selectedNoteIds → Ctrl+X báo "chọn notes")
    if (selectionMarquee) {
      const minBeat = Math.min(selectionMarquee.startBeat, selectionMarquee.currentBeat);
      const maxBeat = Math.max(selectionMarquee.startBeat, selectionMarquee.currentBeat);
      const minPitch = Math.min(selectionMarquee.startPitch, selectionMarquee.currentPitch);
      const maxPitch = Math.max(selectionMarquee.startPitch, selectionMarquee.currentPitch);
      const inMarquee = notes.filter(n => {
        const center = n.start_beat + n.duration_beats / 2;
        return center >= minBeat && center <= maxBeat && n.pitch >= minPitch && n.pitch <= maxPitch;
      });
      setSelectedNoteIds(inMarquee.map(n => n.id));
    }
    setSelectionMarquee(null);
    rightClickDragRef.current = { active: false, startX: 0, startY: 0 };
    if (brushAutoScrollRef.current) {
      clearInterval(brushAutoScrollRef.current.id);
      brushAutoScrollRef.current = null;
    }
    stopPreviewNote();
    previewPitchRef.current = null;
  };

  const handleContextMenu = (e) => {
    e.preventDefault();
    if (swallowContextMenuRef.current) {
      swallowContextMenuRef.current = false;
      return;
    }
    const pos = { x: e.clientX, y: e.clientY, parentKey: null };
    scaleMenuOriginRef.current = { x: pos.x, y: pos.y };
    setScaleMenuPos(pos);
  };

  const ccDragRef = React.useRef(null);
  const brushAutoScrollRef = React.useRef(null);
  const rightClickDragRef = React.useRef({ active: false, startX: 0, startY: 0 });
  const swallowContextMenuRef = React.useRef(false);
  // Status hint động (user 09:40): theo dõi Shift/Ctrl + mouse trong piano roll
  const prKeyStateRef = React.useRef({ shift: false, ctrl: false });
  const prMouseInRef = React.useRef(false);
  React.useEffect(function() {
    const updateHint = function() {
      if (!window.__setPrHint || !prMouseInRef.current) return;
      const ks = prKeyStateRef.current;
      window.__setPrHint(ks.ctrl ? "Ctrl+drag: fast copy notes | Ctrl+click empty: marquee chọn notes" : (ks.shift ? "Shift+Click: select/unselect notes | Shift+Click note: thêm/bớt vào nhóm chọn" : "Scroll: Up/Down | Drag: Draw notes"));
    };
    const kd = function(e) { const ks = prKeyStateRef.current; if (e.shiftKey !== ks.shift || e.ctrlKey !== ks.ctrl) { ks.shift = e.shiftKey; ks.ctrl = e.ctrlKey; updateHint(); } };
    const ku = function(e) { const ks = prKeyStateRef.current; if (e.shiftKey !== ks.shift || e.ctrlKey !== ks.ctrl) { ks.shift = e.shiftKey; ks.ctrl = e.ctrlKey; updateHint(); } };
    window.addEventListener('keydown', kd);
    window.addEventListener('keyup', ku);
    return function() { window.removeEventListener('keydown', kd); window.removeEventListener('keyup', ku); };
  }, []);

  const findCCNoteIndex = (b, mouseY, ccH) => {
    const snapped = getSnapBeat(b, snapValue);
    const hits = [];
    notes.forEach((n, idx) => {
      if (snapped >= n.start_beat && snapped <= n.start_beat + n.duration_beats) {
        const nv = ccMode === 'pan' ? ((n.pan || 0) * 0.5 + 0.5) : (n.velocity !== undefined ? n.velocity : 0.8);
        const stemTop = ccH - (nv * (ccH - 20) + 10);
        hits.push({ idx, dist: Math.abs(stemTop - mouseY) });
      }
    });
    if (hits.length > 0) {
      hits.sort((a, b) => a.dist - b.dist);
      return hits[0].idx;
    }
    let nearest = -1;
    let minDist = Infinity;
    notes.forEach((n, idx) => {
      const center = n.start_beat + n.duration_beats / 2;
      const d = Math.abs(center - snapped);
      if (d < minDist) { minDist = d; nearest = idx; }
    });
    return nearest;
  };

  // TẤT CẢ notes bắt đầu tại cùng beat (chord — velocity bars CÙNG cột) —
  // user 08:25: ctrl+draw qua vị trí → vẽ lại TẤT CẢ velocity cùng vị trí
  // (trước đây findCCNoteIndex chỉ trả 1 note → chord khó draw).
  const findCCNoteIndicesAtBeat = (b) => {
    const snapped = getSnapBeat(b, snapValue);
    const out = [];
    notes.forEach((n, idx) => {
      if (Math.abs(n.start_beat - snapped) < 0.01) out.push(idx);
    });
    return out;
  };

  const handleCCMouseDown = (e) => {
    const canvas = ccCanvasRef.current;
    if (!canvas) return;
    const rect = canvas.getBoundingClientRect();
    const x = e.clientX - rect.left;
    const y = e.clientY - rect.top;
    const h = rect.height;
    const beat = x / pixelsPerBeat - renderBeatOffset;

    const noteIdx = findCCNoteIndex(beat, y, h);
    const val = Math.max(0, Math.min(1, (h - y) / h));

    if (e.ctrlKey) {
      if (selectedNoteIds.length > 0) {
        const cursorNoteIdx = findCCNoteIndex(beat, y, h);
        if (cursorNoteIdx !== -1 && selectedNoteIds.includes(notes[cursorNoteIdx] ? notes[cursorNoteIdx].id : -1)) {
          const currentNote = notes[cursorNoteIdx];
          const currentVal = ccMode === 'pan' ? ((currentNote.pan || 0) / 2.0 + 0.5) : (currentNote.velocity !== undefined ? currentNote.velocity : 0.8);
          if (Math.abs(currentVal - val) > 0.001) {
            const updatedNotes = notes.map((n, i) => {
              if (i !== cursorNoteIdx) return n;
              if (ccMode === 'pan') return { ...n, pan: (val - 0.5) * 2.0 };
              return { ...n, velocity: val };
            });
            setNotes(updatedNotes);
            if (isPlaying && onRescheduleMidi) onRescheduleMidi(updatedNotes);
          }
          ccDragRef.current = { active: true, lastBeat: beat, selectedMode: true, lastPainted: [cursorNoteIdx] };
        } else {
          ccDragRef.current = { active: true, lastBeat: beat, selectedMode: true, lastPainted: [] };
        }
      } else {
        // Không chọn notes: vẽ TẤT CẢ notes cùng beat (chord — user 08:25)
        const idxs = findCCNoteIndicesAtBeat(beat);
        ccDragRef.current = { active: true, lastBeat: beat, lastPainted: idxs };
      }
      return;
    }

    if (noteIdx !== -1) {
      const currentVal = ccMode === 'pan' ? ((notes[noteIdx].pan || 0) / 2.0 + 0.5) : (notes[noteIdx].velocity !== undefined ? notes[noteIdx].velocity : 0.8);
      if (Math.abs(currentVal - val) > 0.001) {
        const updatedNotes = notes.map((n, i) => {
          if (i !== noteIdx) return n;
          if (ccMode === 'pan') return { ...n, pan: (val - 0.5) * 2.0 };
          return { ...n, velocity: val };
        });
        setNotes(updatedNotes);
        if (isPlaying && onRescheduleMidi) onRescheduleMidi(updatedNotes);
      }
    }
  };

  const handleCCMouseMove = (e) => {
    if (!ccDragRef.current || !ccDragRef.current.active) return;
    const canvas = ccCanvasRef.current;
    if (!canvas) return;
    const rect = canvas.getBoundingClientRect();
    const x = e.clientX - rect.left;
    const y = e.clientY - rect.top;
    const h = rect.height;
    const beat = x / pixelsPerBeat - renderBeatOffset;
    const val = Math.max(0, Math.min(1, (h - y) / h));

    const drag = ccDragRef.current;
    const painted = drag.lastPainted || [];

    if (drag.selectedMode && selectedNoteIds.length > 0) {
      const cursorNoteIdx = findCCNoteIndex(beat, y, h);
      if (cursorNoteIdx !== -1) {
        const cursorNote = notes[cursorNoteIdx];
        if (cursorNote && selectedNoteIds.includes(cursorNote.id) && !painted.includes(cursorNoteIdx)) {
          const currentVal = ccMode === 'pan' ? ((cursorNote.pan || 0) / 2.0 + 0.5) : (cursorNote.velocity !== undefined ? cursorNote.velocity : 0.8);
          if (Math.abs(currentVal - val) > 0.001) {
            const updatedNotes = notes.map((n, i) => {
              if (i !== cursorNoteIdx) return n;
              if (ccMode === 'pan') return { ...n, pan: (val - 0.5) * 2.0 };
              return { ...n, velocity: val };
            });
            setNotes(updatedNotes);
            if (isPlaying && onRescheduleMidi) onRescheduleMidi(updatedNotes);
          }
          drag.lastPainted = [...painted, cursorNoteIdx];
        }
      }
      return;
    }

    // Vẽ TẤT CẢ notes cùng beat (chord — user 08:25) — trước đây chỉ 1 note
    const candidateIdxs = findCCNoteIndicesAtBeat(beat);
    const unpainted = candidateIdxs.filter(ci => !painted.includes(ci));
    if (unpainted.length > 0) {
      const updatedNotes = notes.map((n, i) => {
        if (!unpainted.includes(i)) return n;
        if (ccMode === 'pan') return { ...n, pan: (val - 0.5) * 2.0 };
        return { ...n, velocity: val };
      });
      setNotes(updatedNotes);
      if (isPlaying && onRescheduleMidi) onRescheduleMidi(updatedNotes);
      drag.lastPainted = [...painted, ...unpainted];
    }
  };

  const renderKeybed = () => {
    var kbTrk = activeTracks.find(function(t) { return t.id === st.trackId; });
    var kbCtx = resolveTrackInstrumentCtx(kbTrk, activeTracks);
    ensureSonicInstrument(kbCtx);
    const keys = [];
    for (let pitch = 127; pitch >= PITCH_START; pitch--) {
      const isBlack = [1, 3, 6, 8, 10].includes(pitch % 12);
      const notesArray = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
      const octave = Math.floor(pitch / 12) - 1;
      const label = `${notesArray[pitch % 12]}${octave}`;
      const showLabel = pitch % 12 === 0;

      keys.push(
        /*#__PURE__*/React.createElement("div", {
          key: pitch,
          style: { height: `${NoteHeight}px` },
          className: `w-[60px] shrink-0 border-b border-zinc-800 text-[9px] font-bold flex items-center justify-end pr-2 transition cursor-pointer ${activeMidiPitches && activeMidiPitches.has(pitch) ? 'bg-emerald-500 text-white border-emerald-400' : isBlack ? 'bg-zinc-950 text-slate-500 border-zinc-900 hover:bg-zinc-800' : 'bg-white text-zinc-800 border-r border-zinc-400 hover:bg-amber-100'}`,
          onMouseDown: (e) => {
            e.stopPropagation();
            keybedMouseDownRef.current = true;
            try {
              // Đồng bộ mastering + routing SF NGAY trước khi preview — âm
              // keybed PHẢI qua mastering FX của main out khi chain đang bật
              // (trước đây phải bật/tắt power mới có tác dụng).
              try { if (window.__ensureMasteringRouting) window.__ensureMasteringRouting(); } catch (er) {}
              if (window.triggerMidiVuActivity) {
                window.triggerMidiVuActivity((st.parent_tab_id && st.parent_tab_id.startsWith('session_') ? '_sess_' : '') + st.trackId, 100);
              }
              const kbNative = isStandaloneSf() && isSfTrackEngine(kbCtx.synthEngine) && !shouldRouteCarla(kbCtx.synthEngine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive());
              if (kbNative) {
                playNativeSfNote(kbTrk, pitch, 100 / 127, 5000, undefined, 'kb_' + st.trackId + '_' + pitch);
              }
              if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) {
                try { if (window.__ensureBridgeProgram) window.__ensureBridgeProgram(kbTrk ? kbTrk.id : 0, kbCtx.program, kbCtx.synthEngine); } catch (er) {}
                window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_ON', channel: kbTrk ? kbTrk.id : 0, pitch: pitch, velocity: 100 / 127, percussion: !!(kbCtx && kbCtx.synthEngine && kbCtx.synthEngine.soundfont_bank === 128) });
              } else if (window.SonicSF && !kbNative) {
                // ⚠️ FIX: giữ note theo thời gian bấm phím — durationMs lớn (5s)
                // chỉ là auto-off phòng hờ; mouseup/mouseleave gọi stopNote dừng
                // NGAY (trước đây 500ms → note tự tắt giữa chừng khi giữ phím).
                window.SonicSF.playNote(pitch, 100, 5000, undefined, kbCtx.program, null, kbCtx.ch, kbCtx.synthEngine);
              }
              // MIDI → Carla (track VSTi + ARM + Carla local): preview realtime
              if (window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoute(kbCtx.synthEngine, st.isArmed)) {
                window.SonicCarlaMidi.noteOn(kbCtx.ch, pitch, 100);
                if (window.__carlaKeybedTimer) clearTimeout(window.__carlaKeybedTimer);
                window.__carlaKeybedTimer = setTimeout(function () { window.SonicCarlaMidi.noteOff(kbCtx.ch, pitch); }, 650);
              }
            } catch (err) {
              console.error('playNote error:', err);
            }
          },
          onMouseEnter: (e) => {
            if (keybedMouseDownRef.current) {
              try {
                if (window.triggerMidiVuActivity) {
                  window.triggerMidiVuActivity((st.parent_tab_id && st.parent_tab_id.startsWith('session_') ? '_sess_' : '') + st.trackId, 100);
                }
                if (isStandaloneSf() && isSfTrackEngine(kbCtx.synthEngine) && !shouldRouteCarla(kbCtx.synthEngine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive())) {
                  playNativeSfNote(kbTrk, pitch, 100 / 127, 5000, undefined, 'kb_' + st.trackId + '_' + pitch);
                }
                if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) {
                  try { if (window.__ensureBridgeProgram) window.__ensureBridgeProgram(kbTrk ? kbTrk.id : 0, kbCtx.program, kbCtx.synthEngine); } catch (er) {}
                  window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_ON', channel: kbTrk ? kbTrk.id : 0, pitch: pitch, velocity: 100 / 127, percussion: !!(kbCtx && kbCtx.synthEngine && kbCtx.synthEngine.soundfont_bank === 128) });
                } else if (window.SonicSF && !(isStandaloneSf() && isSfTrackEngine(kbCtx.synthEngine) && !shouldRouteCarla(kbCtx.synthEngine))) {
                  // giữ note khi kéo qua phím (mouse enter) — dừng bằng mouseup/leave
                  window.SonicSF.playNote(pitch, 100, 5000, undefined, kbCtx.program, null, kbCtx.ch, kbCtx.synthEngine);
                }
                if (window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoute(kbCtx.synthEngine, st.isArmed)) {
                  window.SonicCarlaMidi.noteOn(kbCtx.ch, pitch, 100);
                  if (window.__carlaKeybedTimer) clearTimeout(window.__carlaKeybedTimer);
                  window.__carlaKeybedTimer = setTimeout(function () { window.SonicCarlaMidi.noteOff(kbCtx.ch, pitch); }, 650);
                }
              } catch (err) { console.error('playNote error:', err); }
            }
          },
          onMouseUp: () => {
            keybedMouseDownRef.current = false;
            // Dừng note khi thả phím — tránh kẹt âm (loop liên tục) với soundfont
            try {
              if (isStandaloneSf()) stopNativeSfNote('kb_' + st.trackId + '_' + pitch);
              if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) {
                window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_OFF', channel: kbTrk ? kbTrk.id : 0, pitch: pitch, velocity: 0, percussion: !!(kbCtx && kbCtx.synthEngine && kbCtx.synthEngine.soundfont_bank === 128) });
              } else if (window.SonicSF && window.SonicSF.stopNote) window.SonicSF.stopNote(kbCtx.ch, pitch);
            } catch (e) {}
            if (window.__carlaKeybedTimer) { clearTimeout(window.__carlaKeybedTimer); window.__carlaKeybedTimer = null; }
            if (window.SonicCarlaMidi) { try { window.SonicCarlaMidi.noteOff(kbCtx.ch, pitch); } catch (e) {} }
          },
          onMouseLeave: () => {
            if (!keybedMouseDownRef.current) return;
            // Kéo chuột ra khỏi phím → dừng note của phím đó
            try {
              if (isStandaloneSf()) stopNativeSfNote('kb_' + st.trackId + '_' + pitch);
              if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) {
                window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_OFF', channel: kbTrk ? kbTrk.id : 0, pitch: pitch, velocity: 0, percussion: !!(kbCtx && kbCtx.synthEngine && kbCtx.synthEngine.soundfont_bank === 128) });
              } else if (window.SonicSF && window.SonicSF.stopNote) window.SonicSF.stopNote(kbCtx.ch, pitch);
            } catch (e) {}
            if (window.SonicCarlaMidi) { try { window.SonicCarlaMidi.noteOff(kbCtx.ch, pitch); } catch (e) {} }
          }
        }, showLabel && label)
      );
    }
    return keys;
  };

  const handleScroll = (e) => {
    if (keybedRef.current) {
      keybedRef.current.scrollTop = e.currentTarget.scrollTop;
    }
    if (rulerScrollRef.current) {
      rulerScrollRef.current.scrollLeft = e.currentTarget.scrollLeft;
    }
    if (ccWrapperRef.current) {
      ccWrapperRef.current.scrollLeft = e.currentTarget.scrollLeft;
    }
    const el = e.currentTarget;
    const THRESHOLD = 200;
    if (el.scrollLeft + el.clientWidth >= el.scrollWidth - THRESHOLD) {
      const newBeats = rollBeatsRef.current + 16;
      setRollBeats(newBeats);
    }
  };
  const handleKeybedScroll = (e) => {
    if (gridScrollRef.current) {
      gridScrollRef.current.scrollTop = e.currentTarget.scrollTop;
    }
  };

  const SCALES = {
    "None": null,
    "Diatonic": {
      "Major": [0, 2, 4, 5, 7, 9, 11],
      "Minor": [0, 2, 3, 5, 7, 8, 10],
      "Dorian": [0, 2, 3, 5, 7, 9, 10],
      "Phrygian": [0, 1, 3, 5, 7, 8, 10],
      "Lydian": [0, 2, 4, 6, 7, 9, 11],
      "Mixolydian": [0, 2, 4, 5, 7, 9, 10],
      "Locrian": [0, 1, 3, 5, 6, 8, 10]
    },
    "Pentatonic": {
      "Major": [0, 2, 4, 7, 9],
      "Minor": [0, 3, 5, 7, 10],
      "Blues": [0, 3, 5, 6, 7, 10],
      "Chinese": [0, 2, 4, 7, 9],
      "Vietnam": [0, 2, 6, 7, 10],
      "India": [0, 2, 5, 7, 9],
      "Japan": [0, 2, 5, 7, 9],
      "Africa": [0, 3, 5, 7, 10]
    },
    "Church": {
      "Dorian": [0, 2, 3, 5, 7, 9, 10],
      "Phrygian": [0, 1, 3, 5, 7, 8, 10],
      "Lydian": [0, 2, 4, 6, 7, 9, 11],
      "Mixolydian": [0, 2, 4, 5, 7, 9, 10]
    },
    "Jazz": {
      "Blues": [0, 3, 5, 6, 7, 10],
      "Bebop": [0, 2, 4, 5, 7, 9, 10, 11],
      "Diminished": [0, 2, 3, 5, 6, 8, 9, 11]
    },
    "Asian Traditional": {
      "Chinese": [0, 2, 4, 7, 9],
      "Vietnam": [0, 2, 6, 7, 10],
      "Japan": [0, 2, 5, 7, 9],
      "India": [0, 2, 5, 7, 9],
      "Gamelan": [0, 2, 4, 7, 9],
      "Korea": [0, 2, 4, 5, 7, 9]
    },
    "Middle Eastern": {
      "Hijaz": [0, 1, 4, 5, 7, 8, 11],
      "Nikriz": [0, 1, 4, 5, 7, 8, 10],
      "Rast": [0, 2, 4, 5, 7, 8, 10],
      "Saba": [0, 1, 3, 4, 7, 8, 10],
      "Bayati": [0, 2, 3, 4, 7, 8, 10]
    }
  };
  const [selectedScale, setSelectedScale] = React.useState(null);
  const selectedScaleRef = React.useRef(null);
  selectedScaleRef.current = selectedScale;
  // Root note (0-11 — C..B) — transpose scale highlight + snap (spec 20:12)
  const [scaleRoot, setScaleRoot] = React.useState(0);
  const scaleRootRef = React.useRef(0);
  scaleRootRef.current = scaleRoot;
  // Modal states: Arpeggiator / Strummer / Humanize (spec 20:12)
  const [arpModal, setArpModal] = React.useState(null);   // { pattern, rate, octaves, gate }
  const [strumModal, setStrumModal] = React.useState(null); // { ms, direction }
  const [humanizeModal, setHumanizeModal] = React.useState(null); // { timingMs, velRange, durRange }
  const [chordType, setChordType] = React.useState('triad'); // chord stamp (spec)
  const [chordStampMode, setChordStampMode] = React.useState(false); // stamp mode toggle
  const [velocityTarget, setVelocityTarget] = React.useState(80); // compress target (spec)
  const snapToScaleRef = React.useRef(true);
  snapToScaleRef.current = st.snapToScale !== undefined ? st.snapToScale : true;
  const [scaleMenuPos, setScaleMenuPos] = React.useState(null);
  const scaleMenuOriginRef = React.useRef(null);

  const snapPitchToScale = (pitch, scale) => {
    if (!scale) return pitch;
    const octave = Math.floor(pitch / 12);
    const noteInOctave = pitch % 12;
    if (scale.includes(noteInOctave)) return pitch;
    let best = noteInOctave;
    let minDist = 12;
    scale.forEach(s => { const dist = Math.abs(s - noteInOctave); if (dist < minDist) { minDist = dist; best = s; } });
    return octave * 12 + best;
  };

  // ── MIDI Transform Engine (spec 20:12 — direct integration) ──
  const scaleWithRoot = () => {
    const sc = selectedScaleRef.current;
    if (!sc) return null;
    return sc.map(s => (s + scaleRootRef.current) % 12);
  };
  const commitNotes = (updated) => {
    setNotes(updated);
    if (isPlaying && onRescheduleMidi) onRescheduleMidi(updated);
  };
  const applyArpeggiate = (p) => {
    const sc = scaleWithRoot();
    const beatSec = 60.0 / (parseInt(bpm) || 120);
    const rateDiv = p.rate === '1/4' ? 1 : p.rate === '1/8' ? 0.5 : p.rate === '1/16' ? 0.25 : p.rate === '1/32' ? 0.125 : 1;
    const rateBeats = rateDiv * (p.triplet ? 2/3 : p.dotted ? 1.5 : 1);
    const base = notes.filter(n => selectedNoteIds.includes(n.id));
    const targets = base.length > 0 ? base : notes;
    if (!targets.length) return;
    const chords = [];
    targets.forEach(n => {
      const k = n.start_beat.toFixed(2);
      const g = chords.find(c => c.k === k);
      if (g) g.notes.push(n); else chords.push({ k, notes: [n] });
    });
    const result = [];
    const rangeNotes = []; // arpeggiated sequence (pitch per step)
    chords.forEach(ch => {
      const sorted = ch.notes.slice().sort((a, b) => a.pitch - b.pitch);
      const notesOut = [];
      for (let o = 0; o < p.octaves; o++) {
        sorted.forEach(n => {
          const octPitch = n.pitch + o * 12;
          if (!rangeNotes.includes(octPitch)) rangeNotes.push(octPitch);
        });
      }
      const seq = [];
      rangeNotes.splice(0, rangeNotes.length);
      chords.forEach(() => {});
      const sortedAsc = ch.notes.slice().sort((a, b) => a.pitch - b.pitch);
      const sortedDesc = sortedAsc.slice().reverse();
      const pool = p.pattern === 'DOWN' ? sortedDesc
        : p.pattern === 'UP-DOWN' ? [...sortedAsc, ...sortedDesc.slice(1, -1)]
        : p.pattern === 'RANDOM' ? sortedAsc.slice().sort(() => Math.random() - 0.5)
        : sortedAsc; // UP / CHORD
      const steps = p.pattern === 'CHORD' ? 1 : pool.length * p.octaves;
      const seqPitches = [];
      for (let i = 0; i < steps; i++) {
        if (p.pattern === 'CHORD') { pool.forEach(n => seqPitches.push(n.pitch)); break; }
        seqPitches.push(pool[i % pool.length].pitch + Math.floor(i / pool.length) * 12);
      }
      const stepDur = p.pattern === 'CHORD' ? rateBeats * pool.length : rateBeats;
      const total = seqPitches.length * stepDur;
      seqPitches.forEach((pitch, i) => {
        const dur = stepDur * (p.gate / 100);
        const vel = ch.notes[0] ? ch.notes[0].velocity : 0.8;
        notesOut.push({ id: 'note_' + Math.random().toString(36).substr(2, 9), pitch, start_beat: ch.notes[0].start_beat + i * stepDur, duration_beats: Math.max(0.05, dur), velocity: vel });
      });
      result.push(...notesOut);
    });
    const others = notes.filter(n => !targets.includes(n));
    commitNotes([...others, ...result].sort((a, b) => a.start_beat - b.start_beat));
    setArpModal(null);
    showToast('Đã arpeggiate ' + targets.length + ' nốt.', 'success');
  };
  const applyStrum = (p) => {
    const secPerBeat = 60.0 / (parseInt(bpm) || 120);
    const strumBeats = (p.ms / 1000.0) / secPerBeat;
    const base = notes.filter(n => selectedNoteIds.includes(n.id));
    const targets = base.length > 0 ? base : notes;
    if (!targets.length) return;
    const groups = [];
    targets.forEach(n => {
      const k = n.start_beat.toFixed(2);
      const g = groups.find(g2 => g2.k === k);
      if (g) g.notes.push(n); else groups.push({ k, notes: [n] });
    });
    let alternateFlip = false;
    const result = targets.map(n => ({ ...n }));
    groups.forEach(g => {
      const sorted = g.notes.slice().sort((a, b) => a.pitch - b.pitch);
      const asc = p.direction === 'UP' ? sorted.slice().reverse() : (p.direction === 'ALTERNATE' ? (alternateFlip ? sorted.slice().reverse() : sorted.slice()) : sorted);
      alternateFlip = !alternateFlip;
      const firstStart = sorted[0].start_beat;
      asc.forEach((note, index) => {
        result.forEach(r => {
          if (r.id === note.id) {
            r.start_beat = parseFloat((firstStart + index * strumBeats).toFixed(3));
            r.velocity = Math.max(0.1, Math.min(1.0, parseFloat((r.velocity - index * 0.03).toFixed(2))));
          }
        });
      });
    });
    commitNotes(result);
    setStrumModal(null);
    showToast('Đã strum ' + targets.length + ' nốt.', 'success');
  };
  const applyHumanizeModal = (p) => {
    const spb = 60.0 / (parseInt(bpm) || 120);
    const maxJitter = (p.timingMs / 1000.0) / spb;
    const base = notes.filter(n => selectedNoteIds.includes(n.id));
    const targets = base.length > 0 ? base : notes;
    if (!targets.length) return;
    const result = targets.map(n => {
      const tj = (Math.random() - 0.5) * 2 * maxJitter;
      const vj = (Math.random() - 0.5) * 2 * (p.velRange / 127);
      const dj = p.durRange ? (Math.random() - 0.5) * 2 * (p.durRange / 100) : 0;
      return {
        ...n,
        start_beat: Math.max(0, parseFloat((n.start_beat + tj).toFixed(3))),
        velocity: Math.max(0.05, Math.min(1.0, parseFloat((n.velocity + vj).toFixed(2)))),
        duration_beats: Math.max(0.05, parseFloat((n.duration_beats * (1 + dj)).toFixed(3)))
      };
    });
    const others = notes.filter(n => !targets.includes(n));
    commitNotes([...others, ...result]);
    setHumanizeModal(null);
    showToast('Đã humanize ' + targets.length + ' nốt.', 'success');
  };
  const applyForceToScale = () => {
    const sc = scaleWithRoot();
    if (!sc) { showToast('Chưa chọn scale (nhấp chuột phải chọn Scale).', 'warning'); return; }
    const base = notes.filter(n => selectedNoteIds.includes(n.id));
    const targets = base.length > 0 ? base : notes;
    const result = targets.map(n => ({ ...n, pitch: snapPitchToScale(n.pitch, sc) }));
    const others = notes.filter(n => !targets.includes(n));
    commitNotes([...others, ...result]);
    showToast('Đã force ' + targets.length + ' nốt về scale.', 'success');
  };
  const CHORD_SHAPES = {
    triad:   [0, 4, 7],
    min7:    [0, 3, 7, 10],
    sus4:    [0, 5, 7],
    add9:    [0, 4, 7, 14]
  };
  const applyChordStamp = (beat, pitch) => {
    const shape = CHORD_SHAPES[chordType] || CHORD_SHAPES.triad;
    const beatSec = 60.0 / (parseInt(bpm) || 120);
    const barBeats = 4;
    const newNotes = shape.map(iv => ({
      id: 'note_' + Math.random().toString(36).substr(2, 9),
      pitch: pitch + iv,
      start_beat: beat,
      duration_beats: 1,
      velocity: 0.8
    }));
    commitNotes([...notes, ...newNotes]);
    showToast('Đã stamp chord (' + shape.length + ' nốt).', 'success');
  };
  const applyHarmonize = (interval) => {
    const base = notes.filter(n => selectedNoteIds.includes(n.id));
    const targets = base.length > 0 ? base : notes;
    if (!targets.length) return;
    const dups = targets.map(n => ({ ...n, id: 'note_' + Math.random().toString(36).substr(2, 9), pitch: n.pitch + interval }));
    commitNotes([...notes, ...dups]);
    showToast('Đã harmonize +' + interval + ' (' + dups.length + ' nốt).', 'success');
  };
  const applyVelocityCompress = () => {
    const base = notes.filter(n => selectedNoteIds.includes(n.id));
    const targets = base.length > 0 ? base : notes;
    if (!targets.length) return;
    const mean = targets.reduce((s, n) => s + (n.velocity !== undefined ? n.velocity : 0.8), 0) / targets.length;
    const target = velocityTarget / 127;
    const result = targets.map(n => {
      const v = (n.velocity !== undefined ? n.velocity : 0.8);
      return { ...n, velocity: Math.max(0.05, Math.min(1.0, v + (target - mean))) };
    });
    const others = notes.filter(n => !targets.includes(n));
    commitNotes([...others, ...result]);
    showToast('Đã compress velocity về ' + velocityTarget + ' (' + targets.length + ' nốt).', 'success');
  };
  const applyVelocityNormalize = () => {
    const base = notes.filter(n => selectedNoteIds.includes(n.id));
    const targets = base.length > 0 ? base : notes;
    if (!targets.length) return;
    const maxV = Math.max(...targets.map(n => (n.velocity !== undefined ? n.velocity : 0.8)));
    if (maxV <= 0) return;
    const result = targets.map(n => ({ ...n, velocity: Math.max(0.05, Math.min(1.0, (n.velocity !== undefined ? n.velocity : 0.8) / maxV)) }));
    const others = notes.filter(n => !targets.includes(n));
    commitNotes([...others, ...result]);
    showToast('Đã normalize velocity (max → 127).', 'success');
  };

  const renderScaleContextMenu = () => {
    const closeMenu = () => setScaleMenuPos(null);
    const origin = scaleMenuOriginRef.current || scaleMenuPos;
    const items = [];
    let subMenu = null;
    const isSameScale = (a, b) => { if (!a || !b) return a === b; if (a.length !== b.length) return false; return a.every((v,i)=>v===b[i]); };
    const pushItem = (label, onClick, onHover) => {
      const isActive = onClick._scale && isSameScale(onClick._scale, selectedScale);
      items.push(React.createElement("div", {
        key: label,
        onClick: () => { onClick(); closeMenu(); },
        onMouseEnter: onHover || undefined,
        className: "px-3 py-1.5 text-xs cursor-pointer hover:bg-amber-600 hover:text-white whitespace-nowrap " + (isActive ? "bg-amber-800/40 text-amber-300" : "text-zinc-300")
      }, label));
    };
    Object.keys(SCALES).forEach(key => {
      const val = SCALES[key];
      if (val === null) { pushItem("None", () => setSelectedScale(null)); return; }
      if (Array.isArray(val)) {
        pushItem(key, () => setSelectedScale(val));
      } else {
        const parentKey = key;
        const isOpen = scaleMenuPos && scaleMenuPos.parentKey === parentKey;
        pushItem(key + " ▸", () => setSelectedScale(null), () => {
          setScaleMenuPos({ x: origin.x, y: origin.y, parentKey });
        });
        if (isOpen) {
          const subs = [];
          Object.keys(val).forEach(subKey => {
            subs.push(React.createElement("div", {
              key: subKey,
              onClick: () => { setSelectedScale(val[subKey]); closeMenu(); },
              className: "px-3 py-1.5 text-xs cursor-pointer hover:bg-amber-600 hover:text-white whitespace-nowrap " + (isSameScale(val[subKey], selectedScale) ? "bg-amber-800/40 text-amber-300" : "text-zinc-300")
            }, subKey));
          });
          var subH = subs.length * 30 + 16;
          var subTop = (origin.y + subH + 20 > window.innerHeight) ? (origin.y - subH) : origin.y;
          subMenu = React.createElement("div", {
            style: { position: "fixed", left: origin.x + 150, top: subTop, zIndex: 10000 },
            className: "bg-[#2a2a2a] border border-zinc-600 rounded shadow-xl py-1 min-w-[120px]"
          }, ...subs);
        }
      }
    });
    var menuH = items.length * 30 + 16;
    var menuTop = (origin.y + menuH + 20 > window.innerHeight) ? Math.max(10, origin.y - menuH) : origin.y;
    return React.createElement(React.Fragment, null,
      React.createElement("div", {
        style: { position: "fixed", left: origin.x, top: menuTop, zIndex: 9999 },
        className: "bg-[#2a2a2a] border border-zinc-600 rounded shadow-xl py-1 min-w-[140px]"
      }, ...items),
      subMenu
    );
  };

  const renderBarLabels = () => {
    const labels = [];
    const barsCount = Math.ceil(viewBeats / 4);
    const barOffset = Math.floor(sessionStartBar);
    for (let bar = 0; bar < barsCount; bar++) {
      const x = bar * 4 * pixelsPerBeat;
      const displayBar = bar + barOffset;
      labels.push(
        /*#__PURE__*/React.createElement("div", {
          key: bar,
          style: {
            position: 'absolute',
            left: `${x}px`,
            top: '4px'
          },
          className: "pl-1 border-l border-zinc-700 h-full select-none cursor-pointer hover:bg-zinc-800/30",
          onClick: (e) => {
            e.stopPropagation();
            const barTime = bar * 4 * beatSec;
            setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, currentTime: barTime } : s));
          }
        }, `Bar ${displayBar}`)
      );
    }
    return labels;
  };

const beatSec = 60.0 / (parseInt(bpm) || 120);
  const playHeadX = (st.currentTime || 0) / beatSec * pixelsPerBeat;

  return React.createElement("div", {
    className: "flex-1 flex overflow-hidden min-h-0 bg-[#1e1e1e] flex-col h-full"
  }, 
    /* 1. TOOLBAR HEADER — 2 hàng (wrap tự nhiên; spacer 100% ép hàng mới) */
    React.createElement("div", {
      className: "bg-[#282828] border-b border-zinc-900 flex flex-wrap items-center gap-x-2 gap-y-1 px-4 py-1.5 shrink-0 text-slate-200"
    }, React.createElement("div", {
      className: "flex items-center gap-4"
    }, React.createElement("select", {
      value: st.trackId || '',
      onChange: function(e) {
        var trkId = e.target.value;
        var trkSel = (activeTracks || []).find(function(t) { return t.id === trkId; });
        if (trkSel && trkSel.midiItems && trkSel.midiItems.length) handleSwitchMidiItem(trkSel.midiItems[0].id);
      },
      className: "bg-zinc-800 border border-zinc-700 text-yellow-500 font-bold text-xs rounded px-1.5 py-0.5 outline-none focus:border-yellow-500 max-w-[180px] uppercase"
    }, (function() {
      var seenTrackOpts = {};
      var trackOpts = [];
      (activeTracks || []).forEach(function(t) {
        if (!t.midiItems || !t.midiItems.length) return;
        if (seenTrackOpts[t.id]) return;
        seenTrackOpts[t.id] = true;
        trackOpts.push(React.createElement("option", { key: t.id, value: t.id }, t.name || t.id));
      });
      return trackOpts;
    }())), activeParentTrackName ? React.createElement("span", {
      className: "text-[9px] text-zinc-500 ml-1"
    }, "(Belongs to: ", React.createElement("span", { className: "text-zinc-400 font-semibold" }, activeParentTrackName), ")") : null, React.createElement("div", {
      className: "flex items-center gap-1 text-xs"
    }, React.createElement("span", {
      className: "text-zinc-500 font-semibold"
    }, "Snap:"), React.createElement("select", {
      value: snapValue,
      onChange: e => { onSnapChange(e.target.value); setRenderTick(t => t + 1); },
      className: "bg-zinc-800 border border-zinc-700 text-zinc-300 rounded px-1.5 py-0.5 outline-none focus:border-yellow-500"
    }, ['free', '4', '1', '1/2', '1/4', '1/8', '1/16', '1/32'].map(v => React.createElement("option", {
      key: v,
      value: v
    }, v)))), React.createElement("button", {
      onClick: () => setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, isArmed: !s.isArmed } : s)),
      className: `px-2 py-1 rounded text-xs font-bold ${st.isArmed ? 'bg-red-600 text-white' : 'bg-zinc-800 text-zinc-400'}`
    }, "ARM"), React.createElement("select", {
      value: selectedMidiInputId || '',
      onChange: e => onMidiInputSelect(e.target.value),
      className: "bg-zinc-800 border border-zinc-700 text-zinc-300 rounded px-1 py-0.5 text-xs outline-none max-w-[100px]"
    }, React.createElement("option", { value: "" }, "Input"), React.createElement("option", { value: "ALL" }, "Omni"), (midiDevices || []).map(d => React.createElement("option", { key: d.id, value: d.id }, d.name || d.id))), React.createElement("div", { className: "flex items-center gap-0.5 shrink-0" }, React.createElement("button", {
      onClick: () => onInstrumentSelect && onInstrumentSelect(st.trackId),
      title: st.instrumentName || "Synth",
      className: `px-1.5 py-0.5 text-[10px] rounded font-mono font-bold border transition flex items-center gap-0.5 max-w-[70px] ${st.instrumentName ? 'bg-violet-900 text-violet-300 border-violet-700' : 'bg-zinc-800 text-zinc-500 border-transparent hover:text-zinc-300'}`
    }, React.createElement("i", { "data-lucide": "music", className: "w-3 h-3 shrink-0" }), React.createElement("span", { className: "truncate text-[9px]" }, activeParentTrackName ? ('(' + activeParentTrackName + ') ' + (st.instrumentName || 'Synth')) : (st.instrumentName || 'Synth'))), (st.instrumentId && typeof st.instrumentId === 'string' && !st.instrumentId.startsWith('sf_') ? React.createElement("button", {
      onClick: () => {
        ensureAndOpenVstGui(st.trackId, st.instrumentId);
      },
      title: "Mở lại GUI VSTi",
      className: "px-1.5 py-0.5 text-[10px] rounded font-mono font-bold border transition flex items-center gap-0.5 shrink-0 bg-zinc-800 text-violet-300 border-violet-800 hover:bg-violet-900/40"
    }, React.createElement("i", { "data-lucide": "sliders-horizontal", className: "w-3 h-3 shrink-0" }), React.createElement("span", { className: "truncate text-[10px]" }, "GUI")) : null)), React.createElement("div", {
      className: "flex items-center gap-1 ml-1 text-xs"
    }, React.createElement("span", { className: "text-zinc-500" }, "AI:"), React.createElement("input", {
      type: "number", value: aiBarStart, onChange: e => setAiBarStart(parseInt(e.target.value) || 0),
      className: "w-8 bg-zinc-900 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5 text-xs text-center"
    }), React.createElement("span", { className: "text-zinc-500" }, "-"), React.createElement("input", {
      type: "number", value: aiBarEnd, onChange: e => setAiBarEnd(parseInt(e.target.value) || 1),
      className: "w-8 bg-zinc-900 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5 text-xs text-center"
    }), React.createElement("span", { className: "text-zinc-500" }, "bar")), React.createElement("select", {
      value: ccMode,
      onChange: e => setCcMode(e.target.value),
      className: "bg-zinc-800 text-zinc-200 border border-zinc-700 rounded px-1.5 py-1 text-xs capitalize cursor-pointer"
    }, React.createElement("option", { value: "velocity" }, "Velocity"), React.createElement("option", { value: "sustain" }, "Sustain"), React.createElement("option", { value: "modulation" }, "Modulation"), React.createElement("option", { value: "pitch_bend" }, "Pitch Bend"), React.createElement("option", { value: "pan" }, "Pan"))), React.createElement("button", {
      onClick: () => setShowCC(!showCC),
      className: `px-2 py-1 rounded text-xs ${showCC ? 'bg-purple-900/60 text-purple-300 border border-purple-700' : 'text-zinc-500 hover:text-zinc-300'}`
    }, ccMode === 'velocity' ? 'Vel' : ccMode === 'sustain' ? 'Sus' : ccMode === 'modulation' ? 'Mod' : ccMode === 'pitch_bend' ? 'Bend' : ccMode === 'pan' ? 'Pan' : 'CC'), React.createElement("button", {
      onClick: function() { setSessionSyncMode(function(p) { return !p; }); },
      className: function() {
        var base = 'px-2 py-1 rounded text-xs ';
        return sessionSyncMode ? base + 'bg-cyan-900/60 text-cyan-300 border border-cyan-700' : base + 'text-zinc-500 hover:text-zinc-300';
      }(),
      title: sessionSyncMode ? "Session-synced mode (ghost visible)" : "Isolated mode (bar 0, no ghost)"
    }, sessionSyncMode ? "\uD83C\uDF10 Session" : "\uD83D\uDCCB Isolated"), React.createElement("button", {
      onClick: function() { setShowGhostNotes(function(p) { return !p; }); },
      disabled: !sessionSyncMode,
      className: function() {
        if (!sessionSyncMode) return 'px-2 py-1 rounded text-xs opacity-30 cursor-not-allowed';
        var base = 'px-2 py-1 rounded text-xs ';
        return showGhostNotes ? base + 'bg-purple-900/60 text-purple-300 border border-purple-700' : base + 'text-zinc-500 hover:text-zinc-300';
      }(),
      title: "Toggle ghost notes visibility"
    }, "👻 MIDI ghost notes"), React.createElement("div", {
      className: "flex items-center gap-1 ml-auto"
    }, React.createElement("button", {
      onClick: () => onSaveNotes(st.id, st.trackId, st.target_id, notes),      className: "px-2.5 py-1 bg-emerald-600 hover:bg-emerald-500 text-white rounded text-xs flex items-center gap-1 transition font-semibold"
    }, React.createElement("i", { "data-lucide": "save", className: "w-3 h-3" }), "L\u01B0u"), React.createElement("button", {
      onClick: () => {
        const ppq = 480;
        const bpmNum = parseInt(bpm) || 120;
        const ticksPerBeat = ppq;
        const events = [];
        (notes || []).forEach(n => {
          const startTick = Math.round((n.start_beat || 0) * ticksPerBeat);
          const durTick = Math.round((n.duration_beats || 1) * ticksPerBeat);
          const pitch = n.pitch || 60;
          const vel = Math.round((n.velocity || 0.8) * 127);
          events.push({ tick: startTick, type: 'note_on', pitch, velocity: vel });
          events.push({ tick: startTick + durTick, type: 'note_off', pitch, velocity: 0 });
        });
        events.sort((a, b) => a.tick - b.tick || (a.type === 'note_off' ? -1 : 1));
        const writeVLQ = (bytes, v) => {
          let val = Math.max(0, v);
          const buf = [];
          buf.push(val & 0x7F);
          while (val > 0x7F) { val >>= 7; buf.push(0x80 | (val & 0x7F)); }
          for (let i = buf.length - 1; i >= 0; i--) bytes.push(buf[i]);
        };
        const trackBytes = [];
        let lastTick = 0;
        events.forEach(ev => {
          const delta = Math.max(0, ev.tick - lastTick);
          writeVLQ(trackBytes, delta);
          trackBytes.push(ev.type === 'note_on' ? 0x90 : 0x80, ev.pitch, ev.velocity);
          lastTick = ev.tick;
        });
        writeVLQ(trackBytes, 0);
        trackBytes.push(0xFF, 0x2F, 0x00);
        const trackData = [0x4D, 0x54, 0x72, 0x6B];
        const len = trackBytes.length;
        trackData.push((len >> 24) & 0xFF, (len >> 16) & 0xFF, (len >> 8) & 0xFF, len & 0xFF);
        trackData.push(...trackBytes);
        const header = [0x4D, 0x54, 0x68, 0x64, 0x00, 0x00, 0x00, 0x06, 0x00, 0x01, 0x00, 0x01, (ppq >> 8) & 0xFF, ppq & 0xFF];
        const all = header.concat(trackData);
        const blob = new Blob([new Uint8Array(all)], { type: 'audio/midi' });
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = (st.label || 'midi') + '.mid';
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        URL.revokeObjectURL(url);
        showToast('Đã xuất file MIDI!', 'success');
      },
      className: "px-2.5 py-1 bg-amber-700 hover:bg-amber-600 text-white rounded text-xs flex items-center gap-1 transition font-semibold"
    }, React.createElement("i", { "data-lucide": "file-down", className: "w-3 h-3" }), "Export MIDI"), React.createElement("button", {
      onClick: async () => {
        // Preview/export MIDI notes với âm VSTi: midi-render (pedalboard) →
        // play wav ngay; nếu pedalboard không render được plugin (VST2/state
        // GUI) fallback carla-play-notes (realtime qua Carla bridge, OSC).
        const se = st.synth_engine || {};
        const instrumentId = se.plugin_id || st.instrumentId || null;
        if (!instrumentId || String(se.type || '').indexOf('vst') === -1) {
          showToast('Chọn VST instrument (nút Synth) trước khi Preview VSTi', 'warning');
          return;
        }
        if (!notes || !notes.length) { showToast('Chưa có nốt nhạc', 'warning'); return; }
        const payload = {
          instrument_id: instrumentId,
          notes: (notes || []).map(n => ({ pitch: n.pitch, start_beat: n.start_beat, duration_beats: n.duration_beats, velocity: n.velocity != null ? n.velocity : 0.8 })),
          bpm: parseFloat(bpm) || 120,
          preset_id: se.preset_id || undefined,
          preset_path: se.preset_path || undefined,
          preset_data: se.preset_data || undefined,
        };
        try {
          const res = await window.SonicAPI.midiRender(payload);
          if (vstiPreviewAudioRef.current) { try { vstiPreviewAudioRef.current.pause(); } catch (e) {} }
          const audio = new Audio(res.url);
          vstiPreviewAudioRef.current = audio;
          audio.play().catch(() => {});
          showToast(`Preview VSTi (${(res.duration_sec || 0).toFixed(1)}s) — ${se.plugin_id || instrumentId}`, 'success');
        } catch (err) {
          // midi-render fail (404 plugin không load / 501 pedalboard) → Carla
          try {
            await window.SonicAPI.carlaPlayNotes({ notes: payload.notes, bpm: payload.bpm, channel: se.midi_channel || 0 });
            showToast('Preview qua Carla (realtime) — nghe loa hệ thống', 'success');
          } catch (err2) {
            showToast('Preview VSTi thất bại: ' + ((err && err.message) || 'lỗi'), 'error');
          }
        }
      },
      className: "px-2.5 py-1 bg-violet-700 hover:bg-violet-600 text-white rounded text-xs flex items-center gap-1 transition font-semibold",
      title: "Render MIDI notes bằng VSTi (pedalboard) và phát — fallback qua Carla realtime"
    }, React.createElement("i", { "data-lucide": "play", className: "w-3 h-3" }), "Preview VSTi"), React.createElement("button", {
      onClick: () => {
        const se = st.synth_engine || {};
        const instrumentId = se.plugin_id || st.instrumentId || null;
        if (!instrumentId || String(se.type || '').indexOf('vst') === -1) {
          showToast('Chọn VST instrument (nút Synth) trước khi Export MIDI → Audio', 'warning');
          return;
        }
        if (!notes || !notes.length) { showToast('Chưa có nốt nhạc', 'warning'); return; }
        if (onExportMidiAudio) onExportMidiAudio(st, notes, bpm);
        else showToast('Không có handler export audio', 'error');
      },
      className: "px-2.5 py-1 bg-fuchsia-700 hover:bg-fuchsia-600 text-white rounded text-xs flex items-center gap-1 transition font-semibold",
      title: "Render MIDI notes bằng VSTi → WAV, chèn clip vào track và tải về"
    }, React.createElement("i", { "data-lucide": "audio-lines", className: "w-3 h-3" }), "Export MIDI → Audio")), React.createElement("div", {
      style: { flexBasis: "100%", height: 0 }
    }), React.createElement("button", {
      onClick: () => setArpModal({ pattern: 'UP', rate: '1/16', octaves: 2, gate: 80, triplet: false, dotted: false }),
      className: "px-2 py-1 rounded text-xs bg-cyan-900/40 text-cyan-300 border border-cyan-700/60 hover:bg-cyan-800/50 transition",
      title: "Arpeggiate (Alt+A)"
    }, "ARP"), React.createElement("button", {
      onClick: () => setStrumModal({ ms: 30, direction: 'DOWN' }),
      className: "px-2 py-1 rounded text-xs bg-teal-900/40 text-teal-300 border border-teal-700/60 hover:bg-teal-800/50 transition",
      title: "Strum (Alt+S)"
    }, "STRUM"), React.createElement("button", {
      onClick: () => setHumanizeModal({ timingMs: Math.round(humanizeStrength * 100), velRange: Math.round(humanizeStrength * 127), durRange: Math.round(humanizeStrength * 50) }),
      className: "px-2 py-1 rounded text-xs bg-amber-900/40 text-amber-300 border border-amber-700/60 hover:bg-amber-800/50 transition",
      title: "Humanize (Alt+R)"
    }, "HUMANIZE"), React.createElement("select", {
      key: "humstr", value: humanizeStrength,
      onChange: function(e) { var v = parseFloat(e.target.value); setHumanizeStrength(v); setHumanizeModal({ timingMs: Math.round(v * 100), velRange: Math.round(v * 127), durRange: Math.round(v * 50) }); },
      className: "px-1 py-1 rounded text-xs bg-zinc-900 border border-zinc-700 text-zinc-300 cursor-pointer",
      title: "Mức humanize (mở modal theo mức)"
    }, React.createElement("option", { key: "l", value: 0.05 }, "Nh\u1EB9"), React.createElement("option", { key: "m", value: 0.10 }, "V\u1EEBa"), React.createElement("option", { key: "s", value: 0.18 }, "M\u1EA1nh")), React.createElement("div", {
      key: "transpose", className: "flex items-center gap-1"
    }, React.createElement("input", {
      key: "in", type: "number", step: 1, min: -24, max: 24, value: transposeSemis,
      onChange: function(e) { setTransposeSemis(e.target.value); },
      className: "w-12 px-1 py-1 rounded text-xs bg-zinc-900 border border-zinc-700 text-center text-zinc-200",
      title: "Semitone offset (vd 2 = cao hơn 1 tone)"
    }), React.createElement("button", {
      key: "btn", onClick: function() { applyTranspose(transposeSemis); },
      className: "px-2 py-1 rounded text-xs bg-sky-900/40 text-sky-300 border border-sky-700/60 hover:bg-sky-800/50 transition",
      title: "Transpose all notes by the semitone offset"
    }, "Transpose")), React.createElement("div", {
      key: "keyshift", className: "flex items-center gap-1"
    }, React.createElement("select", {
      key: "root", value: keyTargetRoot,
      onChange: function(e) { setKeyTargetRoot(e.target.value); },
      className: "px-1 py-1 rounded text-xs bg-zinc-900 border border-zinc-700 text-zinc-300 cursor-pointer",
      title: "Giọng đích (root)"
    }, ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"].map(function(r) { return React.createElement("option", { key: r, value: r }, r); })), React.createElement("select", {
      key: "scale", value: keyTargetScale,
      onChange: function(e) { setKeyTargetScale(e.target.value); },
      className: "px-1 py-1 rounded text-xs bg-zinc-900 border border-zinc-700 text-zinc-300 cursor-pointer",
      title: "Thể scale đích"
    }, React.createElement("option", { key: "maj", value: "major" }, "major"), React.createElement("option", { key: "min", value: "minor" }, "minor")), React.createElement("button", {
      key: "btn", onClick: applyTransposeToKey,
      className: "px-2 py-1 rounded text-xs bg-violet-900/40 text-violet-300 border border-violet-700/60 hover:bg-violet-800/50 transition",
      title: "Chuyển giọng: map degree hiện tại sang giọng đích (auto-detect key nguồn)"
    }, "🎵 Chuyển giọng")), /* ── CÙNG HÀNG (sau Chuyển giọng — user 08:40: gộp 1 hàng, bỏ spacer) ── */ React.createElement("div", {
      className: "flex items-center gap-1 text-xs"
    }, React.createElement("span", {
      className: "text-zinc-500 font-semibold"
    }, "Snap to Scale"), React.createElement("button", {
      onClick: () => setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, snapToScale: !(s.snapToScale !== undefined ? s.snapToScale : true) } : s)),
      className: `w-7 h-4 rounded-full transition-colors relative ${(st.snapToScale !== undefined ? st.snapToScale : true) ? 'bg-yellow-600' : 'bg-zinc-700'}`,
      style: { padding: 0 }
    }, React.createElement("div", {
      className: `w-3 h-3 rounded-full bg-white absolute top-0.5 transition-transform ${(st.snapToScale !== undefined ? st.snapToScale : true) ? 'translate-x-3.5' : 'translate-x-0.5'}`
    }))), React.createElement("div", {
      className: "flex items-center gap-1 text-sm"
    }, React.createElement("span", {
      className: "text-zinc-500 font-semibold"
    }, "Scale:"), React.createElement("select", {
      value: JSON.stringify(selectedScale || null),
      onChange: e => {
        const v = e.target.value;
        setSelectedScale(v === 'null' ? null : JSON.parse(v));
        setRenderTick(t => t + 1);
      },
      className: "bg-zinc-800 border border-zinc-700 text-sm text-zinc-300 rounded px-1.5 py-0.5 outline-none focus:border-yellow-500 max-w-[110px]"
    }, React.createElement("option", { value: "null" }, "None"), (function() {
      const opts = [];
      const pushKey = (label, val) => opts.push(React.createElement("option", { key: label, value: JSON.stringify(val) }, label));
      Object.keys(SCALES || {}).forEach(k => {
        const v = SCALES[k];
        if (v === null) return;
        if (Array.isArray(v)) pushKey(k, v);
        else Object.keys(v).forEach(sk => pushKey(sk, v[sk]));
      });
      return opts;
    }())), React.createElement("select", {
      value: scaleRoot,
      onChange: e => { setScaleRoot(parseInt(e.target.value) || 0); setRenderTick(t => t + 1); },
      className: "bg-zinc-800 border border-zinc-700 text-sm text-zinc-300 rounded px-1.5 py-0.5 outline-none focus:border-yellow-500"
    }, ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'].map((r, i) => React.createElement("option", { key: r, value: i }, r))), React.createElement("button", {
      onClick: () => applyForceToScale(),
      title: "Force selected notes to scale",
      className: "px-1.5 py-0.5 rounded text-sm bg-zinc-800 text-amber-400 border border-zinc-700 hover:border-amber-600"
    }, "Force")), React.createElement("div", {
      className: "flex items-center gap-1 text-sm"
    }, React.createElement("span", {
      className: "text-zinc-500 font-semibold ml-1"
    }, "Chord:"), React.createElement("select", {
      value: chordType,
      onChange: e => setChordType(e.target.value),
      className: "bg-zinc-800 border border-zinc-700 text-zinc-300 rounded px-1 py-0.5 text-sm outline-none"
    }, React.createElement("option", { value: "triad" }, "Triad"), React.createElement("option", { value: "min7" }, "Min7"), React.createElement("option", { value: "sus4" }, "Sus4"), React.createElement("option", { value: "add9" }, "Add9")), React.createElement("button", {
      onClick: () => setChordStampMode(m => !m),
      title: "Chord stamp mode — click canvas to stamp chord (Shift+C)",
      className: `px-1.5 py-0.5 rounded text-sm border ${chordStampMode ? 'bg-amber-800/60 text-amber-300 border-amber-600' : 'bg-zinc-800 text-zinc-400 border-zinc-700 hover:border-amber-600'}`
    }, "Stamp"), React.createElement("button", {
      onClick: () => applyHarmonize(3),
      title: "Harmonize +3rd",
      className: "px-1.5 py-0.5 rounded text-sm bg-zinc-800 text-zinc-300 border border-zinc-700 hover:border-emerald-600"
    }, "+3"), React.createElement("button", {
      onClick: () => applyHarmonize(5),
      title: "Harmonize +5th",
      className: "px-1.5 py-0.5 rounded text-sm bg-zinc-800 text-zinc-300 border border-zinc-700 hover:border-emerald-600"
    }, "+5"), React.createElement("button", {
      onClick: () => applyHarmonize(7),
      title: "Harmonize +7th",
      className: "px-1.5 py-0.5 rounded text-sm bg-zinc-800 text-zinc-300 border border-zinc-700 hover:border-emerald-600"
    }, "+7")), React.createElement("div", {
      className: "flex items-center gap-1 text-sm"
    }, React.createElement("span", {
      className: "text-zinc-500 font-semibold"
    }, "Vel:"), React.createElement("button", {
      onClick: () => applyVelocityCompress(),
      title: "Compress velocity toward target",
      className: "px-1.5 py-0.5 rounded text-sm bg-zinc-800 text-zinc-300 border border-zinc-700 hover:border-purple-600"
    }, "Comp"), React.createElement("input", {
      type: "number", value: velocityTarget, onChange: e => setVelocityTarget(parseInt(e.target.value) || 80),
      className: "w-14 bg-zinc-900 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5 text-sm text-center"
    }), React.createElement("button", {
      onClick: () => applyVelocityNormalize(),
      title: "Normalize (max → 127)",
      className: "px-1.5 py-0.5 rounded text-sm bg-zinc-800 text-zinc-300 border border-zinc-700 hover:border-purple-600"
    }, "Norm"))), 

    /* 2. BAR RULER */
    React.createElement("div", {
      className: "h-6 bg-[#1a1a1f] border-b border-zinc-900 flex shrink-0"
    }, React.createElement("div", {
      className: "w-[120px] bg-[#1e1e22] border-r border-zinc-800 shrink-0 flex items-end"
    }, React.createElement("span", {
      className: "text-[8px] text-zinc-600 font-mono px-1.5 pb-0.5 uppercase tracking-wider"
    }, "Tracks")), React.createElement("div", {
      className: "w-[60px] bg-[#222222] border-r border-zinc-900 shrink-0"
    }), React.createElement("div", {
      ref: rulerScrollRef,
      className: "flex-1 overflow-hidden",
      onMouseDown: (e) => {
        const rect = e.currentTarget.getBoundingClientRect();
        const x = e.clientX - rect.left + e.currentTarget.scrollLeft;
        const clickBeat = x / pixelsPerBeat;
        const clickTime = clickBeat * beatSec;
        const clickInRange = loopStartBeat !== null && loopEndBeat !== null && clickBeat >= loopStartBeat && clickBeat <= loopEndBeat;
        if (e.ctrlKey || e.metaKey) {
          setLoopStartBeat(null);
          setLoopEndBeat(null);
          setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, selectionStart: null, selectionEnd: null } : s));
          return;
        }
        if (e.shiftKey) {
          const beatSnap = getSnapBeat(clickBeat, snapValue);
          if (loopStartBeat === null) {
            setLoopStartBeat(Math.max(0, beatSnap - 4));
            setLoopEndBeat(Math.max(4, beatSnap));
          } else {
            setLoopEndBeat(Math.max(loopStartBeat + 4, beatSnap));
          }
          return;
        }
        if (clickTime >= 0) {
          if (onSeekPlayhead) {
            onSeekPlayhead(clickTime);
          } else {
            setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, currentTime: clickTime } : s));
          }
        }
        const snappedStartBeat = getSnapBeat(clickBeat, snapValue);
        rulerDragRef.current = { startX: e.clientX, startBeat: snappedStartBeat, scrollLeft: e.currentTarget.scrollLeft };
        const onMove = (ev) => {
          const r = rulerScrollRef.current;
          if (!r || !rulerDragRef.current) return;
          const rRect = r.getBoundingClientRect();
          const bx = ev.clientX - rRect.left + rulerDragRef.current.scrollLeft;
          const rawBeat = Math.max(0, bx / pixelsPerBeat);
          const beat = getSnapBeat(rawBeat, snapValue);
          if (Math.abs(ev.clientX - rulerDragRef.current.startX) > 5) {
            if (clickInRange) {
              const rangeWidth = loopEndBeat - loopStartBeat;
              const offset = rulerDragRef.current.startBeat - loopStartBeat;
              const centerBeat = beat - offset;
              const halfRange = rangeWidth / 2;
              const newStart = Math.max(0, centerBeat - halfRange);
              setLoopStartBeat(newStart);
              setLoopEndBeat(newStart + rangeWidth);
              setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, selectionStart: newStart * beatSec, selectionEnd: (newStart + rangeWidth) * beatSec } : s));
            } else {
              const sBeat = Math.max(0, Math.min(rulerDragRef.current.startBeat, beat));
              const eBeat = Math.max(sBeat + 1, Math.max(rulerDragRef.current.startBeat, beat));
              setLoopStartBeat(sBeat);
              setLoopEndBeat(eBeat);
              setSubTabs(prev => prev.map(s => s.id === st.id ? {
                ...s,
                selectionStart: sBeat * beatSec,
                selectionEnd: eBeat * beatSec
              } : s));
            }
          }
        };
        const onUp = () => { rulerDragRef.current = null; document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); };
        document.addEventListener('mousemove', onMove);
        document.addEventListener('mouseup', onUp);
      }
    }, React.createElement("div", {
      style: {
        width: `${viewWidth}px`,
        height: '100%'
      },
      className: "relative h-full font-mono text-[9px] text-zinc-500 font-bold"
    }, renderBarLabels(), loopStartBeat !== null && loopEndBeat !== null && loopEndBeat > loopStartBeat && React.createElement("div", {
      style: {
        left: `${loopStartBeat * pixelsPerBeat}px`,
        width: `${(loopEndBeat - loopStartBeat) * pixelsPerBeat}px`,
        top: 0, bottom: 0
      },
      className: "absolute bg-emerald-500/15 border-l border-r border-emerald-400"
    }, React.createElement("div", {
      style: { position: 'absolute', left: 0, top: 0, bottom: 0, width: '4px', cursor: 'ew-resize' },
      onMouseDown: (e) => {
        e.stopPropagation();
        const startBeat = loopStartBeat;
        const onMove = (ev) => {
          const r = rulerScrollRef.current;
          if (!r) return;
          const rRect = r.getBoundingClientRect();
          const bx = ev.clientX - rRect.left + r.scrollLeft;
          const nBeat = Math.max(0, Math.min(loopEndBeat - 1, getSnapBeat(bx / pixelsPerBeat, snapValue)));
          setLoopStartBeat(nBeat);
          setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, selectionStart: nBeat * beatSec } : s));
        };
        const onUp = () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); };
        document.addEventListener('mousemove', onMove);
        document.addEventListener('mouseup', onUp);
      }
    }), React.createElement("div", {
      style: { position: 'absolute', right: 0, top: 0, bottom: 0, width: '4px', cursor: 'ew-resize' },
      onMouseDown: (e) => {
        e.stopPropagation();
        const onMove = (ev) => {
          const r = rulerScrollRef.current;
          if (!r) return;
          const rRect = r.getBoundingClientRect();
          const bx = ev.clientX - rRect.left + r.scrollLeft;
          const nBeat = Math.max(loopStartBeat + 1, getSnapBeat(bx / pixelsPerBeat, snapValue));
          setLoopEndBeat(nBeat);
          setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, selectionEnd: nBeat * beatSec } : s));
        };
        const onUp = () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); };
        document.addEventListener('mousemove', onMove);
        document.addEventListener('mouseup', onUp);
      }
    }))))), 

    /* 3. MAIN PIANO ROLL GRID (KEYBOARD + TRACK COL + CANVAS) */
    React.createElement("div", {
      className: "flex-1 flex overflow-hidden min-h-0 relative",
      onMouseEnter: function() {
        prMouseInRef.current = true;
        // Status bar gợi ý động (user 09:40): trong piano roll → base hint;
        // giữ Shift → select/unselect; giữ Ctrl → fast copy
        if (window.__setPrHint) {
          window.__setPrHint(prKeyStateRef.current.ctrl ? "Ctrl+drag: fast copy notes | Ctrl+click empty: marquee chọn notes" : (prKeyStateRef.current.shift ? "Shift+Click: select/unselect notes | Shift+Click note: thêm/bớt vào nhóm chọn" : "Scroll: Up/Down | Drag: Draw notes"));
        }
      },
      onMouseLeave: function() { prMouseInRef.current = false; if (window.__setPrHint) window.__setPrHint(null); }
    }, /* Track column */ React.createElement("div", {
      className: "w-[120px] bg-[#16161a] border-r border-zinc-800 shrink-0 flex flex-col z-10",
      style: { height: KeybedPixelHeight + 'px' }
    }, (allMidiItems.length > 0 ? function() {
      var seenTracks = {};
      var els = [];
      allMidiItems.forEach(function(m) {
        if (seenTracks[m._trackId]) return;
        seenTracks[m._trackId] = true;
        var track = (activeTracks || []).find(function(t) { return t.id === m._trackId; });
        var isActive = m._trackId === st.trackId && m.id === st.target_id;
        var isPlayOn = activePlayTrackIds && activePlayTrackIds.indexOf(m._trackId) !== -1;
        els.push(React.createElement("div", {
          key: m._trackId,
          className: "flex items-center gap-0.5 mx-1 my-[2px]"
        }, React.createElement("button", {
          onClick: function() {
            // Click nút tên track → ACTIVE ghost notes của track đó thành MAIN
            // notes để chỉnh sửa (user 09:40)
            handleSwitchMidiItem(m.id);
          },
          className: "flex items-center justify-center flex-1 h-[26px] min-w-0 border border-zinc-600 rounded-md cursor-pointer outline-none " + (isActive ? 'bg-yellow-600 text-black font-bold' : 'bg-zinc-700 text-zinc-300 hover:bg-zinc-600')
        }, React.createElement("span", {
          className: "text-[13px] leading-none font-sans truncate px-1",
          title: track ? track.name : m._trackName
        }, track ? track.name : m._trackName)), React.createElement("button", {
          onClick: function(e) {
            e.stopPropagation();
            var prevList = activePlayTrackIds || [];
            var nextList = prevList.indexOf(m._trackId) !== -1 ? prevList.filter(function(id) { return id !== m._trackId; }) : prevList.concat([m._trackId]);
            setActivePlayTrackIds(nextList);
            if (onRealtimePlay) onRealtimePlay(nextList);
          },
          title: isPlayOn ? "Unmute — ghost track play cùng main notes" : "Mute (mặc định) — click để play ghost cùng main",
          className: "w-6 h-[26px] shrink-0 border border-zinc-600 rounded-md cursor-pointer text-[11px] font-bold outline-none flex items-center justify-center " + (isPlayOn ? 'bg-green-700 text-white' : 'bg-zinc-800 text-zinc-500 hover:text-zinc-300')
        }, isPlayOn ? "\u266A" : "M")));
      });
      return els;
    }() : null)), React.createElement("div", {
      className: "w-[60px] shrink-0 flex flex-col border-r border-zinc-900 overflow-y-auto",
      ref: keybedRef,
      onScroll: handleKeybedScroll,
      style: {
        scrollbarWidth: 'none',
        msOverflowStyle: 'none'
      }
    }, renderKeybed()), React.createElement("div", {
      ref: gridScrollRef,
      onScroll: handleScroll,
      className: "flex-1 overflow-auto bg-[#141414] min-w-0"
    }, React.createElement("div", {
      style: {
        width: `${viewWidth}px`,
        height: `${(128 - PITCH_START) * NoteHeight}px`
      },
      className: "relative"
    }, React.createElement("canvas", {
      ref: canvasRef,
      onMouseDown: handleGridMouseDown,
      onMouseMove: handleGridMouseMove,
      onMouseUp: handleGridMouseUp,
      onMouseLeave: handleGridMouseUp,
      onContextMenu: handleContextMenu,
      className: "absolute inset-0 cursor-crosshair"
    }), loopStartBeat !== null && loopEndBeat !== null && loopEndBeat > loopStartBeat && React.createElement("div", {
      style: {
        left: `${loopStartBeat * pixelsPerBeat}px`,
        width: `${(loopEndBeat - loopStartBeat) * pixelsPerBeat}px`,
        top: 0, bottom: 0
      },
      className: "absolute bg-emerald-500/10 border-l border-r border-emerald-400/50 pointer-events-none"
    })))), 

    /* 4. CC / VELOCITY LANE (ĐẶT Ở ĐÁY) */
    showCC && React.createElement("div", {
      style: { height: `${ccHeight}px` },
      className: "bg-[#161616] border-t border-zinc-900 flex shrink-0 relative"
    }, React.createElement("div", {
      onMouseDown: e => {
        e.preventDefault();
        const startY = e.clientY;
        const startH = ccHeight;
        const onMove = ev => { setCcHeight(Math.max(40, startH + startY - ev.clientY)); };
        const onUp = () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); };
        document.addEventListener('mousemove', onMove);
        document.addEventListener('mouseup', onUp);
      },
      className: "absolute top-0 left-0 right-0 h-1.5 cursor-n-resize z-10 hover:bg-purple-600/30"
    }), React.createElement("div", {
      className: "w-[120px] shrink-0"
    }), React.createElement("div", {
      className: "w-[60px] bg-[#222222] border-r border-zinc-900 flex items-center justify-center text-[10px] text-zinc-500 font-mono shrink-0 font-bold"
    }, ccMode.toUpperCase()), React.createElement("div", {
      ref: ccWrapperRef,
      className: "flex-1 overflow-x-hidden min-w-0"
    }, React.createElement("div", {
      style: {
        width: `${viewWidth}px`,
        height: '100%'
      },
      className: "relative"
    }, React.createElement("canvas", {
      ref: ccCanvasRef,
      onMouseDown: handleCCMouseDown,
      onMouseMove: handleCCMouseMove,
      onMouseUp: () => { if (ccDragRef.current) ccDragRef.current.selectedMode = false; ccDragRef.current = null; },
      onMouseLeave: () => { if (ccDragRef.current) ccDragRef.current.selectedMode = false; ccDragRef.current = null; },
      className: "absolute inset-0"
    }), loopStartBeat !== null && loopEndBeat !== null && loopEndBeat > loopStartBeat && React.createElement("div", {
      style: {
        left: `${loopStartBeat * pixelsPerBeat}px`,
        width: `${(loopEndBeat - loopStartBeat) * pixelsPerBeat}px`,
        top: 0, bottom: 0
      },
      className: "absolute bg-emerald-500/10 border-l border-r border-emerald-400/50 pointer-events-none"
    })))), 

    /* 5. OVERLAY / CONTEXT MENU */
    (arpModal && React.createElement("div", {
      className: "absolute inset-0 z-40 flex items-center justify-center bg-black/60",
      onMouseDown: e => e.stopPropagation(),
      onClick: e => e.stopPropagation()
    }, React.createElement("div", {
      className: "bg-zinc-900 border border-zinc-700 rounded-lg p-4 w-72 shadow-2xl"
    }, React.createElement("div", { className: "text-sm font-bold text-cyan-300 mb-3" }, "Arpeggiate"), React.createElement("div", { className: "grid grid-cols-2 gap-2 text-xs mb-3" }, React.createElement("label", { className: "text-zinc-500" }, "Pattern"), React.createElement("select", { value: arpModal.pattern, onChange: e => setArpModal({ ...arpModal, pattern: e.target.value }), className: "bg-zinc-800 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5" }, ['UP', 'DOWN', 'UP-DOWN', 'RANDOM', 'CHORD'].map(p => React.createElement("option", { key: p, value: p }, p))), React.createElement("label", { className: "text-zinc-500" }, "Rate"), React.createElement("select", { value: arpModal.rate, onChange: e => setArpModal({ ...arpModal, rate: e.target.value }), className: "bg-zinc-800 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5" }, ['1/4', '1/8', '1/16', '1/32'].map(r => React.createElement("option", { key: r, value: r }, r))), React.createElement("label", { className: "text-zinc-500" }, "Octaves"), React.createElement("input", { type: "number", min: 1, max: 4, value: arpModal.octaves, onChange: e => setArpModal({ ...arpModal, octaves: parseInt(e.target.value) || 1 }), className: "bg-zinc-800 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5 w-14" }), React.createElement("label", { className: "text-zinc-500" }, "Gate %"), React.createElement("input", { type: "number", min: 10, max: 200, value: arpModal.gate, onChange: e => setArpModal({ ...arpModal, gate: parseInt(e.target.value) || 80 }), className: "bg-zinc-800 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5 w-14" })), React.createElement("div", { className: "flex gap-1 mb-3" }, React.createElement("label", { className: "flex items-center gap-1 text-[10px] text-zinc-400" }, React.createElement("input", { type: "checkbox", checked: !!arpModal.triplet, onChange: e => setArpModal({ ...arpModal, triplet: e.target.checked }) }), "Triplet"), React.createElement("label", { className: "flex items-center gap-1 text-[10px] text-zinc-400" }, React.createElement("input", { type: "checkbox", checked: !!arpModal.dotted, onChange: e => setArpModal({ ...arpModal, dotted: e.target.checked }) }), "Dotted")), React.createElement("div", { className: "flex gap-2" }, React.createElement("button", { onClick: () => applyArpeggiate(arpModal), className: "flex-1 px-3 py-1.5 rounded text-xs font-bold bg-cyan-700 hover:bg-cyan-600 text-white" }, "Apply"), React.createElement("button", { onClick: () => setArpModal(null), className: "px-3 py-1.5 rounded text-xs bg-zinc-800 hover:bg-zinc-700 text-zinc-300" }, "Cancel"))))),
    (strumModal && React.createElement("div", {
      className: "absolute inset-0 z-40 flex items-center justify-center bg-black/60",
      onMouseDown: e => e.stopPropagation(),
      onClick: e => e.stopPropagation()
    }, React.createElement("div", {
      className: "bg-zinc-900 border border-zinc-700 rounded-lg p-4 w-72 shadow-2xl"
    }, React.createElement("div", { className: "text-sm font-bold text-cyan-300 mb-3" }, "Strum"), React.createElement("div", { className: "grid grid-cols-2 gap-2 text-xs mb-3" }, React.createElement("label", { className: "text-zinc-500" }, "Strum ms"), React.createElement("input", { type: "number", min: 0, max: 120, value: strumModal.ms, onChange: e => setStrumModal({ ...strumModal, ms: parseInt(e.target.value) || 0 }), className: "bg-zinc-800 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5 w-16" }), React.createElement("label", { className: "text-zinc-500" }, "Direction"), React.createElement("select", { value: strumModal.direction, onChange: e => setStrumModal({ ...strumModal, direction: e.target.value }), className: "bg-zinc-800 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5" }, ['DOWN', 'UP', 'ALTERNATE'].map(d => React.createElement("option", { key: d, value: d }, d)))), React.createElement("div", { className: "flex gap-2" }, React.createElement("button", { onClick: () => applyStrum(strumModal), className: "flex-1 px-3 py-1.5 rounded text-xs font-bold bg-cyan-700 hover:bg-cyan-600 text-white" }, "Apply"), React.createElement("button", { onClick: () => setStrumModal(null), className: "px-3 py-1.5 rounded text-xs bg-zinc-800 hover:bg-zinc-700 text-zinc-300" }, "Cancel"))))),
    (humanizeModal && React.createElement("div", {
      className: "absolute inset-0 z-40 flex items-center justify-center bg-black/60",
      onMouseDown: e => e.stopPropagation(),
      onClick: e => e.stopPropagation()
    }, React.createElement("div", {
      className: "bg-zinc-900 border border-zinc-700 rounded-lg p-4 w-72 shadow-2xl"
    }, React.createElement("div", { className: "text-sm font-bold text-cyan-300 mb-3" }, "Humanize"), React.createElement("div", { className: "grid grid-cols-2 gap-2 text-xs mb-3" }, React.createElement("label", { className: "text-zinc-500" }, "Timing ±ms"), React.createElement("input", { type: "number", min: 0, max: 30, value: humanizeModal.timingMs, onChange: e => setHumanizeModal({ ...humanizeModal, timingMs: parseInt(e.target.value) || 0 }), className: "bg-zinc-800 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5 w-16" }), React.createElement("label", { className: "text-zinc-500" }, "Vel ±(0-127)"), React.createElement("input", { type: "number", min: 0, max: 20, value: humanizeModal.velRange, onChange: e => setHumanizeModal({ ...humanizeModal, velRange: parseInt(e.target.value) || 0 }), className: "bg-zinc-800 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5 w-16" }), React.createElement("label", { className: "text-zinc-500" }, "Dur ±%"), React.createElement("input", { type: "number", min: 0, max: 50, value: humanizeModal.durRange, onChange: e => setHumanizeModal({ ...humanizeModal, durRange: parseInt(e.target.value) || 0 }), className: "bg-zinc-800 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5 w-16" })), React.createElement("div", { className: "flex gap-2" }, React.createElement("button", { onClick: () => applyHumanizeModal(humanizeModal), className: "flex-1 px-3 py-1.5 rounded text-xs font-bold bg-cyan-700 hover:bg-cyan-600 text-white" }, "Apply"), React.createElement("button", { onClick: () => setHumanizeModal(null), className: "px-3 py-1.5 rounded text-xs bg-zinc-800 hover:bg-zinc-700 text-zinc-300" }, "Cancel"))))),
    scaleMenuPos && renderScaleContextMenu()
  );
};

const serializeTracksList = (tracksList, secondsPerBar) => {
  return (tracksList || []).map(t => {
    let trackType = "AUDIO";
    if (t.type === 'MIDI' || t.type === 'soundfont' || t.type === 'vst3') trackType = "MIDI";
    else if (t.type === 'SECTION') trackType = "SECTION";
    else if (t.sections && t.sections.length > 0) trackType = "SECTION";
    else if (t.midiItems && t.midiItems.length > 0) trackType = "MIDI";

    const items = [];
    // Serialize EVERY item type present on the track (a track can hold audio
    // clips + MIDI items + section items at once). The old if/else-if chain
    // dropped all but one type per track — silent data loss on save.
    if (t.clips && t.clips.length > 0) {
      t.clips.forEach(c => {
        const durationSec = c.buffer ? c.buffer.duration : 4.0;
        const clipFileId = c.serverFileId || t.serverFileId;
        items.push({
          id: c.id,
          name: c.name || "Audio Clip",
          type: "AUDIO_ITEM",
          start_bar: c.startTime / secondsPerBar,
          duration_bars: (durationSec / (c.speed || 1.0)) / secondsPerBar,
          clip_start_offset_bars: 0.0,
          source_data: {
            audio_file_url: clipFileId ? `/static/audio/uploads/${clipFileId}` : "",
            sample_rate: c.buffer ? c.buffer.sampleRate : 44100,
            channels: c.buffer ? c.buffer.numberOfChannels : 2,
            gain: 1.0,
            server_file_id: clipFileId
          }
        });
      });
    }
    if (t.midiItems && t.midiItems.length > 0) {
      t.midiItems.forEach(m => {
        items.push({
          id: m.id,
          name: m.name || "MIDI Item",
          type: "MIDI_ITEM",
          start_bar: m.startTime / secondsPerBar,
          duration_bars: m.duration ? (m.duration / secondsPerBar) : 4.0,
          clip_start_offset_bars: 0.0,
          source_data: {
            total_buffer_bars: m.duration ? (m.duration / secondsPerBar) : 8.0,
            notes: (m.notes || []).map(n => ({
              id: n.id || 'note_' + Math.random().toString(36).substr(2, 9),
              pitch: n.pitch || 60,
              start_beat: n.start_beat || 0.0,
              duration_beats: n.duration_beats || 1.0,
              velocity: n.velocity || 0.8,
              pan: n.pan || 0.0
            }))
          }
        });
      });
    }
    if (t.sections && t.sections.length > 0) {
      t.sections.forEach(s => {
        items.push({
          id: s.id,
          name: s.name || "Section Item",
          type: "SECTION_ITEM",
          start_bar: s.start / secondsPerBar,
          duration_bars: s.duration / secondsPerBar,
          clip_start_offset_bars: 0.0,
          source_data: {
            // KHÔNG fallback s.id: section item thiếu sectionId (insert thiếu
            // field) → fallback = chính id item → TỰ TRỎ → block lồng + mất
            // nội dung. Chỉ dùng sectionId; undefined → deserialize block rỗng.
            referenced_section_id: s.sectionId
          }
        });
      });
    }

    return {
      id: t.id,
      name: t.name,
      type: trackType,
      color: t.color || null,
      volume_db: t.volumeDb || 0.0,
      pan: t.pan || 0.0,
      mute: t.muted || false,
      solo: t.solo || false,
      mastering_bypass: t.audioBypass || false,
      audio_bypass: t.audioBypass || false,
      midi_bypass: t.midiBypass || false,
      fx_active: t.fxActive !== false,
      instrument_id: t.instrumentId || null,
      instrument_program: t.instrumentProgram !== undefined ? t.instrumentProgram : null,
      instrument_name: t.instrumentName || null,
      instrument_source: t.instrument_source || (t.synth_engine ? t.synth_engine.type : null),
      soundfont_id: t.soundfont_id || (t.synth_engine ? t.synth_engine.soundfont_id : null),
      soundfont_bank: t.soundfont_bank !== undefined ? t.soundfont_bank : (t.synth_engine ? t.synth_engine.soundfont_bank : null),
      soundfont_program: t.soundfont_program !== undefined ? t.soundfont_program : (t.synth_engine ? t.synth_engine.soundfont_program : null),
      synth_engine: t.synth_engine || undefined,
      midi_channel: t.midiChannel !== undefined ? t.midiChannel : null,
      fx_chain: (t.fxChain || []).map(m => typeof m === 'string' ? { type: m, active: true } : { type: m.type, active: m.active !== false }),
      server_file_id: t.serverFileId || null,
      items: items
    };
  });
};

const deserializeTracksList = (schemaTracks, secondsPerBar, sectionStore, visitedSections, depth) => {
  // Guard CHU TRÌNH: SECTION_ITEM trỏ về section đang được deserialize (hoặc
  // chu trình A→B→A) → recursion vô hạn → RangeError: Maximum call stack size
  // exceeded khi restore project. Visited-set chặn cycle; depth chặn lồng sâu.
  const visited = visitedSections || new Set();
  const curDepth = depth || 0;
  if (curDepth > 12) return [];
  return (schemaTracks || []).map(t => {
    const clips = [];
    const sections = [];
    const midiItems = [];

    (t.items || []).forEach(item => {
      if (item.type === "AUDIO_ITEM") {
        const src = item.source_data || {};
        const clipFileId = src.server_file_id || (src.audio_file_url ? src.audio_file_url.split('/').pop() : null) || null;
        clips.push({
          id: item.id,
          name: item.name,
          startTime: item.start_bar * secondsPerBar,
          speed: 1.0,
          duration: item.duration_bars * secondsPerBar,
          serverFileId: clipFileId
        });
      } else if (item.type === "MIDI_ITEM") {
        const src = item.source_data || {};
        midiItems.push({
          id: item.id,
          name: item.name,
          parent_track_id: t.id,
          startTime: item.start_bar * secondsPerBar,
          duration: item.duration_bars * secondsPerBar,
          length_bars: item.duration_bars || 4,
          notes: (src.notes || []).map(n => ({
            id: n.id,
            pitch: n.pitch || 60,
            start_beat: n.start_beat || 0.0,
            duration_beats: n.duration_beats || 1.0,
            velocity: n.velocity || 0.8,
            pan: n.pan || 0.0
          }))
        });
      } else if (item.type === "SECTION_ITEM") {
        const src = item.source_data || {};
        const secId = src.referenced_section_id;
        const secContainer = sectionStore ? sectionStore[secId] : null;
        const isCycle = secContainer ? visited.has(secId) : false;
        sections.push({
          id: item.id,
          name: item.name,
          start: item.start_bar * secondsPerBar,
          duration: item.duration_bars * secondsPerBar,
          length_bars: item.duration_bars || 4,
          sectionId: secId,
          tracks: (secContainer && !isCycle) ? deserializeTracksList(secContainer.tracks, secondsPerBar, sectionStore, new Set(visited).add(secId), curDepth + 1) : null
        });
      }
    });

    return {
      id: t.id,
      name: t.name,
      type: t.type === 'MIDI' ? 'MIDI' : (t.type === 'SECTION' ? 'SECTION' : 'audio'),
      volumeDb: t.volume_db || 0.0,
      pan: t.pan || 0.0,
      muted: t.mute || false,
      solo: t.solo || false,
      masteringBypass: (t.audio_bypass !== undefined ? t.audio_bypass : (t.mastering_bypass || false)),
      audioBypass: (t.audio_bypass !== undefined ? t.audio_bypass : (t.mastering_bypass || false)),
      midiBypass: (t.midi_bypass !== undefined ? t.midi_bypass : (t.mastering_bypass || false)),
      fxActive: t.fx_active !== undefined ? t.fx_active : true,
      color: t.color || (t.id === '1' ? '#0f766e' : '#1d4ed8'),
      startTime: t.start_time || 0,
      height: t.height || 140,
      markers: t.markers || [],
      serverFileId: t.server_file_id || (t.items && t.items.find(function(i) { return i.type === 'AUDIO_ITEM'; })?.source_data?.server_file_id) || (t.items && t.items.find(function(i) { return i.type === 'AUDIO_ITEM'; })?.source_data?.audio_file_url?.split('/').pop()) || null,
      channelInfo: t.channel_info || null,
      isArmed: t.is_armed || false,
      monitoringEnabled: t.monitoring_enabled !== false,
      inputSource: t.input_source ? { deviceType: t.input_source.device_type || 'NONE', deviceId: t.input_source.device_id || '' } : { deviceType: 'NONE', deviceId: '' },
      midiChannel: t.midi_channel != null ? t.midi_channel : undefined,
      is_percussion: t.is_percussion || false,
      clips: clips,
      sections: sections,
      midiItems: midiItems,
      instrumentId: t.instrumentId != null ? t.instrumentId : (t.instrument_id || null),
      instrumentProgram: t.instrumentProgram !== undefined && t.instrumentProgram !== null ? t.instrumentProgram : (t.instrument_program !== null ? t.instrument_program : undefined),
      instrumentName: t.instrument_name || null,
      instrument_source: t.instrument_source || null,
      soundfont_id: t.soundfont_id || null,
      soundfont_bank: t.soundfont_bank !== null ? t.soundfont_bank : undefined,
      soundfont_program: t.soundfont_program !== null ? t.soundfont_program : undefined,
      synth_engine: t.synth_engine || undefined,
      fxChain: (t.fx_chain || []).map(m => typeof m === 'string' ? { type: m, active: true } : { type: m.type || m, active: m.active !== false }),
    };
  });
};

const serializeProjectToSchema = (projectId, name, bpmVal, tracksList, subTabsList, sessionTabsList, masteringSettings) => {
  const secondsPerBar = (60.0 / parseFloat(bpmVal || 120)) * 4;
  const mainTracks = serializeTracksList(tracksList, secondsPerBar);

  const sectionStore = {};
  
  // Helper: compute length_bars from tracks content
  const computeLengthBars = (tracksArr, spb) => {
    let maxSec = 0;
    (tracksArr || []).forEach(tr => {
      (tr.clips || []).forEach(c => {
        const end = (c.startTime || 0) + (c.buffer ? c.buffer.duration / (c.speed || 1.0) : 4);
        if (end > maxSec) maxSec = end;
      });
      (tr.midiItems || []).forEach(m => {
        const end = (m.startTime || 0) + (m.duration || 4);
        if (end > maxSec) maxSec = end;
      });
    });
    return Math.ceil((maxSec || 4) / spb);
  };

  // 1. Populate from sessionTabsList (open tabs)
  (sessionTabsList || []).forEach(st => {
    const serializedTracks = serializeTracksList(st.tracks, secondsPerBar);
    sectionStore[st.sectionId] = {
      id: st.sectionId,
      name: st.name,
      is_root: false,
      length_bars: computeLengthBars(st.tracks, secondsPerBar),
      auto_compute_length: true,
      tracks: serializedTracks,
      color: st.color || null
    };
  });

  // 2. Also populate from tracksList (closed tabs saved inside Section items)
  const scanForSections = (tracks) => {
    (tracks || []).forEach(t => {
      if (t.sections) {
        t.sections.forEach(s => {
          const secId = s.sectionId || s.id;
          if (s.tracks && !sectionStore[secId]) {
            sectionStore[secId] = {
              id: secId,
              name: s.name,
              is_root: false,
              length_bars: computeLengthBars(s.tracks, secondsPerBar),
              auto_compute_length: true,
              tracks: serializeTracksList(s.tracks, secondsPerBar),
              color: s.color || null
            };
          }
          if (s.tracks) {
            scanForSections(s.tracks);
          }
        });
      }
    });
  };
  scanForSections(tracksList);

  const subTabs = (subTabsList || []).map(st => {
    return {
      id: st.id,
      label: st.label,
      type: st.type,
      track_id: st.trackId,
      target_id: st.target_id,
      parent_tab_id: st.parent_tab_id,
      notes: st.notes || [],
      duration: st.duration || 4,
      instrument_program: st.instrumentProgram,
      instrument_name: st.instrumentName,
      synth_engine: st.synth_engine || null,
      current_time: st.currentTime || 0,
      color: st.color || null
    };
  });

  return {
    project_id: projectId || 'proj_' + Date.now(),
    metadata: {
      title: name || "Dự án mới",
      bpm: parseFloat(bpmVal || 120),
      time_signature_numerator: 4,
      time_signature_denominator: 4,
      sample_rate: 44100,
      zoom: window.__sfTimelineZoom || window.__currentZoom || 1.0
    },
    main_session: {
      id: "main",
      name: "MAIN SESSION",
      is_root: true,
      length_bars: (() => {
        let maxBar = 16.0;
        (mainTracks || []).forEach(t => {
          (t.items || []).forEach(item => {
            const end = (item.start_bar || 0) + (item.duration_bars || 4);
            if (end > maxBar) maxBar = end;
          });
        });
        return maxBar;
      })(),
      auto_compute_length: true,
      tracks: mainTracks
    },
    sub_tabs: subTabs,
    section_store: sectionStore,
    mastering_settings: masteringSettings || null
  };
};

const deserializeProjectFromSchema = (schemaObj) => {
  const bpmVal = schemaObj.metadata ? schemaObj.metadata.bpm : 120;
  const secondsPerBar = (60.0 / bpmVal) * 4;

  const sectionStore = schemaObj.section_store || {};
  const restoredTracks = deserializeTracksList(schemaObj.main_session.tracks, secondsPerBar, sectionStore);

  const restoredSessionTabs = [];
  Object.keys(sectionStore).forEach(secId => {
    const secContainer = sectionStore[secId];
    const secTracks = deserializeTracksList(secContainer.tracks, secondsPerBar, sectionStore);
    restoredSessionTabs.push({
      id: 'session_' + secId,
      name: secContainer.name,
      sectionId: secId,
      tracks: secTracks,
      length_bars: secContainer.length_bars || 16.0,
      auto_compute_length: secContainer.auto_compute_length !== undefined ? secContainer.auto_compute_length : true,
      color: secContainer.color || null
    });
  });

  const restoredSubTabs = (schemaObj.sub_tabs || []).map(st => {
    return {
      id: st.id,
      label: st.label,
      type: st.type,
      trackId: st.track_id,
      target_id: st.target_id,
      parent_tab_id: st.parent_tab_id,
      notes: st.notes || [],
      duration: st.duration || 4,
      instrumentProgram: st.instrument_program,
      instrumentName: st.instrument_name,
      synth_engine: st.synth_engine || null,
      currentTime: st.current_time || 0,
      color: st.color || null
    };
  });

  // Migrate mastering settings saved with the OLD imager width scale
  // (−100..+100, 0 = original width) to the new imager_spec.md scale
  // (0..200, 0% = MONO, 100% = original, 200% = double width): old value v
  // meant S × (1 + v/100), which equals the new value (v + 100). Projects
  // saved after the migration carry `imagerScale: 'v2'` and are kept as-is.
  const _migrateMasteringSettings = (ms) => {
    if (!ms) return null;
    const migrated = ms.imagerScale === 'v2'
      ? { ...ms }
      : {
          ...ms,
          w1: (ms.w1 ?? 0) + 100,
          w2: (ms.w2 ?? 0) + 100,
          w3: (ms.w3 ?? 0) + 100,
          w4: (ms.w4 ?? 0) + 100,
          imagerScale: 'v2'
        };
    // mastering_expand.md: extension modules + dynamic chain (default = old chain order)
    if (!Array.isArray(migrated.chain)) {
      migrated.chain = DEFAULT_MASTER_CHAIN.map(m => ({ ...m }));
    }
    if (migrated.compActive === undefined) migrated.compActive = false;
    if (migrated.compThreshold === undefined) migrated.compThreshold = -16;
    if (migrated.compRatio === undefined) migrated.compRatio = 3;
    if (migrated.compMakeup === undefined) migrated.compMakeup = 0;
    if (migrated.limActive === undefined) migrated.limActive = false;
    if (migrated.limThreshold === undefined) migrated.limThreshold = -1.0;
    if (migrated.excActive === undefined) migrated.excActive = false;
    if (migrated.excDrive === undefined) migrated.excDrive = 40;
    if (migrated.rebalActive === undefined) migrated.rebalActive = false;
    if (migrated.rebalMid === undefined) migrated.rebalMid = 0;
    if (migrated.rebalSide === undefined) migrated.rebalSide = 0;
    return migrated;
  };

  return {
    bpm: bpmVal,
    tracks: restoredTracks,
    sessionTabs: restoredSessionTabs,
    subTabs: restoredSubTabs,
    masteringSettings: _migrateMasteringSettings(schemaObj.mastering_settings),
    zoom: schemaObj.metadata && schemaObj.metadata.zoom ? parseFloat(schemaObj.metadata.zoom) : 1.0
  };
};

// ──────────────────────────────────────────────
// MASTERING KNOB COMPONENT (Dynamic pointer events version)
// ──────────────────────────────────────────────
const MasteringKnob = ({ param, min, max, value, unit, label, color, onChange, size = 'small' }) => {
  const [isDragging, setIsDragging] = React.useState(false);
  const startYRef = React.useRef(0);
  const startValRef = React.useRef(0);

  const handlePointerDown = (e) => {
    e.preventDefault();
    setIsDragging(true);
    startYRef.current = e.clientY;
    startValRef.current = value;
    e.currentTarget.setPointerCapture(e.pointerId);
  };

  const handlePointerMove = (e) => {
    if (!isDragging) return;
    const deltaY = startYRef.current - e.clientY;
    let newVal = startValRef.current + (deltaY / 150) * (max - min);
    newVal = Math.min(max, Math.max(min, newVal));
    onChange(param, newVal);
  };

  const handlePointerUp = (e) => {
    setIsDragging(false);
    try { e.currentTarget.releasePointerCapture(e.pointerId); } catch (err) {}
  };

  const pct = (value - min) / (max - min);
  const angle = -135 + pct * 270;

  const isLarge = size === 'large';
  const dialClass = isLarge ? 'w-20 h-20 border-4 bg-slate-900' : 'w-10 h-10 border-2 bg-slate-800';
  const pointerHeight = isLarge ? 'h-6' : 'h-3';
  const valClass = isLarge ? 'text-xs text-cyan-300 font-bold mt-2 z-10' : 'text-[9px] text-slate-300 font-mono mt-1 font-bold';

  return (
    <div className="flex flex-col items-center select-none">
      {label && <span className="text-[10px] font-bold text-slate-400 mb-1.5 uppercase tracking-wide">{label}</span>}
      <div
        className={`${dialClass} rounded-full relative flex items-center justify-center cursor-ns-resize shadow-lg`}
        style={{ borderColor: color }}
        onPointerDown={handlePointerDown}
        onPointerMove={handlePointerMove}
        onPointerUp={handlePointerUp}
        onPointerCancel={handlePointerUp}
      >
        <div
          className="w-0.5 absolute rounded origin-bottom"
          style={{
            backgroundColor: color,
            height: isLarge ? '22px' : '12px',
            top: isLarge ? '6px' : '4px',
            transform: `rotate(${angle}deg)`,
            transformOrigin: '50% 100%'
          }}
        ></div>
        {isLarge && (
          <span className="text-[10px] font-bold text-cyan-300 z-10 bg-slate-950/80 px-1 py-0.5 rounded border border-slate-800">
            {value > 0 && unit === 'dB' ? '+' : ''}{value.toFixed(1)} {unit}
          </span>
        )}
      </div>
      {!isLarge && (
        <span className={valClass}>
          {value > 0 && unit === 'dB' ? '+' : ''}{value.toFixed(1)}{unit}
        </span>
      )}
    </div>
  );
};

// ──────────────────────────────────────────────
// UNIFIED FX RACK PANEL (unified_fx_rack_panel.md) — một panel dùng chung,
// bind động vào track khi bấm [FX]; mọi thay đổi rebuild graph RIÊNG của track.
// ──────────────────────────────────────────────
const TRACK_FX_META = {
  eq: { name: 'EQ 4-Band', icon: 'activity', color: '#22d3ee', sub: '4-Band Peak' },
  eqpro: { name: 'Parametric / Graphic EQ PRO', icon: 'chart-area', color: '#2dd4bf', sub: 'Pro-Q style · 8 bands · interactive' },
  compressor: { name: 'Bus Compressor', icon: 'compress', color: '#fbbf24', sub: 'Glue & Punch' },
  limiter: { name: 'Brickwall Limiter', icon: 'shield-half', color: '#f43f5e', sub: 'True-Peak 20:1' },
  exciter: { name: 'Harmonic Exciter', icon: 'wand-2', color: '#c084fc', sub: 'Saturation & Air' },
  rebalance: { name: 'Master Rebalance', icon: 'sliders-horizontal', color: '#38bdf8', sub: 'M/S Balance' },
  carla: { name: 'Carla Bridge (VST FX)', icon: 'sliders', color: '#14b8a6', sub: 'Native VST audio processing' }
};
const TRACK_FX_DEFAULTS = {
  eq: { g1: 0, g2: 0, g3: 0, g4: 0 },
  eqpro: { amount: 100, bands: EQPRO_DEFAULT_BANDS },
  compressor: { threshold: -16, ratio: 3, makeup: 0 },
  limiter: { ceiling: -1.0 },
  exciter: { drive: 40 },
  rebalance: { mid: 0, side: 0 },
  carla: { plugin: '', plugin_path: '' }
};

// EQ Pro canvas frame renderer (graphic_EQ_interactive_module.md §I-II): grid,
// realtime FFT spectrum, per-band fills, summed master white curve, nodes+wings.
function renderEqProFrame(c, w, h, s, modules, fs) {
  c.clearRect(0, 0, w, h);
  c.font = '9px monospace';
  [20, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000].forEach(f => {
    const x = eqproFreqToX(f, w);
    c.strokeStyle = 'rgba(51,65,85,0.25)'; c.lineWidth = 1;
    c.beginPath(); c.moveTo(x, 0); c.lineTo(x, h); c.stroke();
    c.fillStyle = '#475569';
    c.fillText(f >= 1000 ? (f / 1000) + 'k' : '' + f, x + 3, h - 8);
  });
  [18, 12, 6, 0, -6, -12, -18].forEach(db => {
    const y = eqproGainToY(db, h);
    c.strokeStyle = db === 0 ? 'rgba(45,212,191,0.45)' : 'rgba(51,65,85,0.25)';
    c.lineWidth = db === 0 ? 1.5 : 1;
    c.beginPath(); c.moveTo(0, y); c.lineTo(w, y); c.stroke();
    c.fillStyle = '#475569';
    c.fillText((db > 0 ? '+' : '') + db, w - 34, y - 3);
  });
  // Realtime FFT spectrum — one overlay per live module (audioclip path = sky,
  // soundfont/MIDI path = pink), so both sources are visible when playing.
  const specStyles = ['rgba(56,189,248,0.20)', 'rgba(236,72,153,0.16)'];
  (modules || []).forEach((module, mi) => {
    if (!module || !module.analyser) return;
    const fft = new Uint8Array(module.analyser.frequencyBinCount);
    module.analyser.getByteFrequencyData(fft);
    c.fillStyle = specStyles[mi % specStyles.length];
    c.beginPath(); c.moveTo(0, h);
    for (let x = 0; x <= w; x++) {
      const f = eqproXToFreq(x, w);
      const bin = Math.floor((f / (fs / 2)) * fft.length);
      const v = (fft[bin] || 0) / 255;
      c.lineTo(x, h - v * h * 0.8);
    }
    c.lineTo(w, h); c.closePath(); c.fill();
  });
  // Per-band translucent fills + summed master curve
  const total = new Float32Array(w);
  s.bands.forEach((b, bi) => {
    if (b.active === false) return;
    const col = EQPRO_BAND_COLORS[bi % EQPRO_BAND_COLORS.length];
    c.fillStyle = col.fill;
    c.beginPath(); c.moveTo(0, eqproGainToY(0, h));
    for (let x = 0; x < w; x++) {
      const db = eqproBiquadMagDb(b.type, eqproXToFreq(x, w), b.freq, b.gain, b.q, fs);
      total[x] += db;
      c.lineTo(x, eqproGainToY(db, h));
    }
    c.lineTo(w, eqproGainToY(0, h)); c.closePath(); c.fill();
  });
  c.strokeStyle = '#ffffff'; c.lineWidth = 2.5; c.beginPath();
  for (let x = 0; x < w; x++) {
    const y = eqproGainToY(total[x], h);
    if (x === 0) c.moveTo(x, y); else c.lineTo(x, y);
  }
  c.stroke();
  // Nodes + Q-handle wings
  s.bands.forEach((b, bi) => {
    const nx = eqproFreqToX(b.freq, w);
    const ny = eqproGainToY(eqproNodeDb(b), h);
    const col = EQPRO_BAND_COLORS[bi % EQPRO_BAND_COLORS.length];
    const sel = s.selected === bi;
    const wo = eqproQToWing(b.q);
    if (sel) {
      c.strokeStyle = col.stroke; c.lineWidth = 2;
      c.beginPath(); c.moveTo(nx - wo, ny); c.lineTo(nx + wo, ny); c.stroke();
      [nx - wo, nx + wo].forEach(wx => {
        c.fillStyle = '#0f172a'; c.strokeStyle = col.stroke; c.lineWidth = 2;
        c.beginPath(); c.arc(wx, ny, 4, 0, Math.PI * 2); c.fill(); c.stroke();
      });
    }
    c.fillStyle = sel ? '#ffffff' : col.stroke;
    c.strokeStyle = col.stroke; c.lineWidth = sel ? 3 : 2;
    c.beginPath(); c.arc(nx, ny, sel ? 7 : 5, 0, Math.PI * 2); c.fill(); c.stroke();
    if (b.active === false) {
      c.strokeStyle = '#ef4444'; c.lineWidth = 2;
      c.beginPath(); c.moveTo(nx - 6, ny + 6); c.lineTo(nx + 6, ny - 6); c.stroke();
    }
    c.fillStyle = sel ? '#0f172a' : '#ffffff';
    c.font = 'bold 9px monospace'; c.textAlign = 'center'; c.textBaseline = 'middle';
    c.fillText(String(bi + 1), nx, ny);
  });
}

const InteractiveEqPro = ({ track, params, onChange, getModule, applyTo, spectrumModules }) => {
  const canvasRef = React.useRef(null);
  const st = React.useRef(null);
  if (!st.current) {
    st.current = {
      bands: JSON.parse(JSON.stringify((params && Array.isArray(params.bands)) ? params.bands : EQPRO_DEFAULT_BANDS)),
      amount: (params && params.amount !== undefined) ? params.amount : 100,
      selected: null, dragId: null, mode: null,
      lastSig: null,
      hudPos: null, dragHud: false, hudOffsetX: 0, hudOffsetY: 0
    };
    st.current.lastSig = JSON.stringify(st.current.bands) + '|' + st.current.amount;
  }
  const [tick, setTick] = React.useState(0);
  // External param sync (undo/load/rebuild) — skip while dragging to avoid
  // resetting the node mid-gesture (the flicker/“can't move” bug while playing).
  React.useEffect(() => {
    if (st.current.dragId !== null) return;
    if (params && Array.isArray(params.bands)) {
      const sig = JSON.stringify(params.bands) + '|' + (params.amount !== undefined ? params.amount : 100);
      if (sig !== st.current.lastSig) {
        st.current.lastSig = sig;
        st.current.bands = JSON.parse(JSON.stringify(params.bands));
        st.current.amount = params.amount !== undefined ? params.amount : 100;
      }
    }
  }, [params]);
  // Canvas render loop (rAF)
  React.useEffect(() => {
    let raf;
    const draw = () => {
      raf = requestAnimationFrame(draw);
      const cv = canvasRef.current;
      if (!cv || !cv.clientWidth) return;
      const w = cv.clientWidth, h = cv.clientHeight;
      if (cv.width !== w * 2) { cv.width = w * 2; cv.height = h * 2; }
      const c = cv.getContext('2d');
      c.setTransform(2, 0, 0, 2, 0, 0);
      // Spectrum sources: every live module analyser (audioclip path + soundfont
      // path) is overlaid, so playing audio clips OR midi both show up.
      const specMods = (spectrumModules ? spectrumModules() : null) || (getModule ? [getModule()] : []);
      renderEqProFrame(c, w, h, st.current, specMods, (typeof getAudioContext === 'function' && getAudioContext()) ? getAudioContext().sampleRate : 44100);
    };
    draw();
    return () => cancelAnimationFrame(raf);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);
  const toLocal = (e) => { const r = canvasRef.current.getBoundingClientRect(); return { x: e.clientX - r.left, y: e.clientY - r.top }; };
  const commit = () => { if (onChange) onChange({ bands: JSON.parse(JSON.stringify(st.current.bands)), amount: st.current.amount }); };
  // Apply a mutation to the live module instance(s). Default: BOTH track module
  // instances (audio fxMods + sf sfMods) so the soundfont follows too; mastering
  // passes its own applyTo (masterBus.eqProInstances[modId]).
  const applyAll = applyTo || ((fn) => {
    if (typeof window !== 'undefined' && window.__getTrackFxModule) { const a = window.__getTrackFxModule(track.id, 'eqpro'); if (a) fn(a); }
    if (typeof window !== 'undefined' && window.__getTrackSfFxModule) { const b = window.__getTrackSfFxModule(track.id, 'eqpro'); if (b) fn(b); }
  });
  const onDown = (e) => {
    const cv = canvasRef.current, w = cv.clientWidth, h = cv.clientHeight;
    const { x, y } = toLocal(e);
    const s = st.current;
    let hit = false;
    for (let i = 0; i < s.bands.length; i++) {
      const b = s.bands[i];
      if (Math.hypot(x - eqproFreqToX(b.freq, w), y - eqproGainToY(eqproNodeDb(b), h)) <= 10) {
        s.selected = i; s.dragId = i; s.mode = 'center'; hit = true; break;
      }
    }
    if (!hit && s.selected !== null) {
      const b = s.bands[s.selected];
      const nx = eqproFreqToX(b.freq, w), ny = eqproGainToY(eqproNodeDb(b), h), wo = eqproQToWing(b.q);
      if (Math.hypot(x - (nx - wo), y - ny) <= 8 || Math.hypot(x - (nx + wo), y - ny) <= 8) {
        s.dragId = s.selected; s.mode = 'wing'; hit = true;
      }
    }
    if (!hit) s.selected = null;
    try { cv.setPointerCapture(e.pointerId); } catch (err) { }
    setTick(t => t + 1);
  };
  const onMove = (e) => {
    const s = st.current;
    if (s.dragId === null) return;
    const cv = canvasRef.current, w = cv.clientWidth, h = cv.clientHeight;
    const { x, y } = toLocal(e);
    const mx = Math.max(0, Math.min(w, x)), my = Math.max(0, Math.min(h, y));
    const b = s.bands[s.dragId];
    if (s.mode === 'center') {
      const f = eqproClamp(parseFloat(eqproXToFreq(mx, w).toFixed(1)), EQPRO_F_MIN, EQPRO_F_MAX);
      // Filters without a Gain control (highpass/lowpass/notch/bandpass) stay on
      // the 0 dB axis — only frequency is draggable for them.
      const g = eqproBandHasGain(b.type) ? eqproClamp(parseFloat(eqproYToGain(my, h).toFixed(1)), -EQPRO_MAX_DB, EQPRO_MAX_DB) : 0;
      b.freq = f; b.gain = g;
      applyAll(mm => mm.setBand(s.dragId, { freq: f, gain: g }));
    } else if (s.mode === 'wing') {
      const q = eqproWingToQ(Math.abs(mx - eqproFreqToX(b.freq, w)));
      b.q = q;
      applyAll(mm => mm.setBand(s.dragId, { q }));
    }
    setTick(t => t + 1);
  };
  const onUp = (e) => {
    const s = st.current;
    if (s.dragId !== null) commit();
    s.dragId = null; s.mode = null;
    try { canvasRef.current.releasePointerCapture(e.pointerId); } catch (err) { }
  };
  const onWheel = (e) => {
    e.preventDefault();
    const s = st.current;
    if (s.selected === null) return;
    const b = s.bands[s.selected];
    b.q = eqproClamp(parseFloat((b.q + (e.deltaY < 0 ? 0.2 : -0.2)).toFixed(1)), 0.1, 18);
    applyAll(mm => mm.setBand(s.selected, { q: b.q }));
    setTick(t => t + 1);
  };
  const onDblClick = (e) => {
    const cv = canvasRef.current, w = cv.clientWidth, h = cv.clientHeight;
    const { x, y } = toLocal(e);
    const s = st.current;
    const hitIdx = s.bands.findIndex(b => Math.hypot(x - eqproFreqToX(b.freq, w), y - eqproGainToY(b.gain, h)) <= 10);
    if (hitIdx !== -1) {
      s.bands.splice(hitIdx, 1);
      if (s.selected === hitIdx) s.selected = null; else if (s.selected !== null && s.selected > hitIdx) s.selected--;
    } else {
      if (s.bands.length >= EQPRO_MAX_BANDS) return;
      s.bands.push({ type: 'peaking', freq: eqproClamp(parseFloat(eqproXToFreq(x, w).toFixed(1)), EQPRO_F_MIN, EQPRO_F_MAX), gain: eqproClamp(parseFloat(eqproYToGain(y, h).toFixed(1)), -EQPRO_MAX_DB, EQPRO_MAX_DB), q: 1.2, active: true });
      s.selected = s.bands.length - 1;
    }
    applyAll(mm => mm.syncBands(s.bands));
    commit();
    setTick(t => t + 1);
  };
  const sel = (st.current.selected !== null) ? st.current.bands[st.current.selected] : null;
  const selIdx = st.current.selected;
  const cvW = canvasRef.current ? canvasRef.current.clientWidth : 460;
  const cvH = 240;
  const hudColor = selIdx !== null ? EQPRO_BAND_COLORS[selIdx % EQPRO_BAND_COLORS.length] : EQPRO_BAND_COLORS[0];
  const hudXY = (() => {
    if (st.current.hudPos) {
      // Clamp so the HUD always stays inside the canvas area (buttons clickable).
      return {
        hx: Math.max(0, Math.min((canvasRef.current ? canvasRef.current.clientWidth : 460) - 268, st.current.hudPos.hx)),
        hy: Math.max(0, Math.min((canvasRef.current ? canvasRef.current.clientHeight : 240) - 225, st.current.hudPos.hy))
      };
    }
    if (!sel) return null;
    const nx = eqproFreqToX(sel.freq, cvW), ny = eqproGainToY(sel.gain, cvH);
    let hx = nx - 130, hy = ny - 195;
    if (hx < 8) hx = 8;
    if (hx > cvW - 268) hx = cvW - 268;
    if (hy < 8) hy = ny + 24;
    return { hx, hy };
  })();
  return (
    <div className="space-y-2">
      <div className="flex items-center justify-between text-[11px] font-mono">
        <div className="flex items-center gap-3">
          <span className="text-slate-400">Bands: <span className="text-teal-300 font-bold">{st.current.bands.length}</span>/8</span>
          <label className="flex items-center gap-1.5">
            <span className="text-slate-400">Amount:</span>
            <input type="range" min={0} max={200} value={st.current.amount}
              onChange={e => {
                st.current.amount = parseInt(e.target.value);
                applyAll(mm => mm.setAmount(st.current.amount));
                commit(); setTick(t => t + 1);
              }}
              className="w-20 h-1 cursor-pointer" style={{ accentColor: '#2dd4bf' }} />
            <span className="text-teal-300 font-bold w-10">{st.current.amount}%</span>
          </label>
        </div>
        <div className="flex items-center gap-1">
          <button
            onClick={() => {
              const s = st.current;
              s.bands = JSON.parse(JSON.stringify(EQPRO_DEFAULT_BANDS));
              s.selected = null;
              applyAll(mm => mm.syncBands(s.bands));
              commit(); setTick(t => t + 1);
            }}
            className="px-2 py-1 bg-slate-800 hover:bg-red-900/60 text-slate-300 border border-slate-700 rounded text-[10px]">Reset</button>
          <button
            onClick={() => {
              const s = st.current;
              if (s.bands.length >= EQPRO_MAX_BANDS) return;
              s.bands.push({ type: 'peaking', freq: 1000, gain: 0, q: 1.0, active: true });
              s.selected = s.bands.length - 1;
              applyAll(mm => mm.syncBands(s.bands));
              commit(); setTick(t => t + 1);
            }}
            className="px-2 py-1 bg-teal-900/50 hover:bg-teal-800 text-teal-300 border border-teal-700 rounded text-[10px]">+ Band</button>
        </div>
      </div>
      <div className="relative">
        <canvas ref={canvasRef}
          className="w-full h-60 block cursor-crosshair rounded-lg border border-slate-800 bg-slate-950"
          onPointerDown={onDown} onPointerMove={onMove} onPointerUp={onUp}
          onWheel={onWheel} onDoubleClick={onDblClick} />
        {sel && hudXY && (
          <div className="absolute z-10 w-64 rounded-xl p-3 text-[11px] font-mono space-y-2 pointer-events-auto cursor-move"
            style={{ left: hudXY.hx, top: hudXY.hy, background: 'rgba(15,23,42,0.94)', border: '1px solid rgba(45,212,191,0.3)', boxShadow: '0 10px 30px rgba(0,0,0,0.8)', backdropFilter: 'blur(12px)' }}
            onPointerDown={e => {
              const s = st.current;
              s.dragHud = true;
              const r = e.currentTarget.getBoundingClientRect();
              s.hudOffsetX = e.clientX - r.left;
              s.hudOffsetY = e.clientY - r.top;
              e.currentTarget.setPointerCapture(e.pointerId);
            }}
            onPointerMove={e => {
              const s = st.current;
              if (!s.dragHud) return;
              const cv = canvasRef.current;
              const w = cv ? cv.clientWidth : 460;
              s.hudPos = { hx: Math.max(0, Math.min(w - 268, e.clientX - s.hudOffsetX - (cv ? cv.getBoundingClientRect().left : 0))), hy: Math.max(0, e.clientY - s.hudOffsetY - (cv ? cv.getBoundingClientRect().top : 0)) };
              setTick(t => t + 1);
            }}
            onPointerUp={e => {
              st.current.dragHud = false;
              try { e.currentTarget.releasePointerCapture(e.pointerId); } catch (err) { }
            }}>
            <div className="flex items-center justify-between border-b border-slate-700/80 pb-1.5">
              <div className="flex items-center gap-2">
                <span className="w-4 h-4 rounded-full text-slate-950 font-bold flex items-center justify-center text-[10px]" style={{ background: hudColor.badge }}>{selIdx + 1}</span>
                <span className="font-bold text-white uppercase tracking-wider">Band {selIdx + 1}</span>
              </div>
              <div className="flex items-center gap-1">
                <button title="Bypass band"
                  onClick={() => {
                    const s = st.current;
                    s.bands[s.selected].active = !s.bands[s.selected].active;
                    applyAll(mm => mm.setBand(s.selected, { active: s.bands[s.selected].active }));
                    commit(); setTick(t => t + 1);
                  }}
                  className={`w-5 h-5 rounded flex items-center justify-center text-[10px] ${sel.active ? 'bg-slate-800 text-slate-400' : 'bg-amber-600 text-white'}`}><i className="fa-solid fa-power-off"></i></button>
                <button title="Delete band"
                  onClick={() => {
                    const s = st.current;
                    s.bands.splice(s.selected, 1);
                    s.selected = null;
                    applyAll(mm => mm.syncBands(s.bands));
                    commit(); setTick(t => t + 1);
                  }}
                  className="w-5 h-5 rounded bg-slate-800 hover:bg-red-600 text-slate-400 hover:text-white flex items-center justify-center text-[10px]"><i className="fa-solid fa-xmark"></i></button>
              </div>
            </div>
            <div>
              <label className="block text-[9px] text-slate-400 uppercase tracking-widest mb-1">Filter Shape</label>
              <select value={sel.type}
                onChange={ev => {
                  const s = st.current;
                  s.bands[s.selected].type = ev.target.value;
                  applyAll(mm => mm.setBand(s.selected, { type: ev.target.value }));
                  commit(); setTick(t => t + 1);
                }}
                className="w-full bg-slate-950 border border-slate-700 text-teal-300 rounded px-2 py-1 text-[11px] outline-none">
                {['peaking', 'highpass', 'lowpass', 'lowshelf', 'highshelf', 'notch', 'bandpass'].map(t => (
                  <option key={t} value={t}>{t === 'peaking' ? 'Bell / Peaking' : t === 'highpass' ? 'Low Cut / High Pass' : t === 'lowpass' ? 'High Cut / Low Pass' : t === 'lowshelf' ? 'Low Shelf' : t === 'highshelf' ? 'High Shelf' : t === 'notch' ? 'Notch / Band Stop' : 'Band Pass'}</option>
                ))}
              </select>
            </div>
            <div className="grid grid-cols-3 gap-2 text-center bg-slate-950/80 p-2 rounded-lg border border-slate-800">
              <div><div className="text-[9px] text-slate-500">FREQ</div><div className="text-teal-300 font-bold text-[11px]">{sel.freq >= 1000 ? (sel.freq / 1000).toFixed(2) + ' kHz' : Math.round(sel.freq) + ' Hz'}</div></div>
              <div><div className="text-[9px] text-slate-500">GAIN</div><div className="text-amber-400 font-bold text-[11px]">{eqproBandHasGain(sel.type) ? (sel.gain > 0 ? '+' : '') + sel.gain.toFixed(1) + ' dB' : '0.0 dB'}</div></div>
              <div><div className="text-[9px] text-slate-500">Q</div><div className="text-purple-400 font-bold text-[11px]">{sel.q.toFixed(1)}</div></div>
            </div>
            <div className="text-[9px] text-slate-500 text-center">Drag center: Freq & Gain · Wings/Wheel: Q · Dbl-click: add/delete</div>
          </div>
        )}
      </div>
    </div>
  );
};

const ExportModal = ({ open, onClose, exportSettings, setExportSettings, isExporting, onExport, onBounce }) => {
  if (!open) return null;
  return (
    <div className="fixed inset-0 z-[300] bg-black/70 backdrop-blur-sm flex items-center justify-center p-6" onClick={onClose}>
      <div className="w-full max-w-2xl bg-slate-900 border border-slate-700 rounded-2xl shadow-2xl overflow-hidden" onClick={e => e.stopPropagation()}>
        <div className="h-9 bg-slate-800/80 border-b border-slate-700 flex items-center justify-between px-3 select-none">
          <span className="text-xs font-bold text-cyan-300 font-mono flex items-center gap-1.5">
            <i data-lucide="download-cloud" className="w-3.5 h-3.5" /> EXPORT
          </span>
          <button onClick={onClose} className="text-zinc-400 hover:text-white"><i data-lucide="x" className="w-4 h-4" /></button>
        </div>
        <div className="p-4 max-h-[72vh] overflow-y-auto space-y-3">
          <div className="grid grid-cols-2 gap-2">
            <div>
              <label className="block text-[7px] text-zinc-500 font-bold uppercase mb-0.5">Nguồn</label>
              <select value={exportSettings.source} onChange={e => setExportSettings(p => ({ ...p, source: e.target.value }))}
                className="w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none">
                <option value="project">Project (Mix)</option>
                <option value="track_mix">Track Selection</option>
                <option value="active_clip">Active Clip</option>
                <option value="clip_selection">Clip Selection</option>
              </select>
            </div>
            <div>
              <label className="block text-[7px] text-zinc-500 font-bold uppercase mb-0.5">Định dạng</label>
              <select value={exportSettings.format}
                onChange={e => setExportSettings(p => ({ ...p, format: e.target.value, sampleRate: '44100', bitDepth: '16', quality: '44khz' }))}
                className="w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none">
                <option value="wav">WAV</option>
                <option value="mp3">MP3</option>
                <option value="ogg">OGG</option>
              </select>
            </div>
          </div>
          {exportSettings.format === 'wav' ? (
            <div className="grid grid-cols-2 gap-2">
              <div>
                <label className="block text-[7px] text-zinc-500 font-bold uppercase mb-0.5">SR (Hz)</label>
                <select value={exportSettings.sampleRate} onChange={e => setExportSettings(p => ({ ...p, sampleRate: e.target.value }))}
                  className="w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none">
                  <option value="22500">22500</option>
                  <option value="44100">44100</option>
                </select>
              </div>
              <div>
                <label className="block text-[7px] text-zinc-500 font-bold uppercase mb-0.5">Bit</label>
                <select value={exportSettings.bitDepth} onChange={e => setExportSettings(p => ({ ...p, bitDepth: e.target.value }))}
                  className="w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none">
                  <option value="8">8</option>
                  <option value="16">16</option>
                  <option value="24">24</option>
                </select>
              </div>
            </div>
          ) : (
            <div className="grid grid-cols-2 gap-2">
              <div>
                <label className="block text-[7px] text-zinc-500 font-bold uppercase mb-0.5">Chất lượng</label>
                <select value={exportSettings.quality} onChange={e => setExportSettings(p => ({ ...p, quality: e.target.value }))}
                  className="w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none">
                  <option value="44khz">44kHz</option>
                  <option value="lossless">Lossless</option>
                </select>
              </div>
              <div></div>
            </div>
          )}
          <div className="grid grid-cols-2 gap-2">
            <div>
              <label className="block text-[7px] text-zinc-500 font-bold uppercase mb-0.5">Kênh</label>
              <select value={exportSettings.channels} onChange={e => setExportSettings(p => ({ ...p, channels: e.target.value }))}
                className="w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none">
                <option value="mono">Mono</option>
                <option value="stereo">Stereo</option>
              </select>
            </div>
            <div></div>
          </div>
          <div className="grid grid-cols-2 gap-2 pt-1">
            <button onClick={onBounce} disabled={isExporting}
              title="Bounce realtime — file WAV đầy đủ MIDI + FX Rack + Mastering Chain (chạy lại project thật)"
              className="w-full py-1 bg-fuchsia-800 hover:bg-fuchsia-700 text-zinc-100 font-bold rounded text-xs flex items-center justify-center gap-1">
              <i data-lucide="radio" className="w-3 h-3" /> {isExporting ? '...' : 'Bounce MIDI'}
            </button>
            <button onClick={onExport} disabled={isExporting}
              className="w-full py-1 bg-cyan-700 hover:bg-cyan-600 text-zinc-100 font-bold rounded text-xs flex items-center justify-center gap-1">
              <i data-lucide="download-cloud" className="w-3 h-3" /> {isExporting ? '...' : 'Export'}
            </button>
          </div>
        </div>
      </div>
    </div>
  );
};

const FXRackModal = ({ track, onUpdateTrack, onClose }) => {
  const [activeType, setActiveType] = React.useState('eq');
  const [addModuleOpen, setAddModuleOpen] = React.useState(false);
  const dragChainIndexRef = React.useRef(null);
  // Wave Observer scope state (unified_fx_rack_panel_update.md §III.3)
  const [scopeChannel, setScopeChannel] = React.useState('stereo');
  const [scopeMode, setScopeMode] = React.useState('waveform');
  const [scopeDuration, setScopeDuration] = React.useState(2.0);
  const [scopeZoom, setScopeZoom] = React.useState(0);
  const [scopePaused, setScopePaused] = React.useState(false);
  const scopeCanvasRef = React.useRef(null);
  const scopeMeterLRef = React.useRef(null);
  const scopeMeterRRef = React.useRef(null);
  const eqCurveRef = React.useRef(null);
  const scopeStateRef = React.useRef({ L: null, R: null, head: 0, len: 0, tmpL: null, tmpR: null, freq: null });
  // Carla Bridge (VST FX) — danh sách VST để load vào Carla
  const [fxCarlaVsts, setFxCarlaVsts] = React.useState(null); // null = chưa load
  React.useEffect(() => {
    if (track && window.SonicAPI && window.SonicAPI.listPlugins) {
      window.SonicAPI.listPlugins().then(d => setFxCarlaVsts((d && d.vst_instruments) || [])).catch(() => setFxCarlaVsts([]));
    }
  }, [track && track.id]);

  if (!track) return null;

  const chain = track.fxChain || [];
  const setChain = (next) => {
    if (onUpdateTrack) onUpdateTrack(track.id, { fxChain: next });
    if (window.__rebuildTrackFxGraph) window.__rebuildTrackFxGraph(track.id);
  };
  const paramsOf = (m) => ({ ...(TRACK_FX_DEFAULTS[m.type] || {}), ...(m.params || {}) });
  const setParams = (idx, patch) => {
    const next = chain.map((m, i) => i === idx ? { ...m, params: { ...paramsOf(m), ...patch } } : m);
    setChain(next);
  };
  const toggleMod = (idx) => {
    const next = chain.map((m, i) => i === idx ? { ...m, active: !(m.active !== false) } : m);
    setChain(next);
  };
  const removeMod = (idx) => setChain(chain.filter((_, i) => i !== idx));
  const addMod = (type) => {
    const def = TRACK_FX_DEFAULTS[type] || {};
    // Deep-clone default params so each chain entry owns its data (bands array
    // especially — shared references would corrupt other modules' state).
    const params = Array.isArray(def.bands) ? { ...def, bands: JSON.parse(JSON.stringify(def.bands)) } : { ...def };
    setChain([...chain, { type, active: true, params }]);
    setActiveType(type);
    setAddModuleOpen(false);
  };
  const applyPreset = (key) => {
    const preset = TRACK_EQ_PRESETS[key];
    if (!preset) return;
    const idx = chain.findIndex(m => m.type === 'eq' && m.active !== false);
    if (idx >= 0) setParams(idx, { g1: preset.g[0], g2: preset.g[1], g3: preset.g[2], g4: preset.g[3] });
  };

  const activeMod = chain.find(m => m.type === activeType) || chain[chain.length - 1] || null;
  const activeIdx = chain.findIndex(m => m === activeMod);
  const ap = activeMod ? paramsOf(activeMod) : {};

  const slider = (label, val, min, max, step, color, onChange, fmt) => (
    <div className="flex flex-col gap-1">
      <div className="flex justify-between text-[11px] font-mono">
        <span style={{ color, fontWeight: 700 }}>{label}</span>
        <span className="text-slate-300">{fmt ? fmt(val) : val}</span>
      </div>
      <input type="range" min={min} max={max} step={step || 0.1} value={val}
        onChange={e => onChange(parseFloat(e.target.value))}
        className="w-full h-1 cursor-pointer" style={{ accentColor: color }} />
    </div>
  );

  // ── Wave Observer real-time scope render loop ──
  React.useEffect(() => {
    const canvas = scopeCanvasRef.current;
    if (!canvas || !track) return;
    const st = scopeStateRef.current;
    let raf = null;
    const draw = () => {
      raf = requestAnimationFrame(draw);
      const w = canvas.width, h = canvas.height;
      const ctx = canvas.getContext('2d');
      ctx.clearRect(0, 0, w, h);
      // Grid
      ctx.strokeStyle = 'rgba(51, 65, 85, 0.35)';
      ctx.lineWidth = 1;
      ctx.font = '9px monospace';
      ctx.fillStyle = '#475569';
      for (let i = 1; i < 6; i++) {
        const y = (i / 6) * h;
        ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(w, y); ctx.stroke();
      }
      ctx.fillText('+0.0 dB', 4, 12);
      ctx.fillText('-6.0 dB', 4, h / 2 - 4);
      ctx.fillText('-12 dB', 4, h - 6);
      const ana = window.__getTrackScopeAnalysers ? window.__getTrackScopeAnalysers(track.id) : null;
      if (!ana || scopePaused) {
        if (!ana) { ctx.fillStyle = '#334155'; ctx.font = '11px monospace'; ctx.fillText('Không có tín hiệu — bấm Play để xem waveform', w / 2 - 120, h / 2); }
        return;
      }
      const sr = ana.sr || 44100;
      const maxS = Math.floor(5 * sr);
      if (!st.L || st.L.length !== maxS) { st.L = new Float32Array(maxS); st.R = new Float32Array(maxS); st.head = 0; st.len = 0; }
      if (!st.tmpL || st.tmpL.length !== ana.L.fftSize) { st.tmpL = new Float32Array(ana.L.fftSize); st.tmpR = new Float32Array(ana.R.fftSize); }
      ana.L.getFloatTimeDomainData(st.tmpL);
      ana.R.getFloatTimeDomainData(st.tmpR);
      const nRead = st.tmpL.length;
      for (let i = 0; i < nRead; i++) { st.L[st.head] = st.tmpL[i]; st.R[st.head] = st.tmpR[i]; st.head = (st.head + 1) % maxS; }
      st.len = Math.min(st.len + nRead, maxS);
      const n = Math.min(st.len, Math.floor(scopeDuration * sr));
      if (n < 2) return;
      const amp = Math.pow(10, scopeZoom / 20);
      const getS = (i) => { // i in [0,n)
        const idx = (st.head - n + i + maxS) % maxS;
        const l = st.L[idx], r = st.R[idx];
        if (scopeChannel === 'left') return [l, null];
        if (scopeChannel === 'right') return [r, null];
        if (scopeChannel === 'mid') return [(l + r) / 2, null];
        if (scopeChannel === 'side') return [(l - r) / 2, null];
        return [l, r];
      };
      // Meters
      let pL = 0, pR = 0;
      for (let i = 0; i < n; i += 8) {
        const idx = (st.head - n + i + maxS) % maxS;
        const al = Math.abs(st.L[idx]), ar = Math.abs(st.R[idx]);
        if (al > pL) pL = al; if (ar > pR) pR = ar;
      }
      if (scopeMeterLRef.current) scopeMeterLRef.current.style.width = Math.min(100, pL * 150) + '%';
      if (scopeMeterRRef.current) scopeMeterRRef.current.style.width = Math.min(100, pR * 150) + '%';
      if (scopeMode === 'lissajous') {
        ctx.fillStyle = 'rgba(34, 211, 238, 0.55)';
        const stepN = Math.max(1, Math.floor(n / 700));
        for (let i = 0; i < n; i += stepN) {
          const idx = (st.head - n + i + maxS) % maxS;
          const x = w / 2 + st.L[idx] * amp * (w / 2);
          const y = h / 2 - st.R[idx] * amp * (h / 2);
          ctx.fillRect(x, y, 2, 2);
        }
        ctx.strokeStyle = '#0e7490';
        ctx.beginPath(); ctx.moveTo(w / 2, 0); ctx.lineTo(w / 2, h); ctx.stroke();
        ctx.beginPath(); ctx.moveTo(0, h / 2); ctx.lineTo(w, h / 2); ctx.stroke();
      } else if (scopeMode === 'spectrum') {
        if (!st.freq || st.freq.length !== ana.L.frequencyBinCount) st.freq = new Uint8Array(ana.L.frequencyBinCount);
        ana.L.getByteFrequencyData(st.freq);
        const bins = st.freq.length;
        const bars = 96;
        const fMax = sr / 2;
        ctx.fillStyle = 'rgba(34, 211, 238, 0.85)';
        for (let b = 0; b < bars; b++) {
          const f0 = 20 * Math.pow(fMax / 20, b / bars);
          const f1 = 20 * Math.pow(fMax / 20, (b + 1) / bars);
          const bi0 = Math.max(0, Math.floor(f0 / fMax * bins));
          const bi1 = Math.min(bins - 1, Math.ceil(f1 / fMax * bins));
          let peak = 0;
          for (let k = bi0; k <= bi1; k++) { if (st.freq[k] > peak) peak = st.freq[k]; }
          const bh = (peak / 255) * (h - 8);
          const bx = (b / bars) * w;
          ctx.fillRect(bx, h - bh, Math.max(1, w / bars - 1), bh);
        }
        ctx.fillStyle = '#475569';
        ctx.fillText('20Hz', 4, h - 4);
        ctx.fillText('20kHz', w - 40, h - 4);
      } else {
        // waveform (stereo draws L cyan + R amber; single channel draws cyan)
        const stepX = w / n;
        ctx.lineWidth = 1.5;
        for (const [col, ch] of [['#22d3ee', 0], ['#fbbf24', 1]]) {
          if (ch === 1 && scopeChannel !== 'stereo') continue;
          ctx.strokeStyle = col;
          ctx.beginPath();
          for (let i = 0; i < n; i++) {
            const [l, r] = getS(i);
            const v = ch === 0 ? l : r;
            if (v === null) continue;
            const x = i * stepX;
            const y = h / 2 - v * amp * (h / 2) * 0.9;
            if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
          }
          ctx.stroke();
        }
      }
    };
    draw();
    return () => { if (raf) cancelAnimationFrame(raf); };
  }, [track && track.id, scopeChannel, scopeMode, scopeDuration, scopeZoom, scopePaused]);

  // ── Interactive Module Vector Display: EQ response curve ──
  React.useEffect(() => {
    const cv = eqCurveRef.current;
    if (!cv) return;
    const w = cv.width = (cv.clientWidth || 300) * 2;
    const h = cv.height = 88 * 2;
    const ctx = cv.getContext('2d');
    ctx.clearRect(0, 0, w, h);
    // background grid
    ctx.strokeStyle = 'rgba(51, 65, 85, 0.35)';
    for (let i = 1; i < 5; i++) { const y = (i / 5) * h; ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(w, y); ctx.stroke(); }
    const midY = h / 2;
    ctx.strokeStyle = 'rgba(148, 163, 184, 0.25)';
    ctx.beginPath(); ctx.moveTo(0, midY); ctx.lineTo(w, midY); ctx.stroke();
    // response approximation for 4 cascaded bands (log-domain)
    const bands = [
      { type: 'lowshelf', f0: 100, gain: ap.g1 || 0, q: 0.7 },
      { type: 'peaking', f0: 800, gain: ap.g2 || 0, q: 0.7 },
      { type: 'peaking', f0: 3200, gain: ap.g3 || 0, q: 1.2 },
      { type: 'highshelf', f0: 10000, gain: ap.g4 || 0, q: 0.7 }
    ];
    const pts = [];
    const N = 120;
    for (let i = 0; i <= N; i++) {
      const f = 20 * Math.pow(20000 / 20, i / N);
      let db = 0;
      bands.forEach(b => {
        const lf = Math.log(f / b.f0);
        if (b.type === 'peaking') {
          db += b.gain / (1 + Math.pow(lf * b.q, 2));
        } else if (b.type === 'highshelf') {
          db += b.gain / 2 * (1 + (2 / Math.PI) * Math.atan(lf / (1 / b.q)));
        } else {
          db += b.gain / 2 * (1 - (2 / Math.PI) * Math.atan(lf / (1 / b.q)));
        }
      });
      const x = (i / N) * w;
      const y = midY - (db / 12) * (h / 2);
      pts.push([x, y]);
    }
    // fill
    ctx.beginPath();
    pts.forEach(([x, y], i) => i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y));
    ctx.lineTo(w, h); ctx.lineTo(0, h); ctx.closePath();
    ctx.fillStyle = 'rgba(34, 211, 238, 0.10)';
    ctx.fill();
    // curve
    ctx.beginPath();
    pts.forEach(([x, y], i) => i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y));
    ctx.strokeStyle = '#22d3ee';
    ctx.lineWidth = 2;
    ctx.stroke();
    // dB labels
    ctx.fillStyle = '#475569';
    ctx.font = '9px monospace';
    ctx.fillText('+12 dB', 4, midY - h / 2 + 10);
    ctx.fillText('0 dB', 4, midY + 3);
    ctx.fillText('-12 dB', 4, midY + h / 2 - 4);
    ctx.fillText('20Hz', 4, h - 2);
    ctx.fillText('20kHz', w - 38, h - 2);
  }, [ap.g1, ap.g2, ap.g3, ap.g4, activeMod]);

  return (
    <div className="fixed inset-0 z-[115] bg-black/80 backdrop-blur-sm flex items-center justify-center p-4" onClick={onClose}>
      <div className="w-full max-w-4xl bg-slate-900 border border-slate-700 rounded-2xl p-5 space-y-4 shadow-2xl flex flex-col max-h-[90vh] text-slate-200" onClick={e => e.stopPropagation()}>
        {/* HEADER */}
        <div className="flex items-center justify-between border-b border-slate-800 pb-3">
          <div className="flex items-center gap-2">
            <div className="w-3 h-3 rounded-full bg-cyan-400 animate-pulse"></div>
            <h3 className="text-xs font-bold text-white uppercase tracking-wider font-mono">
              {track.name} — FX RACK PANEL
            </h3>
          </div>
          <button onClick={onClose} className="text-slate-400 hover:text-white text-base"><i data-lucide="x" className="w-4 h-4"></i></button>
        </div>

        {/* CHAIN RACK STRIP */}
        <div className="h-16 bg-slate-950 border border-slate-800 rounded-xl px-3 flex items-center gap-2 overflow-x-auto shrink-0 select-none">
          <span className="text-[10px] font-bold text-slate-500 uppercase tracking-widest mr-1 font-mono shrink-0">CHAIN:</span>
          {chain.length === 0 && <span className="text-[10px] text-slate-500 font-mono">Chưa có FX — bấm [+] để thêm (signal đi thẳng: Source → Fader)</span>}
          {chain.map((m, idx) => {
            const meta = TRACK_FX_META[m.type] || { name: m.type, icon: 'circle', color: '#94a3b8', sub: '' };
            const on = m.active !== false;
            return (
              <div key={idx}
                draggable
                onDragStart={e => { dragChainIndexRef.current = idx; e.dataTransfer.effectAllowed = 'move'; }}
                onDragOver={e => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; }}
                onDrop={e => { e.preventDefault(); const from = dragChainIndexRef.current; if (from !== null && from !== idx) { const next = [...chain]; const mv = next.splice(from, 1)[0]; next.splice(idx, 0, mv); setChain(next); } dragChainIndexRef.current = null; }}
                onClick={() => setActiveType(m.type)}
                className={`w-36 h-12 rounded-lg p-2 flex items-center justify-between cursor-pointer transition-all shrink-0 ${activeType === m.type ? 'border-2 border-cyan-400 bg-slate-800' : 'bg-slate-900 border border-slate-800 hover:border-slate-600'}`}>
                <div className="flex items-center gap-2 min-w-0">
                  <button onClick={e => { e.stopPropagation(); toggleMod(idx); }} className="w-4 h-4 rounded-full flex items-center justify-center text-[9px] font-bold shrink-0" style={{ backgroundColor: on ? '#38bdf8' : '#334155', color: on ? '#0f172a' : '#94a3b8' }}>
                    <i data-lucide="power" className="w-2.5 h-2.5"></i>
                  </button>
                  <div className="min-w-0">
                    <div className="text-[11px] font-bold text-slate-200 truncate">{meta.name}</div>
                    <div className="text-[9px] font-mono truncate" style={{ color: meta.color }}>{idx + 1}. {meta.sub}</div>
                  </div>
                </div>
                <button onClick={e => { e.stopPropagation(); removeMod(idx); }} className="text-slate-600 hover:text-red-400 text-xs px-0.5 shrink-0" title="Xóa module">
                  <i data-lucide="x" className="w-3 h-3"></i>
                </button>
              </div>
            );
          })}
          <button onClick={() => setAddModuleOpen(true)} className="w-16 h-12 rounded-lg border border-dashed border-slate-700 hover:border-cyan-500 flex items-center justify-center text-slate-500 hover:text-cyan-400 cursor-pointer transition-all bg-slate-900/40 shrink-0" title="Thêm module">
            <i data-lucide="plus" className="w-4 h-4"></i>
          </button>
        </div>

        {/* WORKSPACE CONTROLS */}
        <div className="flex-1 bg-slate-950 border border-slate-800 rounded-xl p-4 flex flex-col gap-4 min-h-[240px] overflow-y-auto oz-scrollbar">
          <div className="h-8 bg-slate-900/80 border-b border-slate-800/80 px-3 flex items-center justify-between text-xs font-mono rounded-t-lg shrink-0">
            <span className="text-slate-400">Active Module: <strong className="text-cyan-400">{activeMod ? (TRACK_FX_META[activeMod.type]?.name || activeMod.type) : '—'}</strong></span>
            {activeMod && activeMod.type === 'eq' && (
              <div className="flex items-center gap-2">
                <span className="text-slate-400">EQ Preset:</span>
                <select value="flat" onChange={e => applyPreset(e.target.value)} className="bg-slate-950 border border-slate-700 text-cyan-300 rounded px-2 py-0.5 text-xs outline-none focus:border-cyan-500">
                  {Object.keys(TRACK_EQ_PRESETS).map(k => <option key={k} value={k}>{TRACK_EQ_PRESETS[k].name}</option>)}
                </select>
              </div>
            )}
          </div>

          {!activeMod && <div className="text-xs text-slate-500 font-mono text-center py-10">Chưa có module. Bấm [+] để thêm EQ / Compressor / Limiter / Exciter / Rebalance.</div>}

          {activeMod && activeMod.type === 'eq' && activeIdx >= 0 && (
            <div className="space-y-3">
              <div className="grid grid-cols-4 gap-4">
                {[['g1', 'BAND 1 (LOW)', '100 Hz', '#22d3ee'], ['g2', 'BAND 2 (MID LOW)', '800 Hz', '#fbbf24'], ['g3', 'BAND 3 (MID HIGH)', '3.2 kHz', '#a855f7'], ['g4', 'BAND 4 (HIGH)', '10 kHz', '#34d399']].map(([key, label, freq, color]) => (
                  <div key={key} className="bg-slate-900/80 border border-slate-800 p-3 rounded-lg space-y-2">
                    {slider(label, ap[key] !== undefined ? ap[key] : 0, -12, 12, 0.1, color, v => setParams(activeIdx, { [key]: v }), v => `${v > 0 ? '+' : ''}${v.toFixed(1)} dB`)}
                    <div className="text-[9px] text-slate-500 font-mono text-center">{freq}</div>
                  </div>
                ))}
              </div>
              {/* Interactive Module Vector Display Canvas (EQ response curve) */}
              <div className="bg-slate-900/80 border border-slate-800 rounded-lg p-2">
                <div className="text-[9px] text-slate-500 font-mono mb-1 flex justify-between"><span>VECTOR DISPLAY — EQ RESPONSE</span><span>20Hz – 20kHz</span></div>
                <canvas ref={eqCurveRef} className="w-full h-[88px] block"></canvas>
              </div>
            </div>
          )}

          {activeMod && activeMod.type === 'eqpro' && activeIdx >= 0 && (
            <div className="space-y-3">
              <div className="flex items-center justify-between">
                <span className="text-[9px] text-teal-400 font-mono uppercase tracking-widest">PARAMETRIC / GRAPHIC EQ PRO — Pro-Q style interactive</span>
                <span className="text-[9px] text-slate-500 font-mono">20Hz – 20kHz · ±24dB · {EQPRO_MAX_BANDS} bands max</span>
              </div>
              <InteractiveEqPro
                track={track}
                params={ap}
                onChange={(next) => setParams(activeIdx, next)}
                getModule={() => window.__getTrackFxModule ? window.__getTrackFxModule(track.id, 'eqpro') : null}
                spectrumModules={() => {
                  const a = window.__getTrackFxModule ? window.__getTrackFxModule(track.id, 'eqpro') : null;
                  const b = window.__getTrackSfFxModule ? window.__getTrackSfFxModule(track.id, 'eqpro') : null;
                  return [a, b].filter(Boolean);
                }}
              />
            </div>
          )}

          {activeMod && activeMod.type === 'compressor' && activeIdx >= 0 && (
            <div className="grid grid-cols-3 gap-4">
              <div className="bg-slate-900/80 border border-slate-800 p-3 rounded-lg">{slider('THRESHOLD', ap.threshold, -60, 0, 0.5, '#fbbf24', v => setParams(activeIdx, { threshold: v }), v => `${v.toFixed(1)} dB`)}</div>
              <div className="bg-slate-900/80 border border-slate-800 p-3 rounded-lg">{slider('RATIO', ap.ratio, 1, 20, 0.5, '#f59e0b', v => setParams(activeIdx, { ratio: v }), v => `${v.toFixed(1)} : 1`)}</div>
              <div className="bg-slate-900/80 border border-slate-800 p-3 rounded-lg">{slider('MAKE-UP', ap.makeup, 0, 12, 0.1, '#f59e0b', v => setParams(activeIdx, { makeup: v }), v => `${v.toFixed(1)} dB`)}</div>
            </div>
          )}

          {activeMod && activeMod.type === 'limiter' && activeIdx >= 0 && (
            <div className="grid grid-cols-2 gap-4">
              <div className="bg-slate-900/80 border border-slate-800 p-3 rounded-lg">{slider('CEILING', ap.ceiling, -24, 0, 0.1, '#f43f5e', v => setParams(activeIdx, { ceiling: v }), v => `${v.toFixed(1)} dB`)}</div>
              <div className="bg-slate-900/80 border border-slate-800 p-3 rounded-lg flex items-center justify-center text-[10px] text-slate-500 font-mono text-center leading-snug">True-Peak limiting<br/>Ratio 20:1 · Knee 0dB</div>
            </div>
          )}

          {activeMod && activeMod.type === 'exciter' && activeIdx >= 0 && (
            <div className="grid grid-cols-2 gap-4">
              <div className="bg-slate-900/80 border border-slate-800 p-3 rounded-lg">{slider('DRIVE / MIX', ap.drive, 0, 100, 1, '#c084fc', v => setParams(activeIdx, { drive: v }), v => `${v}%`)}</div>
              <div className="bg-slate-900/80 border border-slate-800 p-3 rounded-lg flex items-center justify-center text-[10px] text-slate-500 font-mono text-center leading-snug">WaveShaper saturation<br/>High-pass 2kHz · 4× oversampled</div>
            </div>
          )}

          {activeMod && activeMod.type === 'rebalance' && activeIdx >= 0 && (
            <div className="grid grid-cols-2 gap-4">
              <div className="bg-slate-900/80 border border-slate-800 p-3 rounded-lg">{slider('MID GAIN', ap.mid, -12, 12, 0.1, '#38bdf8', v => setParams(activeIdx, { mid: v }), v => `${v > 0 ? '+' : ''}${v.toFixed(1)} dB`)}</div>
              <div className="bg-slate-900/80 border border-slate-800 p-3 rounded-lg">{slider('SIDE GAIN', ap.side, -12, 12, 0.1, '#22d3ee', v => setParams(activeIdx, { side: v }), v => `${v > 0 ? '+' : ''}${v.toFixed(1)} dB`)}</div>
            </div>
          )}

          {activeMod && activeMod.type === 'carla' && activeIdx >= 0 && (
            <div className="space-y-3">
              <div className="flex items-center justify-between">
                <span className="text-[9px] text-teal-400 font-mono uppercase tracking-widest">CARLA BRIDGE — VST FX CHỈNH SỬA ÂM THANH</span>
                <span className="text-[9px] text-slate-500 font-mono">Carla chạy ngoài (user tự cài) · native GUI</span>
              </div>
              <div className="bg-slate-900/80 border border-teal-900/60 p-3 rounded-lg space-y-3">
                <div className="flex items-end gap-2 flex-wrap">
                  <div className="flex-1 min-w-[220px]">
                    <div className="text-[9px] text-slate-500 font-mono mb-1">CHỌN VST FX (đã scan trên máy)</div>
                    <select
                      value={ap.plugin || ''}
                      onChange={e => setParams(activeIdx, { plugin: e.target.value, plugin_path: '' })}
                      className="w-full bg-slate-950 border border-slate-700 text-teal-300 rounded px-2 py-1.5 text-xs outline-none focus:border-teal-500"
                    >
                      <option value="">— Chọn VST FX —</option>
                      {(fxCarlaVsts || []).map(v => <option key={v.id || v.name} value={v.id || v.name}>{v.name || v.id}</option>)}
                    </select>
                  </div>
                  <button
                    onClick={() => {
                      if (!ap.plugin) { window.showToast && window.showToast('Chọn VST FX trước khi Load Carla', 'warning'); return; }
                      window.SonicAPI.openInCarla(ap.plugin, ap.plugin_path).then(r => {
                        if (r && r.success) { window.showToast && window.showToast('Đã mở Carla với ' + ap.plugin + ' (VST FX) — chỉnh âm thanh trong Carla', 'success'); }
                        else { window.showToast && window.showToast('Không mở được Carla', 'error'); }
                      }).catch(err => window.showToast && window.showToast('Lỗi mở Carla: ' + (err.message || err), 'error'));
                    }}
                    className="px-4 py-1.5 bg-teal-700 hover:bg-teal-600 text-white text-xs font-semibold rounded transition flex items-center gap-1 shrink-0"
                  >
                    <i data-lucide="play" className="w-3 h-3"></i> Load Carla Bridge
                  </button>
                  <button
                    onClick={() => {
                      if (window.SonicCarlaMidi && window.SonicCarlaMidi.stopBridge) window.SonicCarlaMidi.stopBridge();
                      window.showToast && window.showToast('Đã ngắt kết nối Carla Bridge', 'info');
                    }}
                    className="px-4 py-1.5 bg-zinc-800 hover:bg-zinc-700 text-red-300 text-xs font-semibold rounded transition flex items-center gap-1 shrink-0"
                  >
                    <i data-lucide="square" className="w-3 h-3"></i> Stop / Unload
                  </button>
                </div>
                <div className="text-[10px] text-slate-500 font-mono leading-relaxed">
                  {ap.plugin
                    ? <>Đã chọn: <span className="text-teal-300">{ap.plugin}</span> — bấm <b>Load Carla Bridge</b> để mở native GUI VST và chỉnh sửa âm thanh.</>
                    : 'Chọn VST FX từ danh sách đã scan, rồi bấm Load Carla Bridge để mở Carla (native GUI).'}
                </div>
              </div>
            </div>
          )}
        </div>

        {/* WAVE OBSERVER — REAL-TIME OSCILLOSCOPE (unified_fx_rack_panel_update.md §III.3) */}
        <div className="bg-slate-950 border border-slate-800 rounded-xl p-3 flex flex-col gap-2 shrink-0">
          <div className="flex items-center justify-between text-xs font-mono border-b border-slate-800/80 pb-1.5">
            <div className="flex items-center gap-2">
              <span className="font-bold text-white tracking-wider">WAVE OBSERVER</span>
              <span className="text-[9px] bg-cyan-950 border border-cyan-800 text-cyan-400 px-1.5 py-0.5 rounded">Real-time Oscilloscope</span>
            </div>
            <span className="text-[10px] text-slate-500 truncate max-w-[200px]">Context: {track.name}</span>
          </div>
          <div className="relative w-full h-36 bg-slate-950 border border-slate-900 rounded-lg overflow-hidden">
            <canvas ref={scopeCanvasRef} className="w-full h-full block cursor-crosshair"></canvas>
          </div>
          <div className="flex items-center justify-between bg-slate-900/90 border border-slate-800 rounded-lg px-3 py-1.5 text-xs font-mono flex-wrap gap-2">
            <div className="flex items-center gap-2 pr-3 border-r border-slate-800">
              <span className="text-[10px] text-slate-400">Input</span>
              <div className="flex flex-col gap-1 w-12">
                <div className="h-1.5 bg-slate-950 rounded overflow-hidden flex"><div ref={scopeMeterLRef} className="h-full bg-cyan-400 w-0 transition-all"></div></div>
                <div className="h-1.5 bg-slate-950 rounded overflow-hidden flex"><div ref={scopeMeterRRef} className="h-full bg-cyan-400 w-0 transition-all"></div></div>
              </div>
            </div>
            <div className="flex items-center gap-1.5">
              <span className="text-slate-500 text-[10px]">Channel:</span>
              <select value={scopeChannel} onChange={e => setScopeChannel(e.target.value)} className="bg-slate-950 border border-slate-800 text-slate-300 rounded px-1.5 py-0.5 text-[11px] outline-none">
                <option value="stereo">Stereo</option><option value="left">Left</option><option value="right">Right</option><option value="mid">Mid</option><option value="side">Side</option>
              </select>
            </div>
            <div className="flex items-center gap-1.5">
              <span className="text-slate-500 text-[10px]">Mode:</span>
              <select value={scopeMode} onChange={e => setScopeMode(e.target.value)} className="bg-slate-950 border border-slate-800 text-slate-300 rounded px-1.5 py-0.5 text-[11px] outline-none">
                <option value="waveform">Waveform</option><option value="lissajous">Lissajous</option><option value="spectrum">Spectrum</option>
              </select>
            </div>
            <div className="flex items-center gap-1.5">
              <span className="text-slate-500 text-[10px]">Duration:</span>
              <span className="text-cyan-400 font-bold text-[11px] w-12">{scopeDuration.toFixed(2)}s</span>
              <input type="range" min="0.1" max="5.0" step="0.1" value={scopeDuration} onChange={e => setScopeDuration(parseFloat(e.target.value))} className="w-16 h-1 cursor-pointer" style={{ accentColor: '#38bdf8' }} />
            </div>
            <div className="flex items-center gap-1.5">
              <span className="text-slate-500 text-[10px]">V.Zoom:</span>
              <span className="text-cyan-400 font-bold text-[11px] w-12">{scopeZoom > 0 ? '+' : ''}{scopeZoom.toFixed(1)} dB</span>
              <input type="range" min="-12" max="20" step="0.5" value={scopeZoom} onChange={e => setScopeZoom(parseFloat(e.target.value))} className="w-16 h-1 cursor-pointer" style={{ accentColor: '#38bdf8' }} />
            </div>
            <button onClick={() => setScopePaused(p => !p)} className={`px-3 py-1 font-semibold rounded text-[11px] border transition-colors ${scopePaused ? 'bg-amber-600 border-amber-400 text-white' : 'bg-slate-800 hover:bg-slate-700 text-slate-300 border-slate-700'}`}>
              {scopePaused ? 'Resume' : 'Pause'}
            </button>
          </div>
        </div>
      </div>

      {/* ADD MODULE POPUP */}
      {addModuleOpen && (
        <div className="fixed inset-0 z-[120] bg-black/80 backdrop-blur-sm flex items-center justify-center p-4" onClick={() => setAddModuleOpen(false)}>
          <div className="w-full max-w-lg bg-slate-900 border border-slate-700 rounded-2xl p-5 space-y-4 shadow-2xl" onClick={e => e.stopPropagation()}>
            <div className="flex items-center justify-between border-b border-slate-800 pb-3">
              <h3 className="text-xs font-bold text-white uppercase tracking-wider font-mono">THÊM MODULE VÀO FX CHAIN</h3>
              <button onClick={() => setAddModuleOpen(false)} className="text-slate-400 hover:text-white"><i data-lucide="x" className="w-4 h-4"></i></button>
            </div>
            <div className="grid grid-cols-2 gap-3 text-xs">
              {Object.keys(TRACK_FX_META).map(t => {
                const meta = TRACK_FX_META[t];
                return (
                  <button key={t} onClick={() => addMod(t)} className="p-3 bg-slate-950 hover:bg-slate-800 border border-slate-800 rounded-xl text-left space-y-1 transition-colors">
                    <div className="font-bold flex items-center gap-1.5" style={{ color: meta.color }}><i data-lucide={meta.icon} className="w-3.5 h-3.5"></i> {meta.name}</div>
                    <div className="text-[10px] text-slate-400">{meta.sub}</div>
                  </button>
                );
              })}
            </div>
          </div>
        </div>
      )}
    </div>
  );
};

// ──────────────────────────────────────────────
// MASTERING MODAL COMPONENT (from md/45_MASTERING_MODULE.md)
// ──────────────────────────────────────────────
const MasteringModal = ({ isOpen, onClose, masteringSettings, setMasteringSettings }) => {
  const ozState = masteringSettings;
  const setOzState = setMasteringSettings;
  const [isPlaying, setIsPlaying] = React.useState(false);
  const masterConnected = ozState.masterConnected;
  const setMasterConnected = (val) => {
    setOzState(prev => ({
      ...prev,
      masterConnected: typeof val === 'function' ? val(prev.masterConnected) : val
    }));
  };

  const audioRef = React.useRef({ source: null });
  const eqCanvasRef = React.useRef(null);
  const imagerCanvasRef = React.useRef(null);
  const inMeterCanvasRef = React.useRef(null);
  const outMeterCanvasRef = React.useRef(null);
  const animFrameRef = React.useRef(null);
  const lufsAccRef = React.useRef([]);
  const knobsInitializedRef = React.useRef(false);

  // Wave Observer Refs & States
  const woCanvasRef = React.useRef(null);
  const woLeftHistoryRef = React.useRef(new Float32Array(400).fill(0));
  const woRightHistoryRef = React.useRef(new Float32Array(400).fill(0));
  const woLeftMeterRef = React.useRef(null);
  const woRightMeterRef = React.useRef(null);

  const [woPaused, setWoPaused] = React.useState(false);
  const [woChannel, setWoChannel] = React.useState('stereo');
  const [woMode, setWoMode] = React.useState('waveform');
  const [woDuration, setWoDuration] = React.useState(2.0);
  const [woZoom, setWoZoom] = React.useState(0.0);

  const woPausedRef = React.useRef(woPaused);
  woPausedRef.current = woPaused;
  const woChannelRef = React.useRef(woChannel);
  woChannelRef.current = woChannel;
  const woModeRef = React.useRef(woMode);
  woModeRef.current = woMode;
  const woDurationRef = React.useRef(woDuration);
  woDurationRef.current = woDuration;
  const woZoomRef = React.useRef(woZoom);
  woZoomRef.current = woZoom;

  const ozStateRef = React.useRef(ozState);
  ozStateRef.current = ozState;

  function startAudioDemo() {
    getAudioContext();
    const ctx = audioCtx;
    if (ctx.state === 'suspended') ctx.resume();
    stopAudioDemo();

    const sampleRate = ctx.sampleRate;
    const bufferSize = sampleRate * 4;
    const buffer = ctx.createBuffer(2, bufferSize, sampleRate);
    const left = buffer.getChannelData(0);
    const right = buffer.getChannelData(1);

    for (let i = 0; i < bufferSize; i++) {
      const t = i / sampleRate;
      const kickEnv = Math.max(0, 1 - (t % 0.5) * 8);
      const kick = Math.sin(2 * Math.PI * (55 * Math.exp(-(t % 0.5) * 18))) * kickEnv;
      const snareEnv = Math.max(0, 1 - ((t + 0.25) % 0.5) * 6);
      const snare = (Math.random() * 2 - 1) * snareEnv * 0.3;
      const synth = (Math.sin(2 * Math.PI * 261.63 * t) + Math.sin(2 * Math.PI * 311.13 * t) + Math.sin(2 * Math.PI * 392.0 * t)) * 0.12;
      left[i] = kick * 0.6 + snare + synth;
      right[i] = kick * 0.6 + snare * 0.9 + synth * 0.95;
    }

    const source = ctx.createBufferSource();
    source.buffer = buffer;
    source.loop = true;
    source.connect(masterBus.inputAnalyser);
    source.start();
    audioRef.current.source = source;
    setIsPlaying(true);
  }

  function stopAudioDemo() {
    const src = audioRef.current.source;
    if (src) {
      try { src.stop(); } catch(e) {}
      try { src.disconnect(); } catch(e) {}
      audioRef.current.source = null;
    }
    setIsPlaying(false);
  }

  React.useEffect(() => {
    if (!isOpen) return;
    getAudioContext();
    
    function resizeAll() {
      const resizeCanvas = (ref) => {
        const el = ref.current;
        if (el) { el.width = el.clientWidth; el.height = el.clientHeight; }
      };
      resizeCanvas(eqCanvasRef);
      resizeCanvas(imagerCanvasRef);
      resizeCanvas(inMeterCanvasRef);
      resizeCanvas(outMeterCanvasRef);
      resizeCanvas(woCanvasRef);
    }
    resizeAll();
    window.addEventListener('resize', resizeAll);

    const fftData = new Uint8Array(1024);

    function getPeakLevel(analyser) {
      if (!analyser) return 0;
      const bufferLength = analyser.fftSize;
      const dataArray = new Float32Array(bufferLength);
      analyser.getFloatTimeDomainData(dataArray);
      
      let maxVal = 0;
      for (let i = 0; i < bufferLength; i++) {
        const val = Math.abs(dataArray[i]);
        if (val > maxVal) {
          maxVal = val;
        }
      }
      return maxVal;
    }

    function renderFrame() {
      animFrameRef.current = requestAnimationFrame(renderFrame);
      const s = ozStateRef.current;

      // EQ Spectrum
      const eqCanvas = eqCanvasRef.current;
      if (eqCanvas) {
        const w = eqCanvas.width, h = eqCanvas.height;
        const eqCtx = eqCanvas.getContext('2d');
        eqCtx.clearRect(0, 0, w, h);
        eqCtx.strokeStyle = 'rgba(51, 65, 85, 0.3)';
        eqCtx.lineWidth = 1;
        eqCtx.font = '9px JetBrains Mono';
        eqCtx.fillStyle = '#475569';
        const freqs = [20, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000];
        freqs.forEach(f => {
          const x = (Math.log10(f / 20) / Math.log10(20000 / 20)) * w;
          eqCtx.beginPath(); eqCtx.moveTo(x, 0); eqCtx.lineTo(x, h); eqCtx.stroke();
          if (f >= 1000) eqCtx.fillText(`${f/1000}k`, x + 3, h - 6);
          else eqCtx.fillText(`${f}`, x + 3, h - 6);
        });
        if (masterBus && masterBus.outputAnalyser) {
          masterBus.outputAnalyser.getByteFrequencyData(fftData);
          eqCtx.fillStyle = 'rgba(56, 189, 248, 0.15)';
          const barWidth = w / 128;
          for (let i = 0; i < 128; i++) {
            const val = fftData[i * 4] / 255;
            eqCtx.fillRect(i * barWidth, h - val * h, barWidth - 1, val * h);
          }
        }
        eqCtx.strokeStyle = '#38bdf8';
        eqCtx.lineWidth = 2.5;
        eqCtx.beginPath();
        for (let x = 0; x < w; x++) {
          const freq = 20 * Math.pow(20000 / 20, x / w);
          let gainDb = 0;
          if (s.eqActive) {
            gainDb += s.eqLowGain / (1 + Math.pow(freq / 100, 2));
            gainDb += s.eqMid1Gain * Math.exp(-Math.pow(Math.log(freq / 822), 2) * 2);
            gainDb += s.eqMid2Gain * Math.exp(-Math.pow(Math.log(freq / 3200), 2) * 2);
            gainDb += s.eqHighGain / (1 + Math.pow(10000 / freq, 2));
          }
          const y = h / 2 - (gainDb / 18) * (h / 2);
          if (x === 0) eqCtx.moveTo(x, y); else eqCtx.lineTo(x, y);
        }
        eqCtx.stroke();
      }

      // Imager Vectorscope (real M/S polar trace) + Phase Correlation meter
      const imagerCanvas = imagerCanvasRef.current;
      if (imagerCanvas) {
        const iw = imagerCanvas.width, ih = imagerCanvas.height;
        const ic = imagerCanvas.getContext('2d');
        ic.clearRect(0, 0, iw, ih);
        // Reference circle + crosshair
        ic.strokeStyle = 'rgba(51, 65, 85, 0.4)';
        ic.lineWidth = 1;
        ic.beginPath();
        ic.arc(iw / 2, ih / 2, ih / 3, 0, Math.PI * 2);
        ic.stroke();
        ic.beginPath();
        ic.moveTo(iw / 2 - ih / 3, ih / 2); ic.lineTo(iw / 2 + ih / 3, ih / 2);
        ic.moveTo(iw / 2, ih / 2 - ih / 3); ic.lineTo(iw / 2, ih / 2 + ih / 3);
        ic.stroke();

        const hasL = masterBus && masterBus.leftAnalyser;
        const hasR = masterBus && masterBus.rightAnalyser;
        if (hasL && hasR) {
          try {
            masterBus.leftAnalyser.getFloatTimeDomainData(_imagerBufL);
            masterBus.rightAnalyser.getFloatTimeDomainData(_imagerBufR);
            // Polar (M/S) projection: X = L + R (mid), Y = L − R (side).
            // A mono signal collapses onto the horizontal axis; widening the
            // stereo field grows the vertical spread.
            const scale = ih / 3.2;
            ic.fillStyle = 'rgba(56, 189, 248, 0.45)';
            for (let i = 0; i < 2048; i += 8) {
              const x = iw / 2 + (_imagerBufL[i] + _imagerBufR[i]) * scale;
              const y = ih / 2 - (_imagerBufL[i] - _imagerBufR[i]) * scale;
              if (x >= 0 && x <= iw && y >= 0 && y <= ih) ic.fillRect(x, y, 2, 2);
            }
          } catch (e) {}
        }

        // Phase Correlation meter: −1.0 … +1.0 with spec color zones
        const corr = computeStereoCorrelation();
        const gaugeW = iw - 40;
        const gaugeY = ih - 12;
        // track
        ic.fillStyle = 'rgba(30, 41, 59, 0.9)';
        ic.fillRect(20, gaugeY, gaugeW, 7);
        // zones: danger (<0 red), caution (0..0.5 amber), safe (0.5..1 green)
        const zx = (v) => 20 + (v + 1) / 2 * gaugeW;
        ic.fillStyle = 'rgba(239, 68, 68, 0.55)'; ic.fillRect(zx(-1), gaugeY, zx(0) - zx(-1), 7);
        ic.fillStyle = 'rgba(245, 158, 11, 0.55)'; ic.fillRect(zx(0), gaugeY, zx(0.5) - zx(0), 7);
        ic.fillStyle = 'rgba(52, 211, 153, 0.55)'; ic.fillRect(zx(0.5), gaugeY, zx(1) - zx(0.5), 7);
        // marker
        ic.fillStyle = '#f8fafc';
        ic.fillRect(zx(corr) - 1, gaugeY - 2, 2, 11);
        // labels
        ic.fillStyle = 'rgba(148, 163, 184, 0.8)';
        ic.font = '9px monospace';
        ic.fillText('−1', 20, gaugeY - 4);
        ic.fillText('0', iw / 2 - 3, gaugeY - 4);
        ic.fillText('+1', iw - 26, gaugeY - 4);
        const corrEl = document.getElementById('corrText');
        if (corrEl) {
          corrEl.innerText = corr.toFixed(2);
          corrEl.style.color = corr >= 0.5 ? '#34d399' : (corr >= 0 ? '#fbbf24' : '#ef4444');
        }
      }

      // I/O Meters
      const renderMeter = (analyser, ctxRef, textId) => {
        const canvas = ctxRef.current;
        if (!canvas) return;
        const w = canvas.width, h = canvas.height;
        const ctx = canvas.getContext('2d');
        ctx.clearRect(0, 0, w, h);
        
        const peak = getPeakLevel(analyser);
        const barH = Math.min(1.0, peak) * h;
        
        const grad = ctx.createLinearGradient(0, h, 0, 0);
        grad.addColorStop(0, '#38bdf8');
        grad.addColorStop(0.7, '#f59e0b');
        grad.addColorStop(1, '#ef4444');
        ctx.fillStyle = grad;
        ctx.fillRect(2, h - barH, w - 4, barH);
        
        const el = document.getElementById(textId);
        if (el) {
          if (peak > 0) {
            const dbVal = 20 * Math.log10(peak);
            el.innerText = dbVal < -90 ? '-inf dB' : `${dbVal.toFixed(1)} dB`;
          } else {
            el.innerText = '-inf dB';
          }
        }
      };
      renderMeter(masterBus && masterBus.inputAnalyser, inMeterCanvasRef, 'inPeakText');
      renderMeter(masterBus && masterBus.outputAnalyser, outMeterCanvasRef, 'outPeakText');

      // Loudness measurement (post-mastering output): RMS dBFS + LUFS approx
      // (ITU-R BS.1770 mean-square blocks, ~400ms, without K-weighting — close
      // enough to gauge perceived loudness of the processed master).
      const outAn = masterBus && masterBus.outputAnalyser;
      if (outAn) {
        const outBuf = new Float32Array(outAn.fftSize);
        outAn.getFloatTimeDomainData(outBuf);
        let sq = 0;
        for (let i = 0; i < outBuf.length; i++) sq += outBuf[i] * outBuf[i];
        const rmsDb = 20 * Math.log10(Math.sqrt(sq / outBuf.length) + 1e-12);
        const rmsEl = document.getElementById('outRmsText');
        if (rmsEl) rmsEl.innerText = rmsDb < -90 ? '-inf' : rmsDb.toFixed(1) + ' dB';
        if (!lufsAccRef.current) lufsAccRef.current = [];
        lufsAccRef.current.push(sq / outBuf.length);
        const accLen = 24; // ~0.4s at 60fps rAF
        if (lufsAccRef.current.length >= accLen) {
          const blocks = lufsAccRef.current.splice(0, accLen);
          const m = blocks.reduce((a, b) => a + b, 0) / blocks.length;
          const lufs = -0.691 + 10 * Math.log10(m + 1e-12);
          const lufsEl = document.getElementById('lufsText');
          if (lufsEl) lufsEl.innerText = lufs.toFixed(1) + ' LUFS';
        }
      }

      // Safety watchdog: if the mastering chain is broken (signal in, silence
      // out — e.g. a biquad in a bad state), fall back to the direct routing so
      // audio is NEVER globally silent. The user can re-enable mastering after.
      const masterInPk = getPeakLevel(masterBus && masterBus.inputAnalyser);
      const masterOutPk = getPeakLevel(masterBus && masterBus.outputAnalyser);
      if (masterBus && masterBus.masteringActive && masterInPk > 0.01 && masterOutPk < 0.001) {
        console.warn('[Mastering] Chain broken (signal in, no signal out) — bypassing mastering to restore audio.');
        toggleMasteringOnMaster(false, false);
      }

      // Wave Observer Oscilloscope Rendering
      const woCanvas = woCanvasRef.current;
      if (woCanvas) {
        const w = woCanvas.width, h = woCanvas.height;
        const woCtx = woCanvas.getContext('2d');
        woCtx.clearRect(0, 0, w, h);

        // Draw grid
        woCtx.strokeStyle = 'rgba(51, 65, 85, 0.2)';
        woCtx.lineWidth = 1;
        woCtx.font = '8px JetBrains Mono, monospace';
        woCtx.fillStyle = '#475569';

        const centerY = h / 2;
        const gridLines = [-0.75, -0.5, -0.25, 0, 0.25, 0.5, 0.75];
        gridLines.forEach(g => {
          const y = centerY + g * centerY;
          woCtx.beginPath();
          woCtx.moveTo(0, y);
          woCtx.lineTo(w, y);
          woCtx.stroke();
        });

        // Vertical lines
        const ticksCount = 10;
        for (let i = 1; i <= ticksCount; i++) {
          const x = (i / (ticksCount + 1)) * w;
          woCtx.beginPath();
          woCtx.moveTo(x, 0);
          woCtx.lineTo(x, h);
          woCtx.stroke();
        }

        // dB labels on left side
        woCtx.fillText('-6.0 dB', 5, centerY - 0.5 * centerY + 3);
        woCtx.fillText('-9.0 dB', 5, centerY - 0.35 * centerY + 3);
        woCtx.fillText('-15.0 dB', 5, centerY - 0.18 * centerY + 3);
        woCtx.fillText('-27.0 dB', 5, centerY - 0.05 * centerY + 3);

        woCtx.fillText('-27.0 dB', 5, centerY + 0.05 * centerY + 3);
        woCtx.fillText('-15.0 dB', 5, centerY + 0.18 * centerY + 3);
        woCtx.fillText('-9.0 dB', 5, centerY + 0.35 * centerY + 3);
        woCtx.fillText('-6.0 dB', 5, centerY + 0.5 * centerY + 3);

        // Time indicators at the bottom
        const durationSec = woDurationRef.current;
        for (let i = 1; i <= 5; i++) {
          const timeVal = (i / 6) * durationSec;
          const x = (i / 6) * w;
          woCtx.fillText(timeVal.toFixed(2) + 's', x - 10, h - 4);
        }

        let leftPeak = 0;
        let rightPeak = 0;
        
        if (!woPausedRef.current && masterBus && masterBus.leftAnalyser && masterBus.rightAnalyser) {
          const leftData = new Float32Array(512);
          const rightData = new Float32Array(512);
          masterBus.leftAnalyser.getFloatTimeDomainData(leftData);
          masterBus.rightAnalyser.getFloatTimeDomainData(rightData);

          for (let i = 0; i < 512; i++) {
            const l = Math.abs(leftData[i]);
            const r = Math.abs(rightData[i]);
            if (l > leftPeak) leftPeak = l;
            if (r > rightPeak) rightPeak = r;
          }

          const lHistory = woLeftHistoryRef.current;
          const rHistory = woRightHistoryRef.current;

          // Shift history buffer Left
          for (let i = 0; i < lHistory.length - 1; i++) {
            lHistory[i] = lHistory[i + 1];
            rHistory[i] = rHistory[i + 1];
          }

          if (woModeRef.current === 'envelope') {
            lHistory[lHistory.length - 1] = leftPeak;
            rHistory[rHistory.length - 1] = rightPeak;
          } else {
            lHistory[lHistory.length - 1] = leftData[0];
            rHistory[rHistory.length - 1] = rightData[0];
          }
        }

        const lHistory = woLeftHistoryRef.current;
        const rHistory = woRightHistoryRef.current;
        const zoomGain = Math.pow(10, woZoomRef.current / 20);

        // Update input meter bars
        if (woLeftMeterRef.current && woRightMeterRef.current) {
          const lPct = Math.min(100, leftPeak * 100);
          const rPct = Math.min(100, rightPeak * 100);
          woLeftMeterRef.current.style.width = `${lPct}%`;
          woRightMeterRef.current.style.width = `${rPct}%`;
        }

        // Left channel line (cyan)
        if (woChannelRef.current === 'stereo' || woChannelRef.current === 'left') {
          woCtx.strokeStyle = '#22d3ee';
          woCtx.lineWidth = 1.2;
          woCtx.beginPath();
          for (let i = 0; i < lHistory.length; i++) {
            const x = (i / (lHistory.length - 1)) * w;
            const val = lHistory[i] * zoomGain;
            const y = centerY - val * centerY;
            if (i === 0) woCtx.moveTo(x, y);
            else woCtx.lineTo(x, y);
          }
          woCtx.stroke();

          if (woModeRef.current === 'envelope') {
            woCtx.beginPath();
            for (let i = 0; i < lHistory.length; i++) {
              const x = (i / (lHistory.length - 1)) * w;
              const val = lHistory[i] * zoomGain;
              const y = centerY + val * centerY;
              if (i === 0) woCtx.moveTo(x, y);
              else woCtx.lineTo(x, y);
            }
            woCtx.stroke();
          }
        }

        // Right channel line (teal)
        if (woChannelRef.current === 'stereo' || woChannelRef.current === 'right') {
          woCtx.strokeStyle = '#0d9488';
          woCtx.lineWidth = 1.2;
          woCtx.beginPath();
          for (let i = 0; i < rHistory.length; i++) {
            const x = (i / (rHistory.length - 1)) * w;
            const val = rHistory[i] * zoomGain;
            const y = centerY - val * centerY;
            if (i === 0) woCtx.moveTo(x, y);
            else woCtx.lineTo(x, y);
          }
          woCtx.stroke();

          if (woModeRef.current === 'envelope') {
            woCtx.beginPath();
            for (let i = 0; i < rHistory.length; i++) {
              const x = (i / (rHistory.length - 1)) * w;
              const val = rHistory[i] * zoomGain;
              const y = centerY + val * centerY;
              if (i === 0) woCtx.moveTo(x, y);
              else woCtx.lineTo(x, y);
            }
            woCtx.stroke();
          }
        }
      }
    }

    renderFrame();

    return () => {
      window.removeEventListener('resize', resizeAll);
      if (animFrameRef.current) cancelAnimationFrame(animFrameRef.current);
    };
  }, [isOpen]);

  React.useEffect(() => {
    if (!isOpen) {
      stopAudioDemo();
      knobsInitializedRef.current = false;
    } else {
      setTimeout(() => { try { window.lucide.createIcons(); } catch(e) {} }, 100);
    }
  }, [isOpen]);

  // Hooks MUST be declared before the early return below — React requires a
  // stable hook count across renders (error #310 otherwise).
  const dragChainIndexRef = React.useRef(null);
  const [addModuleOpen, setAddModuleOpen] = React.useState(false);
  // Carla Bridge (VST FX) — danh sách VST để load vào Carla (master chain)
  const [masterCarlaVsts, setMasterCarlaVsts] = React.useState(null); // null = chưa load
  React.useEffect(() => {
    if (isOpen && window.SonicAPI && window.SonicAPI.listPlugins) {
      window.SonicAPI.listPlugins().then(d => setMasterCarlaVsts((d && d.vst_instruments) || [])).catch(() => setMasterCarlaVsts([]));
    }
  }, [isOpen]);

  if (!isOpen) return null;

  const switchModule = (name) => setOzState(prev => ({ ...prev, activeModule: name }));

  // ── Mastering expand: dynamic module chain (mastering_expand.md §II.3) ──
  const MODULE_META = {
    eq: { name: 'Dynamic EQ', sub: '4-Band Peak', icon: 'activity', color: '#22d3ee' },
    eqpro: { name: 'Parametric / Graphic EQ PRO', sub: 'Pro-Q style · 8 bands · interactive', icon: 'chart-area', color: '#2dd4bf' },
    imager: { name: 'Imager', sub: '4-Band Width', icon: 'radio', color: '#a855f7' },
    maximizer: { name: 'Maximizer', sub: 'IRC IV True Peak', icon: 'gauge', color: '#34d399' },
    compressor: { name: 'Bus Compressor', sub: 'Glue & Punch', icon: 'compress', color: '#fbbf24' },
    limiter: { name: 'Brickwall Limiter', sub: 'True-Peak 20:1', icon: 'shield-half', color: '#f43f5e' },
    exciter: { name: 'Harmonic Exciter', sub: 'Saturation & Air', icon: 'wand-2', color: '#c084fc' },
    rebalance: { name: 'Master Rebalance', sub: 'M/S Balance', icon: 'sliders-horizontal', color: '#38bdf8' },
    carla: { name: 'Carla Bridge (VST FX)', sub: 'Native VST audio processing', icon: 'sliders', color: '#14b8a6' }
  };
  const chainFlag = (type) => type === 'eq' ? 'eqActive' : type === 'eqpro' ? 'eqproActive' : type === 'imager' ? 'imagerActive' : type === 'maximizer' ? 'maximizerActive' : type === 'compressor' ? 'compActive' : type === 'limiter' ? 'limActive' : type === 'exciter' ? 'excActive' : type === 'carla' ? 'carlaBridgeActive' : 'rebalActive';
  const chainActive = (type) => !!ozState[chainFlag(type)];

  const toggleChainModule = (modId) => {
    setOzState(prev => {
      const chain = prev.chain.map(m => {
        if (m.id !== modId) return m;
        const next = !m.active;
        return { ...m, active: next };
      });
      const flags = {};
      chain.forEach(m => { flags[chainFlag(m.type)] = !!m.active; });
      return { ...prev, chain, ...flags };
    });
  };
  const removeChainModule = (modId) => {
    setOzState(prev => {
      let chain = prev.chain.filter(m => m.id !== modId);
      if (chain.length === 0) chain = DEFAULT_MASTER_CHAIN.map(m => ({ ...m })); // keep ≥1
      const flags = {};
      chain.forEach(m => { flags[chainFlag(m.type)] = !!m.active; });
      const activeModule = prev.activeModule;
      return { ...prev, chain, ...flags, activeModule: chain.some(m => m.type === activeModule) ? activeModule : chain[chain.length - 1].type };
    });
  };
  const reorderChain = (fromIdx, toIdx) => {
    if (fromIdx === toIdx) return;
    setOzState(prev => {
      const chain = [...prev.chain];
      const moved = chain.splice(fromIdx, 1)[0];
      chain.splice(toIdx, 0, moved);
      return { ...prev, chain };
    });
  };
  const addModuleToChain = (type) => {
    const meta = MODULE_META[type];
    const id = 'mod_' + type + '_' + Date.now();
    const entry = { id, type, name: meta.name, active: true };
    if (type === 'eqpro') entry.params = { amount: 100, bands: JSON.parse(JSON.stringify(EQPRO_DEFAULT_BANDS)) };
    if (type === 'carla') entry.params = { plugin: '', plugin_path: '' };
    setOzState(prev => ({
      ...prev,
      chain: [...(prev.chain || []), entry],
      [chainFlag(type)]: true,
      activeModule: type
    }));
    setAddModuleOpen(false);
  };
  const updateChainEntryParams = (modId, patch) => {
    setOzState(prev => ({
      ...prev,
      chain: (prev.chain || []).map(m => m.id === modId ? { ...m, params: { ...(m.params || {}), ...patch } } : m)
    }));
  };

  // ── EQ Preset Library (mastering_expand.md §II.1) ──
  const EQ_PRESET_LIBRARY = {
    flat: { name: 'Flat / Reset', bands: [
      { id: 1, type: 'lowshelf', freq: 100, gain: 0.0, q: 0.7 },
      { id: 2, type: 'peaking', freq: 800, gain: 0.0, q: 0.7 },
      { id: 3, type: 'peaking', freq: 3200, gain: 0.0, q: 1.2 },
      { id: 4, type: 'highshelf', freq: 10000, gain: 0.0, q: 0.7 }] },
    vocal_clarity: { name: 'Vocal Unmask & Clarity', bands: [
      { id: 1, type: 'lowshelf', freq: 90, gain: -2.5, q: 0.7 },
      { id: 2, type: 'peaking', freq: 500, gain: -1.8, q: 1.0 },
      { id: 3, type: 'peaking', freq: 2800, gain: 3.2, q: 1.2 },
      { id: 4, type: 'highshelf', freq: 12000, gain: 2.0, q: 0.7 }] },
    bass_punch: { name: 'EDM Low-End Punch', bands: [
      { id: 1, type: 'lowshelf', freq: 80, gain: 4.0, q: 0.8 },
      { id: 2, type: 'peaking', freq: 300, gain: -3.0, q: 1.4 },
      { id: 3, type: 'peaking', freq: 4000, gain: 1.5, q: 1.0 },
      { id: 4, type: 'highshelf', freq: 10000, gain: 1.0, q: 0.7 }] },
    warm_tape: { name: 'Warm Vintage Analog', bands: [
      { id: 1, type: 'lowshelf', freq: 120, gain: 2.0, q: 0.6 },
      { id: 2, type: 'peaking', freq: 1500, gain: 1.0, q: 0.5 },
      { id: 3, type: 'peaking', freq: 5000, gain: -2.0, q: 1.0 },
      { id: 4, type: 'highshelf', freq: 8000, gain: -3.0, q: 0.7 }] }
  };
  const applyEQPreset = (presetKey) => {
    const preset = EQ_PRESET_LIBRARY[presetKey];
    if (!preset) return;
    const bus = masterBus;
    if (bus && bus.eqLowFilter) {
      const now = audioCtx ? audioCtx.currentTime : 0;
      const filters = [bus.eqLowFilter, bus.eqMid1Filter, bus.eqMid2Filter, bus.eqHighFilter];
      preset.bands.forEach((bd, i) => {
        const f = filters[i];
        if (!f) return;
        try {
          // setValueAtTime (không automation) — tránh "BiquadFilterNode: state is bad"
          f.frequency.setValueAtTime(bd.freq, now);
          f.gain.setValueAtTime(bd.gain, now);
          f.Q.setValueAtTime(bd.q, now);
        } catch (e) {}
      });
    }
    // Sync UI knobs + canvas: [eqLowGain, eqMid1Gain, eqMid2Gain, eqHighGain]
    setOzState(prev => ({
      ...prev,
      eqLowGain: preset.bands[0] ? preset.bands[0].gain : prev.eqLowGain,
      eqMid1Gain: preset.bands[1] ? preset.bands[1].gain : prev.eqMid1Gain,
      eqMid2Gain: preset.bands[2] ? preset.bands[2].gain : prev.eqMid2Gain,
      eqHighGain: preset.bands[3] ? preset.bands[3].gain : prev.eqHighGain,
      eqPreset: presetKey
    }));
  };

  const bandKnob = (param, min, max, val, unit, label, freq, color, filterType) => (
    <div className="bg-slate-900/80 border border-slate-800 p-2 flex flex-col justify-between items-center rounded-lg">
      <div className="flex items-center justify-between text-[10px] font-bold w-full" style={{color}}>
        <span>{label}</span>
        <span className="text-[9px] font-mono text-slate-400">{freq}</span>
      </div>
      <div className="my-1">
        <MasteringKnob
          param={param}
          min={min}
          max={max}
          value={ozState[param] !== undefined ? ozState[param] : val}
          unit={unit}
          color={color}
          onChange={(p, v) => setOzState(prev => ({ ...prev, [p]: v }))}
        />
      </div>
      <div className="text-[9px] text-slate-500 font-mono text-center mt-1">{filterType}</div>
    </div>
  );

  return (
    <div className="fixed inset-0 z-[100] flex items-center justify-center bg-black/70 backdrop-blur-sm" onClick={onClose}>
      <div id="masteringModalBody" className="flex flex-col bg-slate-950 text-slate-200 w-[95vw] h-[92vh] max-w-[1400px] rounded-2xl border border-slate-800/80 shadow-2xl overflow-hidden" onClick={e => e.stopPropagation()} style={{fontFamily: "'Inter', sans-serif"}}>

        {/* TOP TRANSPORT & SESSION BAR */}
        <header className="h-12 bg-slate-900/90 border-b border-slate-800/80 flex items-center justify-between px-4 z-30 shrink-0">
          <div className="flex items-center gap-3">
            <div className="w-7 h-7 rounded-lg bg-cyan-600 flex items-center justify-center font-bold text-white shadow-lg shadow-cyan-950">
              <i data-lucide="zap" className="w-3.5 h-3.5"></i>
            </div>
            <div>
              <h1 className="text-xs font-bold tracking-wider text-white flex items-center gap-2">
                MASTERING SUITE <span className="text-[9px] bg-cyan-950 text-cyan-400 border border-cyan-800/80 px-1.5 py-0.5 rounded oz-font-mono">WEB MASTERING V10.5</span>
              </h1>
            </div>
          </div>
          <div className="flex items-center gap-3 bg-slate-950 px-3 py-1 rounded-lg border border-slate-800/80">
            <button onClick={startAudioDemo} className="px-3 py-1 bg-cyan-600 hover:bg-cyan-500 text-white rounded text-xs font-semibold flex items-center gap-1.5 transition-all shadow-md shadow-cyan-950">
              <i data-lucide="play" className="w-3 h-3"></i> <span>Play Reference</span>
            </button>
            <button onClick={stopAudioDemo} className="px-3 py-1 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded text-xs font-semibold flex items-center gap-1.5 transition-all">
              <i data-lucide="square" className="w-3 h-3"></i> <span>Stop</span>
            </button>
            <div className="h-4 w-[1px] bg-slate-800 mx-1"></div>
            <div className="flex items-center gap-2 text-xs oz-font-mono">
              <span className="text-slate-400 text-[11px]">Preset:</span>
              <select className="bg-slate-900 border border-slate-700 rounded px-2 py-0.5 text-xs text-cyan-300 outline-none focus:border-cyan-500">
                <option value="adaptive">Adaptive Dynamic Master</option>
                <option value="edm_club">EDM / Club Punch Maximizer</option>
                <option value="wide_space">Cinematic Stereo Expansion</option>
                <option value="transparent">Transparent High-Clarity Limiter</option>
              </select>
            </div>
          </div>
          <div className="flex items-center gap-3 text-xs oz-font-mono">
            <span className="text-slate-400 text-[11px]">Target LUFS:</span>
            <span className="text-cyan-400 font-bold bg-slate-950 border border-cyan-900/60 px-2 py-0.5 rounded">-11.0 LUFS</span>
            <button onClick={() => { getAudioContext(); setMasterConnected(prev => !prev); }}
              className={`px-2 py-0.5 rounded text-[10px] font-bold border transition-colors ${masterConnected ? 'bg-emerald-700 border-emerald-500 text-white' : 'bg-slate-800 border-slate-700 text-slate-300 hover:bg-slate-700'}`}>
              {masterConnected ? 'Master ON' : 'Master OFF'}
            </button>
            <button onClick={onClose}
              className="px-2 py-0.5 rounded text-[10px] font-bold border transition-colors bg-slate-800 border-slate-700 text-slate-300 hover:bg-[#c2410c] hover:border-red-600 hover:text-white">
              Đóng
            </button>
            <button onClick={onClose} className="text-slate-500 hover:text-slate-300 ml-1">
              <i data-lucide="x" className="w-4 h-4"></i>
            </button>
          </div>
        </header>

        {/* MODULE CHAIN STRIP (dynamic — mastering_expand.md §II.3) */}
        <div className="h-16 bg-slate-950 border-b border-slate-800 px-4 flex items-center gap-2 overflow-x-auto shrink-0 oz-scrollbar">
          <span className="text-[10px] font-bold text-slate-500 uppercase tracking-widest mr-1 oz-font-mono shrink-0">CHAIN:</span>

          {(ozState.chain || []).map((mod, idx) => {
            const meta = MODULE_META[mod.type] || { name: mod.type, sub: '', icon: 'circle', color: '#94a3b8' };
            const isEditing = ozState.activeModule === mod.type;
            const isOn = chainActive(mod.type);
            return (
              <div key={mod.id}
                draggable
                onDragStart={e => { dragChainIndexRef.current = idx; e.dataTransfer.effectAllowed = 'move'; }}
                onDragOver={e => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; }}
                onDrop={e => { e.preventDefault(); const from = dragChainIndexRef.current; if (from !== null && from !== idx) reorderChain(from, idx); dragChainIndexRef.current = null; }}
                onClick={() => switchModule(mod.type)}
                className={`w-40 h-12 rounded-lg p-2 flex items-center justify-between cursor-pointer transition-all shrink-0 ${isEditing ? 'oz-card-active' : 'oz-card'}`}>
                <div className="flex items-center gap-2 min-w-0">
                  <button onClick={e => { e.stopPropagation(); toggleChainModule(mod.id); }} className="w-4 h-4 rounded-full flex items-center justify-center text-[9px] font-bold shrink-0" style={{backgroundColor: isOn ? '#38bdf8' : '#334155', color: isOn ? '#0f172a' : '#94a3b8'}}>
                    <i data-lucide="power" className="w-2.5 h-2.5"></i>
                  </button>
                  <div className="min-w-0">
                    <div className="text-[11px] font-bold text-slate-200 truncate">{meta.name}</div>
                    <div className="text-[9px] oz-font-mono truncate" style={{color: meta.color}}>{idx + 1}. {meta.sub}</div>
                  </div>
                </div>
                <div className="flex items-center gap-1 shrink-0">
                  <i data-lucide={meta.icon} className="w-3 h-3 text-slate-500"></i>
                  <button onClick={e => { e.stopPropagation(); removeChainModule(mod.id); }} className="text-slate-600 hover:text-red-400 text-xs px-0.5" title="Xóa module">
                    <i data-lucide="x" className="w-3 h-3"></i>
                  </button>
                </div>
              </div>
            );
          })}

          <button onClick={() => setAddModuleOpen(true)} className="w-24 h-12 rounded-lg border border-dashed border-slate-800 flex items-center justify-center text-slate-600 hover:text-slate-400 hover:border-slate-600 cursor-pointer transition-all shrink-0" title="Thêm module vào chain">
            <i data-lucide="plus" className="w-4 h-4"></i>
          </button>
        </div>

        {/* MAIN WORKSPACE */}
        <main className="flex-1 flex overflow-hidden min-h-0">

          {/* LEFT: MODULE VIEWS */}
          <div className="flex-1 flex flex-col bg-slate-950 relative overflow-y-auto oz-scrollbar">

            {/* SUB HEADER TOOLBAR */}
            <div className="h-9 bg-slate-900/60 border-b border-slate-800/80 px-4 flex items-center justify-between text-xs oz-font-mono shrink-0">
              <div className="flex items-center gap-3">
                <span className="text-slate-400 flex items-center gap-1.5"><i data-lucide="headphones" className="w-3 h-3 text-cyan-400"></i> Delta Listen</span>
                <select className="bg-slate-950 border border-slate-800 rounded px-2 py-0.5 text-[11px] text-slate-300 outline-none">
                  <option value="irc4">IRC IV - Classic</option>
                  <option value="irc3">IRC III - Balanced</option>
                  <option value="irc2">IRC II - Crisp</option>
                </select>
              </div>
              <div className="flex items-center gap-2 text-[11px]">
                {ozState.activeModule === 'eq' && (
                  <div className="flex items-center gap-1.5">
                    <span className="text-slate-400">EQ Presets:</span>
                    <select
                      value={ozState.eqPreset || 'flat'}
                      onChange={e => applyEQPreset(e.target.value)}
                      className="bg-slate-950 border border-slate-700 text-cyan-300 rounded px-2 py-0.5 text-[11px] outline-none focus:border-cyan-500">
                      {Object.keys(EQ_PRESET_LIBRARY).map(k => (
                        <option key={k} value={k}>{EQ_PRESET_LIBRARY[k].name}</option>
                      ))}
                    </select>
                  </div>
                )}
                <span className="text-slate-400">Learn Input Gain:</span>
                <button className="bg-cyan-950 text-cyan-300 border border-cyan-800/80 px-2 py-0.5 rounded font-bold hover:bg-cyan-900 transition-colors">-11.0 LUFS</button>
              </div>
            </div>

            {/* VIEW: DYNAMIC EQ */}
            <div className={`flex-1 flex flex-col p-4 gap-4 ${ozState.activeModule === 'eq' ? '' : 'hidden'}`}>
              <div className="relative w-full h-64 bg-slate-950 border border-slate-800/90 rounded-xl overflow-hidden shadow-inner cursor-crosshair">
                <canvas ref={eqCanvasRef} className="w-full h-full block"></canvas>
              </div>
              <div className="oz-panel p-3 rounded-xl grid grid-cols-4 gap-3">
                {bandKnob('eqLowGain', -12, 12, 1.5, 'dB', 'BAND 1 (LOW)', '100 Hz', '#22d3ee', 'Shelf Filter')}
                {bandKnob('eqMid1Gain', -12, 12, -1.0, 'dB', 'BAND 2 (MID LOW)', '822 Hz', '#fbbf24', 'Dynamic Bell (Q: 0.7)')}
                {bandKnob('eqMid2Gain', -12, 12, 2.0, 'dB', 'BAND 3 (MID HIGH)', '3.2 kHz', '#a855f7', 'Dynamic Bell (Q: 1.2)')}
                {bandKnob('eqHighGain', -12, 12, 1.8, 'dB', 'BAND 4 (HIGH)', '10 kHz', '#34d399', 'High Shelf')}
              </div>
            </div>

            {/* VIEW: PARAMETRIC / GRAPHIC EQ PRO (graphic_EQ_interactive_module.md) */}
            <div className={`flex-1 flex flex-col p-4 gap-4 ${ozState.activeModule === 'eqpro' ? '' : 'hidden'}`}>
              {(() => {
                const chainMods = ozState.chain || [];
                const cm = chainMods.find(m => m.type === 'eqpro' && m.active !== false) || chainMods.find(m => m.type === 'eqpro');
                if (!cm) return <div className="text-xs text-slate-500 font-mono py-10 text-center">Chưa có module EQ PRO trong chain — bấm [+] để thêm.</div>;
                return <InteractiveEqPro
                  track={null}
                  params={cm.params || {}}
                  onChange={(next) => updateChainEntryParams(cm.id, next)}
                  getModule={() => masterBus && masterBus.eqProInstances ? masterBus.eqProInstances[cm.id] : null}
                  spectrumModules={() => { const m = masterBus && masterBus.eqProInstances ? masterBus.eqProInstances[cm.id] : null; return m ? [m] : []; }}
                  applyTo={(fn) => { const m = masterBus && masterBus.eqProInstances ? masterBus.eqProInstances[cm.id] : null; if (m) fn(m); }}
                />;
              })()}
            </div>

            {/* VIEW: STEREO IMAGER */}
            <div className={`flex-1 flex flex-col p-4 gap-4 ${ozState.activeModule === 'imager' ? '' : 'hidden'}`}>
              <div className="grid grid-cols-12 gap-4 flex-1">
                <div className="col-span-8 bg-slate-950 border border-slate-800/90 rounded-xl p-3 flex flex-col relative shadow-inner">
                  <span className="text-xs font-bold text-cyan-400 uppercase oz-font-mono mb-2 flex items-center justify-between w-full">
                    <span><i data-lucide="radio" className="w-3.5 h-3.5"></i> Polar Vectorscope & Correlation Meter</span>
                    <span className="text-[11px] text-slate-400 normal-case">Corr: <span id="corrText" className="font-bold text-emerald-400">1.00</span></span>
                  </span>
                  <div className="flex-1 relative w-full h-56 flex items-center justify-center">
                    <canvas ref={imagerCanvasRef} className="w-full h-full block"></canvas>
                  </div>
                </div>
                <div className="col-span-4 oz-panel p-3 rounded-xl flex flex-col justify-between">
                  <span className="text-xs font-bold text-slate-300 uppercase oz-font-mono">4-Band Stereo Width</span>
                  <div className="text-[10px] text-slate-500 oz-font-mono leading-snug">0% = Mono · 100% = Original · 200% = 2× Width</div>
                  <div className="space-y-3 my-auto">
                    {[{id:'w1',label:'Band 1 (20-100Hz)',color:'#22d3ee',val:ozState.w1},
                      {id:'w2',label:'Band 2 (100Hz-1kHz)',color:'#fbbf24',val:ozState.w2},
                      {id:'w3',label:'Band 3 (1k-6kHz)',color:'#a855f7',val:ozState.w3},
                      {id:'w4',label:'Band 4 (6k-20kHz)',color:'#34d399',val:ozState.w4}].map(b => (
                      <div key={b.id}>
                        <div className="flex justify-between text-[11px] font-mono mb-1">
                          <span style={{color:b.color,fontWeight:700}}>{b.label}</span>
                          <span id={b.id+'Val'}>{b.val}%</span>
                        </div>
                        <input type="range" min="0" max="200" value={b.val}
                          onChange={e => setOzState(prev => ({...prev, [b.id]: parseInt(e.target.value)}))}
                          className="w-full h-1 cursor-pointer" style={{accentColor: b.color}} />
                      </div>
                    ))}
                  </div>
                </div>
              </div>
            </div>

            {/* VIEW: MAXIMIZER */}
            <div className={`flex-1 flex flex-col p-4 gap-4 ${ozState.activeModule === 'maximizer' ? '' : 'hidden'}`}>
              <div className="oz-panel p-4 rounded-xl flex-1 grid grid-cols-12 gap-6 items-center">
                <div className="col-span-4 flex flex-col items-center justify-center border-r border-slate-800 pr-4">
                  <span className="text-xs font-bold text-slate-400 uppercase oz-font-mono mb-3">Maximizer Gain Boost</span>
                  <div className="my-2">
                    <MasteringKnob
                      param="maxGain"
                      min={0}
                      max={12}
                      value={ozState.maxGain}
                      unit="dB"
                      color="#22d3ee"
                      size="large"
                      onChange={(p, v) => setOzState(prev => ({ ...prev, [p]: v }))}
                    />
                  </div>
                  <div className="w-full mt-4 bg-slate-950 p-2 rounded-lg border border-slate-800 text-xs oz-font-mono flex flex-col gap-1.5">
                    <div className="flex justify-between items-center w-full">
                      <span className="text-slate-400">Ceiling Level:</span>
                      <span className="text-cyan-400 font-bold">{ozState.ceiling.toFixed(2)} dB</span>
                    </div>
                    <input
                      type="range"
                      min="-12"
                      max="0"
                      step="0.1"
                      value={ozState.ceiling}
                      onChange={e => setOzState(prev => ({ ...prev, ceiling: parseFloat(e.target.value) }))}
                      className="w-full h-1 cursor-pointer accent-cyan-400"
                    />
                  </div>
                </div>
                <div className="col-span-8 grid grid-cols-3 gap-4">
                  <div className="bg-slate-950 p-3 border border-slate-800 rounded-xl flex flex-col items-center justify-center h-full">
                    <MasteringKnob
                      param="maxUpward"
                      min={0}
                      max={10}
                      value={ozState.maxUpward}
                      unit="dB"
                      label="UPWARD COMPRESS"
                      color="#22d3ee"
                      onChange={(p, v) => setOzState(prev => ({ ...prev, [p]: v }))}
                    />
                  </div>
                  <div className="bg-slate-950 p-3 border border-slate-800 rounded-xl flex flex-col items-center justify-center h-full">
                    <MasteringKnob
                      param="maxSoftClip"
                      min={0}
                      max={100}
                      value={ozState.maxSoftClip}
                      unit="%"
                      label="SOFT CLIPPER"
                      color="#fbbf24"
                      onChange={(p, v) => setOzState(prev => ({ ...prev, [p]: v }))}
                    />
                  </div>
                  <div className="bg-slate-950 p-3 border border-slate-800 rounded-xl flex flex-col items-center justify-center h-full">
                    <MasteringKnob
                      param="maxTransient"
                      min={0}
                      max={100}
                      value={ozState.maxTransient}
                      unit="%"
                      label="TRANSIENT EMPHASIS"
                      color="#34d399"
                      onChange={(p, v) => setOzState(prev => ({ ...prev, [p]: v }))}
                    />
                  </div>
                </div>
              </div>
            </div>

            {/* VIEW: BUS COMPRESSOR */}
            <div className={`flex-1 flex flex-col p-4 gap-4 ${ozState.activeModule === 'compressor' ? '' : 'hidden'}`}>
              <div className="oz-panel p-4 rounded-xl flex-1 grid grid-cols-12 gap-6 items-center">
                <div className="col-span-4 flex flex-col items-center justify-center border-r border-slate-800 pr-4">
                  <span className="text-xs font-bold text-amber-400 uppercase oz-font-mono mb-3">Bus Compressor</span>
                  <div className="my-2">
                    <MasteringKnob
                      param="compMakeup"
                      min={0} max={12}
                      value={ozState.compMakeup}
                      unit="dB" label="MAKE-UP GAIN"
                      color="#fbbf24"
                      onChange={(p, v) => setOzState(prev => ({ ...prev, [p]: v }))}
                    />
                  </div>
                  <button onClick={() => toggleChainModule((ozState.chain || []).find(m => m.type === 'compressor')?.id)}
                    className={`px-3 py-1 rounded text-[10px] font-bold border transition-colors mt-2 ${ozState.compActive ? 'bg-amber-700 border-amber-500 text-white' : 'bg-slate-800 border-slate-700 text-slate-300'}`}>
                    {ozState.compActive ? 'ON' : 'OFF'}
                  </button>
                </div>
                <div className="col-span-8 grid grid-cols-3 gap-4">
                  <div className="bg-slate-950 p-3 border border-slate-800 rounded-xl flex flex-col items-center justify-center h-full">
                    <MasteringKnob param="compThreshold" min={-60} max={0} value={ozState.compThreshold} unit="dB" label="THRESHOLD" color="#fbbf24" onChange={(p, v) => setOzState(prev => ({ ...prev, [p]: v }))} />
                  </div>
                  <div className="bg-slate-950 p-3 border border-slate-800 rounded-xl flex flex-col items-center justify-center h-full">
                    <MasteringKnob param="compRatio" min={1} max={20} value={ozState.compRatio} unit=":1" label="RATIO" color="#f59e0b" onChange={(p, v) => setOzState(prev => ({ ...prev, [p]: v }))} />
                  </div>
                  <div className="bg-slate-950 p-3 border border-slate-800 rounded-xl flex flex-col items-center justify-center h-full">
                    <span className="text-[10px] text-slate-500 oz-font-mono text-center leading-snug px-2">Glue & Punch<br/>Attack 20ms · Release 250ms · Knee 8dB</span>
                  </div>
                </div>
              </div>
            </div>

            {/* VIEW: BRICKWALL LIMITER */}
            <div className={`flex-1 flex flex-col p-4 gap-4 ${ozState.activeModule === 'limiter' ? '' : 'hidden'}`}>
              <div className="oz-panel p-4 rounded-xl flex-1 grid grid-cols-12 gap-6 items-center">
                <div className="col-span-4 flex flex-col items-center justify-center border-r border-slate-800 pr-4">
                  <span className="text-xs font-bold text-rose-400 uppercase oz-font-mono mb-3">Brickwall Limiter</span>
                  <button onClick={() => toggleChainModule((ozState.chain || []).find(m => m.type === 'limiter')?.id)}
                    className={`px-3 py-1 rounded text-[10px] font-bold border transition-colors mt-2 ${ozState.limActive ? 'bg-rose-700 border-rose-500 text-white' : 'bg-slate-800 border-slate-700 text-slate-300'}`}>
                    {ozState.limActive ? 'ON' : 'OFF'}
                  </button>
                </div>
                <div className="col-span-8 grid grid-cols-3 gap-4">
                  <div className="bg-slate-950 p-3 border border-slate-800 rounded-xl flex flex-col items-center justify-center h-full">
                    <MasteringKnob param="limThreshold" min={-24} max={0} value={ozState.limThreshold} unit="dB" label="CEILING" color="#f43f5e" onChange={(p, v) => setOzState(prev => ({ ...prev, [p]: v }))} />
                  </div>
                  <div className="bg-slate-950 p-3 border border-slate-800 rounded-xl flex flex-col items-center justify-center h-full">
                    <span className="text-[10px] text-slate-500 oz-font-mono text-center leading-snug px-2">True-Peak limiting<br/>Ratio 20:1 · Knee 0dB<br/>Attack 1ms · Release 50ms</span>
                  </div>
                </div>
              </div>
            </div>

            {/* VIEW: HARMONIC EXCITER */}
            <div className={`flex-1 flex flex-col p-4 gap-4 ${ozState.activeModule === 'exciter' ? '' : 'hidden'}`}>
              <div className="oz-panel p-4 rounded-xl flex-1 grid grid-cols-12 gap-6 items-center">
                <div className="col-span-4 flex flex-col items-center justify-center border-r border-slate-800 pr-4">
                  <span className="text-xs font-bold text-purple-400 uppercase oz-font-mono mb-3">Harmonic Exciter</span>
                  <button onClick={() => toggleChainModule((ozState.chain || []).find(m => m.type === 'exciter')?.id)}
                    className={`px-3 py-1 rounded text-[10px] font-bold border transition-colors mt-2 ${ozState.excActive ? 'bg-purple-700 border-purple-500 text-white' : 'bg-slate-800 border-slate-700 text-slate-300'}`}>
                    {ozState.excActive ? 'ON' : 'OFF'}
                  </button>
                </div>
                <div className="col-span-8 grid grid-cols-3 gap-4">
                  <div className="bg-slate-950 p-3 border border-slate-800 rounded-xl flex flex-col items-center justify-center h-full">
                    <MasteringKnob param="excDrive" min={0} max={100} value={ozState.excDrive} unit="%" label="DRIVE / MIX" color="#c084fc" onChange={(p, v) => setOzState(prev => ({ ...prev, [p]: v }))} />
                  </div>
                  <div className="bg-slate-950 p-3 border border-slate-800 rounded-xl flex flex-col items-center justify-center h-full">
                    <span className="text-[10px] text-slate-500 oz-font-mono text-center leading-snug px-2">WaveShaper saturation<br/>High-pass 2kHz<br/>4× oversampled · wet/dry mix</span>
                  </div>
                </div>
              </div>
            </div>

            {/* VIEW: MASTER REBALANCE */}
            <div className={`flex-1 flex flex-col p-4 gap-4 ${ozState.activeModule === 'rebalance' ? '' : 'hidden'}`}>
              <div className="oz-panel p-4 rounded-xl flex-1 grid grid-cols-12 gap-6 items-center">
                <div className="col-span-4 flex flex-col items-center justify-center border-r border-slate-800 pr-4">
                  <span className="text-xs font-bold text-sky-400 uppercase oz-font-mono mb-3">Master Rebalance (M/S)</span>
                  <button onClick={() => toggleChainModule((ozState.chain || []).find(m => m.type === 'rebalance')?.id)}
                    className={`px-3 py-1 rounded text-[10px] font-bold border transition-colors mt-2 ${ozState.rebalActive ? 'bg-sky-700 border-sky-500 text-white' : 'bg-slate-800 border-slate-700 text-slate-300'}`}>
                    {ozState.rebalActive ? 'ON' : 'OFF'}
                  </button>
                </div>
                <div className="col-span-8 grid grid-cols-3 gap-4">
                  <div className="bg-slate-950 p-3 border border-slate-800 rounded-xl flex flex-col items-center justify-center h-full">
                    <MasteringKnob param="rebalMid" min={-12} max={12} value={ozState.rebalMid} unit="dB" label="MID GAIN" color="#38bdf8" onChange={(p, v) => setOzState(prev => ({ ...prev, [p]: v }))} />
                  </div>
                  <div className="bg-slate-950 p-3 border border-slate-800 rounded-xl flex flex-col items-center justify-center h-full">
                    <MasteringKnob param="rebalSide" min={-12} max={12} value={ozState.rebalSide} unit="dB" label="SIDE GAIN" color="#22d3ee" onChange={(p, v) => setOzState(prev => ({ ...prev, [p]: v }))} />
                  </div>
                  <div className="bg-slate-950 p-3 border border-slate-800 rounded-xl flex flex-col items-center justify-center h-full">
                    <span className="text-[10px] text-slate-500 oz-font-mono text-center leading-snug px-2">ChannelSplitter + M/S gains<br/>Center (vocal/bass) vs Sides (stereo width)</span>
                  </div>
                </div>
              </div>
            </div>

            {/* VIEW: CARLA BRIDGE (VST FX) — load carla bridge để dùng VST chỉnh sửa âm thanh */}
            <div className={`flex-1 flex flex-col p-4 gap-4 ${ozState.activeModule === 'carla' ? '' : 'hidden'}`}>
              <div className="oz-panel p-4 rounded-xl flex-1 grid grid-cols-12 gap-6 items-center">
                <div className="col-span-4 flex flex-col items-center justify-center border-r border-slate-800 pr-4">
                  <span className="text-xs font-bold text-teal-400 uppercase oz-font-mono mb-3">Carla Bridge (VST FX)</span>
                  <button onClick={() => toggleChainModule((ozState.chain || []).find(m => m.type === 'carla')?.id)}
                    className={`px-3 py-1 rounded text-[10px] font-bold border transition-colors mt-2 ${ozState.carlaBridgeActive ? 'bg-teal-700 border-teal-500 text-white' : 'bg-slate-800 border-slate-700 text-slate-300'}`}>
                    {ozState.carlaBridgeActive ? 'ON' : 'OFF'}
                  </button>
                </div>
                <div className="col-span-8 space-y-3">
                  <div className="flex items-end gap-2 flex-wrap">
                    <div className="flex-1 min-w-[220px]">
                      <div className="text-[9px] text-slate-500 oz-font-mono mb-1">CHỌN VST FX (đã scan trên máy)</div>
                      <select
                        value={(ozState.carlaBridge && ozState.carlaBridge.plugin) || ''}
                        onChange={e => setOzState(prev => ({ ...prev, carlaBridge: { ...(prev.carlaBridge || {}), plugin: e.target.value, plugin_path: '' } }))}
                        className="w-full bg-slate-950 border border-slate-700 text-teal-300 rounded px-2 py-1.5 text-xs outline-none focus:border-teal-500"
                      >
                        <option value="">— Chọn VST FX —</option>
                        {(masterCarlaVsts || []).map(v => <option key={v.id || v.name} value={v.id || v.name}>{v.name || v.id}</option>)}
                      </select>
                    </div>
                    <button
                      onClick={() => {
                        const plugin = (ozState.carlaBridge && ozState.carlaBridge.plugin) || '';
                        if (!plugin) { window.showToast && window.showToast('Chọn VST FX trước khi Load Carla', 'warning'); return; }
                        window.SonicAPI.openInCarla(plugin, ozState.carlaBridge.plugin_path).then(r => {
                          if (r && r.success) {
                            setOzState(prev => ({ ...prev, carlaBridge: { ...(prev.carlaBridge || {}), connected: true } }));
                            window.showToast && window.showToast('Đã mở Carla với ' + plugin + ' (VST FX) — chỉnh âm thanh trong Carla', 'success');
                          } else { window.showToast && window.showToast('Không mở được Carla', 'error'); }
                        }).catch(err => window.showToast && window.showToast('Lỗi mở Carla: ' + (err.message || err), 'error'));
                      }}
                      className="px-4 py-1.5 bg-teal-700 hover:bg-teal-600 text-white text-xs font-semibold rounded transition flex items-center gap-1 shrink-0"
                    >
                      <i data-lucide="play" className="w-3 h-3"></i> Load Carla Bridge
                    </button>
                    <button
                      onClick={() => {
                        if (window.SonicCarlaMidi && window.SonicCarlaMidi.stopBridge) window.SonicCarlaMidi.stopBridge();
                        setOzState(prev => ({ ...prev, carlaBridge: { ...(prev.carlaBridge || {}), connected: false } }));
                        window.showToast && window.showToast('Đã ngắt kết nối Carla Bridge', 'info');
                      }}
                      className="px-4 py-1.5 bg-zinc-800 hover:bg-zinc-700 text-red-300 text-xs font-semibold rounded transition flex items-center gap-1 shrink-0"
                    >
                      <i data-lucide="square" className="w-3 h-3"></i> Stop / Unload
                    </button>
                  </div>
                  <div className="text-[10px] text-slate-500 oz-font-mono leading-relaxed">
                    Mở Carla với VST FX để chỉnh sửa âm thanh master (native GUI). Module này là pass-through trong master chain (không thêm DSP WebAudio).
                  </div>
                </div>
              </div>
            </div>

            {/* WAVE OBSERVER INTEGRATION */}
            <div className="border-t border-slate-800 bg-slate-900/60 p-3 flex flex-col shrink-0">
              {/* Header */}
              <div className="flex items-center justify-between pb-1.5 border-b border-slate-800/80 mb-2">
                <div className="flex items-center gap-2">
                  <span className="text-[11px] font-bold text-white uppercase tracking-wider">Wave Observer</span>
                  <span className="text-[9px] bg-cyan-950 text-cyan-400 border border-cyan-800/60 px-1.5 py-0.2 rounded font-mono">Real-time Oscilloscope</span>
                </div>
                <div className="flex gap-2">
                  {['Scope', 'Settings', 'Help', 'About'].map(tab => (
                    <button key={tab} className={`px-2 py-0.5 rounded text-[10px] font-bold ${tab === 'Scope' ? 'bg-cyan-950 text-cyan-300 border border-cyan-800/60' : 'text-slate-400 hover:text-slate-200'}`}>
                      {tab}
                    </button>
                  ))}
                </div>
              </div>

              {/* Scope Canvas */}
              <div className="relative w-full h-32 bg-[#090d16] border border-slate-800/80 rounded-lg overflow-hidden mb-2">
                <canvas ref={woCanvasRef} className="w-full h-full block"></canvas>
              </div>

              {/* Controls bar */}
              <div className="flex items-center justify-between gap-4 text-[10px] oz-font-mono text-slate-400 mt-1 select-none">
                {/* Input level meters */}
                <div className="flex items-center gap-2 bg-slate-950 px-2.5 py-1.5 rounded border border-slate-800/80 shrink-0">
                  <span className="text-[10px] font-bold text-slate-300">Input</span>
                  <div className="flex flex-col gap-1 w-20">
                    <div className="flex items-center gap-1">
                      <span className="text-[8px] text-slate-500 w-2">L</span>
                      <div className="w-16 h-1.5 bg-slate-900 rounded-sm overflow-hidden flex items-center">
                        <div ref={woLeftMeterRef} className="h-full bg-cyan-400 transition-all duration-75" style={{ width: '0%' }}></div>
                      </div>
                    </div>
                    <div className="flex items-center gap-1">
                      <span className="text-[8px] text-slate-500 w-2">R</span>
                      <div className="w-16 h-1.5 bg-slate-900 rounded-sm overflow-hidden flex items-center">
                        <div ref={woRightMeterRef} className="h-full bg-teal-500 transition-all duration-75" style={{ width: '0%' }}></div>
                      </div>
                    </div>
                  </div>
                </div>

                {/* Scope Controls */}
                <div className="flex items-center gap-4 bg-slate-950 px-3 py-1 rounded border border-slate-800/80 flex-1 justify-around">
                  <span className="font-bold text-slate-300 uppercase tracking-widest text-[9px]">Scope</span>
                  
                  <div className="flex items-center gap-1.5">
                    <span>Channel</span>
                    <select value={woChannel} onChange={e => setWoChannel(e.target.value)} className="bg-slate-900 border border-slate-700 rounded px-1.5 py-0.5 text-[9px] text-slate-300 outline-none">
                      <option value="stereo">Stereo</option>
                      <option value="left">Left Only</option>
                      <option value="right">Right Only</option>
                    </select>
                  </div>

                  <div className="flex items-center gap-1.5">
                    <span>Mode</span>
                    <select value={woMode} onChange={e => setWoMode(e.target.value)} className="bg-slate-900 border border-slate-700 rounded px-1.5 py-0.5 text-[9px] text-slate-300 outline-none">
                      <option value="waveform">Waveform</option>
                      <option value="envelope">Envelope</option>
                    </select>
                  </div>

                  <div className="flex items-center gap-1.5">
                    <span>Duration:</span>
                    <span className="text-cyan-400 font-bold w-10 text-right">{woDuration.toFixed(3)}s</span>
                    <input type="range" min="0.5" max="5.0" step="0.1" value={woDuration} onChange={e => setWoDuration(parseFloat(e.target.value))} className="w-16 h-1 cursor-pointer accent-cyan-400" />
                  </div>

                  <div className="flex items-center gap-1.5">
                    <span>V.Zoom:</span>
                    <span className="text-cyan-400 font-bold w-12 text-right">{woZoom.toFixed(1)} dB</span>
                    <input type="range" min="-12" max="24" step="0.5" value={woZoom} onChange={e => setWoZoom(parseFloat(e.target.value))} className="w-16 h-1 cursor-pointer accent-cyan-400" />
                  </div>
                </div>

                <button onClick={() => setWoPaused(!woPaused)} className={`px-3 py-1 rounded font-bold border transition shadow-md shrink-0 flex items-center gap-1 ${woPaused ? 'bg-amber-800 hover:bg-amber-700 border-amber-500 text-amber-100' : 'bg-slate-800 hover:bg-slate-700 border-slate-700 text-slate-200'}`}>
                  {woPaused ? 'Resume' : 'Pause'}
                </button>
              </div>
            </div>
          </div>

          {/* RIGHT SIDEBAR: I/O METERS */}
          <div className="w-64 bg-slate-900 border-l border-slate-800/80 p-3 flex flex-col justify-between shadow-2xl oz-font-mono text-xs z-20 shrink-0">
            <div className="flex items-center justify-between border-b border-slate-800 pb-2 mb-2">
              <span className="text-xs font-bold text-white tracking-wider uppercase flex items-center gap-1.5">
                <i data-lucide="sliders" className="w-3 h-3 text-cyan-400"></i> I/O METERS
              </span>
              <span className="text-[9px] bg-cyan-950 border border-cyan-800 text-cyan-300 px-1 py-0.5 rounded">TRUE PEAK</span>
            </div>

            <div className="grid grid-cols-2 gap-2 text-[10px] text-center mb-2">
              <div className="bg-slate-950 border border-slate-800/80 p-1.5 rounded">
                <div className="text-slate-500 font-bold">IN PEAK</div>
                <div id="inPeakText" className="text-cyan-400 font-bold oz-font-mono">-inf dB</div>
              </div>
              <div className="bg-slate-950 border border-slate-800/80 p-1.5 rounded">
                <div className="text-slate-500 font-bold">OUT PEAK</div>
                <div id="outPeakText" className="text-emerald-400 font-bold oz-font-mono">-inf dB</div>
              </div>
            </div>

            {/* Loudness measurement (post-mastering) — đánh giá chất lượng âm thanh */}
            <div className="grid grid-cols-2 gap-2 text-[10px] text-center mb-2">
              <div className="bg-slate-950 border border-slate-800/80 p-1.5 rounded">
                <div className="text-slate-500 font-bold">OUT RMS</div>
                <div id="outRmsText" className="text-sky-300 font-bold oz-font-mono">-inf</div>
              </div>
              <div className="bg-slate-950 border border-slate-800/80 p-1.5 rounded">
                <div className="text-slate-500 font-bold">LOUDNESS</div>
                <div id="lufsText" className="text-fuchsia-300 font-bold oz-font-mono">--.-</div>
              </div>
            </div>

            <div className="flex-1 bg-slate-950 border border-slate-800/90 rounded-xl p-2 flex justify-around relative overflow-hidden my-1">
              <div className="flex flex-col items-center h-full">
                <span className="text-[9px] text-slate-500 mb-1">IN</span>
                <div className="flex-1 w-5 bg-slate-900 border border-slate-800 rounded relative overflow-hidden flex items-end">
                  <canvas ref={inMeterCanvasRef} className="w-full h-full block"></canvas>
                </div>
              </div>
              <div className="flex flex-col items-center h-full">
                <span className="text-[9px] text-slate-500 mb-1">OUT</span>
                <div className="flex-1 w-6 bg-slate-900 border border-slate-800 rounded relative overflow-hidden flex items-end">
                  <canvas ref={outMeterCanvasRef} className="w-full h-full block"></canvas>
                </div>
              </div>
            </div>

            <div className="grid grid-cols-2 gap-1.5 mt-2">
              <button onClick={() => setOzState(prev => ({ ...prev, isBypassed: !prev.isBypassed }))}
                className={`py-1.5 rounded text-[10px] font-bold border transition-colors ${ozState.isBypassed ? 'bg-cyan-600 border-cyan-500 text-white' : 'bg-slate-800 hover:bg-slate-700 text-slate-200 border-slate-700'}`}>
                Bypass
              </button>
              <button className="bg-slate-800 hover:bg-slate-700 text-slate-200 py-1.5 rounded text-[10px] font-bold border border-slate-700 transition-colors">Gain Match</button>
              <button className="bg-slate-800 hover:bg-slate-700 text-slate-300 py-1.5 rounded text-[10px] border border-slate-700">Codec</button>
              <button className="bg-slate-800 hover:bg-slate-700 text-slate-300 py-1.5 rounded text-[10px] border border-slate-700">Dither</button>
            </div>
          </div>

        </main>
      </div>

      {/* ADD MODULE POPUP (mastering_expand.md §III) */}
      {addModuleOpen && (
        <div className="fixed inset-0 z-[110] bg-black/80 backdrop-blur-sm flex items-center justify-center p-4" onClick={() => setAddModuleOpen(false)}>
          <div className="w-full max-w-lg bg-slate-900 border border-slate-700 rounded-2xl p-5 space-y-4 shadow-2xl" onClick={e => e.stopPropagation()}>
            <div className="flex items-center justify-between border-b border-slate-800 pb-3">
              <h3 className="text-xs font-bold text-white uppercase tracking-wider oz-font-mono">THÊM MODULE VÀO MASTERING CHAIN</h3>
              <button onClick={() => setAddModuleOpen(false)} className="text-slate-400 hover:text-white"><i data-lucide="x" className="w-4 h-4"></i></button>
            </div>
            <div className="grid grid-cols-2 gap-3 text-xs">
              {[
                { t: 'eqpro', c: 'text-teal-400', i: 'chart-area', d: 'Parametric / Graphic EQ PRO — Pro-Q style, 8 bands, interactive canvas + spectrum.' },
                { t: 'compressor', c: 'text-amber-400', i: 'compress', d: 'Nén dynamic range, glue & punch cho master.' },
                { t: 'limiter', c: 'text-rose-400', i: 'shield-half', d: 'True-Peak ceiling (ratio 20:1, knee 0) chống clipping.' },
                { t: 'exciter', c: 'text-purple-400', i: 'wand-2', d: 'Saturation hài cho warmth và top-end brilliance.' },
                { t: 'rebalance', c: 'text-sky-400', i: 'sliders-horizontal', d: 'Cân bằng Mid/Side (Vocal/Bass vs stereo width).' },
                { t: 'eq', c: 'text-cyan-400', i: 'activity', d: 'EQ 4-band (lowshelf, 2× peaking, highshelf) + presets.' },
                { t: 'imager', c: 'text-fuchsia-400', i: 'radio', d: 'Stereo width 4-band M/S + vectorscope/correlation.' },
                { t: 'maximizer', c: 'text-emerald-400', i: 'gauge', d: 'Maximizer: boost, soft clip, upward comp, ceiling.' }
              ].map(m => (
                <button key={m.t} onClick={() => addModuleToChain(m.t)} className="p-3 bg-slate-950 hover:bg-slate-800 border border-slate-800 rounded-xl text-left space-y-1 transition-colors">
                  <div className={`font-bold ${m.c} flex items-center gap-1.5`}><i data-lucide={m.i} className="w-3.5 h-3.5"></i> {MODULE_META[m.t].name}</div>
                  <div className="text-[10px] text-slate-400">{m.d}</div>
                </button>
              ))}
            </div>
          </div>
        </div>
      )}
    </div>
  );
};

// ── Media Explorer Panel (from md/51_MEDIA_EXPLORER.md) ──
const MEDIA_LIBRARY_SAMPLES = [
  { name: "MIDI_Loop_01.mid", events: 95, lengthQn: 16, time: "0:08.000", tpqn: 480, bpm: 130, kind: "midi" },
  { name: "MIDI_Loop_02_Bass.mid", events: 48, lengthQn: 16, time: "0:08.000", tpqn: 480, bpm: 130, kind: "midi" },
  { name: "MIDI_Loop_03_Lead.mid", events: 110, lengthQn: 16, time: "0:08.000", tpqn: 480, bpm: 130, kind: "midi" },
  { name: "MIDI_Loop_04.mid", events: 76, lengthQn: 16, time: "0:08.000", tpqn: 480, bpm: 130, kind: "midi" },
  { name: "MIDI_Loop_05_Bass.mid", events: 52, lengthQn: 16, time: "0:08.000", tpqn: 480, bpm: 130, kind: "midi" },
  { name: "MIDI_Loop_06.mid", events: 88, lengthQn: 16, time: "0:08.000", tpqn: 480, bpm: 130, kind: "midi" }
];

const MediaExplorerPanel = ({ height, clipboardRef, active }) => {
  const [userFiles, setUserFiles] = React.useState([]);
  const [folder, setFolder] = React.useState('library');
  const [selected, setSelected] = React.useState(null);
  const [filterText, setFilterText] = React.useState('');
  const [viewMode, setViewMode] = React.useState('details');
  const [isPlaying, setIsPlaying] = React.useState(false);
  const [isPaused, setIsPaused] = React.useState(false);
  const [isLooping, setIsLooping] = React.useState(false);
  const [autoPlay, setAutoPlay] = React.useState(true);
  const [pitch, setPitch] = React.useState(0.0);
  const [rate, setRate] = React.useState(1.0);
  const [volumeDb, setVolumeDb] = React.useState(0.0);
  const [currentTime, setCurrentTime] = React.useState(0);
  const [peaks, setPeaks] = React.useState(null);
  const [audioBuffer, setAudioBuffer] = React.useState(null);
  const [audioDuration, setAudioDuration] = React.useState(0);
  const [midiNotes, setMidiNotes] = React.useState(null);
  const [midiTotal, setMidiTotal] = React.useState(4);
  const [midiBars, setMidiBars] = React.useState(1);
  const [midiTotalBeats, setMidiTotalBeats] = React.useState(16);
  const [midiFileBpm, setMidiFileBpm] = React.useState(120);
  const [tempo, setTempo] = React.useState(function() {
    var saved = localStorage.getItem('studio_media_explorer_tempo');
    return saved ? parseInt(saved) : 120;
  });
  const [tempoText, setTempoText] = React.useState(String(function() {
    var saved = localStorage.getItem('studio_media_explorer_tempo');
    return saved ? parseInt(saved) : 120;
  }()));

  const [zoom, setZoom] = React.useState(1.0);
  // Đồng bộ zoom ra window ref — serializeProjectToSchema lưu vào metadata.zoom
  // (module-level không truy cập state) → restore/open khôi phục đúng zoom.
  React.useEffect(() => { window.__currentZoom = zoom; }, [zoom]);
  const [scrollOffset, setScrollOffset] = React.useState(0);
  const scrollOffsetRef = React.useRef(0);
  scrollOffsetRef.current = scrollOffset;
  const [selStart, setSelStart] = React.useState(null);
  const [selEnd, setSelEnd] = React.useState(null);
  const [isDragging, setIsDragging] = React.useState(false);
  const [previewCtxMenu, setPreviewCtxMenu] = React.useState(null); // { x, y }
  const containerRef = React.useRef(null);
  // Refs mirror latest state so drawCanvas (also called from rAF clock with a
  // stale closure) always draws the currently selected file, not the old one.
  const selectedRef = React.useRef(null);
  const peaksRef = React.useRef(null);
  const audioBufferRef = React.useRef(null);
  const audioDurationRef = React.useRef(0);
  const midiNotesRef = React.useRef(null);
  const midiTotalRef = React.useRef(4);
  const midiBarsRef = React.useRef(1);
  const midiTotalBeatsRef = React.useRef(16);
  const midiFileBpmRef = React.useRef(120);
  const isPlayingRef = React.useRef(false);
  const isPausedRef = React.useRef(false);
  const folderRef = React.useRef('library');
  const tempoRef = React.useRef(120);
  const currentTimeRef = React.useRef(0);
  const selStartRef = React.useRef(null);
  const selEndRef = React.useRef(null);
  const isLoopingRef = React.useRef(false);
  selectedRef.current = selected;
  peaksRef.current = peaks;
  audioBufferRef.current = audioBuffer;
  audioDurationRef.current = audioDuration;
  midiNotesRef.current = midiNotes;
  midiTotalRef.current = midiTotal;
  midiBarsRef.current = midiBars;
  midiTotalBeatsRef.current = midiTotalBeats;
  midiFileBpmRef.current = midiFileBpm;
  isPlayingRef.current = isPlaying;
  isPausedRef.current = isPaused;
  folderRef.current = folder;
  tempoRef.current = tempo;
  currentTimeRef.current = currentTime;
  selStartRef.current = selStart;
  selEndRef.current = selEnd;
  isLoopingRef.current = isLooping;
  const computerPathRef = React.useRef(null);
  const computerTreeRef = React.useRef({});
  const computerRootsRef = React.useRef(null);
  const treePaneRef = React.useRef(null);
  const browseComputerDirRef = React.useRef(null);

  React.useEffect(() => {
    setScrollOffset(0);
  }, [selected]);

  // Keep the tempo text field in sync when tempo changes from elsewhere
  // (e.g. auto-set from a clicked MIDI file).
  React.useEffect(() => {
    setTempoText(String(tempo));
  }, [tempo]);

  React.useEffect(() => {
    window.mediaExplorerActive = true;
    const handleDocumentClick = (e) => {
      if (containerRef.current && containerRef.current.contains(e.target)) {
        window.mediaExplorerActive = true;
      } else {
        window.mediaExplorerActive = false;
      }
    };
    document.addEventListener('mousedown', handleDocumentClick, { capture: true });
    return () => {
      document.removeEventListener('mousedown', handleDocumentClick, { capture: true });
      window.mediaExplorerActive = false;
    };
  }, []);

  const getCanvasLayout = () => {
    const canvas = canvasRef.current;
    if (!canvas) return { w: 300, h: 100, pxPerBeat: 42, pxPerSec: 84, offset: 0, dur: 0, contentW: 0 };
    const w = canvas.clientWidth;
    const h = canvas.clientHeight;
    const f = selectedRef.current;
    const isMidi = isMidiFile(f);
    const curTempo = tempoRef.current || 120;
    const pxPerBeat = 42 * zoom;
    const pxPerSec = pxPerBeat * curTempo / 60;
    const dur = fileDuration(f);
    
    let contentW = 0;
    if (isMidi) {
      const isRealMidi = midiNotesRef.current && midiNotesRef.current.length && midiTotalBeatsRef.current > 0;
      const totalBeats = isRealMidi ? Math.max(midiTotalBeatsRef.current, 4) : Math.max(4, Math.ceil(dur * curTempo / 60) || 16);
      contentW = Math.max(1, totalBeats * pxPerBeat);
    } else {
      contentW = Math.max(1, dur * pxPerSec);
    }
    
    let offset = scrollOffset;
    if (isPlayingRef.current && contentW > w && dur > 0) {
      offset = Math.max(0, Math.min(contentW - w, currentTimeRef.current * pxPerSec - w / 2));
    } else {
      offset = Math.max(0, Math.min(contentW - w, scrollOffset));
    }
    return { w, h, pxPerBeat, pxPerSec, offset, dur, contentW };
  };

  const getSelectedAudioBuffer = async () => {
    if (audioBufferRef.current) return audioBufferRef.current;
    if (!selectedRef.current) return null;
    const buf = await readLocalFileBuffer(selectedRef.current);
    if (!buf) return null;
    try {
      const ctx = getAudioContext();
      const decoded = await ctx.decodeAudioData(buf);
      setAudioBuffer(decoded);
      return decoded;
    } catch (e) {
      console.error(e);
      return null;
    }
  };

  const handleCanvasWheel = (e) => {
    if (!selected) return;
    e.preventDefault();
    if (e.shiftKey) {
      const scrollSpeed = 45;
      const direction = e.deltaY > 0 ? 1 : -1;
      setScrollOffset(prev => {
        const { contentW, w } = getCanvasLayout();
        const maxScroll = Math.max(0, contentW - w);
        return Math.max(0, Math.min(maxScroll, prev + direction * scrollSpeed));
      });
      return;
    }
    const zoomFactor = 1.15;
    if (e.deltaY < 0) {
      setZoom(prev => Math.min(10.0, prev * zoomFactor));
    } else {
      setZoom(prev => Math.max(0.2, prev / zoomFactor));
    }
  };
  // React attaches onWheel passively at the root, so e.preventDefault() there is
  // ignored and Chrome logs "Unable to preventDefault inside passive event listener".
  // Use a native non-passive wheel listener so page scroll is actually blocked.
  const handleCanvasWheelRef = React.useRef(handleCanvasWheel);
  handleCanvasWheelRef.current = handleCanvasWheel;
  React.useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const h = (e) => handleCanvasWheelRef.current(e);
    canvas.addEventListener('wheel', h, { passive: false });
    return () => canvas.removeEventListener('wheel', h);
  }, []);

  const handleCanvasMouseDown = (e) => {
    if (!selected) return;
    if (e.button === 2) return; // Right click context menu
    const canvas = canvasRef.current;
    if (!canvas) return;
    const rect = canvas.getBoundingClientRect();
    const clientX = e.clientX - rect.left;
    const { pxPerSec, pxPerBeat, offset } = getCanvasLayout();
    
    const isMidi = isMidiFile(selected);
    const value = isMidi 
      ? (clientX + offset) / pxPerBeat // in beats
      : (clientX + offset) / pxPerSec; // in seconds
      
    setSelStart(value);
    setSelEnd(value);
    setIsDragging(true);
    setPreviewCtxMenu(null);
  };

  const handleCanvasMouseMove = (e) => {
    if (!isDragging || !selected) return;
    const canvas = canvasRef.current;
    if (!canvas) return;
    const rect = canvas.getBoundingClientRect();
    const clientX = e.clientX - rect.left;
    const { pxPerSec, pxPerBeat, offset } = getCanvasLayout();
    
    const isMidi = isMidiFile(selected);
    const value = isMidi 
      ? (clientX + offset) / pxPerBeat 
      : (clientX + offset) / pxPerSec;
      
    setSelEnd(value);
  };

  const handleCanvasMouseUp = (e) => {
    if (isDragging) {
      setIsDragging(false);
    }
  };

  const handleCanvasContextMenu = (e) => {
    e.preventDefault();
    if (!selected || selStart === null || selEnd === null || Math.abs(selStart - selEnd) < 0.01) return;
    setPreviewCtxMenu({
      x: e.clientX,
      y: e.clientY
    });
  };

  const handleCopySelection = async () => {
    if (!selected || selStart === null || selEnd === null) return;
    const isMidi = isMidiFile(selected);
    const startVal = Math.min(selStart, selEnd);
    const endVal = Math.max(selStart, selEnd);
    
    if (isMidi) {
      if (!midiNotes || !midiNotes.length) {
        window.showToast && window.showToast('Không có dữ liệu MIDI để sao chép', 'warning');
        return;
      }
      
      const copiedNotes = midiNotes
        .filter(n => n.start_beat >= startVal && n.start_beat <= endVal)
        .map(n => ({
          ...n,
          start_beat: n.start_beat - startVal
        }));
        
      if (!copiedNotes.length) {
        window.showToast && window.showToast('Không có note MIDI nào trong vùng chọn', 'warning');
        return;
      }
      
      const selectDurBeats = endVal - startVal;
      const secondsPerBeat = 60 / (tempo || 120);
      const selectDurSec = selectDurBeats * secondsPerBeat;
      
      const clipObj = {
        type: 'midi',
        notes: copiedNotes,
        duration: selectDurSec,
        name: selected.name || 'MIDI Selection',
        color: '#a855f7'
      };
      
      if (clipboardRef) clipboardRef.current = clipObj;
      window.globalStudioClipboard = clipObj;
      
      window.showToast && window.showToast(`Đã sao chép ${copiedNotes.length} notes MIDI.`, 'success');
    } else {
      const activeBuf = await getSelectedAudioBuffer();
      if (!activeBuf) {
        window.showToast && window.showToast('Không thể tải dữ liệu âm thanh để sao chép', 'error');
        return;
      }
      
      const sr = activeBuf.sampleRate;
      const startSample = Math.floor(startVal * sr);
      const endSample = Math.floor(endVal * sr);
      const len = Math.max(1, endSample - startSample);
      
      try {
        const ctx = getAudioContext();
        const numCh = activeBuf.numberOfChannels || 1;
        const clipBuffer = ctx.createBuffer(numCh, len, sr);
        for (let ch = 0; ch < numCh; ch++) {
          clipBuffer.copyToChannel(activeBuf.getChannelData(ch).subarray(startSample, endSample), ch);
        }
        
        const clipObj = {
          buffer: clipBuffer,
          name: selected.name || 'Audio Selection',
          volumeDb: 0,
          pan: 0,
          color: '#10b981',
          sampleRate: sr,
          channels: numCh,
          speed: 1.0
        };
        
        if (clipboardRef) clipboardRef.current = clipObj;
        window.globalStudioClipboard = clipObj;
        
        window.showToast && window.showToast(`Đã sao chép đoạn audio dài ${(len / sr).toFixed(2)}s.`, 'success');
      } catch (e) {
        console.error(e);
        window.showToast && window.showToast('Lỗi khi trích xuất âm thanh vùng chọn', 'error');
      }
    }
  };

  React.useEffect(() => {
    const handleGlobalClick = () => setPreviewCtxMenu(null);
    window.addEventListener('click', handleGlobalClick);
    return () => window.removeEventListener('click', handleGlobalClick);
  }, []);

  React.useEffect(() => {
    const handleKeyDown = (e) => {
      if (e.key === ' ' || e.code === 'Space') {
        if (window.mediaExplorerActive) {
          const target = e.target;
          if (target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable)) {
            return;
          }
          e.preventDefault();
          e.stopPropagation();
          if (isPlayingRef.current) {
            stopMediaPlayback();
          } else {
            const cur = selectedRef.current;
            if (cur) {
              selectTokenRef.current++;
              playSelected(cur, selectTokenRef.current);
            }
          }
        }
      }
      if (e.key === 'ArrowUp' || e.key === 'ArrowDown' || e.key === 'ArrowLeft' || e.key === 'ArrowRight') {
        if (window.mediaExplorerActive && folderRef.current === 'computer') {
          const target = e.target;
          if (target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable)) {
            return;
          }
          e.preventDefault();
          e.stopPropagation();
          // Build the flat list of visible (expanded) tree nodes.
          const paths = [];
          const walk = (entry, depth) => {
            if (!entry || !entry.path) return;
            paths.push({ path: entry.path, entry, depth });
            const n = computerTreeRef.current[entry.path];
            if (n && n.expanded && n.dirs) (n.dirs || []).forEach(d => walk(d, depth + 1));
          };
          (computerRootsRef.current || []).forEach(r => walk(r, 0));
          if (!paths.length) return;
          const curPath = computerPathRef.current;
          let idx = paths.findIndex(p => p.path === curPath);
          if (e.key === 'ArrowUp') {
            const ni = idx < 0 ? paths.length - 1 : Math.max(0, idx - 1);
            navigateTreeTo(paths[ni].path);
          } else if (e.key === 'ArrowDown') {
            const ni = Math.min(paths.length - 1, (idx < 0 ? -1 : idx) + 1);
            navigateTreeTo(paths[ni].path);
          } else if (e.key === 'ArrowRight') {
            const node = computerTreeRef.current[curPath];
            if (node && !node.expanded) {
              setComputerTree(prev => ({ ...prev, [curPath]: { ...(prev[curPath] || {}), expanded: true } }));
              if (!node.dirs || !node.dirs.length) {
                if (browseComputerDirRef.current) browseComputerDirRef.current({ name: String(curPath).split('/').pop() || curPath, path: curPath, is_dir: true, handle: node.handle });
              }
            } else if (idx < 0) {
              const p0 = paths[0];
              if (p0) navigateTreeTo(p0.path);
            }
          } else if (e.key === 'ArrowLeft') {
            const node = computerTreeRef.current[curPath];
            if (node && node.expanded) {
              setComputerTree(prev => ({ ...prev, [curPath]: { ...(prev[curPath] || {}), expanded: false } }));
            } else if (curPath) {
              const i = curPath.lastIndexOf('/');
              if (i > 0) {
                navigateTreeTo(curPath.substring(0, i));
              }
            }
          }
        }
      }
      if (e.key === 'c' && (e.ctrlKey || e.metaKey)) {
        if (window.mediaExplorerActive) {
          const target = e.target;
          if (target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable)) {
            return;
          }
          if (selStartRef.current !== null && selEndRef.current !== null && Math.abs(selStartRef.current - selEndRef.current) > 0.01) {
            e.preventDefault();
            e.stopPropagation();
            handleCopySelection();
          }
        }
      }
    };
    window.addEventListener('keydown', handleKeyDown, { capture: true });
    return () => {
      window.removeEventListener('keydown', handleKeyDown, { capture: true });
    };
  }, []);

  const [computerRoots, setComputerRoots] = React.useState(null);
  const [computerTree, setComputerTree] = React.useState({});
  const [computerPath, setComputerPath] = React.useState(null);
  const [computerFiles, setComputerFiles] = React.useState([]);
  const [computerMode, setComputerMode] = React.useState('server');
  const [clientRoot, setClientRoot] = React.useState(null);
  computerPathRef.current = computerPath;
  computerTreeRef.current = computerTree;
  computerRootsRef.current = computerRoots;
  const [favorites, setFavorites] = React.useState(function() {
    try { return JSON.parse(localStorage.getItem('studio_media_explorer_favorites_v1') || '[]'); } catch (e) { return []; }
  }());
  const [favContext, setFavContext] = React.useState(null);
  const [favoritedExpanded, setFavoritedExpanded] = React.useState(true);
  const [colWidths, setColWidths] = React.useState({ file: 260, size: 100, type: 100 });
  const startColResize = (colKey, e) => {
    e.preventDefault();
    const startX = e.clientX;
    const startWidth = colWidths[colKey];
    const onMouseMove = (moveEvent) => {
      const deltaX = moveEvent.clientX - startX;
      const newWidth = Math.max(50, startWidth + deltaX);
      setColWidths(prev => ({ ...prev, [colKey]: newWidth }));
    };
    const onMouseUp = () => {
      document.removeEventListener('mousemove', onMouseMove);
      document.removeEventListener('mouseup', onMouseUp);
    };
    document.addEventListener('mousemove', onMouseMove);
    document.addEventListener('mouseup', onMouseUp);
  };
  const [synthInst, setSynthInst] = React.useState(function() {
    var saved = localStorage.getItem('studio_media_explorer_synth');
    return saved ? JSON.parse(saved) : null;
  }());
  const [synthOpen, setSynthOpen] = React.useState(false);
  const [synthFilter, setSynthFilter] = React.useState('');
  const [synthList, setSynthList] = React.useState(null);
  const [synthLoading, setSynthLoading] = React.useState(false);
  const synthListRef = React.useRef(null);
  synthListRef.current = synthList;
  const synthInstRef = React.useRef(null);
  synthInstRef.current = synthInst;
  const [treeWidth, setTreeWidth] = React.useState(function() {
    var saved = localStorage.getItem('studio_media_explorer_tree_width');
    return saved ? parseInt(saved) : 176;
  }());
  const treeWidthRef = React.useRef(176);
  treeWidthRef.current = treeWidth;
  const startTreeResize = (e) => {
    e.preventDefault();
    const startX = e.clientX;
    const startW = treeWidthRef.current;
    const onMove = (ev) => {
      const newW = Math.max(110, Math.min(420, startW + (ev.clientX - startX)));
      treeWidthRef.current = newW;
      setTreeWidth(newW);
    };
    const onUp = () => {
      document.removeEventListener('mousemove', onMove);
      document.removeEventListener('mouseup', onUp);
      localStorage.setItem('studio_media_explorer_tree_width', treeWidthRef.current.toString());
    };
    document.addEventListener('mousemove', onMove);
    document.addEventListener('mouseup', onUp);
  };
  const canvasRef = React.useRef(null);
  const playStateRef = React.useRef(null);
  const rafRef = React.useRef(null);
  const loopTimerRef = React.useRef(null);
  const selectTokenRef = React.useRef(0);

  React.useEffect(() => {
    if (window.SonicAPI && window.SonicAPI.listMyFiles) {
      window.SonicAPI.listMyFiles([]).then(data => setUserFiles(data || [])).catch(() => {});
    }
    return () => { stopMediaPlayback(); };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  // ── Session persistence: keep loaded folder/files/tree across panel toggles & reloads ──
  const SESSION_KEY = 'studio_media_explorer_session_v1';
  const saveClientRootHandle = (key = 'client_root', handle = clientRoot) => {
    if (!handle || !window.indexedDB) return;
    try {
      const req = indexedDB.open('sonicforge_media_explorer', 1);
      req.onupgradeneeded = (e) => {
        const db = e.target.result;
        if (!db.objectStoreNames.contains('root_handle')) db.createObjectStore('root_handle');
      };
      req.onsuccess = () => {
        const db = req.result;
        const tx = db.transaction('root_handle', 'readwrite');
        tx.objectStore('root_handle').put(handle, key);
      };
    } catch (e) {}
  };
  const loadClientRootHandle = (key = 'client_root') => {
    return new Promise((resolve) => {
      if (!window.indexedDB) { resolve(null); return; }
      try {
        const req = indexedDB.open('sonicforge_media_explorer', 1);
        req.onupgradeneeded = (e) => {
          const db = e.target.result;
          if (!db.objectStoreNames.contains('root_handle')) db.createObjectStore('root_handle');
        };
        req.onsuccess = () => {
          const db = req.result;
          try {
            const tx = db.transaction('root_handle', 'readonly');
            const g = tx.objectStore('root_handle').get(key);
            g.onsuccess = () => resolve(g.result || null);
            g.onerror = () => resolve(null);
          } catch (e2) { resolve(null); }
        };
        req.onerror = () => resolve(null);
      } catch (e) { resolve(null); }
    });
  };
  const saveSession = React.useCallback(() => {
    try {
      const stripHandle = (o) => {
        if (Array.isArray(o)) return o.map(stripHandle);
        if (o && typeof o === 'object') {
          const out = {};
          for (const k of Object.keys(o)) {
            if (k === 'handle') continue;
            out[k] = stripHandle(o[k]);
          }
          return out;
        }
        return o;
      };
      const treeSnapshot = {};
      Object.keys(computerTree).forEach(path => {
        const node = computerTree[path];
        treeSnapshot[path] = {
          dirs: stripHandle(node ? node.dirs : []),
          expanded: !!(node && node.expanded)
        };
      });
      const snap = {
        folder,
        computerMode,
        computerPath,
        clientRootName: clientRoot ? clientRoot.name : null,
        computerRoots: stripHandle(computerRoots || []),
        computerTree: treeSnapshot,
        computerFiles: stripHandle(computerFiles || []),
        selected: selected ? stripHandle({ name: selected.name, path: selected.path, kind: selected.kind, is_dir: selected.is_dir, size_mb: selected.size_mb }) : null,
        savedAt: Date.now()
      };
      localStorage.setItem(SESSION_KEY, JSON.stringify(snap));
    } catch (e) {}
  }, [folder, computerMode, computerPath, clientRoot, computerRoots, computerTree, computerFiles, selected]);

  React.useEffect(() => {
    saveSession();
    if (computerMode === 'client' && clientRoot && clientRoot.kind === 'directory') saveClientRootHandle();
  }, [saveSession, computerMode, clientRoot]);

  React.useEffect(() => {
    (async () => {
      let lastFolder = 'library';
      let lastComputerPath = 'favorited';
      try {
        const raw = localStorage.getItem(SESSION_KEY);
        if (raw) {
          const snap = JSON.parse(raw);
          if (snap.folder) lastFolder = snap.folder;
          if (snap.computerPath) lastComputerPath = snap.computerPath;
          setFolder(lastFolder);
        }
      } catch (e) {}

      // 1. Khôi phục client-side root handle từ IndexedDB
      const savedHandle = await loadClientRootHandle();
      let restoredClientRoot = null;
      if (savedHandle && savedHandle.kind === 'directory') {
        setClientRoot(savedHandle);
        restoredClientRoot = savedHandle;
        setComputerMode('client');
        const rootEntry = { name: savedHandle.name, path: 'root', is_dir: true, handle: savedHandle };
        setComputerRoots([rootEntry]);
        // Quét đúng 1 cấp con dưới root của client
        await browseComputerDir(rootEntry);
      } else {
        setComputerRoots(null);
        setComputerMode('client');
      }

      // 2. Khôi phục thư mục của phiên làm việc trước
      if (lastFolder === 'computer' && lastComputerPath && lastComputerPath !== 'my_computer' && lastComputerPath !== 'favorited') {
        if (restoredClientRoot) {
          if (lastComputerPath === 'root') {
            browseComputerDir({ name: restoredClientRoot.name, path: 'root', is_dir: true, handle: restoredClientRoot });
          } else if (lastComputerPath.startsWith('root/')) {
            const segments = lastComputerPath.split('/').slice(1);
            let curHandle = restoredClientRoot;
            let success = true;
            for (const seg of segments) {
              try {
                curHandle = await curHandle.getDirectoryHandle(seg);
              } catch (err) {
                success = false;
                break;
              }
            }
            if (success) {
              browseComputerDir({
                name: segments[segments.length - 1] || restoredClientRoot.name,
                path: lastComputerPath,
                is_dir: true,
                handle: curHandle
              });
            } else {
              setComputerPath('favorited');
            }
          } else {
            setComputerPath('favorited');
          }
        } else {
          setComputerPath('favorited');
        }
      } else {
        setComputerPath(lastComputerPath === 'my_computer' ? 'favorited' : (lastComputerPath || 'favorited'));
      }
    })();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  const isMidiFile = f => f && (f.kind === 'midi' || /\.(mid|midi)$/i.test(f.name || f.original_name || ''));
  const fileDuration = f => {
    if (!f) return 0;
    if (isMidiFile(f)) {
      if (midiTotalRef.current && midiNotesRef.current && midiNotesRef.current.length) return midiTotalRef.current;
      return (f.lengthQn || 16) * 60 / (f.bpm || tempoRef.current || 120);
    }
    const sel = selectedRef.current;
    const matches = sel && ((f.path && f.path === sel.path) || (!f.path && (f.file_id || f.fileId) === (sel.file_id || sel.fileId)));
    return f.duration || (matches && audioDurationRef.current ? audioDurationRef.current : 0) || (audioBufferRef.current && matches ? audioBufferRef.current.duration : 0) || 0;
  };

  const folderFiles = React.useMemo(() => {
    if (folder === 'library') return MEDIA_LIBRARY_SAMPLES;
    if (folder === 'computer') {
      if (computerPath === 'my_computer' || computerPath === 'favorited' || !computerPath) {
        return favorites || [];
      }
      const node = computerTree[computerPath] || { dirs: [] };
      return [...(node.dirs || []), ...computerFiles];
    }
    return userFiles.filter(f => (folder === 'uploads' ? (f.type || 'Upload') === 'Upload' : (f.type || 'Processed') === 'Processed'));
  }, [folder, userFiles, computerFiles, computerTree, computerPath, favorites]);

  const visibleFiles = React.useMemo(() => {
    const q = filterText.toLowerCase().trim();
    if (!q) return folderFiles;
    return folderFiles.filter(f => (f.name || f.original_name || '').toLowerCase().includes(q));
  }, [folderFiles, filterText]);

  const loadWaveform = async (f) => {
    const token = selectTokenRef.current;
    if (f && (f.handle || f.path)) {
      try {
        const buf = await readLocalFileBuffer(f);
        if (selectTokenRef.current !== token) return;
        if (!buf) { setPeaks(null); return; }
        const ctx = getAudioContext();
        const decoded = await ctx.decodeAudioData(buf);
        if (selectTokenRef.current !== token) return;
        setAudioBuffer(decoded);
        setAudioDuration(decoded.duration);
        const data = decoded.getChannelData(0);
        const count = 600;
        const step = Math.max(1, Math.floor(data.length / count));
        const pk = [];
        for (let i = 0; i < data.length; i += step) {
          let m = 0;
          for (let j = i; j < Math.min(i + step, data.length); j++) {
            const v = Math.abs(data[j]);
            if (v > m) m = v;
          }
          pk.push(m);
        }
        setPeaks(pk);
      } catch (e) { if (selectTokenRef.current === token) setPeaks(null); }
      return;
    }
    const fid = f.file_id || f.fileId;
    if (!fid) { setPeaks(null); return; }
    try {
      const resp = await fetch(`${API_BASE_URL}/api/v1/audio/waveform/${fid}?num_peaks=600`);
      const data = await resp.json();
      if (selectTokenRef.current !== token) return;
      setPeaks(data.peaks || []);
      if (data.duration) setAudioDuration(data.duration);
    } catch (e) { setPeaks(null); }
  };

  const openFavorited = () => {
    setFolder('computer');
    setComputerPath('favorited');
    setComputerFiles([]);
  };

  const openMyComputer = async () => {
    setFolder('computer');
    const useClientRoot = async (rootHandle) => {
      setComputerTree({}); // Dọn sạch cache cây thư mục cũ
      setComputerMode('client');
      setClientRoot(rootHandle);
      const rootEntry = { name: rootHandle.name, path: 'root', is_dir: true, handle: rootHandle };
      setComputerRoots([rootEntry]);
      setComputerPath('root');
      setComputerFiles([]);

      // Lưu handle vào IndexedDB làm client_root và làm link Favorited
      const favKey = 'client:' + rootHandle.name;
      saveClientRootHandle('client_root', rootHandle);
      saveClientRootHandle(favKey, rootHandle);

      // Tự động thêm link đến thư mục đó ở Favorited (không ghi đè các mục cũ)
      setFavorites(prev => {
        const exists = prev.some(f => f.path === favKey);
        if (exists) return prev;
        const next = [...prev, { path: favKey, name: rootHandle.name, is_dir: true }];
        try { localStorage.setItem('studio_media_explorer_favorites_v1', JSON.stringify(next)); } catch (e2) {}
        return next;
      });
      window.showToast && window.showToast('Đã thêm thư mục client vào Favorited: ' + rootHandle.name, 'info');

      await browseComputerDir(rootEntry);
      return true;
    };

    // Luôn bắt buộc hiện Window Picker chọn thư mục của client-side khi click vào My Computer
    if (window.showDirectoryPicker) {
      try {
        const picked = await window.showDirectoryPicker({ mode: 'read' });
        if (picked && picked.kind === 'directory') {
          return useClientRoot(picked);
        }
      } catch (e) {
        // User cancelled or error
      }
    }

    // Fallback sang Server-side chỉ khi browser không hỗ trợ
    setComputerMode('server');
    setComputerRoots(null);
    try {
      const resp = await fetch(`${API_BASE_URL}/api/v1/media/computer`);
      if (!resp.ok) throw new Error('HTTP ' + resp.status);
      const data = await resp.json();
      const roots = data.roots || [];
      setComputerRoots(roots.length ? roots : [{ path: '/', name: 'Root (/)', is_dir: true }]);
      if (!roots.length) window.showToast && window.showToast('Không tìm thấy ổ đĩa nào', 'warning');
      const rootList = roots.length ? roots : [{ path: '/', name: 'Root (/)', is_dir: true }];
      rootList.slice(0, 10).forEach(async (root) => {
        await browseComputerDir(root);
      });
    } catch (e) {
      setComputerRoots([{ path: '/', name: 'Root (/)', is_dir: true }]);
      window.showToast && window.showToast('Không thể truy cập My Computer: ' + e.message, 'error');
    }
  };

  const browseClientDir = async (entry) => {
    const handle = entry && entry.handle;
    if (!handle || !handle.entries) return null;
    const dirs = [];
    const files = [];
    const parentPath = entry.path;
    try {
      for await (const [name, h] of handle.entries()) {
        if (name.startsWith('.')) continue;
        if (h.kind === 'directory') {
          dirs.push({ name, path: parentPath + '/' + name, is_dir: true, handle: h, parent: handle });
        } else {
          const ext = (name.split('.').pop() || '').toLowerCase();
          const kind = ext === 'mid' || ext === 'midi' ? 'midi' : (['wav','mp3','ogg','flac','aiff','aif','m4a','aac','opus'].includes(ext) ? 'audio' : 'other');
          let size = 0;
          try { const f = await h.getFile(); size = f.size; } catch (e2) {}
          files.push({ name, path: parentPath + '/' + name, is_dir: false, size_mb: size ? +(size / 1048576).toFixed(2) : 0, ext: '.' + ext, kind, handle: h });
        }
      }
      dirs.sort((a, b) => a.name.localeCompare(b.name));
      files.sort((a, b) => a.name.localeCompare(b.name));
      setComputerPath(parentPath);
      setComputerFiles(files);
      setSelected(null);
      setCurrentTime(0);
      stopMediaPlayback();
      setComputerTree(prev => ({ ...prev, [parentPath]: { handle, parent: entry.parent || null, dirs, expanded: true } }));
      return { dirs, files };
    } catch (e) {
      window.showToast && window.showToast('Không thể đọc thư mục: ' + e.message, 'error');
      return null;
    }
  };

  const browseComputerDir = async (entry) => {
    if (!entry) return null;
    if (computerMode === 'client' || entry.handle) {
      if (entry.handle && entry.handle.entries) {
        const parentInfo = computerTree[entry.path];
        return await browseClientDir({ ...entry, parent: parentInfo ? parentInfo.parent : entry.parent });
      }
    }
    const path = entry.path || entry;
    if (!path) return null;
    try {
      const resp = await fetch(`${API_BASE_URL}/api/v1/media/browse?path=${encodeURIComponent(path)}`);
      if (!resp.ok) throw new Error('HTTP ' + resp.status);
      const data = await resp.json();
      setComputerPath(data.path);
      setComputerFiles(data.files || []);
      setSelected(null);
      setCurrentTime(0);
      stopMediaPlayback();
      setComputerTree(prev => ({ ...prev, [path]: { dirs: data.dirs || [], expanded: true } }));
      return { dirs: data.dirs || [], files: data.files || [] };
    } catch (e) {
      window.showToast && window.showToast('Không thể mở thư mục: ' + e.message, 'error');
      return null;
    }
  };
  browseComputerDirRef.current = browseComputerDir;

  const toggleComputerDir = async (entry) => {
    if (!entry) return;
    const path = entry.path || entry;
    const node = computerTree[path];
    if (node && node.expanded) {      setComputerTree(prev => ({ ...prev, [path]: { ...prev[path], expanded: false } }));
    } else {
      await browseComputerDir(entry);
    }
  };

  // ── Favorites: thư mục yêu thích ──
  const isFavorite = (entry) => {
    if (!entry) return false;
    return (favorites || []).some(f => f.path === (entry.path || entry));
  };
  const toggleFavorite = (entry, e) => {
    if (e && e.stopPropagation) e.stopPropagation();
    if (!entry) return;
    const path = entry.path || entry;
    const name = entry.name || path.split('/').pop() || path;
    setFavorites(prev => {
      const exists = prev.some(f => f.path === path);
      const next = exists ? prev.filter(f => f.path !== path) : [...prev, { path, name, is_dir: true }];
      try { localStorage.setItem('studio_media_explorer_favorites_v1', JSON.stringify(next)); } catch (e2) {}
      return next;
    });
    window.showToast && window.showToast('Đã ' + (isFavorite(entry) ? 'gỡ khỏi' : 'thêm vào') + ' Favorited: ' + name, 'info');
  };
  const openFavorite = (fav) => {
    if (!fav) return;
    setFolder('computer');
    const entry = { path: fav.path, name: fav.name, is_dir: true };
    // Cố gắng tìm handle trong cây đã load để mở trực tiếp
    const node = computerTree[fav.path];
    if (node && node.handle) {
      setComputerTree({}); // Reset cache
      browseComputerDir({ ...entry, handle: node.handle });
    } else if (fav.path && fav.path.startsWith('client:')) {
      (async () => {
        let favHandle = null;
        try { favHandle = await loadClientRootHandle(fav.path); } catch (e) {}
        if (favHandle) {
          // Yêu cầu quyền đọc (permission có thể bị thu hồi)
          let permitted = true;
          try {
            if (typeof favHandle.queryPermission === 'function') {
              const st = await favHandle.queryPermission({ mode: 'read' });
              if (st !== 'granted' && typeof favHandle.requestPermission === 'function') {
                const r = await favHandle.requestPermission({ mode: 'read' });
                permitted = r === 'granted';
              }
            }
          } catch (e) { permitted = false; }

          if (permitted) {
            setComputerTree({}); // Reset cache
            setClientRoot(favHandle);
            setComputerMode('client');
            const rootEntry = { name: favHandle.name, path: 'root', is_dir: true, handle: favHandle };
            setComputerRoots([rootEntry]);
            setComputerPath('root');
            setComputerFiles([]);
            await browseComputerDir(rootEntry);
          } else {
            window.showToast && window.showToast('Không có quyền truy cập thư mục này', 'warning');
          }
        } else {
          window.showToast && window.showToast('Không thể khôi phục quyền truy cập thư mục này', 'error');
        }
      })();
    } else if (fav.path === 'root' && clientRoot) {
      browseComputerDir({ name: clientRoot.name, path: 'root', is_dir: true, handle: clientRoot });
    } else if (fav.path && fav.path.startsWith('root/') && clientRoot) {
      // Duyệt lại từ root handle tới folder favorite
      const segments = fav.path.split('/').slice(1);
      let curHandle = clientRoot;
      const walk = async (idx) => {
        if (idx >= segments.length) {
          browseComputerDir({ name: segments[idx - 1] || clientRoot.name, path: fav.path, is_dir: true, handle: curHandle });
          return;
        }
        try {
          const child = await curHandle.getDirectoryHandle(segments[idx]);
          curHandle = child;
          walk(idx + 1);
        } catch (e) {
          window.showToast && window.showToast('Không mở được thư mục favorite', 'error');
        }
      };
      walk(0);
    } else if (fav.path) {
      browseComputerDir(entry);
    }
  };

  const goComputerParent = () => {    if (!computerPath) return;
    if (computerMode === 'client' || clientRoot) {
      const node = computerTree[computerPath];
      const parentHandle = node && node.parent;
      if (parentHandle) {
        const parentEntry = computerRoots && computerRoots[0] && parentHandle === clientRoot
          ? computerRoots[0]
          : { name: parentHandle.name, path: (computerPath.split('/').slice(0, -1).join('/')) || 'root', is_dir: true, handle: parentHandle };
        browseComputerDir({ ...parentEntry, parent: parentHandle === clientRoot ? null : (computerTree[parentEntry.path] ? computerTree[parentEntry.path].parent : null) });
      }
      return;
    }
    const isUnix = computerPath.startsWith('/');
    const parts = computerPath.split(/[\\/]/).filter(Boolean);
    parts.pop();
    if (isUnix) {
      browseComputerDir(parts.length ? '/' + parts.join('/') : '/');
    } else {
      // Windows: quay về root ổ đĩa nếu đã lên tới đỉnh
      browseComputerDir(parts.length ? parts.join('\\') : computerPath.split(/[\\/]/)[0] + '\\');
    }
  };

  const readLocalFileBuffer = async (f) => {
    if (f && f.handle && typeof f.handle.getFile === 'function') {
      const file = await f.handle.getFile();
      return await file.arrayBuffer();
    }
    const url = filePreviewUrl(f);
    if (!url) return null;
    const resp = await fetch(url);
    return await resp.arrayBuffer();
  };

  const filePreviewUrl = (f) => {
    if (f && f.path && !(f.handle && f.handle.getFile)) return `${API_BASE_URL}/api/v1/media/file?path=${encodeURIComponent(f.path)}`;
    // Media Library demo MIDI: chỉ có name/kind — bytes ở /static/midi (bản
    // resolveMediaExplorerDropFile cũng dùng cùng endpoint).
    if (f && f.kind === 'midi' && f.name) return `${API_BASE_URL}/static/midi/${encodeURIComponent(f.name)}`;
    const fid = f && (f.file_id || f.fileId);
    return fid ? `${API_BASE_URL}/api/v1/audio/download/${fid}` : null;
  };

  const toggleSynthDropdown = () => {
    if (synthOpen) { setSynthOpen(false); return; }
    setSynthFilter(''); // Clear search filter on open
    setSynthOpen(true);
    if (synthListRef.current) return;
    setSynthLoading(true);
    (window.SonicAPI && window.SonicAPI.listPlugins ? window.SonicAPI.listPlugins() : Promise.resolve({ soundfonts: [] }))
      .then(async (data) => {
        const sfonts = (data && data.soundfonts) || [];
        if (!sfonts.length) { setSynthList([]); setSynthLoading(false); return; }
        const results = await Promise.all(sfonts.map(sf => {
          const baseId = String(sf.id || '').replace('sf_', '');
          return (window.SonicAPI.listSoundfontInstruments ? window.SonicAPI.listSoundfontInstruments(baseId) : Promise.resolve({ presets: [] }))
            .then(r => ({ sf, presets: (r && r.presets) || [] }))
            .catch(() => ({ sf, presets: [] }));
        }));
        setSynthList(results);
        setSynthLoading(false);
      })
      .catch(() => { setSynthList([]); setSynthLoading(false); });
  };

  const selectSynthInst = (inst) => {
    setSynthInst(inst);
    synthInstRef.current = inst;
    setSynthOpen(false);
    localStorage.setItem('studio_media_explorer_synth', JSON.stringify(inst));
    // Realtime: re-schedule current MIDI preview with the newly selected instrument
    const cur = selectedRef.current;
    if (isPlayingRef.current && cur && isMidiFile(cur) && (cur.handle || cur.path || cur.file_id || cur.fileId || (cur.kind === 'midi' && cur.name))) {
      selectTokenRef.current++;
      const token = selectTokenRef.current;
      try { if (window.SonicMidiRouter) window.SonicMidiRouter.panic(); else if (window.SonicSF && typeof window.SonicSF.stopAll === 'function') window.SonicSF.stopAll(); } catch (e) {}
      stopAllNativeSfNotes();
      playMidiPreview(cur, token);
    }
  };

  const filteredSynthList = React.useMemo(() => {
    if (!synthList) return [];
    if (!synthFilter.trim()) return synthList;
    const query = synthFilter.toLowerCase().trim();
    return synthList.map(group => {
      const presets = (group.presets || []).filter(p => 
        (p.name || '').toLowerCase().includes(query) || 
        String(p.program).includes(query)
      );
      return { ...group, presets };
    }).filter(group => group.presets.length > 0);
  }, [synthList, synthFilter]);

  const playMidiPreview = async (f, token) => {
    // Play real MIDI file through selected synth instrument (SonicSF)
    if (!f || !window.SonicSF) return;
    try {
      const buf = await readLocalFileBuffer(f);
      if (!buf) return;
      if (selectTokenRef.current !== (token || selectTokenRef.current)) return;
      const midiResult = (typeof parseMidiFile === 'function' ? parseMidiFile : window.parseMidiFile)(buf);
      if (!midiResult || !midiResult.length) return;
      // Feature: selecting/playing a MIDI file auto-sets the playback tempo to
      // the file's own BPM (so the preview plays in time).
      const fileBpm = (midiResult[0] && midiResult[0].bpm) || 120;
      const newTempo = Math.max(40, Math.min(300, Math.round(fileBpm)));
      setTempo(newTempo);
      setTempoText(String(newTempo));
      tempoRef.current = newTempo;
      try { localStorage.setItem('studio_media_explorer_tempo', String(newTempo)); } catch (e) {}
      const ctx = getAudioContext();
      // Đồng bộ mastering + routing SF trước khi preview MIDI file (âm qua
      // mastering FX của main out khi chain bật)
      try { if (window.__ensureMasteringRouting) window.__ensureMasteringRouting(); } catch (e3) {}
      const bpmVal = tempoRef.current || 120;
      const secondsPerBeat = 60.0 / bpmVal;
      const startWallTime = ctx.currentTime + 0.05;
      const curInst = synthInstRef.current;
      const program = curInst ? curInst.program : undefined;
      const sfId = curInst ? curInst.sfId : undefined;
      const bank = curInst ? curInst.bank : 0;
      // Preview dùng channel RIÊNG (qua _engineChMap) — KHÔNG đè channel 0 mà
      // track đang dùng → không làm sai instrument của track/soundfont khác
      // (trước đây cứng channel 0: preview MIDI Keyboard có thể chơi sai
      // instrument khi track khác đang dùng chung channel).
      let pvCh = 0;
      if (curInst && window.SonicSF.applyAITrackInstrument) {
        try { pvCh = window.SonicSF.applyAITrackInstrument(bank, program, { soundfont_id: sfId, soundfont_bank: bank, soundfont_program: program !== undefined ? program : 0 }) || 0; } catch (e2) { pvCh = 0; }
      } else if (curInst && window.SonicSF.selectInstrument) {
        try { await window.SonicSF.selectInstrument(pvCh, bank, program, sfId); } catch (e2) {}
      }
      // Re-check token after the async await — stale playMidiPreview (older file)
      // must not schedule notes over the newly selected file.
      if (selectTokenRef.current !== (token || selectTokenRef.current)) return;
      const prog = (curInst && curInst.program !== undefined) ? curInst.program : undefined;
      const eng = curInst ? { soundfont_id: sfId, soundfont_bank: bank, soundfont_program: prog !== undefined ? prog : 0 } : undefined;
      const totalSec = midiResult[0].duration || 4;
      const totalBeats = midiResult[0].totalBeats || 16;
      const hasSelection = selStart !== null && selEnd !== null && Math.abs(selStart - selEnd) > 0.01;
      const loopStartBeats = hasSelection ? Math.min(selStart, selEnd) : 0;
      const loopEndBeats = hasSelection ? Math.max(selStart, selEnd) : totalBeats;
      const loopDurationBeats = loopEndBeats - loopStartBeats;
      const loopDurationSec = loopDurationBeats * secondsPerBeat;
      const loopStartSec = loopStartBeats * secondsPerBeat;

      const allNotes = [];
      midiResult.forEach(track => {
        (track.notes || []).forEach(note => {
          allNotes.push(Object.assign({}, note, { trackOffset: track.startTime || 0 }));
        });
      });
      // Standalone + instrument soundfont → render CẢ FILE bằng native
      // FluidSynth (backend) → play WAV (loop theo flag/lựa chọn), bỏ per-note
      // WASM (WASM không phải luồng âm của standalone).
      if (isStandaloneSf() && sfId) {
        const nativeNotes = allNotes
          .filter(note => !(hasSelection && ((note.start_beat || 0) < loopStartBeats || (note.start_beat || 0) >= loopEndBeats)))
          .map(note => {
            const shiftedStartBeat = hasSelection ? ((note.start_beat || 0) - loopStartBeats) : (note.start_beat || 0);
            return {
              pitch: note.pitch || 60,
              start_beat: shiftedStartBeat + ((note.trackOffset || 0) / secondsPerBeat),
              duration_beats: note.duration_beats || 1,
              velocity: note.velocity != null ? note.velocity : 0.8
            };
          });
        if (nativeNotes.length) {
          window.SonicAPI.soundfontRender({ soundfont_id: sfId, bank: bank, program: prog !== undefined ? prog : 0, bpm: bpmVal, notes: nativeNotes }).then(function (res) {
            if (!res || !res.success || !res.url) return;
            if (selectTokenRef.current !== (token || selectTokenRef.current)) return;
            const audio = new Audio(API_BASE_URL + res.url);
            audio.loop = !!isLoopingRef.current;
            _nativeSfPreviews['midifile'] = { audio: audio, token: selectTokenRef.current };
            audio.play().catch(function () {});
          });
        }
        const startedAtTime = startWallTime - loopStartSec;
        playStateRef.current = { source: null, ctx, startedAt: startedAtTime, fakeStart: startedAtTime, midiTotal: totalSec };
        setMidiNotes(allNotes);
        setMidiTotal(totalSec);
        setMidiBars(midiResult[0].bars || 1);
        setMidiTotalBeats(midiResult[0].totalBeats || 16);
        setMidiFileBpm(midiResult[0].bpm || 120);
        setIsPlaying(true);
        setIsPaused(false);
        startCanvasClock();
        return;
      }
      const schedulePass = (passStartTime) => {
        allNotes.forEach(note => {
          const noteStartBeat = note.start_beat || 0;
          if (hasSelection) {
            if (noteStartBeat < loopStartBeats || noteStartBeat >= loopEndBeats) return;
          }
          const shiftedStartBeat = hasSelection ? (noteStartBeat - loopStartBeats) : noteStartBeat;
          const startSec = shiftedStartBeat * secondsPerBeat + (note.trackOffset || 0);
          const durMs = Math.max(80, (note.duration_beats || 1) * secondsPerBeat * 1000);
          window.SonicSF.playNote(note.pitch || 60, (note.velocity || 0.8), durMs, passStartTime + startSec, prog, null, pvCh, eng);
        });
      };
      schedulePass(startWallTime);
      // Loop scheduling: keep looping continuously until Stop is pressed.
      if (loopTimerRef.current) { clearInterval(loopTimerRef.current); loopTimerRef.current = null; }
      if (isLoopingRef.current) {
        loopTimerRef.current = setInterval(() => {
          if (selectTokenRef.current !== (token || selectTokenRef.current)) { clearInterval(loopTimerRef.current); loopTimerRef.current = null; return; }
          if (!isPlayingRef.current || isPausedRef.current) return;
          if (!isLoopingRef.current) { clearInterval(loopTimerRef.current); loopTimerRef.current = null; return; }
          const passStart = ctx.currentTime + 0.05;
          schedulePass(passStart);
          playStateRef.current = Object.assign({}, playStateRef.current, { startedAt: passStart, fakeStart: passStart });
        }, Math.max(200, loopDurationSec * 1000));
      }
      // Keep a fake clock so canvas playhead animates; loop uses playStateRef
      const startedAtTime = startWallTime - loopStartSec;
      playStateRef.current = { source: null, ctx, startedAt: startedAtTime, fakeStart: startedAtTime, midiTotal: totalSec };
      setMidiNotes(allNotes);
      setMidiTotal(totalSec);
      setMidiBars(midiResult[0].bars || 1);
      setMidiTotalBeats(midiResult[0].totalBeats || 16);
      setMidiFileBpm(midiResult[0].bpm || 120);
      setIsPlaying(true);
      setIsPaused(false);
      startCanvasClock();
    } catch (e) { console.error('MIDI preview failed', e); }
  };

  const stopMediaPlayback = React.useCallback(() => {
    if (playStateRef.current) {
      try { if (playStateRef.current.source) playStateRef.current.source.stop(); } catch (e) {}
      try { if (playStateRef.current.source) playStateRef.current.source.disconnect(); } catch (e) {}
      playStateRef.current = null;
    }
    if (rafRef.current) cancelAnimationFrame(rafRef.current);
    rafRef.current = null;
    if (loopTimerRef.current) { clearInterval(loopTimerRef.current); loopTimerRef.current = null; }
    stopNativeSfNote('midifile');
    if (window.SonicSF && typeof window.SonicSF.stopAll === 'function') {
      try { window.SonicSF.stopAll(); } catch (e) {}
    }
    setIsPlaying(false);
    setIsPaused(false);
  }, []);

  // F6 close / panel bị ẩn (display:none — KHÔNG unmount) → tắt preview âm
  // thanh (trước đây preview tiếp tục phát sau khi đóng Media Explorer).
  React.useEffect(function() {
    if (active === false) stopMediaPlayback();
  }, [active, stopMediaPlayback]);

  const playSelected = async (f, token) => {
    if (!f) return;
    stopMediaPlayback();
    if (isMidiFile(f)) {
      if (f.handle || f.path || f.file_id || f.fileId || (f.kind === 'midi' && f.name)) {
        // Real MIDI file: play through selected synth instrument
        await playMidiPreview(f, token);
        return;
      }
      playStateRef.current = { source: null, ctx: null, fakeStart: performance.now() / 1000 };
      setIsPlaying(true);
      setIsPaused(false);
      startCanvasClock();
      return;
    }
    const fid = f.file_id || f.fileId;
    const buf = await readLocalFileBuffer(f);
    if (selectTokenRef.current !== (token || selectTokenRef.current)) return;
    if (!buf) return;
    try {
      const ctx = getAudioContext();
      const decoded = await ctx.decodeAudioData(buf);
      if (selectTokenRef.current !== (token || selectTokenRef.current)) return;
      setAudioBuffer(decoded);
      setAudioDuration(decoded.duration);
      const src = ctx.createBufferSource();
      src.buffer = decoded;
      const sStart = selStartRef.current;
      const sEnd = selEndRef.current;
      const hasSelection = sStart !== null && sEnd !== null && Math.abs(sStart - sEnd) > 0.01;
      let startOffset = 0;
      if (hasSelection) {
        startOffset = Math.min(sStart, sEnd);
      }
      src.loop = isLoopingRef.current;
      if (isLoopingRef.current) {
        if (hasSelection) {
          src.loopStart = Math.min(sStart, sEnd);
          src.loopEnd = Math.max(sStart, sEnd);
        } else {
          src.loopStart = 0;
          src.loopEnd = decoded.duration;
        }
      }
      src.playbackRate.value = rate;
      const gain = ctx.createGain();
      const linear = volumeDb <= -50 ? 0 : Math.pow(10, volumeDb / 20);
      gain.gain.value = linear;
      src.connect(gain);
      gain.connect(ctx.destination);
      src.start(0, startOffset);
      const startedAt = ctx.currentTime - startOffset;
      playStateRef.current = { source: src, ctx, startedAt };
      setIsPlaying(true);
      setIsPaused(false);
      startCanvasClock();
    } catch (e) { console.error('Preview failed', e); }
  };

  const togglePause = () => {
    const st = playStateRef.current;
    if (!st) return;
    if (isPaused) {
      if (st.ctx) st.ctx.resume();
      setIsPaused(false);
    } else {
      if (st.ctx) st.ctx.suspend();
      setIsPaused(true);
    }
  };

  const startCanvasClock = () => {
    if (rafRef.current) cancelAnimationFrame(rafRef.current);
    const tick = () => {
      rafRef.current = requestAnimationFrame(tick);
      let t = currentTimeRef.current;
      const st = playStateRef.current;
      if (st && !isPausedRef.current) {
        const rawElapsed = st.ctx ? st.ctx.currentTime - st.startedAt : performance.now() / 1000 - st.fakeStart;
        const f = selectedRef.current;
        const dur = fileDuration(f);
        const isMidi = isMidiFile(f);
        const bpmVal = tempoRef.current || 120;
        const secondsPerBeat = 60.0 / bpmVal;
        const sStart = selStartRef.current;
        const sEnd = selEndRef.current;
        const hasSelection = sStart !== null && sEnd !== null && Math.abs(sStart - sEnd) > 0.01;

        // Loop plays continuously (forever) until Stop is pressed.
        if (isLoopingRef.current) {
          if (isMidi) {
            const totalBeats = midiTotalBeatsRef.current || 16;
            const loopStartBeats = hasSelection ? Math.min(sStart, sEnd) : 0;
            const loopEndBeats = hasSelection ? Math.max(sStart, sEnd) : totalBeats;
            const loopDurationBeats = loopEndBeats - loopStartBeats;
            const loopDurationSec = loopDurationBeats * secondsPerBeat;
            const loopStartSec = loopStartBeats * secondsPerBeat;
            
            t = loopStartSec + Math.max(0, (rawElapsed - loopStartSec) % Math.max(0.1, loopDurationSec));
          } else {
            const loopStartSec = hasSelection ? Math.min(sStart, sEnd) : 0;
            const loopEndSec = hasSelection ? Math.max(sStart, sEnd) : dur;
            const loopDurationSec = loopEndSec - loopStartSec;
            
            t = loopStartSec + Math.max(0, (rawElapsed - loopStartSec) % Math.max(0.1, loopDurationSec));
          }
        } else {
          t = rawElapsed;
          const endLimit = hasSelection ? (isMidi ? (Math.max(sStart, sEnd) * secondsPerBeat) : Math.max(sStart, sEnd)) : dur;
          if (t >= endLimit) {
            t = endLimit;
            stopMediaPlayback();
          }
        }
      }
      setCurrentTime(t);
      currentTimeRef.current = t;
      drawCanvas(t);
    };
    tick();
  };

  const drawCanvas = (t) => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const ctx = canvas.getContext('2d');
    const w = canvas.clientWidth, h = canvas.clientHeight;
    if (!w || !h) return;
    canvas.width = w * 2; canvas.height = h * 2;
    ctx.scale(2, 2);
    ctx.clearRect(0, 0, w, h);
    ctx.fillStyle = '#181818'; ctx.fillRect(0, 0, w, h);
    
    const f = selectedRef.current;
    const curPeaks = peaksRef.current;
    const curAudioBuffer = audioBufferRef.current;
    const curMidiNotes = midiNotesRef.current;
    const curMidiTotal = midiTotalRef.current;
    const curMidiBars = midiBarsRef.current;
    const curMidiTotalBeats = midiTotalBeatsRef.current;
    const curTempo = tempoRef.current;
    const playing = isPlayingRef.current;
    if (!f) {
      ctx.fillStyle = '#555'; ctx.font = '12px JetBrains Mono, monospace';
      ctx.fillText('No file selected', 10, h / 2);
      return;
    }
    const dur = fileDuration(f);
    
    // Read selection from refs so rAF-driven redraws always show current selection
    const drawSelStart = selStartRef.current;
    const drawSelEnd = selEndRef.current;
    const drawHasSel = drawSelStart !== null && drawSelEnd !== null && Math.abs(drawSelStart - drawSelEnd) > 0.01;
    
    // Scale layout parameters using zoom
    const pxPerBeat = 42 * zoom;
    const pxPerSec = pxPerBeat * (curTempo || 120) / 60;
    
    if (isMidiFile(f)) {
      ctx.strokeStyle = '#333';
      for (let y = 0; y < h - 14; y += 10) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(w, y); ctx.stroke(); }
      ctx.strokeStyle = '#444';
      
      const isRealMidi = curMidiNotes && curMidiNotes.length && curMidiTotalBeats > 0;
      const totalBeats = isRealMidi ? curMidiTotalBeats : Math.max(curMidiTotal * (curTempo || 120) / 60, 4);
      const beats = Math.max(totalBeats, 4);
      const contentW = Math.max(w, beats * pxPerBeat);
      
      const offsetVal = scrollOffsetRef.current;
      let offset = offsetVal;
      if (playing && contentW > w && dur > 0) {
        offset = Math.max(0, Math.min(contentW - w, t * pxPerSec - w / 2));
      } else {
        offset = Math.max(0, Math.min(contentW - w, offsetVal));
      }
      
      // bar lines (every 4 beats)
      for (let b = 0; b <= beats; b += 4) {
        const x = b * pxPerBeat - offset;
        if (x < -10 || x > w + 10) continue;
        ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, h - 14); ctx.stroke();
      }
      
      ctx.fillStyle = '#9ca3af';
      if (curMidiNotes && curMidiNotes.length) {
        const pitchMin = 48, pitchMax = 84;
        const pitchRange = Math.max(1, pitchMax - pitchMin);
        curMidiNotes.forEach(n => {
          const x = (n.start_beat * pxPerBeat) - offset;
          if (x < -30 || x > w + 30) return;
          const nw = Math.max(3, (n.duration_beats || 1) * pxPerBeat);
          const y = h - 14 - 8 - (((Math.min(pitchMax, Math.max(pitchMin, n.pitch)) - pitchMin) / pitchRange) * (h - 30));
          ctx.fillRect(x, y, nw, 5);
        });
      } else {
        const events = f.events || 76;
        for (let i = 0; i < events; i++) {
          const nx = ((i * 17 + 76) % 95) / 100 * contentW - offset;
          const ny = ((i * 13 + 76) % (h - 30)) + 4;
          ctx.fillRect(nx, ny, Math.max(8, (i % 5 + 1) * 10), 3);
        }
      }
      
      // Draw selection overlay
      if (drawHasSel) {
        const startVal = Math.min(drawSelStart, drawSelEnd);
        const endVal = Math.max(drawSelStart, drawSelEnd);
        const xStart = startVal * pxPerBeat - offset;
        const xEnd = endVal * pxPerBeat - offset;
        ctx.fillStyle = 'rgba(59, 130, 246, 0.25)';
        ctx.fillRect(xStart, 0, xEnd - xStart, h - 14);
        ctx.strokeStyle = '#3b82f6';
        ctx.lineWidth = 1;
        ctx.beginPath();
        ctx.moveTo(xStart, 0); ctx.lineTo(xStart, h - 14);
        ctx.moveTo(xEnd, 0); ctx.lineTo(xEnd, h - 14);
        ctx.stroke();
      }
      
      const midiPlayheadX = contentW > w ? Math.max(0, Math.min(w, t * pxPerSec - offset)) : Math.min(w, t * pxPerSec);
      if (playing && dur > 0) {
        ctx.fillStyle = '#ef4444';
        ctx.fillRect(midiPlayheadX - 1, 0, 2, h - 14);
      }
    } else {
      const pk = curPeaks && curPeaks.length > 0 ? curPeaks : null;
      if (pk) {
        const contentW = Math.max(1, dur * pxPerSec);
        const offsetVal = scrollOffsetRef.current;
        let offset = offsetVal;
        if (playing && contentW > w && dur > 0) {
          offset = Math.max(0, Math.min(contentW - w, t * pxPerSec - w / 2));
        } else {
          offset = Math.max(0, Math.min(contentW - w, offsetVal));
        }
        const mid = h / 2;
        ctx.fillStyle = '#22c55e';
        const barW = Math.max(1, contentW / pk.length);
        for (let i = 0; i < pk.length; i++) {
          const x = (i / pk.length) * contentW - offset;
          if (x < -3 || x > w + 3) continue;
          const ph = Math.max(2, pk[i] * (h / 2 - 4));
          ctx.fillRect(x, mid - ph, barW, ph * 2);
        }
        
        // Draw selection overlay
        if (drawHasSel) {
          const startVal = Math.min(drawSelStart, drawSelEnd);
          const endVal = Math.max(drawSelStart, drawSelEnd);
          const xStart = startVal * pxPerSec - offset;
          const xEnd = endVal * pxPerSec - offset;
          ctx.fillStyle = 'rgba(59, 130, 246, 0.25)';
          ctx.fillRect(xStart, 0, xEnd - xStart, h - 14);
          ctx.strokeStyle = '#3b82f6';
          ctx.lineWidth = 1;
          ctx.beginPath();
          ctx.moveTo(xStart, 0); ctx.lineTo(xStart, h - 14);
          ctx.moveTo(xEnd, 0); ctx.lineTo(xEnd, h - 14);
          ctx.stroke();
        }
        
        const audioPlayheadX = contentW > w ? Math.max(0, Math.min(w, t * pxPerSec - offset)) : (t * pxPerSec);
        if (playing && dur > 0) {
          ctx.fillStyle = '#ef4444';
          ctx.fillRect(audioPlayheadX - 1, 0, 2, h - 14);
        }
      } else {
        ctx.fillStyle = '#666'; ctx.font = '11px monospace';
        ctx.fillText('Waveform unavailable', 10, h / 2);
      }
    }
    
    // ruler
    ctx.fillStyle = '#111'; ctx.fillRect(0, h - 14, w, 14);
    ctx.fillStyle = '#888'; ctx.font = '9px JetBrains Mono, monospace';
    const isRealMidi = isMidiFile(f) && curMidiNotes && curMidiNotes.length && curMidiTotalBeats > 0;
    const rulerTotalBeats = isRealMidi ? Math.max(curMidiTotalBeats, 4) : Math.max(4, Math.ceil(dur * (curTempo || 120) / 60) || 16);
    const rulerContentW = Math.max(1, rulerTotalBeats * pxPerBeat);
    const rulerOffset = (playing && rulerContentW > w && dur > 0) ? Math.max(0, Math.min(rulerContentW - w, t * pxPerSec - w / 2)) : 0;
    for (let b = 0; b <= rulerTotalBeats; b += 4) {
      const x = b * pxPerBeat - rulerOffset;
      if (x < -20 || x > w + 20) continue;
      ctx.fillText(String(Math.floor(b / 4)), x + 2, h - 3);
    }
  };

  React.useEffect(() => { drawCanvas(currentTime); }, [peaks, audioBuffer, audioDuration, midiNotes, selected, folder, isPlaying, zoom, selStart, selEnd, scrollOffset]);
  React.useEffect(() => () => { if (rafRef.current) cancelAnimationFrame(rafRef.current); }, []);

  const commitTempo = (v) => {
    const clamped = Math.max(40, Math.min(300, Math.round(v) || 120));
    setTempo(clamped);
    setTempoText(String(clamped));
    tempoRef.current = clamped;
    try { localStorage.setItem('studio_media_explorer_tempo', String(clamped)); } catch (e) {}
    // Re-schedule a currently playing MIDI preview at the new tempo.
    const cur = selectedRef.current;
    if (isPlayingRef.current && cur && isMidiFile(cur) && (cur.handle || cur.path || cur.file_id || cur.fileId || (cur.kind === 'midi' && cur.name))) {
      selectTokenRef.current++;
      const token = selectTokenRef.current;
      try { if (window.SonicMidiRouter) window.SonicMidiRouter.panic(); else if (window.SonicSF && typeof window.SonicSF.stopAll === 'function') window.SonicSF.stopAll(); } catch (e) {}
      stopAllNativeSfNotes();
      playMidiPreview(cur, token);
    }
  };

  // Center a folder node vertically in the tree pane (feature: file click focuses
  // its parent folder in the middle of the tree).
  const centerTreeNodeInPane = (path) => {
    setTimeout(() => {
      const pane = treePaneRef.current;
      if (!pane) return;
      const el = document.querySelector(`[data-tree-path="${path}"]`);
      if (!el) return;
      const paneRect = pane.getBoundingClientRect();
      const elRect = el.getBoundingClientRect();
      const target = pane.scrollTop + (elRect.top - paneRect.top) - paneRect.height / 2 + elRect.height / 2;
      pane.scrollTo({ top: Math.max(0, target), behavior: 'smooth' });
    }, 80);
  };

  // Flat list of currently visible (expanded) tree nodes, in render order.
  const getVisibleTreePaths = () => {
    const out = [];
    const walk = (entry, depth) => {
      if (!entry || !entry.path) return;
      out.push({ path: entry.path, entry, depth });
      const node = computerTreeRef.current[entry.path];
      if (node && node.expanded && node.dirs) {
        (node.dirs || []).forEach(d => walk(d, depth + 1));
      }
    };
    (computerRootsRef.current || []).forEach(r => walk(r, 0));
    return out;
  };

  const navigateTreeTo = (path) => {
    const paths = getVisibleTreePaths();
    const target = paths.find(p => p.path === path);
    if (!target) return;
    // Expand all ancestor nodes so the target is visible in the tree.
    setComputerTree(prev => {
      const next = { ...prev };
      let current = path;
      while (current) {
        const n = next[current];
        if (n) next[current] = { ...n, expanded: true };
        const idx = current.lastIndexOf('/');
        if (idx <= 0) break;
        current = current.substring(0, idx);
      }
      return next;
    });
    setComputerPath(path);
    if (browseComputerDirRef.current) browseComputerDirRef.current(target.entry);
    centerTreeNodeInPane(path);
  };

  const handleSelect = (f) => {
    if (!f || f.is_dir) return;

    // When a MIDI file is clicked, automatically set the play tempo to the
    // MIDI file's tempo (BPM from metadata, or after parsing in playMidiPreview).
    if (isMidiFile(f) && f.bpm) {
      const v = Math.max(40, Math.min(300, Math.round(parseFloat(f.bpm) || 120)));
      setTempo(v);
      setTempoText(String(v));
      tempoRef.current = v;
      try { localStorage.setItem('studio_media_explorer_tempo', String(v)); } catch (e) {}
    }

    // Find parent path of selected file and scroll it into view in Tree pane
    if (f.path) {
      const lastSlash = f.path.lastIndexOf('/');
      if (lastSlash > 0) {
        const parentPath = f.path.substring(0, lastSlash);
        setComputerPath(parentPath);

        // Expand all parent nodes in computerTree
        setComputerTree(prev => {
          const next = { ...prev };
          let current = parentPath;
          while (current) {
            if (!next[current]) {
              next[current] = { dirs: [], expanded: true };
            } else {
              next[current] = { ...next[current], expanded: true };
            }
            const idx = current.lastIndexOf('/');
            if (idx <= 0) break;
            current = current.substring(0, idx);
          }
          return next;
        });

        // Center the parent folder node in the middle of the tree pane
        centerTreeNodeInPane(parentPath);
      }
    }

    selectTokenRef.current++;
    const token = selectTokenRef.current;
    setSelected(f);
    setCurrentTime(0);
    setPeaks(null);
    setAudioBuffer(null);
    setAudioDuration(0);
    setMidiNotes(null);
    setSelStart(null);
    setSelEnd(null);
    setPreviewCtxMenu(null);
    stopMediaPlayback();
    if (f.kind === 'other') return;
    if (autoPlay) { playSelected(f, token); }
    if (!isMidiFile(f) && (f.path || f.file_id || f.fileId)) loadWaveform(f);
  };

  const renderComputerNode = (entry, depth, isRoot) => {
    const nodePath = entry.path;
    const node = computerTree[nodePath];
    const expanded = node && node.expanded;
    const dirs = node ? node.dirs : [];
    const pad = 12 + depth * 12;
    const fav = isFavorite(entry);
    return (
      <React.Fragment key={nodePath}>
        <div data-tree-path={nodePath} className={`flex items-center gap-1 px-1 py-0.5 cursor-pointer rounded-sm ${computerPath === nodePath ? 'bg-slate-300 text-slate-900' : 'hover:bg-slate-200 text-slate-800'}`}
          style={{ paddingLeft: pad }}
          onClick={() => browseComputerDir(entry)}
          onDoubleClick={e => { e.stopPropagation(); toggleComputerDir(entry); }}
          onContextMenu={e => { e.preventDefault(); e.stopPropagation(); setFavContext(Object.assign({}, entry, { x: e.clientX, y: e.clientY })); }}>
          <i className={`fa-solid ${expanded ? 'fa-minus' : 'fa-plus'} text-[9px] text-slate-500 w-3 text-center shrink-0`} onClick={e => { e.stopPropagation(); toggleComputerDir(entry); }}></i>
          <i className={`fa-solid ${isRoot ? 'fa-hard-drive text-[#6ea8dc]' : 'fa-folder text-[#d9a752]'} shrink-0`}></i>
          <span className="truncate flex-1">{entry.name}</span>
          {fav && <i className="fa-solid fa-star text-amber-500 text-[9px] shrink-0"></i>}
        </div>
        {expanded && dirs.map(d => renderComputerNode(d, depth + 1, false))}
      </React.Fragment>
    );
  };

  const toggleLoop = () => {
    setIsLooping(prev => {
      const next = !prev;
      // Sync ref immediately so playMidiPreview (called below) sees the new value
      isLoopingRef.current = next;
      const cur = selectedRef.current;
      const st = playStateRef.current;
      const sStart = selStartRef.current;
      const sEnd = selEndRef.current;
      const hasSelection = sStart !== null && sEnd !== null && Math.abs(sStart - sEnd) > 0.01;
      if (st && st.source) {
        st.source.loop = next;
        // When enabling loop for a currently playing audio buffer, also update
        // the loop points to the current selection so it loops continuously
        // over the selected region until Stop is pressed.
        if (next && st.source.buffer) {
          if (hasSelection) {
            st.source.loopStart = Math.min(sStart, sEnd);
            st.source.loopEnd = Math.max(sStart, sEnd);
          } else {
            st.source.loopStart = 0;
            st.source.loopEnd = st.source.buffer.duration;
          }
        }
      }
      if (next && isMidiFile(cur)) {
        // Re-schedule loop for the currently previewing MIDI file
        if (cur && (cur.handle || cur.path || cur.file_id || cur.fileId || (cur.kind === 'midi' && cur.name))) {
          selectTokenRef.current++;
          const token = selectTokenRef.current;
          try { if (window.SonicMidiRouter) window.SonicMidiRouter.panic(); else if (window.SonicSF && typeof window.SonicSF.stopAll === 'function') window.SonicSF.stopAll(); } catch (e) {}
          stopAllNativeSfNotes();
          playMidiPreview(cur, token);
        }
      }
      return next;
    });
  };

  const toggleRate = (dir) => setRate(prev => Math.max(0.25, Math.min(4.0, Math.round((prev + dir * 0.1) * 100) / 100)));
  const togglePitch = (dir) => setPitch(prev => Math.max(-24, Math.min(24, prev + dir * 0.5)));

  const selIsMidi = isMidiFile(selected);
  const selDur = fileDuration(selected);
  const selBpm = (selected && selected.bpm) || tempo;

  return (
    <div ref={containerRef} className="flex flex-col w-full h-full text-slate-900 overflow-hidden select-none" style={{ fontFamily: "'Inter', sans-serif" }}>
      {/* 1. TOP NAVIGATION TOOLBAR */}
      <div className="h-8 bg-[#d4d0c8] border-b border-[#808080] px-2 flex items-center justify-between text-xs shrink-0">
        <div className="flex items-center gap-1 flex-1 mr-2">
          <button className="w-5 h-5 bg-[#e0e0e0] hover:bg-white border border-[#707070] rounded-sm flex items-center justify-center text-[10px]" title="Back" onClick={() => folder === 'computer' ? goComputerParent() : setFolder('library')}>
            <i className="fa-solid fa-arrow-left"></i>
          </button>
          <button className="w-5 h-5 bg-[#e0e0e0] hover:bg-white border border-[#707070] rounded-sm flex items-center justify-center text-[10px]" title="Forward" onClick={() => setFolder('uploads')}>
            <i className="fa-solid fa-arrow-right"></i>
          </button>
          <button className="w-5 h-5 bg-[#e0e0e0] hover:bg-white border border-[#707070] rounded-sm flex items-center justify-center text-[10px]" title="Up Directory" onClick={() => folder === 'computer' ? goComputerParent() : setFolder('library')}>
            <i className="fa-solid fa-arrow-up"></i>
          </button>
          <button className="w-5 h-5 bg-[#e0e0e0] hover:bg-white border border-[#707070] rounded-sm flex items-center justify-center text-[10px]" title="Refresh" onClick={() => { if (folder === 'computer') { const node = computerTree[computerPath]; if (node && node.handle) browseComputerDir({ name: computerPath.split('/').pop() || computerPath, path: computerPath, is_dir: true, handle: node.handle }); else if (computerPath) browseComputerDir({ name: computerPath.split(/[\\/]/).pop() || computerPath, path: computerPath, is_dir: true }); } else if (window.SonicAPI && window.SonicAPI.listMyFiles) window.SonicAPI.listMyFiles([]).then(d => setUserFiles(d || [])); }}>
            <i className="fa-solid fa-rotate-right"></i>
          </button>
          <div className="flex-1 flex items-center bg-white border border-[#808080] h-5 px-1">
            <i className="fa-solid fa-folder-open text-[#d9a752] text-[11px] mr-1.5"></i>
            <span className="flex-1 text-xs text-slate-800 truncate">{folder === 'computer' ? (computerPath || 'My Computer') : (folder === 'library' ? 'Media Library' : folder === 'uploads' ? 'Uploads' : 'Processed')}</span>
            <i className="fa-solid fa-caret-down text-slate-600 text-[10px] ml-1"></i>
          </div>
        </div>
        <div className="flex items-center gap-2">
          <div className="flex items-center bg-white border border-[#808080] h-5 px-1 w-40">
            <input type="text" placeholder="Filter/Search..." value={filterText} onChange={e => setFilterText(e.target.value)} className="w-full text-xs outline-none bg-transparent font-sans text-slate-800" />
            <i className="fa-solid fa-caret-down text-slate-600 text-[10px] ml-1"></i>
          </div>
          <button className="px-2 py-0.5 bg-[#e0e0e0] hover:bg-white border border-[#707070] rounded-sm text-[11px] flex items-center gap-1 font-semibold" onClick={() => setViewMode(viewMode === 'details' ? 'list' : 'details')}>
            <span>{viewMode === 'details' ? 'Details' : 'List'}</span> <i className="fa-solid fa-caret-down text-[9px]"></i>
          </button>
        </div>
      </div>

      {/* 2. MIDDLE SPLIT VIEW */}
      <div className="flex-1 flex overflow-hidden min-h-0 relative">
        {/* DIRECTORY TREE */}
        <div className="flex bg-white border border-[#808080] m-1 mr-0 text-xs select-none shrink-0 overflow-hidden">
          <div ref={treePaneRef} className="overflow-y-auto p-1" style={{ width: treeWidth + 'px', minWidth: treeWidth + 'px', maxWidth: treeWidth + 'px' }}>
            <div className="space-y-0.5 font-sans">
            <div className="flex items-center gap-1 px-1 py-0.5 text-slate-700"><span className="w-3"></span> &lt;Track Templates&gt;</div>
            <div className="flex items-center gap-1 px-1 py-0.5 text-slate-700"><span className="w-3"></span> &lt;Project Directory&gt;</div>
            {/* FAVORITED */}
            <div className={`flex items-center gap-1 px-1 py-0.5 cursor-pointer rounded-sm font-bold ${folder === 'computer' && computerPath === 'favorited' ? 'bg-slate-300 text-slate-900' : 'hover:bg-slate-200 text-slate-800'}`} 
              onClick={() => { openFavorited(); setFavoritedExpanded(!favoritedExpanded); }}>
              <i className="fa-solid fa-star text-amber-500 text-[11px] font-bold"></i> Favorited
            </div>
            {favoritedExpanded && (
              <div className="pl-3 space-y-0.5">
                {favorites.map((fav, fi) => (
                  <div key={fav.path + fi}
                    data-tree-path={fav.path}
                    className="flex items-center gap-1 px-1 py-0.5 cursor-pointer rounded-sm hover:bg-amber-100 text-slate-800"
                    style={{ paddingLeft: 20 }}
                    onClick={() => openFavorite(fav)}
                    onContextMenu={e => { e.preventDefault(); e.stopPropagation(); setFavContext(Object.assign({}, fav, { x: e.clientX, y: e.clientY })); }}>
                    <i className="fa-solid fa-folder text-[#d9a752] shrink-0"></i>
                    <span className="truncate flex-1">{fav.name}</span>
                    <i className="fa-solid fa-star text-amber-500 text-[9px] shrink-0"></i>
                  </div>
                ))}
                {favorites.length === 0 && (
                  <div className="pl-4 py-0.5 text-slate-400 italic text-[11px]">No favorites</div>
                )}
              </div>
            )}

            {/* MY COMPUTER */}
            <div className={`flex items-center gap-1 px-1 py-0.5 cursor-pointer rounded-sm ${folder === 'computer' && computerPath !== 'favorited' ? 'bg-slate-300 text-slate-900 font-semibold' : 'hover:bg-slate-200 text-slate-800'}`} onClick={openMyComputer}>
              <i className="fa-solid fa-computer text-[11px] text-slate-600"></i> My Computer
            </div>
            {folder === 'computer' && computerPath !== 'favorited' && computerRoots && (
              <div className="pl-3 space-y-0.5">
                {computerRoots.map(root => renderComputerNode(root, 0, true))}
              </div>
            )}

            <div className={`flex items-center gap-1 px-1 py-0.5 cursor-pointer font-semibold rounded-sm ${folder === 'library' ? 'bg-slate-300 text-slate-900' : 'hover:bg-slate-200 text-slate-800'}`} onClick={() => setFolder('library')}>
              <i className="fa-solid fa-folder-open text-[#d9a752]"></i> Media Library
            </div>
            <div className="pl-3 space-y-0.5">
              <div className={`flex items-center gap-1 px-1 py-0.5 cursor-pointer pl-4 rounded-sm ${folder === 'uploads' ? 'bg-slate-300 text-slate-900' : 'hover:bg-slate-200 text-slate-800'}`} onClick={() => setFolder('uploads')}>
                <i className="fa-solid fa-folder text-[#d9a752]"></i> Uploads
              </div>
              <div className={`flex items-center gap-1 px-1 py-0.5 cursor-pointer pl-4 rounded-sm ${folder === 'processed' ? 'bg-slate-300 text-slate-900' : 'hover:bg-slate-200 text-slate-800'}`} onClick={() => setFolder('processed')}>
                <i className="fa-solid fa-folder text-[#d9a752]"></i> Processed
              </div>
            </div>
          </div>
          </div>
          <div className="w-1.5 cursor-ew-resize hover:bg-blue-500/40 active:bg-blue-500/60 transition-colors shrink-0"
            onMouseDown={startTreeResize}
            title="Kéo để thay đổi chiều rộng"></div>
        </div>

        {/* CONTEXT MENU (right-click folder → Favorite) */}
        {favContext && (
          <div className="fixed z-[100] bg-white border border-[#808080] shadow-lg rounded-sm text-xs font-sans text-slate-800 min-w-[180px]"
            style={{ left: favContext.x, top: favContext.y }}
            onMouseLeave={() => setFavContext(null)}>
            <div className={`px-2 py-1 cursor-pointer hover:bg-blue-100 flex items-center gap-1.5 ${isFavorite(favContext) ? 'text-amber-700' : ''}`}
              onClick={() => { toggleFavorite(favContext); setFavContext(null); }}>
              <i className={`fa-solid ${isFavorite(favContext) ? 'fa-star text-amber-500' : 'fa-star text-slate-400'} text-[11px]`}></i>
              {isFavorite(favContext) ? 'Gỡ khỏi Favorited' : 'Thêm vào Favorited'}
            </div>
            <div className="px-2 py-1 cursor-pointer hover:bg-blue-100 flex items-center gap-1.5" onClick={() => { if (favContext) browseComputerDir(favContext); setFavContext(null); }}>
              <i className="fa-solid fa-folder-open text-[#d9a752] text-[11px]"></i> Mở thư mục
            </div>
          </div>
        )}

        {/* FILE LIST */}
        <div className="flex-1 bg-white border border-[#808080] m-1 overflow-y-auto relative">
          <table className="w-full text-xs text-left border-collapse" style={{ tableLayout: 'fixed' }}>
            <thead className="sticky top-0 bg-[#e0e0e0] border-b border-[#a0a0a0] text-slate-800 font-semibold select-none shadow-sm z-10">
              <tr>
                <th className="py-1 px-2 border-r border-[#b0b0b0] relative" style={viewMode === 'details' ? { width: colWidths.file } : undefined}>
                  <div className="truncate pr-2">File</div>
                  <div className="absolute right-0 top-0 bottom-0 w-1.5 cursor-col-resize hover:bg-slate-400"
                    onMouseDown={e => startColResize('file', e)}></div>
                </th>
                {viewMode === 'details' && (
                  <>
                    <th className="py-1 px-2 border-r border-[#b0b0b0] relative" style={{ width: colWidths.size }}>
                      <div className="truncate pr-2">Size</div>
                      <div className="absolute right-0 top-0 bottom-0 w-1.5 cursor-col-resize hover:bg-slate-400"
                        onMouseDown={e => startColResize('size', e)}></div>
                    </th>
                    <th className="py-1 px-2 border-r border-[#b0b0b0] relative" style={{ width: colWidths.type }}>
                      <div className="truncate pr-2">Type</div>
                      <div className="absolute right-0 top-0 bottom-0 w-1.5 cursor-col-resize hover:bg-slate-400"
                        onMouseDown={e => startColResize('type', e)}></div>
                    </th>
                  </>
                )}
              </tr>
            </thead>
            <tbody className="font-sans text-slate-800">
              {visibleFiles.map((f, i) => {
                const isSel = selected && (selected.name || selected.file_id) === (f.name || f.file_id);
                const isMidi = isMidiFile(f);
                const icon = f.is_dir ? 'fa-folder text-[#d9a752]' : (isMidi ? 'fa-music text-purple-600' : (f.kind === 'audio' ? 'fa-file-audio text-emerald-600' : 'fa-file text-zinc-500'));
                return (
                  <tr key={(f.path || f.file_id || f.name) + i}
                    draggable={!f.is_dir}
                    onDragStart={e => {
                      if (f.is_dir) { e.preventDefault(); return; }
                      e.dataTransfer.setData('text/plain', f.name || f.original_name || '');
                      e.dataTransfer.effectAllowed = 'copy';
                      window.__mediaExplorerDragFile = f;
                    }}
                    onDragEnd={() => { window.__mediaExplorerDragFile = null; }}
                    className={`cursor-pointer hover:bg-blue-100 ${isSel ? 'file-row-selected' : ''}`}
                    onClick={() => f.is_dir ? browseComputerDir(f) : handleSelect(f)}
                    onDoubleClick={() => f.is_dir && browseComputerDir(f)}
                    onContextMenu={e => {
                      if (f.is_dir) {
                        e.preventDefault();
                        e.stopPropagation();
                        setFavContext(Object.assign({}, f, { x: e.clientX, y: e.clientY }));
                      }
                    }}>
                    <td className="py-1 px-2 truncate"><i className={`fa-solid ${icon} mr-2`}></i>{f.name || f.original_name}</td>
                    {viewMode === 'details' && <td className="py-1 px-2 truncate">{f.size_mb != null ? f.size_mb.toFixed(2) + ' MB' : (isMidi ? (f.tpqn || 'MIDI') + ' TPQN' : '-')}</td>}
                    {viewMode === 'details' && <td className="py-1 px-2 truncate">{f.is_dir ? 'Folder' : (isMidi ? 'MIDI' : (f.kind === 'audio' ? 'Audio' : 'File'))}</td>}
                  </tr>
                );
              })}
              {visibleFiles.length === 0 && (
                <tr><td className="py-3 px-2 text-slate-400 italic" colSpan={viewMode === 'details' ? 3 : 1}>No files</td></tr>
              )}
            </tbody>
          </table>
        </div>
      </div>

      {/* 3. BOTTOM PREVIEW & TRANSPORT */}
      <div className="h-[38%] min-h-[110px] bg-[#d4d0c8] border-t border-[#808080] p-1.5 flex flex-col justify-between text-xs shrink-0 select-none">
        {/* CONTROLS ROW */}
        <div className="flex items-center justify-between gap-2">
          <div className="flex items-center gap-1">
            <button id="btnStop" className="w-6 h-6 bg-[#e0e0e0] hover:bg-white border border-[#707070] rounded-sm flex items-center justify-center text-slate-800" title="Stop" onClick={stopMediaPlayback}>
              <i className="fa-solid fa-square text-[10px]"></i>
            </button>
            <button id="btnPlay" className="w-6 h-6 bg-[#e0e0e0] hover:bg-white border border-[#707070] rounded-sm flex items-center justify-center text-emerald-700 font-bold" title="Play" onClick={() => isPlaying ? togglePause() : playSelected(selected)}>
              <i className={`fa-solid ${isPlaying && !isPaused ? 'fa-play' : 'fa-play'} text-xs`}></i>
            </button>
            <button id="btnPause" className="w-6 h-6 bg-[#e0e0e0] hover:bg-white border border-[#707070] rounded-sm flex items-center justify-center text-amber-700" title="Pause" onClick={togglePause}>
              <i className="fa-solid fa-pause text-xs"></i>
            </button>
            <button id="btnLoop" className={`w-6 h-6 border rounded-sm flex items-center justify-center text-xs ${isLooping ? 'bg-cyan-600 text-white border-cyan-700' : 'bg-[#e0e0e0] hover:bg-white text-slate-700 border-[#707070]'}`} title="Loop / Repeat" onClick={toggleLoop}>
              <i className="fa-solid fa-rotate-right"></i>
            </button>
            <button id="btnAutoPlay" className={`h-6 px-2 border rounded-sm font-bold text-[10px] flex items-center gap-1 ${autoPlay ? 'bg-gradient-to-r from-cyan-600 to-emerald-600 text-white border-slate-700' : 'bg-[#e0e0e0] text-slate-600 border-[#707070]'}`} onClick={() => setAutoPlay(p => !p)}>
              <i className="fa-solid fa-bolt text-[9px]"></i> <span>Auto-Play</span>
            </button>
            <div className="relative">
              <button id="btnSynth" className={`h-6 px-2 border rounded-sm font-bold text-[10px] flex items-center gap-1 ${synthInst ? 'bg-gradient-to-r from-violet-600 to-fuchsia-600 text-white border-slate-700' : 'bg-[#e0e0e0] text-slate-600 border-[#707070]'}`} title="Chọn instrument để preview MIDI" onClick={toggleSynthDropdown}>
                <i className="fa-solid fa-music text-[9px]"></i> <span>Synth{synthInst ? ': ' + (synthInst.name || '?') : ''}</span>
                <i className="fa-solid fa-caret-down text-[8px]"></i>
              </button>
              {synthOpen && (
                <div className="absolute bottom-8 left-0 z-40 w-64 bg-white border border-[#808080] shadow-xl rounded-sm text-xs text-slate-800 font-sans max-h-72 overflow-y-auto">
                  <div className="sticky top-0 bg-[#e0e0e0] px-2 py-1 font-bold border-b border-[#a0a0a0] flex items-center justify-between z-20">
                    <span>Select Instrument</span>
                    <button onClick={() => setSynthOpen(false)} className="text-slate-500 hover:text-slate-900"><i className="fa-solid fa-xmark"></i></button>
                  </div>
                  {/* SEARCH INPUT */}
                  <div className="sticky top-[23px] bg-white p-1 border-b border-[#c0c0c0] z-20 flex items-center gap-1">
                    <i className="fa-solid fa-magnifying-glass text-slate-400 pl-1 text-[10px]"></i>
                    <input 
                      type="text" 
                      placeholder="Tìm nhạc cụ..." 
                      value={synthFilter}
                      onChange={e => setSynthFilter(e.target.value)}
                      onClick={e => e.stopPropagation()}
                      className="w-full px-1 py-0.5 border border-[#c0c0c0] rounded-sm text-xs font-sans focus:outline-none focus:border-blue-500" 
                    />
                    {synthFilter && (
                      <button 
                        onClick={(e) => { e.stopPropagation(); setSynthFilter(''); }} 
                        className="text-slate-400 hover:text-slate-700 px-1"
                      >
                        <i className="fa-solid fa-xmark text-[10px]"></i>
                      </button>
                    )}
                  </div>
                  {synthLoading && <div className="px-2 py-2 text-slate-400 italic">Loading...</div>}
                  {!synthLoading && (!synthList || synthList.length === 0) && (
                    <div className="px-2 py-2 text-slate-400 italic">Không có SoundFont nào</div>
                  )}
                  <div className={`flex items-center gap-1 px-2 py-1 cursor-pointer hover:bg-blue-100 ${!synthInst ? 'bg-slate-200' : ''}`} onClick={() => selectSynthInst(null)}>
                    <i className="fa-solid fa-ban text-slate-400"></i> None (mặc định)
                  </div>
                  {!synthLoading && filteredSynthList && filteredSynthList.map(group => (
                    <div key={group.sf.id || group.sf.name}>
                      <div className="px-2 py-1 bg-[#f0f0f0] font-semibold text-slate-600 border-t border-[#e0e0e0] truncate">{group.sf.display || group.sf.name || group.sf.id}</div>
                      {(group.presets || []).slice(0, 200).map((p, pi) => {
                        const progId = p.id || p.name || ('preset_' + pi);
                        return (
                          <div key={progId} className={`flex items-center gap-1 px-2 pl-4 py-0.5 cursor-pointer hover:bg-blue-100 truncate ${synthInst && synthInst.program === p.program && synthInst.sfId === (group.sf.id || group.sf.name) ? 'bg-slate-200' : ''}`}
                            onClick={() => selectSynthInst({ sfId: group.sf.id, sfName: group.sf.display || group.sf.name || group.sf.id, bank: p.bank || 0, program: p.program, name: p.name || ('Program ' + p.program) })}>
                            {p.bank === 128 ? <i className="fa-solid fa-drum text-slate-400"></i> : <i className="fa-solid fa-music text-slate-400"></i>} {p.name || ('Program ' + p.program)}
                          </div>
                        );
                      })}
                    </div>
                  ))}
                  {!synthLoading && filteredSynthList && filteredSynthList.length === 0 && synthFilter && (
                    <div className="px-2 py-2 text-slate-400 italic">Không tìm thấy nhạc cụ trùng khớp</div>
                  )}
                </div>
              )}
            </div>
            <div className="flex items-center gap-1" title="Tempo preview MIDI">
              <span className="font-mono text-[10px] text-slate-700">Tempo:</span>
              <button className="px-1 bg-[#e0e0e0] hover:bg-white border border-[#707070] rounded-sm text-[10px]" onClick={() => commitTempo((tempo || 120) - 1)}>-</button>
              <div className="bg-white border border-[#808080] px-1 h-5 flex items-center w-14"><input type="number" min="40" max="300" value={tempoText} onChange={e => { const raw = e.target.value; setTempoText(raw); const n = parseInt(raw); if (n >= 40 && n <= 300) commitTempo(n); }} onBlur={() => { const n = parseInt(tempoText); commitTempo(n); }} onKeyDown={e => { if (e.key === 'Enter') { e.currentTarget.blur(); } }} className="w-full text-xs text-right outline-none bg-transparent" /></div>
              <button className="px-1 bg-[#e0e0e0] hover:bg-white border border-[#707070] rounded-sm text-[10px]" onClick={() => commitTempo((tempo || 120) + 1)}>+</button>
              <span className="text-slate-600 text-[10px]">BPM</span>
            </div>
          </div>
          <div className="flex items-center gap-3 font-mono text-[11px]">
            <div className="flex items-center gap-1">
              <span>Pitch:</span>
              <button className="px-1 bg-[#e0e0e0] hover:bg-white border border-[#707070] rounded-sm text-[10px]" onClick={() => togglePitch(-0.5)}>-</button>
              <div className="bg-white border border-[#808080] px-1 h-5 flex items-center w-14"><input type="number" value={pitch.toFixed(1)} step="0.5" onChange={e => setPitch(parseFloat(e.target.value) || 0)} className="w-full text-xs text-right outline-none bg-transparent" /></div>
              <button className="px-1 bg-[#e0e0e0] hover:bg-white border border-[#707070] rounded-sm text-[10px]" onClick={() => togglePitch(0.5)}>+</button>
            </div>
            <div className="flex items-center gap-1">
              <span>Rate:</span>
              <div className="bg-white border border-[#808080] px-1 h-5 flex items-center w-12"><input type="number" value={rate.toFixed(2)} step="0.1" onChange={e => setRate(Math.max(0.25, Math.min(4, parseFloat(e.target.value) || 1)))} className="w-full text-xs text-right outline-none bg-transparent" /></div>
              <button className="w-5 h-5 bg-[#e0e0e0] hover:bg-white border border-[#707070] rounded-sm text-[9px]" onClick={() => toggleRate(-1)}>-</button>
              <button className="w-5 h-5 bg-[#e0e0e0] hover:bg-white border border-[#707070] rounded-sm text-[9px]" onClick={() => toggleRate(1)}>+</button>
            </div>
          </div>
          <div className="flex items-center gap-2">
            <span className="font-sans text-slate-700">Volume:</span>
            <input type="range" min="-60" max="12" step="0.5" value={volumeDb} onChange={e => setVolumeDb(parseFloat(e.target.value))} className="me-fader-slider w-24" />
            <div className="bg-white border border-[#808080] px-1 h-5 flex items-center justify-center w-14 font-mono text-[11px]">{volumeDb <= -50 ? '-inf' : volumeDb.toFixed(1)} dB</div>
          </div>
          <div className={`px-2 py-0.5 border font-mono font-bold text-[10px] rounded-sm ${selIsMidi ? 'bg-purple-950 text-purple-300 border-purple-800' : 'bg-emerald-950 text-emerald-300 border-emerald-800'}`}>
            {selIsMidi ? 'MIDI' : 'Audio'}
          </div>
        </div>

        {/* CANVAS + METADATA */}
        <div className="flex items-stretch gap-2 my-1 flex-1 min-h-0">
          <div className="flex-1 bg-[#181818] border border-[#3a3a3a] relative overflow-hidden p-0.5">
            <canvas 
              ref={canvasRef} 
              className="w-full h-full block cursor-pointer"
              onMouseDown={handleCanvasMouseDown}
              onMouseMove={handleCanvasMouseMove}
              onMouseUp={handleCanvasMouseUp}
              onContextMenu={handleCanvasContextMenu}
            ></canvas>
            
            {/* FLOATING ZOOM CONTROLS */}
            <div className="absolute top-1.5 right-1.5 flex gap-1 z-10">
              <button 
                className="w-5 h-5 bg-[#2a2a2a] hover:bg-slate-700 border border-[#444] rounded flex items-center justify-center text-white text-xs font-bold select-none cursor-pointer" 
                title="Zoom In" 
                onClick={() => setZoom(prev => Math.min(10.0, prev * 1.25))}
              >
                +
              </button>
              <button 
                className="w-5 h-5 bg-[#2a2a2a] hover:bg-slate-700 border border-[#444] rounded flex items-center justify-center text-white text-xs font-bold select-none cursor-pointer" 
                title="Zoom Out" 
                onClick={() => setZoom(prev => Math.max(0.2, prev / 1.25))}
              >
                -
              </button>
              <button 
                className="w-5 h-5 bg-[#2a2a2a] hover:bg-slate-700 border border-[#444] rounded flex items-center justify-center text-white text-[9px] select-none cursor-pointer" 
                title="Reset Zoom" 
                onClick={() => setZoom(1.0)}
              >
                1x
              </button>
            </div>

            {/* PREVIEW CONTEXT MENU */}
            {previewCtxMenu && (
              <div 
                className="fixed bg-[#1e1e24] border border-[#3e3e4a] rounded shadow-md z-[9999] py-1 font-sans text-xs text-slate-300 w-32 cursor-pointer select-none"
                style={{ top: previewCtxMenu.y, left: previewCtxMenu.x }}
                onClick={(e) => e.stopPropagation()}
              >
                <div 
                  className="px-3 py-1.5 hover:bg-slate-700 hover:text-white flex items-center gap-2"
                  onClick={() => {
                    handleCopySelection();
                    setPreviewCtxMenu(null);
                  }}
                >
                  <i className="fa-solid fa-copy"></i> Copy
                </div>
                <div 
                  className="px-3 py-1.5 hover:bg-slate-700 hover:text-white border-t border-[#3e3e4a] flex items-center gap-2"
                  onClick={() => setPreviewCtxMenu(null)}
                >
                  <i className="fa-solid fa-xmark"></i> Cancel
                </div>
              </div>
            )}
          </div>
          <div className="w-48 bg-[#181818] border border-[#3a3a3a] p-1.5 font-mono text-[10px] text-slate-300 leading-tight overflow-y-auto shrink-0">
            {selected ? (
              selIsMidi && !selected.path ? (
                <React.Fragment>
                  <div>{selected.events} MIDI events</div>
                  <div>Length: {selected.lengthQn} quarter notes</div>
                  <div>Length: {selected.time} (est)</div>
                  <div>Ticks per quarter note: {selected.tpqn}</div>
                </React.Fragment>
              ) : (
                <React.Fragment>
                  <div>Size: {selected.size_mb != null ? selected.size_mb.toFixed(2) + ' MB' : '-'}</div>
                  {selIsMidi ? (
                    <React.Fragment>
                      <div>Bars: {midiBars || 1}</div>
                      <div>Beats: {Math.round(midiTotalBeats || 16)}</div>
                      <div>BPM: {midiFileBpm || 120}</div>
                      <div>Duration: {selDur.toFixed(2)}s</div>
                    </React.Fragment>
                  ) : (
                    <React.Fragment>
                      <div>Duration: {selDur.toFixed(2)}s</div>
                      <div>Sample Rate: 44100 Hz</div>
                      <div>Type: {selected.path ? (selected.kind === 'other' ? 'Local File' : 'Local Audio') : (selected.type || 'Audio')}</div>
                    </React.Fragment>
                  )}
                </React.Fragment>
              )
            ) : <div>No file selected</div>}
          </div>
        </div>

        {/* FOOTER STATUS BAR */}
        <div className="h-5 bg-[#c0c0c0] border-t border-white flex items-center justify-between text-[11px] font-mono px-1 shrink-0">
          <div className="flex items-center gap-3">
            {selIsMidi && midiNotes && midiNotes.length ? (
              <div className="bg-white border border-[#808080] px-1.5 text-slate-900 font-bold">
                Bar {Math.max(1, Math.floor(currentTime / (4 * 60 / (tempo || 120))) + 1)} / {midiBars || 1}
                <span className="text-slate-500 ml-1">| {formatTime(currentTime)} / {formatTime(selDur)}</span>
              </div>
            ) : (
              <div className="bg-white border border-[#808080] px-1.5 text-slate-900 font-bold">{formatTime(currentTime)} / {formatTime(selDur)}</div>
            )}
          </div>
          <div className="text-slate-800 font-bold truncate max-w-[40%]">{selected ? (selected.name || selected.original_name) : 'No file selected'}</div>
          <div className="text-slate-700">{selBpm} bpm x{rate.toFixed(2)}</div>
        </div>
      </div>
    </div>
  );
};

const App = () => {
  // ── State Definitions ──
  const [tracks, setTracks] = useState([{
    id: '1',
    name: 'Track 01',
    buffer: null,
    startTime: 0,
    volumeDb: 0,
    pan: 0,
    muted: false,
    solo: false,
    color: '#0f766e',
    markers: [],
    serverFileId: null,
    clips: [],
    sections: [],
    midiItems: [],
    isArmed: false,
    monitoringEnabled: true,
    inputSource: { deviceType: 'NONE', deviceId: '' }
  }, {
    id: '2',
    name: 'Track 02',
    buffer: null,
    startTime: 0,
    volumeDb: 0,
    pan: 0,
    muted: false,
    solo: false,
    color: '#1d4ed8',
    markers: [],
    serverFileId: null,
    clips: [],
    sections: [],
    midiItems: [],
    isArmed: false,
    monitoringEnabled: true,
    inputSource: { deviceType: 'NONE', deviceId: '' }
  }]);
  const [appWarningModal, setAppWarningModal] = useState(null);
  const [bpm, setBpm] = useState(localStorage.getItem('studio_bpm') || '120');
  const prevBpmRef = useRef(bpm);
  const [draggedClip, setDraggedClip] = useState(null);
  const [hoveredTrackId, setHoveredTrackId] = useState(null);
  // Recalculate item/section/selection durations when BPM changes
  useEffect(() => {
    const oldSpb = prevBpmRef.current ? (60.0 / parseFloat(prevBpmRef.current)) * 4 : null;
    const bpmVal = parseFloat(bpm) || 120;
    const secondsPerBar = (60.0 / bpmVal) * 4;
    // Recalculate range loop selection to maintain bar count (tempo mode only)
    if (oldSpb && selectionFollowsTempo && selectionStart !== null && selectionEnd !== null && selectionEnd > selectionStart) {
      const startBar = selectionStart / oldSpb;
      const endBar = selectionEnd / oldSpb;
      if (endBar - startBar > 0.01) {
        setSelectionStart(startBar * secondsPerBar);
        setSelectionEnd(endBar * secondsPerBar);
      }
    }
    prevBpmRef.current = bpm;
    // Force canvas redraw
    setCanvasRedrawCount(n => n + 1);
    // Recalculate item/section durations
    updateActiveTracks(prev => prev.map(t => ({
      ...t,
      midiItems: (t.midiItems || []).map(m => {
        if (m.length_bars) return { ...m, duration: m.length_bars * secondsPerBar };
        if (m.duration) {
          // Legacy item without length_bars: compute bars from current duration/BPM
          const bars = Math.max(0.25, Math.round(m.duration / secondsPerBar * 4) / 4);
          return { ...m, length_bars: bars, duration: bars * secondsPerBar };
        }
        return m;
      }),
      sections: (t.sections || []).map(s => {
        if (s.length_bars) return { ...s, duration: s.length_bars * secondsPerBar };
        if (s.duration) {
          const bars = Math.max(0.25, Math.round(s.duration / secondsPerBar * 4) / 4);
          return { ...s, length_bars: bars, duration: bars * secondsPerBar };
        }
        return s;
      })
    })));
  }, [bpm]);
  // LAN access badge (standalone): server bind 0.0.0.0 — client browser ở máy
  // khác trên mạng mở app qua http://<LAN-IP>:port. Hiện URL trên máy chủ;
  // client LAN thấy origin đang dùng.
  useEffect(() => {
    let mounted = true;
    let bar = null;
    let iv = null;
    const tryShow = () => {
      const caps = window.SonicRuntime && window.SonicRuntime.capabilities;
      const lan = caps && caps.lan;
      if (!lan || !lan.urls || !lan.urls.length) return false;
      if (document.getElementById('sf-lan-bar')) return true;
      const hostname = window.location.hostname;
      const isLocal = hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1';
      bar = document.createElement('div');
      bar.id = 'sf-lan-bar';
      bar.style.cssText = 'position:fixed;top:8px;right:12px;z-index:9999;background:rgba(16,185,129,0.12);border:1px solid #10b981;color:#a7f3d0;font-size:11px;line-height:1.5;padding:6px 10px;border-radius:8px;max-width:min(70vw,420px);cursor:default;';
      bar.title = 'Client browser ở máy khác trên LAN mở địa chỉ này để làm việc realtime với máy chạy SonicForge';
      bar.innerHTML = isLocal
        ? '<b>LAN:</b> ' + lan.urls.map(function (u) { return '<a href="' + u + '" target="_blank" style="color:#a7f3d0;text-decoration:underline">' + u + '</a>'; }).join(' &nbsp;·&nbsp; ')
        : '<b>Kết nối:</b> ' + window.location.origin;
      document.body.appendChild(bar);
      return true;
    };
    if (!tryShow()) {
      iv = setInterval(function () { if (mounted && tryShow()) clearInterval(iv); }, 700);
      setTimeout(function () { if (iv) clearInterval(iv); }, 20000);
    }
    return () => { mounted = false; if (iv) clearInterval(iv); if (bar && bar.parentNode) bar.parentNode.removeChild(bar); };
  }, []);
  const openPanel = id => {
    if (id === 'export') setShowExportPanel(true); else if (id === 'ai') setShowAIPanel(true); else if (id === 'python_tools') setShowPythonToolsPanel(true); else if (id === 'selection') setShowSelectionPanel(true);
  };
  const [instrumentSelectorTrackId, setInstrumentSelectorTrackId] = useState(null);
  const [synthTrackDropdownId, setSynthTrackDropdownId] = useState(null);
  const [fxSelectorTrackId, setFxSelectorTrackId] = useState(null);
  const handleSetTrackFx = (trackId, fxType) => {
    updateActiveTracks(prev => prev.map(t => t.id === trackId ? { ...t, fxType } : t));
    setFxSelectorTrackId(null);
    setTimeout(() => lucide.createIcons(), 50);
  };
  const [instrumentSelectorData, setInstrumentSelectorData] = useState(null);
  const [instrumentRefreshKey, setInstrumentRefreshKey] = useState(0);
  const openInstrumentSelector = trackId => {
    setInstrumentSelectorTrackId(trackId);
    setSfPresetSearchQuery('');
    setInstrumentRefreshKey(k => k + 1);
    setSfPresets(null);
    // Fetch instruments fresh from API for all soundfonts
    window.SonicAPI.listPlugins().then(async data => {
      var sfonts = data.soundfonts || [];
      if (sfonts.length === 0) { setSfPresets([]); return; }
      try {
        var results = await Promise.all(sfonts.map(function(sf) {
          var sfId = sf.id.replace('sf_', '');
          return window.SonicAPI.listSoundfontInstruments(sfId)
            .then(function(r) { return { sf: sf, presets: r.presets || [] }; })
            .catch(function() { return { sf: sf, presets: [] }; });
        }));
        var all = [];
        results.forEach(function(r) {
          var sf = r.sf;
          var sfId = sf.id.startsWith('sf_') ? sf.id : 'sf_' + sf.id;
          var sfName = sf.display || sf.name || sf.id;
          (r.presets || []).forEach(function(p) {
            all.push({ ...p, _sfId: sfId, _sfName: sfName, _sfDisplay: sfName.substring(0, 30) });
          });
        });
        setSfPresets(all.length > 0 ? all : []);
      } catch(e) { setSfPresets([]); }
    }).catch(function() { setSfPresets([]); });
  };
  const closeInstrumentSelector = () => {
    setInstrumentSelectorTrackId(null);
    setSynthCategory(null);
    setSelectedSoundFontId(null);
    setSfPresetSearchQuery('');
  };
  const [synthCategory, setSynthCategory] = useState(null); // 'vst' | 'soundfont'
  const [selectedSoundFontId, setSelectedSoundFontId] = useState(null);
  const [sfPresets, setSfPresets] = useState(null); // presets from SoundFont
  const [instrumentDropdownTrackId, setInstrumentDropdownTrackId] = useState(null);
  const [instrumentDropdownBtnRect, setInstrumentDropdownBtnRect] = useState(null);
  const [instrumentSearchQuery, setInstrumentSearchQuery] = useState('');
  const [sfPresetSearchQuery, setSfPresetSearchQuery] = useState('');
  const filteredInstruments = useMemo(() => {
    if (!instrumentSelectorData || !instrumentSearchQuery) return { soundfonts: instrumentSelectorData?.soundfonts || [], vst: instrumentSelectorData?.vst_instruments || [] };
    const q = instrumentSearchQuery.toLowerCase();
    return {
      soundfonts: (instrumentSelectorData.soundfonts || []).filter(sf => (sf.display || sf.name || sf.id).toLowerCase().includes(q)),
      vst: (instrumentSelectorData.vst_instruments || []).filter(v => (v.name || v.id).toLowerCase().includes(q))
    };
  }, [instrumentSearchQuery, instrumentSelectorData]);
  useEffect(() => {
    window.SonicAPI.listPlugins().then(async data => {
      try {
        const catResp = await window.SonicAPI.getSoundfontCatalog?.() ?? await fetch('/api/v1/plugins/soundfonts/catalog').then(r => r.json());
        const catalog = catResp.full_catalog || {};
        data.soundfonts = (data.soundfonts || []).map(sf => {
          const sfId = sf.id.replace('sf_', '');
          const catEntry = catalog[sfId.toLowerCase()] || catalog[sfId];
          if (catEntry && catEntry.instruments) {
            return { ...sf, presets: catEntry.instruments };
          }
          return sf;
        });
      } catch (e) { console.warn('Catalog fetch error:', e); }
      setInstrumentSelectorData(data);
    }).catch(() => {});
  }, [instrumentRefreshKey]);
  useEffect(() => {
    if (!instrumentDropdownTrackId) return;
    const handler = e => {
      setInstrumentDropdownTrackId(null);
      setInstrumentDropdownBtnRect(null);
      setInstrumentSearchQuery('');
    };
    document.addEventListener('click', handler);
    return () => document.removeEventListener('click', handler);
  }, [instrumentDropdownTrackId]);
  const GM_INSTRUMENTS = [
    "Acoustic Grand Piano","Bright Acoustic Piano","Electric Grand Piano","Honky-tonk Piano","Electric Piano 1","Electric Piano 2","Harpsichord","Clavi",
    "Celesta","Glockenspiel","Music Box","Vibraphone","Marimba","Xylophone","Tubular Bells","Dulcimer",
    "Drawbar Organ","Percussive Organ","Rock Organ","Church Organ","Reed Organ","Accordion","Harmonica","Tango Accordion",
    "Acoustic Guitar (nylon)","Acoustic Guitar (steel)","Electric Guitar (jazz)","Electric Guitar (clean)","Electric Guitar (muted)","Overdriven Guitar","Distortion Guitar","Guitar harmonics",
    "Acoustic Bass","Electric Bass (finger)","Electric Bass (pick)","Fretless Bass","Slap Bass 1","Slap Bass 2","Synth Bass 1","Synth Bass 2",
    "Violin","Viola","Cello","Contrabass","Tremolo Strings","Pizzicato Strings","Orchestral Harp","Timpani",
    "String Ensemble 1","String Ensemble 2","Synth Strings 1","Synth Strings 2","Choir Aahs","Voice Oohs","Synth Voice","Orchestra Hit",
    "Trumpet","Trombone","Tuba","Muted Trumpet","French Horn","Brass Section","Synth Brass 1","Synth Brass 2",
    "Soprano Sax","Alto Sax","Tenor Sax","Baritone Sax","Oboe","English Horn","Bassoon","Clarinet",
    "Piccolo","Flute","Recorder","Pan Flute","Blown Bottle","Shakuhachi","Whistle","Ocarina",
    "Lead 1 (square)","Lead 2 (sawtooth)","Lead 3 (calliope)","Lead 4 (chiff)","Lead 5 (charang)","Lead 6 (voice)","Lead 7 (fifths)","Lead 8 (bass+lead)",
    "Pad 1 (new age)","Pad 2 (warm)","Pad 3 (polysynth)","Pad 4 (choir)","Pad 5 (bowed)","Pad 6 (metallic)","Pad 7 (halo)","Pad 8 (sweep)",
    "FX 1 (rain)","FX 2 (soundtrack)","FX 3 (crystal)","FX 4 (atmosphere)","FX 5 (brightness)","FX 6 (goblins)","FX 7 (echoes)","FX 8 (sci-fi)",
    "Sitar","Banjo","Shamisen","Koto","Kalimba","Bag pipe","Fiddle","Shanai",
    "Tinkle Bell","Agogo","Steel Drums","Woodblock","Taiko Drum","Melodic Tom","Synth Drum","Reverse Cymbal",
    "Guitar Fret Noise","Breath Noise","Seashore","Bird Tweet","Telephone Ring","Helicopter","Applause","Gunshot"
  ];
  // D7: bridge active → load instrument vào bridge tại channel router dùng cho
  // track (SonicMidiRouter.allocateChannel — cùng allocation scheduleMidiNoteDispatch/
  // keybed dùng khi push NOTE_ON). Không load → MIDI tới channel rỗng → C++ silent.
  const loadTrackInstrumentToBridge = (trackId, instrumentId, bank) => {
    if (!window.SonicMidiRouter || !window.SonicMidiRouter.isBridgeActive() || !instrumentId) return;
    const isSf = typeof instrumentId === 'string' && instrumentId.startsWith('sf_');
    const bch = window.SonicMidiRouter.allocateChannel(trackId, bank === 128);
    // V9 GUI-button bug: switch VSTi → SF2 phải xóa flag "đã load VST" của
    // channel, nếu không ensureAndOpenVstGui sẽ bỏ qua load khi chọn lại VST
    // (flag cũ còn true) → OPEN_GUI đến bridge khi channel chưa có instrument
    // → deferred mãi, nút GUI không mở gì.
    if (isSf && window.__bridgeLoadedChannels) delete window.__bridgeLoadedChannels[bch];
    const btype = isSf ? 'SF2' : 'VST3';
    let bpath = null;
    if (instrumentSelectorData) {
      if (isSf) {
        const sid = instrumentId.replace('sf_', '');
        const s0 = (instrumentSelectorData.soundfonts || []).find(s => String(s.id).replace('sf_', '') === sid);
        bpath = s0 && (s0.file || s0.path);
      } else {
        const v0 = (instrumentSelectorData.vst_instruments || []).find(v => v.id === instrumentId || v.name === instrumentId);
        bpath = v0 && v0.path;
      }
    }
    (async () => {
      let p = bpath;
      try {
        const r = await window.SonicAPI.bridgeLoad({ name: instrumentId, path: bpath || null, instrumentType: btype, channel: bch });
        if (r && r.path) p = r.path;
      } catch (e) { console.warn('[Bridge] resolve fail:', e); }
      if (p) {
        const ok = await window.NativeBridgeService.loadInstrument(p, btype, bch);
        console.log('[Bridge] track', trackId, '-> bridge', btype, 'ch', bch, ok ? 'OK' : 'FAIL', p);
        // Reset program-change dedupe của track — LOAD async (C++ worker): CC/
        // PROGRAM gửi trước khi load xong bị drop (inst chưa có) → note kế tiếp
        // phải gửi lại program change để ra ĐÚNG instrument.
        if (ok && window.__bridgeLastProgram) {
          const _pfx = trackId + ':';
          Object.keys(window.__bridgeLastProgram).forEach(k => { if (k.indexOf(_pfx) === 0) delete window.__bridgeLastProgram[k]; });
        }
        if (ok) { if (!window.__bridgeLoadedChannels) window.__bridgeLoadedChannels = {}; window.__bridgeLoadedChannels[bch] = true; }
        if (ok && btype === 'VST3' && p) {
          try {
            if (!window.__bridgePluginPaths) window.__bridgePluginPaths = {};
            window.__bridgePluginPaths[instrumentId] = p;
            updateActiveTracks(prev => prev.map(t => t.id === trackId ? { ...t, synth_engine: { ...(t.synth_engine || {}), plugin_path: p } } : t));
          } catch (e) {}
        }
        // Requirement 2: chọn VST3 qua nút Synth → load xong mở native GUI
        // (C++ attach editor vào cửa sổ bridge tự tạo — control type=4, hwnd=0).
        if (ok && btype === 'VST3' && window.NativeBridgeService.openNativeGUI) {
          try { await openVstGuiRetry(instrumentId, bch, 20); } catch (e) {}
        }
      }
    })();
  };
  const setTrackInstrumentWithProgram = (trackId, instrumentId, programNumber, displayName, bankNumber) => {
    const isSfInstrument = instrumentId && typeof instrumentId === 'string' && instrumentId.startsWith('sf_');
    const sfBank = bankNumber !== undefined ? bankNumber : (isSfInstrument ? 0 : undefined);
    const sfProg = programNumber !== undefined ? programNumber : undefined;
    var mt = activeTracksRef.current || tracks;
    var curTrk = null;
    for (var ci = 0; ci < mt.length; ci++) { if (mt[ci].id === trackId) { curTrk = mt[ci]; break; } }
    // ── Unload Carla bridge khi chuyển từ VSTi sang instrument KHÔNG phải VST ──
    // (soundfont/GM/default). Nếu không, Carla vẫn chạy với VSTi cũ → MIDI vẫn
    // play qua Carla bridge (âm sai instrument + âm kẹt không dừng được).
    try {
      const _hasInst = !!instrumentId;
      const _wasVst = curTrk && curTrk.synth_engine && String(curTrk.synth_engine.type || '').indexOf('vst') !== -1;
      const _nowVst = _hasInst && !isSfInstrument;
      if (_wasVst && !_nowVst && window.SonicCarlaMidi && window.SonicCarlaMidi.stopBridge) {
        window.SonicCarlaMidi.stopBridge();
        try { if (window.SonicMidiRouter) window.SonicMidiRouter.panic(); else if (window.SonicSF && window.SonicSF.stopAll) window.SonicSF.stopAll(); } catch (e) {}
        stopAllNativeSfNotes();
        console.log('[Instrument] Carla bridge unloaded — track', trackId, 'switched from VSTi to non-VST');
      }
    } catch (e) { console.warn('[Instrument] carla-stop on instrument switch error:', e); }
    var mch = curTrk ? assignTrackMidiChannel(curTrk, mt) : (sfBank === 128 ? 9 : 0);
    updateActiveTracks(prev => prev.map(t => {
      if (t.id !== trackId) return t;
      const hasInstrument = !!instrumentId;
      const instrType = isSfInstrument ? 'soundfont' : (hasInstrument ? 'vst3' : 'default');
      const synthEngine = hasInstrument ? {
        type: instrType,
        plugin_id: instrumentId,
        soundfont_bank: sfBank !== undefined ? sfBank : 0,
        soundfont_program: sfProg !== undefined ? sfProg : 0,
        soundfont_id: isSfInstrument ? instrumentId.replace('sf_', '') : ''
      } : undefined;
      return { ...t, midiChannel: mch, instrumentId, instrumentProgram: sfProg, instrumentName: displayName, soundfont_bank: sfBank, soundfont_program: sfProg, synth_engine: synthEngine, type: hasInstrument ? 'MIDI' : (t.type === 'MIDI' ? 'audio' : t.type) };
    }));
    setInstrumentDropdownTrackId(null);
    setInstrumentDropdownBtnRect(null);
    setSynthCategory(null);
    // Trigger FluidSynth load + program change when soundfont instrument selected
    if (window.SonicSF && window.SonicSF.selectInstrument && instrumentId && isSfInstrument) {
      const sfId = instrumentId.replace('sf_', '');
      window.SonicSF.selectInstrument(mch, sfBank || 0, sfProg || 0, sfId);
    }
    // D7: bridge active → load instrument vào bridge tại channel router sẽ dùng
    loadTrackInstrumentToBridge(trackId, instrumentId, sfBank);
    setSubTabs(prev => prev.map(s => {
      if (s.trackId !== trackId) return s;
      return { ...s, instrumentProgram: programNumber !== undefined ? programNumber : undefined, instrumentName: displayName, instrumentId };
    }));
    const playingSub = subTabs.find(s => s.trackId === trackId && s.type === 'PIANO_ROLL' && s.isPlaying);
    if (playingSub) {
      const pid = playingSub.id;
      const ctx = getAudioContext();
      const tNode = activeTrackNodesRef.current[trackId];
      if (tNode && tNode.gainNode) {
        tNode.gainNode.gain.setValueAtTime(tNode.gainNode.gain.value || 1, ctx.currentTime);
        tNode.gainNode.gain.linearRampToValueAtTime(0.001, ctx.currentTime + 0.04);
      }
      setTimeout(() => {
        stopAllPlayback();
        if (tNode && tNode.gainNode) {
          const trackData = activeTracksRef.current ? activeTracksRef.current.find(t => t.id === trackId) : null;
          const volDb = trackData ? (trackData.volumeDb ?? 0) : 0;
          const volLinear = volDb <= -50 ? 0 : Math.pow(10, volDb / 20);
          tNode.gainNode.gain.setValueAtTime(0.001, ctx.currentTime);
          tNode.gainNode.gain.linearRampToValueAtTime(volLinear || 0.8, ctx.currentTime + 0.015);
        }
        const context = getAudioContext();
        const offset = playingSub.currentTime || 0;
        startOffsetTimeRef.current = offset;
        startAudioTimeRef.current = context.currentTime;
        startBufferOffsetRef.current = offset * (playingSub.speed || 1.0);
        schedulePianoRollMidi(playingSub, offset);
        startSubTabPlayback(playingSub, offset);
        setSubTabs(prev => prev.map(s => s.id === pid ? { ...s, isPlaying: true } : s));
        if (subTabsRef.current) {
          subTabsRef.current = subTabsRef.current.map(s => s.id === pid ? { ...s, isPlaying: true } : s);
        }
        animationFrameIdRef.current = requestAnimationFrame(updatePlayhead);
      }, 60);
    }
    setTimeout(() => lucide.createIcons(), 50);
  };
  const setTrackInstrument = (trackId, instrumentId, displayName) => {
    setInstrumentDropdownTrackId(null);
    setInstrumentDropdownBtnRect(null);
    if (instrumentId && instrumentId.startsWith('sf_')) {
      // Set instrument on track immediately so Synth button shows the name
      var mt2 = activeTracksRef.current || tracks;
      var qTrk = null;
      for (var qi = 0; qi < mt2.length; qi++) { if (mt2[qi].id === trackId) { qTrk = mt2[qi]; break; } }
      var qch = qTrk ? assignTrackMidiChannel(qTrk, mt2) : 0;
      updateActiveTracks(prev => prev.map(t => {
        if (t.id !== trackId) return t;
        const sfClean = instrumentId.replace('sf_', '');
        const synthEngine = { type: 'soundfont', plugin_id: instrumentId, soundfont_bank: 0, soundfont_program: 0, soundfont_id: sfClean };
        return { ...t, midiChannel: qch, instrumentId, instrumentProgram: undefined, instrumentName: displayName, synth_engine: synthEngine };
      }));
      setSelectedSoundFontId(instrumentId);
      setSynthCategory('soundfont');
      setInstrumentSelectorTrackId(trackId);
      setSfPresets(null);
      setSfPresetSearchQuery('');
      const sfIdParam = instrumentId.replace('sf_', '');
      // Use cached presets from instrumentSelectorData
      const cachedSf = (instrumentSelectorData?.soundfonts || []).find(s => s.id === instrumentId || s.id === sfIdParam);
      if (cachedSf && cachedSf.presets) {
        setSfPresets(cachedSf.presets);
      } else {
        window.SonicAPI.listSoundfontInstruments(sfIdParam)
          .then(data => setSfPresets(data.presets || []))
          .catch(e => { console.error('listSoundfontInstruments failed:', e); setSfPresets([]); });
      }
    } else {
      setTrackInstrumentWithUndo(trackId, instrumentId, displayName);
    }
  };
  const [activeTool, setActiveTool] = useState('select'); // 'select' | 'grab' | 'razor'
  const [snapValue, onSnapChangeue] = useState('1'); // 'free', '1', '1/2', '1/4', '1/8', '1/16', '1/32'

  const snapTime = (time, snapValue, bpmVal) => {
    if (snapValue === 'free') return time;
    const beatDuration = 60 / parseFloat(bpmVal || 120);
    let divisor = 1;
    if (snapValue === '4') divisor = 4; else if (snapValue === '1') divisor = 1; else if (snapValue === '1/2') divisor = 0.5; else if (snapValue === '1/4') divisor = 0.25; else if (snapValue === '1/8') divisor = 0.125; else if (snapValue === '1/16') divisor = 0.0625; else if (snapValue === '1/32') divisor = 0.03125;
    const gridSpacing = beatDuration * divisor;
    return Math.round(time / gridSpacing) * gridSpacing;
  };
  const snapValueRef = useRef(snapValue);
  snapValueRef.current = snapValue;
  const bpmRef = useRef(bpm);
  bpmRef.current = bpm;
  // BMP for Tempo Track - LOOP_EDITOR_2.md §6
  const [selectedTrackId, setSelectedTrackId] = useState('1');
  const [selectedItemIds, setSelectedItemIds] = useState(new Set());
  const selectedItemIdsRef = useRef(new Set());
  selectedItemIdsRef.current = selectedItemIds;
  const [currentTime, setCurrentTime] = useState(0);
  const [isPlaying, setIsPlaying] = useState(false);
  // Ref theo dõi isPlaying — setTimeout VU trigger (main-midi delayed + section
  // delayed) check ref này trước khi fire: stop → isPlaying=false → setTimeout
  // sót không fire → VU không nhảy sau khi âm đã dừng (user: VU vẫn animation
  // khi hết âm).
  const isPlayingRef = useRef(false);
  isPlayingRef.current = isPlaying;
  const [selectionStart, setSelectionStart] = useState(null);
  const [selectionEnd, setSelectionEnd] = useState(null);
  const [selectionFollowsTempo, setSelectionFollowsTempo] = useState(true);
  const selectionRef = useRef({ start: null, end: null });
  selectionRef.current = { start: selectionStart, end: selectionEnd };
  const [selectionMode, setSelectionMode] = useState(null); // 'global' (from ruler) | 'local' (from track)
  const [sweepSelect, setSweepSelect] = useState(null);
  const isSweepingRef = useRef(false);
  const sweepStartRef = useRef(0);
  const sweepTrackIdRef = useRef(null);
  const sweepStartYRef = useRef(0);
  const sweepEndYRef = useRef(0);
  const sweepSelectRef = useRef(null);
  const pendingDragRef = useRef(null); // { trackId, itemType, itemId, clickOffset, startX, startY }
  const handleSectionItemDragStartRef = useRef(null);
  const [localSelectionTrackId, setLocalSelectionTrackId] = useState(null);
  const [localSelectionStart, setLocalSelectionStart] = useState(null);
  const [localSelectionEnd, setLocalSelectionEnd] = useState(null);
  const [zoom, setZoom] = useState(() => {
    // Persist zoom qua reload (user yêu cầu giữ kích thước items sau refresh)
    try {
      const v = parseFloat(localStorage.getItem('sf_zoom'));
      if (isFinite(v) && v > 0) return v;
    } catch (e) { }
    return 100;
  });
  React.useEffect(() => {
    try { localStorage.setItem('sf_zoom', String(zoom)); } catch (e) { }
    // Zoom TIMELINE (App) — serialize project dùng cái này (window.__currentZoom
    // là zoom của Media Explorer — lưu nhầm làm project load ra zoom 1.0 →
    // timeline thu nhỏ ở góc — user phải zoom in mới về đúng).
    window.__sfTimelineZoom = zoom;
  }, [zoom]);
  const [isLoopingSelection, setIsLoopingSelection] = useState(false);
  const [beginBar, setBeginBar] = useState(0);
  const [endBar, setEndBar] = useState(0);
  const [numberBar, setNumberBar] = useState(1);
  // Quét chọn (sweep — tempo track/timeline) → đồng bộ bars hiển thị dưới
  // statusbar: beginBar = bar chứa selectionStart, endBar = bar chứa
  // selectionEnd, numberBar = số bar được chọn. Trước đây beginBar/endBar/
  // numberBar CHỈ đổi khi nhập tay input — quét không cập nhật → statusbar
  // luôn hiển thị 0/0/1.
  useEffect(() => {
    if (selectionStart === null || selectionEnd === null || selectionEnd <= selectionStart) return;
    const _beatDur = 60 / (parseInt(bpm) || 120);
    const _secPerBar = _beatDur * 4;
    const _b1 = Math.floor((selectionStart || 0) / _secPerBar);
    // endBar = bar KẾT THÚC vùng quét (chạm vạch) — KHÔNG trừ 1: quét 8 bars
    // từ bar 0 → hiển thị "0 đến 8" (user yêu cầu) — số bars = ceil − floor
    // (8 − 0 = 8 — KHÔNG cộng 1).
    const _b2 = Math.ceil((selectionEnd || 0) / _secPerBar);
    if (_b2 > _b1) {
      setBeginBar(_b1);
      setEndBar(_b2);
      setNumberBar(_b2 - _b1);
    }
  }, [selectionStart, selectionEnd, bpm]);
  const [subTabHeight, setSubTabHeight] = useState(96);
  const [isExporting, setIsExporting] = useState(false);
  const [projectName, setProjectName] = useState(() => localStorage.getItem('sonic_project_name') || '');
  const [currentProjectId, setCurrentProjectId] = useState(() => localStorage.getItem('sonic_project_id') || null);
  const [saveProjectModalOpen, setSaveProjectModalOpen] = useState(false);
  const [saveAsModalOpen, setSaveAsModalOpen] = useState(false);
  const [openProjectModalOpen, setOpenProjectModalOpen] = useState(false);
  const hasAnySolo = tracks.some(t => t.solo);
  const [toastMessage, setToastMessage] = useState(null);
  const [audioDevices, setAudioDevices] = useState([]);
  const [midiDevices, setMidiDevices] = useState([]);
  const [selectedMidiInputId, setSelectedMidiInputId] = useState('');
  const selectedMidiInputIdRef = useRef('');
  const handleMidiInputSelect = (id) => {
    setSelectedMidiInputId(id);
    selectedMidiInputIdRef.current = id;
    if (window.SonicRecorderManager) {
      window.SonicRecorderManager.setSelectedMidiInputId(id);
    }
  };

  useEffect(() => {
    if (navigator.mediaDevices && navigator.mediaDevices.enumerateDevices) {
      navigator.mediaDevices.enumerateDevices().then(devices => {
        setAudioDevices(devices.filter(d => d.kind === 'audioinput'));
      }).catch(err => console.log('Enumerate audio devices error:', err));
    }
    if (navigator.requestMIDIAccess) {
      navigator.requestMIDIAccess().then(access => {
        const inputs = [];

        function attachMidiHandler(input) {
          input.onmidimessage = msg => {
            // Filter by selected MIDI input device
            const selId = selectedMidiInputIdRef.current;
            if (selId && selId !== 'ALL' && input.id !== selId) return;

            console.log(`[DevLog] [Raw MIDI] Received from "${input.name}" (ID: ${input.id}) - Data:`, Array.from(msg.data));
            if (msg.data.length < 3) return;
               const cmd = msg.data[0] >> 4;
             const pitch = msg.data[1];
             const rawVel = msg.data[2];
             // Scale low MIDI velocity naturally (min 1, max 127)
             const scaledVel = Math.min(127, Math.max(1, Math.round(rawVel)));
               if (cmd === 0x9 && rawVel > 0) {
                  lastMidiNoteRef.current = { pitch, velocity: scaledVel, startTime: performance.now(), length: 0 };
                  setLastMidiNote({ pitch, velocity: scaledVel, length: 0, time: Date.now() });
                  activeMidiPitchesRef.current.add(pitch);
                  setActiveMidiPitches(new Set(activeMidiPitchesRef.current));
                  // MIDI Keyboard → Mastering FX Chain (main out): đồng bộ routing
                  // mastering + SF NGAY trước khi phát — âm MIDI keyboard hardware
                  // PHẢI qua mastering FX khi chain đang bật (giống keybed; trước
                  // đây chỉ keybed ảo gọi __ensureMasteringRouting).
                  try { if (window.__ensureMasteringRouting) window.__ensureMasteringRouting(); } catch (er) {}
                  // Route MIDI input to ALL armed tracks on their dedicated channels
                  // Do NOT use raw MIDI hardware channel (msg.data[0] & 0x0F)
                  if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) {
                    // D1: bridge active -> 1 cổng router (per-track channel alloc) →
                    // Rust SHM → C++ bridge; giữ SonicSF/Carla fallback ở nhánh else.
                    var bTracks = activeTracksRef.current || [];
                    bTracks.forEach(function (bt) {
                      if (!bt.isArmed) return;
                      if (window.triggerMidiVuActivity) {
                        window.triggerMidiVuActivity(bt.id, scaledVel);
                      }
                      try {
                        heldMidiNotesRef.current[bt.id] = (heldMidiNotesRef.current[bt.id] || 0) + 1;
                      } catch (err) {}
                      try { window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_ON', channel: bt.id, pitch: pitch, velocity: scaledVel / 127, percussion: !!(bt.synth_engine && bt.synth_engine.soundfont_bank === 128) }); } catch (e) {}
                    });
                  } else if (window.SonicSF) {
                     var allTracks = activeTracksRef.current || [];
                     var arSubs = subTabsRef && subTabsRef.current ? subTabsRef.current.filter(function(s) { return s.type === 'PIANO_ROLL' && s.isArmed; }) : [];
                     var armedTracks = allTracks.filter(function(t) { return t.isArmed; });
                     // Piano Roll arming has priority: route to each armed sub-tab's parent track
                     if (arSubs.length > 0) {
                       arSubs.forEach(function(as) {
                         var asTrk = allTracks.find(function(t) { return t.id === as.trackId; });
                         var asCh = asTrk ? assignTrackMidiChannel(asTrk, allTracks) : 0;
                         var asProg = as.instrumentProgram;
                         var asSe = as.synth_engine;
                          if (window.triggerMidiVuActivity) {
                            window.triggerMidiVuActivity(as.trackId, scaledVel);
                          }
                          // Giữ VU theo trường độ: tăng counter note đang giữ
                          // (tick giữ peak cho tới khi note-off)
                          try {
                            heldMidiNotesRef.current[as.trackId] = (heldMidiNotesRef.current[as.trackId] || 0) + 1;
                          } catch (err) { }
                          if (isStandaloneSf() && isSfTrackEngine(asSe) && !shouldRouteCarla(asSe)) {
                            playNativeSfNote(asTrk, pitch, scaledVel / 127, 60000, undefined, 'midi_' + as.trackId + '_' + pitch);
                          } else {
                            window.SonicSF.playNote(pitch, scaledVel, 60000, undefined, asProg, null, asCh, asSe);
                          }
                          // MIDI Keyboard → Carla bridge (piano roll sub-tab ARM):
                          // track VSTi của sub-tab phát VSTi realtime giống keybed.
                          if (window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoute(asSe, true)) {
                            try { window.SonicCarlaMidi.noteOn(asCh, pitch, scaledVel); } catch (e) {}
                          }
                       });
                     }
                     // Route to ALL armed tracks (not just the first one)
                     armedTracks.forEach(function(at) {
                       var atCh = assignTrackMidiChannel(at, allTracks);
                       var atProg = at.instrumentProgram;
                       var atSe = at.synth_engine;
                       var atDest = activeTrackNodesRef.current[at.id]?.gainNode || null;
                        if (window.triggerMidiVuActivity) {
                          window.triggerMidiVuActivity(at.id, scaledVel);
                        }
                        // Giữ VU theo trường độ: tăng counter note đang giữ
                        try {
                          heldMidiNotesRef.current[at.id] = (heldMidiNotesRef.current[at.id] || 0) + 1;
                        } catch (err) { }
                        if (isStandaloneSf() && isSfTrackEngine(atSe) && !shouldRouteCarla(atSe)) {
                          playNativeSfNote(at, pitch, scaledVel / 127, 60000, undefined, 'midi_' + at.id + '_' + pitch);
                        } else {
                          window.SonicSF.playNote(pitch, scaledVel, 60000, undefined, atProg, atDest, atCh, atSe);
                        }
                        // MIDI Keyboard → Carla bridge (track VSTi + ARM + Carla
                        // local): phát VSTi realtime — giống hệt keybed ảo. Trước
                        // đây chỉ keybed gọi SonicCarlaMidi; keyboard hardware bị
                        // bỏ sót → track VSTi không kêu khi bấm đàn thật.
                        if (window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoute(atSe, at.isArmed)) {
                          try { window.SonicCarlaMidi.noteOn(atCh, pitch, scaledVel); } catch (e) {}
                        }
                        });
                   }
               } else if (cmd === 0x8 || (cmd === 0x9 && rawVel === 0)) {
                 activeMidiPitchesRef.current.delete(pitch);
                 setActiveMidiPitches(new Set(activeMidiPitchesRef.current));
                 const current = lastMidiNoteRef.current;
               if (current && current.pitch === pitch) {
                 const lenSec = (performance.now() - current.startTime) / 1000;
                 lastMidiNoteRef.current = { ...current, length: lenSec };
                 setLastMidiNote(prev => prev && prev.pitch === pitch ? { ...prev, length: lenSec, time: Date.now() } : prev);
               }
                // Stop the note on ALL tracks (not just armed) to prevent stuck notes
                // when ARM is toggled off while a key is held
                 if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) {
                   // D1: bridge note-off qua router (channel trùng NOTE_ON — track.id).
                   var bStopTracks = activeTracksRef.current || [];
                   bStopTracks.forEach(function (bst) {
                     try {
                       if (heldMidiNotesRef.current[bst.id] !== undefined) {
                         heldMidiNotesRef.current[bst.id] = Math.max(0, heldMidiNotesRef.current[bst.id] - 1);
                         if (heldMidiNotesRef.current[bst.id] === 0) {
                           delete heldMidiNotesRef.current[bst.id];
                         }
                       }
                     } catch (err) {}
                     try { window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_OFF', channel: bst.id, pitch: pitch, velocity: 0, percussion: !!(bst.synth_engine && bst.synth_engine.soundfont_bank === 128) }); } catch (e) {}
                   });
                 } else if (window.SonicSF && window.SonicSF.stopNote) {
                   var stopTracks = activeTracksRef.current || [];
                   stopTracks.forEach(function(st) {
                     // Only stop channels that actually carry this track's notes —
                     // an index-based fallback could hit another track's dedicated
                     // channel and kill its sound.
                     if (!st.synth_engine && st.midiChannel === undefined) return;
                     var stCh = assignTrackMidiChannel(st, stopTracks);
                     if (isStandaloneSf()) stopNativeSfNote('midi_' + st.id + '_' + pitch);
                     window.SonicSF.stopNote(stCh, pitch);
                     // MIDI Keyboard → Carla bridge: note-off khi thả phím — tránh
                     // kẹt âm VSTi (giống keybed ảo).
                     if (window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoute(st.synth_engine, st.isArmed)) {
                       try { window.SonicCarlaMidi.noteOff(stCh, pitch); } catch (e) {}
                     }
                     // Giảm counter note đang giữ — hết note → VU được phép decay/tắt
                     // (tick sẽ thấy counter = 0 và không còn giữ peak nữa)
                     try {
                       if (heldMidiNotesRef.current[st.id] !== undefined) {
                         heldMidiNotesRef.current[st.id] = Math.max(0, heldMidiNotesRef.current[st.id] - 1);
                         if (heldMidiNotesRef.current[st.id] === 0) {
                           delete heldMidiNotesRef.current[st.id];
                         }
                       }
                     } catch (err) { }
                   });
                 }
              }

            // ── Sustain (CC64), Modulation (CC1), Pitch Bend ──
            const midiCh = msg.data[0] & 0x0F;
            if (cmd === 0xB) {
              // Controller Change: forward to ALL armed tracks' dedicated channels
              const cc = msg.data[1];
              const val = msg.data[2];
              if (window.SonicSF && window.SonicSF.controllerChange) {
                var ccTracks = activeTracksRef.current || [];
                var hasArmed = ccTracks.some(function(t) { return t.isArmed; });
                if (hasArmed) {
                   ccTracks.forEach(function(ct) {
                     if (!ct.isArmed) return;
                     var ctCh = assignTrackMidiChannel(ct, ccTracks);
                     window.SonicSF.controllerChange(ctCh, cc, val);
                   });
                } else {
                  window.SonicSF.controllerChange(midiCh, cc, val);
                }
              }
            } else if (cmd === 0xE) {
              // Pitch Bend: forward to ALL armed tracks' dedicated channels
              const lsb = msg.data[1];
              const msb = msg.data[2];
              const bendVal = (msb << 7) | lsb;
              if (window.SonicSF && window.SonicSF.pitchBend) {
                var pbTracks = activeTracksRef.current || [];
                var hasArmedPB = pbTracks.some(function(t) { return t.isArmed; });
                if (hasArmedPB) {
                   pbTracks.forEach(function(pt) {
                     if (!pt.isArmed) return;
                     var ptCh = assignTrackMidiChannel(pt, pbTracks);
                     window.SonicSF.pitchBend(ptCh, bendVal);
                   });
                } else {
                  window.SonicSF.pitchBend(midiCh, bendVal);
                }
              }
            }

            // Forward to active MIDI recorders
            if (activeMIDIRecordersRef.current) {
              for (let trackId in activeMIDIRecordersRef.current) {
                const rec = activeMIDIRecordersRef.current[trackId];
                if (rec) {
                  rec.handleMIDIMessage(msg, input.id);
                }
              }
            }
          };
        }

        for (let input of access.inputs.values()) {
          inputs.push(input);
          attachMidiHandler(input);
        }
        setMidiDevices(inputs);
        access.onstatechange = () => {
          const inputs = [];
          for (let input of access.inputs.values()) {
            inputs.push(input);
            // Re-attach handler to ensure new devices get it
            if (!input.onmidimessage) {
              attachMidiHandler(input);
            }
          }
          setMidiDevices(inputs);
        };
      }).catch(err => console.log('MIDI access error:', err));
    }
  }, []);

  const [showAIConfig, setShowAIConfig] = useState(false);
  const [recordingState, setRecordingState] = useState('IDLE'); // 'IDLE' | 'COUNT_IN' | 'RECORDING'
  const [recTempMidiNotes, setRecTempMidiNotes] = useState([]);
  const [recTempAudioBuffer, setRecTempAudioBuffer] = useState(null);
  const [recStartTimelineTime, setRecStartTimelineTime] = useState(0);
  const [canvasRedrawCount, setCanvasRedrawCount] = useState(0);
  const [lastMidiNote, setLastMidiNote] = useState(null);
  const lastMidiNoteRef = useRef(null);

  const activeMIDIRecordersRef = useRef({});
  const activeAudioRecordersRef = useRef({});
  const pianoRollRecorderRef = useRef(null);
  const activeMidiPitchesRef = useRef(new Set());
  const [activeMidiPitches, setActiveMidiPitches] = useState(new Set());
  const recordingPCMDataRef = useRef({});
  const recordingStartTimeRef = useRef(0);
  const recordingSyncRef = useRef(null);
  const recordingStateRef = useRef(recordingState);
  recordingStateRef.current = recordingState;
  const lastTempCompileTimeRef = useRef(0);
  const nextMetronomeBeatRef = useRef(0);
  const [showExportPanel, setShowExportPanel] = useState(false);
  React.useEffect(() => {
    if (showExportPanel) setTimeout(() => { try { window.lucide && window.lucide.createIcons(); } catch (e) { } }, 80);
  }, [showExportPanel]);
  const [showAIPanel, setShowAIPanel] = useState(true);
  const [showSelectionPanel, setShowSelectionPanel] = useState(false);
  const [showPythonToolsPanel, setShowPythonToolsPanel] = useState(false);
  const [showMediaExplorer, setShowMediaExplorer] = useState(false);
  const [scrollBufferExtra, setScrollBufferExtra] = useState(0);
  const scrollBufferExtraRef = useRef(0);
  scrollBufferExtraRef.current = scrollBufferExtra;
  const [showFxRack, setShowFxRack] = useState(false);
  const [showMidiEvents, setShowMidiEvents] = useState(false);
  const [showMixer, setShowMixer] = useState(false);
  const setShowMixerRef = useRef(setShowMixer);
  setShowMixerRef.current = setShowMixer;
  const setShowMediaExplorerRef = useRef(setShowMediaExplorer);
  setShowMediaExplorerRef.current = setShowMediaExplorer;
  // F6/F7 mutual exclusion: enabling one panel disables the other
  window.__toggleMixerRef = function() {
    setShowMixer(function(p) {
      const next = !p;
      if (next) setShowMediaExplorerRef.current(false);
      return next;
    });
  };

  // Live per-track bypass (split into two buttons per user request):
  //  - 'audio': bypass FX+mastering for AUDIO items (audio clips + section items)
  //  - 'midi' : bypass FX+mastering for MIDI items (soundfont instrument)
  window.__setTrackBypass = function(trackId, which, on) {
    if (which === 'audio') {
      trackAudioBypassMap[trackId] = !!on;
      trackMasteringBypassMap[trackId] = !!on;
      // Live re-route the main track node AND every section sub-node of this
      // track (keys "<parent>_sub_<trackId>") so section items bypass instantly.
      Object.keys(activeTrackNodesRef.current).forEach(function(k) {
        if (k === trackId || k.endsWith('_sub_' + trackId)) {
          const n = activeTrackNodesRef.current[k];
          if (n && n.route) setMasteringRoute(n.route, !!on);
        }
      });
    } else if (which === 'midi') {
      trackMidiBypassMap[trackId] = !!on;
      // ♪ bypasses Mastering FX Chain for the soundfont ONLY — the SF path
      // keeps flowing through its own sfModules (track FX Rack, PWR-controlled).
      const node = activeTrackNodesRef.current[trackId];
      if (node && node.sfRouteGain && node.sfDryGain) {
        setMasteringRoute({ routeGain: node.sfRouteGain, dryGain: node.sfDryGain, _trackId: trackId }, !!on);
      }
      // SECTION: nút ♪ cũng toggle route của sub-node MIDI (key <parent>_sub_<trackId>)
      Object.keys(activeTrackNodesRef.current).forEach(function(k) {
        if (k.endsWith('_sub_' + trackId)) {
          const sn = activeTrackNodesRef.current[k];
          if (sn && sn.route) setMasteringRoute(sn.route, !!on);
        }
      });
      try { if (updateSfRoutingRef.current) updateSfRoutingRef.current(); } catch (e) { }
    }
  };
  // Legacy single-button bypass (kept for backward compatibility): sets both.
  window.__setTrackMasteringBypass = function(trackId, bypass) {
    trackAudioBypassMap[trackId] = !!bypass;
    trackMasteringBypassMap[trackId] = !!bypass;
    trackMidiBypassMap[trackId] = !!bypass;
    const node = activeTrackNodesRef.current[trackId];
    if (node && node.route) setMasteringRoute(node.route, !!bypass);
    try { if (updateSfRoutingRef.current) updateSfRoutingRef.current(); } catch (e) { }
  };

  // Realtime mute/solo: applies the (patched) mute/solo state to every active
  // track node in the current context. Called from the M/S buttons of the
  // mixer strips + track strips so toggling muting/soloing affects the items
  // already playing on that track immediately (20ms crossfade, no click).
  const applyAllTrackMuteSolo = (trackId, patch) => {
    const list = (activeTracksRef.current && activeTracksRef.current.length) ? activeTracksRef.current : tracks;
    const effective = (trackId && patch) ? list.map(t => t.id === trackId ? { ...t, ...patch } : t) : list;
    let becameAudible = false;
    effective.forEach(t => {
      const audible = computeTrackAudibleGain(effective, t) > 0;
      const wasAudible = trackAudibleRef.current[t.id] !== false;
      const node = activeTrackNodesRef.current[t.id];
      const audibleGain = computeTrackAudibleGain(effective, t);
      if (node) setTrackNodeGain(node, audibleGain);
      // Section sub-nodes (key: <parent>_sub_<trackId>) — mute/solo của
      // SECTION-TAB phải áp gain cho node play section — CHỈ trong section
      // (activeTracksRef = tracks section), KHÔNG đụng track MAIN.
      Object.keys(activeTrackNodesRef.current).forEach(function(k) {
        if (k.endsWith('_sub_' + t.id)) {
          const sn = activeTrackNodesRef.current[k];
          if (sn && sn.gainNode) setTrackNodeGain(sn, audibleGain);
        }
      });
      if (node && node.sfEntry) {
        try { node.sfEntry.gain.setTargetAtTime(audibleGain, getAudioContext().currentTime, 0.02); } catch (e) {}
      }
      // MIDI items: FluidSynth mixes ALL channels into ONE shared gain node, so
      // the per-track gain cannot silence them. Every MIDI track owns a
      // dedicated channel (ensureTrackMidiChannel) → mute/solo via CC7 (channel
      // volume) applies realtime, even to notes already sounding.
      // CC7 = VOLUME của track (không chỉ 100/0) — volume slider MIDI track tác
      // dụng qua channel volume (yêu cầu channel RIÊNG cho từng track).
      if ((t.midiItems && t.midiItems.length > 0) || t.type === 'MIDI') {
        try {
          if (window.SonicSF && typeof window.SonicSF.controllerChange === 'function') {
            const ch = ensureTrackMidiChannel(t, effective);
            var cc7 = 100;
            if (audible) {
              var volDb = (typeof t.volumeDb === 'number' && isFinite(t.volumeDb)) ? t.volumeDb : 0;
              var volLin = volDb <= -50 ? 0 : Math.pow(10, volDb / 20);
              cc7 = Math.max(1, Math.min(127, Math.round(volLin * 100)));
            } else {
              cc7 = 0;
            }
            window.SonicSF.controllerChange(ch, 7, cc7);
          }
        } catch (e) {}
      }
      if (audible && !wasAudible) becameAudible = true;
      trackAudibleRef.current[t.id] = audible;
    });
    // A track that just became audible may have had its audio sources dropped
    // by a loop restart while it was inaudible (startTrackPlayback skips
    // muted/soloed tracks, so unmute alone cannot resurrect them). Re-schedule
    // from the current playhead — the same approach toggleTrackSoloEvaluate
    // already uses for solo.
    if (becameAudible && isPlaying && recordingStateRef.current !== 'RECORDING' && activeTracksRef.current && activeTracksRef.current.length) {
      try {
        stopAllPlayback();
        const ctx = getAudioContext();
        const resumeAt = currentTimeRef.current || 0;
        startOffsetTimeRef.current = resumeAt;
        startAudioTimeRef.current = ctx.currentTime;
        startBufferOffsetRef.current = resumeAt;
        setIsPlaying(true);
        startTrackPlayback(resumeAt);
      } catch (e) {
        console.warn('mute/solo resume error:', e);
      }
    }
    updateSfRouting();
  };
  window.__applyTrackMuteSolo = applyAllTrackMuteSolo;
  window.__toggleMediaExplorerRef = function() {
    setShowMediaExplorer(function(p) {
      const next = !p;
      if (next) setShowMixerRef.current(false);
      return next;
    });
  };
  const [mixerHeight, setMixerHeight] = useState(function() {
    var saved = localStorage.getItem('studio_mixer_height');
    return saved ? parseInt(saved) : 200;
  }());
  const [mediaExplorerPanelHeight, setMediaExplorerPanelHeight] = useState(function() {
    var saved = localStorage.getItem('studio_media_explorer_height');
    return saved ? parseInt(saved) : 240;
  }());
  const [masterVolume, setMasterVolume] = useState(0); // dB
  const [masterVU, setMasterVU] = useState(0); // 0-1
  const [masterMeterPeak, setMasterMeterPeak] = useState(0);
  const [rightSidebarWidth, setRightSidebarWidth] = useState(320);
  const [tcpWidth, setTcpWidth] = useState(320);
  const [mediaExplorerHeight, setMediaExplorerHeight] = useState(50);
  const [panelPositions, setPanelPositions] = useState({
    export: 'bottom',
    ai: 'right',
    python_tools: 'bottom',
    selection: 'bottom',
    media_explorer: 'bottom',
    fx_rack: 'bottom',
    midi_events: 'bottom'
  });
  const [panelDropZone, setPanelDropZone] = useState(null);
  const [dragGhostPos, setDragGhostPos] = useState(null);
  const [dragGhostPanel, setDragGhostPanel] = useState(null);
  const panelDragRef = useRef(null);
  const trackVuRefs = useRef({});
  const workspaceRef = useRef(null);
  const colResizerRef = useRef(null);
  const rowResizerRef = useRef(null);
  const [aiConfig, setAiConfig] = useState({
    baseUrl: localStorage.getItem('ai_base_url') || `${API_BASE_URL}`,
    apiKey: localStorage.getItem('ai_api_key') || '',
    model: localStorage.getItem('ai_model') || 'deepseek-chat'
  });
  const [aiProviders, setAiProviders] = useState([]);
  useEffect(() => {
    (async () => {
      try {
        const data = await window.SonicAPI.getAIConfigs();
        if (data && data.providers) {
          setAiProviders(data.providers);
          const active = data.providers.find(p => p.is_active) || data.providers[0];
          if (active) setSelectedProviderId(active.id);
        }
      } catch (e) { /* server may not have config endpoint */ }
    })();
  }, []);
  const [analysisState, setAnalysisState] = useState({
    status: 'Sẵn sàng. Chạy AI để phân tích nhịp.',
    data: null,
    isRunning: false,
  });
  const [aiPrompt, setAiPrompt] = useState('');
  const [aiComposeMode, setAiComposeMode] = useState('SIMILAR_VARIATION');
  const [promptHistory, setPromptHistory] = useState([]);
  const [promptHistIdx, setPromptHistIdx] = useState(-1);
  const promptHistRef = useRef([]);
  const aiPromptUndoRef = useRef({ stack: [], idx: -1, max: 30 });
  const aiPromptUndoPush = (text) => {
    const u = aiPromptUndoRef.current;
    u.stack.push(text);
    if (u.stack.length > u.max) u.stack.shift();
    u.idx = u.stack.length - 1;
  };
  const [aiProvider, setAiProvider] = useState('OpenAI');
  const [aiModel, setAiModel] = useState('GPT-4o');
  const [aiActionLog, setAiActionLog] = useState([]);
  const actionLogContainerRef = useRef(null);
  useEffect(() => {
    if (actionLogContainerRef.current) {
      actionLogContainerRef.current.scrollTop = actionLogContainerRef.current.scrollHeight;
    }
  }, [aiActionLog]);
  const [aiProcessing, setAiProcessing] = useState(false);
  const [aiSuggestions, setAiSuggestions] = useState([]);
  const [showAiTypeahead, setShowAiTypeahead] = useState(false);
  const [showAISuggestions, setShowAISuggestions] = useState(true);
  const [showAIActionLog, setShowAIActionLog] = useState(false);
  const aiPromptMgrRef = useRef(null);
  const aiTypeaheadRef = useRef(null);
  const [selectedProviderId, setSelectedProviderId] = useState('');
  const [exportSettings, setExportSettings] = useState({
    sampleRate: '44100',
    bitDepth: '16',
    format: 'wav',
    source: 'project',
    quality: '44khz',
    channels: 'stereo'
  });
  const [serverStatus, setServerStatus] = useState('checking...');
  const [menuOpen, setMenuOpen] = useState(null);
  const [selectedClipId, setSelectedClipId] = useState(null); // { trackId, clipId }
  const [stretchedClip, setStretchedClip] = useState(null); // { trackId, clipId, originalDuration, startTime, originalSpeed, beforeSnap }
  const [draggedSectionItem, setDraggedSectionItem] = useState(null);
  const [resizedSectionItem, setResizedSectionItem] = useState(null);
  const [editingTrackName, setEditingTrackName] = useState(null); // trackId being edited
  const [editingClipName, setEditingClipName] = useState(null); // { trackId, clipId }
  const [editNameInput, setEditNameInput] = useState('');

  // ── Context Menu & Clipboard ──
  const [contextMenu, setContextMenu] = useState(null); // { x, y, trackId }
  // Gợi ý động PIANO ROLL trên status bar (user 09:40) — piano roll set qua
  // window.__setPrHint (mouse in/out + Shift/Ctrl state)
  const [prHint, setPrHint] = useState(null);
  React.useEffect(() => {
    window.__setPrHint = (h) => setPrHint(h || null);
    return () => { delete window.__setPrHint; };
  }, []);
  const clipboardRef = useRef(null); // { buffer, name, volume, color } for copy/paste

  // ── Undo/Redo Engine (LOOP_EDITOR.md §4 + Global Extension) ──
  const [undoStack, setUndoStack] = useState([]);
  const [redoStack, setRedoStack] = useState([]);
  const MAX_UNDO = 30;
  const pushAction = (actionType, trackId, beforeState, afterState) => {
    const node = {
      action_type: actionType,
      track_id: trackId,
      timestamp: Date.now(),
      before_state: beforeState,
      after_state: afterState
    };
    setUndoStack(prev => {
      const next = [...prev, node];
      if (next.length > MAX_UNDO) next.shift();
      return next;
    });
    setRedoStack([]);
  };
  const handleUndo = () => {
    if (window.UndoRedoEngine && window.UndoRedoEngine.canUndo()) {
      const entry = window.UndoRedoEngine.undo();
      if (entry) {
        if (entry.undo && typeof entry.undo === 'function') entry.undo(entry);
        showToast(`Undo: ${entry.label || entry.type}`, 'info');
        return;
      }
    }
    if (undoStack.length === 0) return;
    const last = undoStack[undoStack.length - 1];
    setUndoStack(prev => prev.slice(0, -1));
    setRedoStack(prev => [...prev, last]);
    applyTrackState(last.track_id, last.before_state);
    showToast(`Undo: ${last.action_type}`, 'info');
  };
  const handleRedo = () => {
    if (window.UndoRedoEngine && window.UndoRedoEngine.canRedo()) {
      const entry = window.UndoRedoEngine.redo();
      if (entry) {
        if (entry.redo && typeof entry.redo === 'function') entry.redo(entry);
        showToast(`Redo: ${entry.label || entry.type}`, 'info');
        return;
      }
    }
    if (redoStack.length === 0) return;
    const last = redoStack[redoStack.length - 1];
    setRedoStack(prev => prev.slice(0, -1));
    setUndoStack(prev => [...prev, last]);
    applyTrackState(last.track_id, last.after_state);
    showToast(`Redo: ${last.action_type}`, 'info');
  };
  const applyTrackState = (trackId, state) => {
    if (trackId === 'ALL_TRACKS') {
      state.forEach(entry => {
        applyTrackState(entry.trackId, entry.state);
      });
      return;
    }
    updateActiveTracks(prev => prev.map(t => {
      if (t.id !== trackId) return t;
      var updated = { ...t, ...state };
      if (state.sections !== undefined) {
        updated.sections = state.sections;
      }
      if (state.midiItems !== undefined) {
        updated.midiItems = state.midiItems;
      }
      if (state.clips !== undefined) {
        updated.clips = state.clips;
      }
      return updated;
    }));
  };
  const getSelectedMidiItemInfo = () => {
    if (!selectedItemIds || selectedItemIds.size !== 1) return null;
    const selId = selectedItemIds.values().next().value;
    const tlist = activeTracks || tracks || [];
    for (const t of tlist) {
      const found = (t.midiItems || []).find(m => m.id === selId);
      if (found) return { itemName: found.name, trackName: t.name, trackId: t.id, itemId: selId, notes: found.notes || [], startTime: found.startTime || 0, duration: found.duration || 4 };
    }
    return null;
  };
  const captureTrackSnapshot = trackId => {
    const track = activeTracks.find(t => t.id === trackId);
    if (!track) return null;
    return {
      volumeDb: track.volumeDb,
      pan: track.pan,
      muted: track.muted,
      name: track.name,
      markers: JSON.parse(JSON.stringify(track.markers || [])),
      buffer: track.buffer,
      startTime: track.startTime || 0,
      clips: track.clips ? track.clips.map(c => ({
        id: c.id,
        buffer: c.buffer,
        startTime: c.startTime,
        name: c.name
      })) : null,
      sections: track.sections ? track.sections.map(function(s) { return { id: s.id, start: s.start, duration: s.duration, name: s.name, color: s.color, notes: s.notes ? s.notes.map(function(n) { return { id: n.id, pitch: n.pitch, start_beat: n.start_beat, duration_beats: n.duration_beats, velocity: n.velocity }; }) : null, tracks: s.tracks ? s.tracks.map(function(st) { return { id: st.id, name: st.name, color: st.color, clips: st.clips ? st.clips.map(function(c) { return { id: c.id, startTime: c.startTime, name: c.name, speed: c.speed, buffer: c.buffer }; }) : null, midiItems: st.midiItems ? st.midiItems.map(function(m) { return { id: m.id, startTime: m.startTime, duration: m.duration, name: m.name, notes: m.notes ? m.notes.map(function(n) { return { id: n.id, pitch: n.pitch, start_beat: n.start_beat, duration_beats: n.duration_beats, velocity: n.velocity }; }) : null }; }) : null }; }) : null }; }) : null,
      midiItems: track.midiItems ? track.midiItems.map(function(m) { return { id: m.id, startTime: m.startTime, duration: m.duration, name: m.name, notes: m.notes ? m.notes.map(function(n) { return { id: n.id, pitch: n.pitch, start_beat: n.start_beat, duration_beats: n.duration_beats, velocity: n.velocity }; }) : null }; }) : null
    };
  };
  const captureAllTracksSnapshot = () => {
    const curTracks = activeTracksRef.current || activeTracks || [];
    return curTracks.map(t => ({
      trackId: t.id,
      state: captureTrackSnapshot(t.id)
    }));
  };

  const setBpmWithUndo = (newBpm) => {
    const oldBpm = bpmRef.current;
    if (String(oldBpm) === String(newBpm)) return;
    const entry = {
      type: 'SET_BPM',
      scope: 'global',
      label: `BPM ${oldBpm} → ${newBpm}`,
      before: oldBpm,
      after: newBpm,
      undo: (e) => { setBpm(e.before); showToast(`Undo: BPM → ${e.before}`, 'info'); },
      redo: (e) => { setBpm(e.after); showToast(`Redo: BPM → ${e.after}`, 'info'); }
    };
    if (window.UndoRedoEngine) window.UndoRedoEngine.execute(entry);
    setBpm(String(newBpm));
  };

  const setPlayheadWithUndo = (newTime) => {
    const oldTime = currentTime;
    if (Math.abs(oldTime - newTime) < 0.001) return;
    const entry = {
      type: 'SET_PLAYHEAD',
      scope: 'global',
      label: `Playhead ${formatTimeSimple(oldTime)} → ${formatTimeSimple(newTime)}`,
      before: oldTime,
      after: newTime,
      undo: (e) => { applyPlayheadDirect(e.before); showToast(`Undo: Playhead`, 'info'); },
      redo: (e) => { applyPlayheadDirect(e.after); showToast(`Redo: Playhead`, 'info'); }
    };
    if (window.UndoRedoEngine) window.UndoRedoEngine.execute(entry);
    applyPlayheadDirect(newTime);
  };
  const applyPlayheadDirect = (time) => {
    localSelectionAnchorRef.current = time;
    if (isPlaying) {
      setCurrentTime(time);
      stopAllPlayback();
      setTimeout(() => {
        startOffsetTimeRef.current = time;
        startAudioTimeRef.current = getAudioContext().currentTime;
        startTrackPlayback(time);
        setIsPlaying(true);
      }, 50);
    } else {
      setCurrentTime(time);
    }
  };

  const setSelectionWithUndo = (newStart, newEnd, mode) => {
    const oldStart = selectionRef.current.start;
    const oldEnd = selectionRef.current.end;
    const oldMode = selectionMode;
    if (oldStart === newStart && oldEnd === newEnd && oldMode === mode) return;
    const entry = {
      type: 'SET_SELECTION',
      scope: 'global',
      label: `Selection`,
      before: { start: oldStart, end: oldEnd, mode: oldMode },
      after: { start: newStart, end: newEnd, mode: mode },
      undo: (e) => {
        setSelectionStart(e.before.start);
        setSelectionEnd(e.before.end);
        setSelectionMode(e.before.mode);
        showToast(`Undo: Selection`, 'info');
      },
      redo: (e) => {
        setSelectionStart(e.after.start);
        setSelectionEnd(e.after.end);
        setSelectionMode(e.after.mode);
        showToast(`Redo: Selection`, 'info');
      }
    };
    if (window.UndoRedoEngine) window.UndoRedoEngine.execute(entry);
    setSelectionStart(newStart);
    setSelectionEnd(newEnd);
    if (mode !== undefined) setSelectionMode(mode);
  };

  const setSelectedItemsWithUndo = (newSet) => {
    const oldSet = selectedItemIdsRef.current;
    if (oldSet && newSet && oldSet.size === newSet.size && [...oldSet].every(x => newSet.has(x))) return;
    const entry = {
      type: 'SELECT_ITEMS',
      scope: 'global',
      label: `Selection`,
      before: [...oldSet],
      after: [...newSet],
      undo: (e) => { setSelectedItemIds(new Set(e.before)); showToast(`Undo: Selection`, 'info'); },
      redo: (e) => { setSelectedItemIds(new Set(e.after)); showToast(`Redo: Selection`, 'info'); }
    };
    if (window.UndoRedoEngine) window.UndoRedoEngine.execute(entry);
    setSelectedItemIds(newSet);
  };

  const setAiPromptWithUndo = (newText) => {
    const oldText = aiPrompt;
    if (oldText === newText) return;
    const entry = {
      type: 'SET_AI_PROMPT',
      scope: 'global',
      label: `AI Prompt`,
      before: oldText,
      after: newText,
      undo: (e) => { setAiPrompt(e.before); showToast(`Undo: AI Prompt`, 'info'); },
      redo: (e) => { setAiPrompt(e.after); showToast(`Redo: AI Prompt`, 'info'); }
    };
    if (window.UndoRedoEngine) window.UndoRedoEngine.execute(entry);
    setAiPrompt(newText);
  };

  // Gán preset VST3 (.vstpreset từ thư viện storage/presets — cầu nối
  // Carla → pedalboard) vào track: preset_id nằm trong synth_engine → render
  // engine tải qua load_preset khi render → âm render = âm đã chỉnh trong Carla.
  const setTrackPreset = (trackId, presetId) => {
    if (!trackId || !presetId) return;
    var mt = activeTracksRef.current || tracks;
    var cur = null;
    for (var ci = 0; ci < mt.length; ci++) { if (mt[ci].id === trackId) { cur = mt[ci]; break; } }
    if (!cur) return;
    var se = cur.synth_engine || {};
    if (!se.plugin_id || String(se.type || '').indexOf('vst') === -1) {
      showToast('Chọn VST instrument trước khi gán preset', 'warning');
      return;
    }
    updateActiveTracks(prev => prev.map(t => t.id === trackId ? { ...t, synth_engine: { ...(t.synth_engine || {}), preset_id: presetId } } : t));
    setInstrumentDropdownTrackId(null);
    setInstrumentDropdownBtnRect(null);
    var p = ((window.SonicRuntime && window.SonicRuntime.presets) || []).find(function (x) { return x.id === presetId; });
    showToast('Đã gán preset: ' + ((p && p.name) || presetId), 'success');
  };

  const setTrackInstrumentWithUndo = (trackId, instrumentId, displayName, bankNumber, programNumber) => {
    const track = activeTracks.find(t => t.id === trackId);
    if (!track) return;
    const oldInstrumentId = track.instrumentId;
    const oldInstrumentName = track.instrumentName;
    if (oldInstrumentId === instrumentId && oldInstrumentName === displayName) return;
    const entry = {
      type: 'SET_INSTRUMENT',
      scope: 'track:' + trackId,
      label: `Instrument ${track.name}`,
      before: { instrumentId: oldInstrumentId, instrumentName: oldInstrumentName, bankNumber: track.soundfont_bank, programNumber: track.instrumentProgram },
      after: { instrumentId, instrumentName: displayName, bankNumber, programNumber },
      undo: (e) => {
        setTrackInstrumentWithProgram(trackId, e.before.instrumentId, e.before.programNumber, e.before.instrumentName, e.before.bankNumber);
        showToast(`Undo: Instrument`, 'info');
      },
      redo: (e) => {
        setTrackInstrumentWithProgram(trackId, e.after.instrumentId, e.after.programNumber, e.after.instrumentName, e.after.bankNumber);
        showToast(`Redo: Instrument`, 'info');
      }
    };
    if (window.UndoRedoEngine) window.UndoRedoEngine.execute(entry);
    setTrackInstrumentWithProgram(trackId, instrumentId, programNumber, displayName, bankNumber);
  };

  const createSectionWithUndo = (trackId, section) => {
    const entry = {
      type: 'CREATE_SECTION',
      scope: 'track:' + trackId,
      label: `Create Section`,
      before: null,
      after: section,
      undo: (e) => {
        updateActiveTracks(prev => prev.map(t => t.id === trackId ? { ...t, sections: (t.sections || []).filter(s => s.id !== e.after.id) } : t));
        showToast(`Undo: Create Section`, 'info');
      },
      redo: (e) => {
        updateActiveTracks(prev => prev.map(t => t.id === trackId ? { ...t, sections: [...(t.sections || []), e.after] } : t));
        showToast(`Redo: Create Section`, 'info');
      }
    };
    if (window.UndoRedoEngine) window.UndoRedoEngine.execute(entry);
  };

  const createMidiWithUndo = (trackId, midiItem) => {
    const entry = {
      type: 'CREATE_MIDI',
      scope: 'track:' + trackId,
      label: `Create MIDI Item`,
      before: null,
      after: midiItem,
      undo: (e) => {
        updateActiveTracks(prev => prev.map(t => t.id === trackId ? { ...t, midiItems: (t.midiItems || []).filter(m => m.id !== e.after.id) } : t));
        showToast(`Undo: Create MIDI`, 'info');
      },
      redo: (e) => {
        updateActiveTracks(prev => prev.map(t => t.id === trackId ? { ...t, midiItems: [...(t.midiItems || []), e.after] } : t));
        showToast(`Redo: Create MIDI`, 'info');
      }
    };
    if (window.UndoRedoEngine) window.UndoRedoEngine.execute(entry);
  };

  // Export MIDI notes → audio bằng VSTi: midi-render (pedalboard) → chèn clip
  // vào track (cùng flow insert sound clip, có undo) + download file wav.
  const handleExportMidiAudio = async (st, notes, bpm) => {
    const se = st.synth_engine || {};
    const instrumentId = se.plugin_id || st.instrumentId || null;
    if (!instrumentId || String(se.type || '').indexOf('vst') === -1) {
      showToast('Chọn VST instrument (nút Synth) trước khi Export MIDI → Audio', 'warning');
      return;
    }
    if (!notes || !notes.length) { showToast('Chưa có nốt nhạc', 'warning'); return; }
    const payload = {
      instrument_id: instrumentId,
      notes: (notes || []).map(n => ({ pitch: n.pitch, start_beat: n.start_beat, duration_beats: n.duration_beats, velocity: n.velocity != null ? n.velocity : 0.8 })),
      bpm: parseFloat(bpm) || 120,
      preset_id: se.preset_id || undefined,
      preset_path: se.preset_path || undefined,
      preset_data: se.preset_data || undefined,
    };
    try {
      const res = await window.SonicAPI.midiRender(payload);
      // Decode wav để chèn clip có buffer (phát được ngay trong track)
      const resp = await fetch(res.url);
      if (!resp.ok) throw new Error('Không tải được wav render');
      const blob = await resp.blob();
      const { audioBuffer, channelInfo } = await window.SonicAudio.decodeAudioFile(blob);
      const clipId = `clip_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
      const track = activeTracks.find(t => t.id === st.trackId);
      const newClip = {
        id: clipId,
        buffer: audioBuffer,
        channelInfo: channelInfo,
        startTime: currentTime || 0,
        name: `MIDI ${st.label || 'render'}`,
        speed: 1.0,
        serverFileId: res.file_id,
      };
      createClipWithUndo(st.trackId, newClip);
      updateActiveTracks(prev => prev.map(t => t.id === st.trackId ? {
        ...t,
        clips: [...(t.clips || []), newClip],
        serverFileId: t.serverFileId || res.file_id,
      } : t));
      // Download wav (đã lưu trên server processed)
      const a = document.createElement('a');
      a.href = res.url;
      a.download = ((st.label || 'midi').replace(/[^a-z0-9-_]/gi, '_')) + '_vsti.wav';
      document.body.appendChild(a);
      a.click();
      document.body.removeChild(a);
      showToast(`Đã render MIDI → Audio (${(res.duration_sec || 0).toFixed(1)}s) + chèn clip`, 'success');
    } catch (err) {
      showToast('Export MIDI → Audio thất bại: ' + ((err && err.message) || 'lỗi'), 'error');
    }
  };

  const createClipWithUndo = (trackId, clip) => {
    const entry = {
      type: 'CREATE_CLIP',
      scope: 'track:' + trackId,
      label: `Create Audio Clip`,
      before: null,
      after: clip,
      undo: (e) => {
        updateActiveTracks(prev => prev.map(t => t.id === trackId ? {
          ...t,
          clips: (t.clips || []).filter(c => c.id !== e.after.id),
          buffer: (t.clips || []).filter(c => c.id !== e.after.id)[0]?.buffer || null,
          startTime: (t.clips || []).filter(c => c.id !== e.after.id)[0]?.startTime || 0,
          name: (t.clips || []).filter(c => c.id !== e.after.id)[0]?.name || t.name
        } : t));
        showToast(`Undo: Create Clip`, 'info');
      },
      redo: (e) => {
        updateActiveTracks(prev => prev.map(t => t.id === trackId ? { ...t, clips: [...(t.clips || []), e.after] } : t));
        showToast(`Redo: Create Clip`, 'info');
      }
    };
    if (window.UndoRedoEngine) window.UndoRedoEngine.execute(entry);
  };

  const deleteTrackWithUndo = (trackId, trackData) => {
    const entry = {
      type: 'DELETE_TRACK',
      scope: 'global',
      label: `Delete Track`,
      before: trackData,
      after: null,
      undo: (e) => {
        if (e.before) {
          setTracks(prev => [...prev, e.before]);
          showToast(`Undo: Delete Track`, 'info');
        }
      },
      redo: (e) => {
        setTracks(prev => prev.filter(t => t.id !== trackId));
        showToast(`Redo: Delete Track`, 'info');
      }
    };
    if (window.UndoRedoEngine) window.UndoRedoEngine.execute(entry);
  };

  // ── Tab System (LOOP_EDITOR_2.md §1) ──
  const [activeTab, setActiveTab] = useState('main');
  const [subTabSelectedNodeTime, setSubTabSelectedNodeTime] = useState(null);
  const [subTabNormVal, setSubTabNormVal] = useState(0);
  const [subTabGainVal, setSubTabGainVal] = useState(100);
  const [subTabPitchVal, setSubTabPitchVal] = useState(0);
  const [subTabs, setSubTabs] = useState([]); // [{id, label, trackId, startTime, endTime, buffer, effects, currentTime, selectionStart, selectionEnd, isPlaying}, ...]
  const [pianoRollClipboard, setPianoRollClipboard] = useState(null); // notes copy giữa các PIANO ROLL TAB
  const [sessionTabs, setSessionTabs] = useState([]); // [{id, name, tracks}, ...]
  const [tabContextMenu, setTabContextMenu] = useState(null); // { x, y, tabId, tabType }
  const handleSetTabColor = (tabId, tabType, color) => {
    if (tabType === 'session') {
      setSessionTabs(prev => prev.map(s => s.id === tabId ? { ...s, color: color } : s));
      const tab = sessionTabs.find(s => s.id === tabId);
      if (tab) {
        setTracks(prev => prev.map(t => {
          if (!t.sections || t.sections.length === 0) return t;
          return {
            ...t,
            sections: t.sections.map(s => {
              if (s.sectionId === tab.sectionId) {
                return { ...s, color: color };
              }
              return s;
            })
          };
        }));
      }
    } else {
      setSubTabs(prev => prev.map(s => s.id === tabId ? { ...s, color: color } : s));
    }
  };
  const activeTracks = useMemo(() => {
    var st = sessionTabs.find(s => s.id === activeTab);
    if (st) return st.tracks;
    var pr = subTabs.find(s => s.id === activeTab && s.type === 'PIANO_ROLL');
    if (pr && pr.parent_tab_id && pr.parent_tab_id.startsWith('session_')) {
      var parentSt = sessionTabs.find(s => s.id === pr.parent_tab_id);
      if (parentSt) return parentSt.tracks;
    }
    return tracks;
  }, [activeTab, sessionTabs, subTabs, tracks]);
  const activeTracksRef = useRef([]);
  activeTracksRef.current = activeTracks;
  const sessionTabsRef = useRef(sessionTabs);
  sessionTabsRef.current = sessionTabs;

  // Vị trí MIDI item (trong context của piano roll) — dùng để đồng bộ playhead
  // 2 chiều SECTION-TAB ↔ PIANO ROLL: item.startTime (local section / main) +
  // st.currentTime (vị trí TRONG item). Dùng REFS (activeTracksRef/
  // sessionTabsRef) — an toàn với closure cũ (updatePlayhead/rAF loop).
  const getPianoRollItemInfo = st => {
    try {
      if (!st) return null;
      let list = null;
      if (st.parent_tab_id && st.parent_tab_id.startsWith('session_')) {
        const _pst = sessionTabsRef.current.find(s => s.id === st.parent_tab_id);
        list = _pst ? _pst.tracks : null;
      } else {
        list = activeTracksRef.current;
      }
      if (!list) return null;
      const _trk = list.find(t => t.id === st.trackId);
      if (!_trk) return null;
      const _item = (_trk.midiItems || []).find(m => m.id === st.target_id);
      if (!_item) return null;
      return { startTime: _item.startTime || 0, duration: _item.duration || 4 };
    } catch (e) {
      return null;
    }
  };

  // Track mute/solo/volume signature: only re-apply gains when something that
  // affects audibility actually changed (loads, undo, AI ops, section tabs).
  const trackMuteSoloSigRef = useRef({});
  // Last-known audibility per track id (used to detect inaudible→audible
  // transitions that need a playback re-schedule).
  const trackAudibleRef = useRef({});
  // Audio-relevant signature (mute/solo/vol/bypass/items count) — đổi màu/
  // rename KHÔNG trigger re-sync/re-route (tránh mất âm khi play).
  const trackAudioSyncSigRef = useRef(null);

  // Keep the mastering-bypass routing map + realtime mute/solo in sync with the
  // tracks state (loads, undo, AI ops…). Placed AFTER the tracks/subTabs/
  // sessionTabs declarations (TDZ-safe).
  useEffect(() => {
    // Sync theo thứ tự: session/clone TRƯỚC, MAIN tracks SAU — khi id trùng
    // (section clone giữ id sub-track = id track main), MAIN thắng: map là
    // fallback cho track không có field; clone có field (reset 0330) nên route
    // dùng field object (context đúng) — map chéo không ảnh hưởng.
    const all = [...(sessionTabs || []).reduce((acc, s) => acc.concat(s.tracks || []), []), ...(tracks || [])];
    all.forEach(t => {
      trackMasteringBypassMap[t.id] = !!t.masteringBypass;
      trackAudioBypassMap[t.id] = !!t.audioBypass;
      trackMidiBypassMap[t.id] = !!t.midiBypass;
    });
    const list = (activeTracksRef.current && activeTracksRef.current.length) ? activeTracksRef.current : all;
    // Audio-relevant signature: đổi MÀU/rename track (không liên quan audio)
    // KHÔNG được trigger re-sync/re-route (tránh mất âm khi play sau đó —
    // updateSfRouting chạy thừa có thể đặt SF destination sai thời điểm).
    const audioSig = list.map(t => (t.muted ? '1' : '0') + (t.solo ? '1' : '0') + ':' + (t.volumeDb ?? 0) + ':' + (t.audioBypass ? '1' : '0') + (t.midiBypass ? '1' : '0') + ':' + (t.midiItems || []).length + ':' + (t.clips || []).length + ':' + (t.sections || []).length).join('|');
    if (trackAudioSyncSigRef.current === audioSig) return;
    trackAudioSyncSigRef.current = audioSig;
    list.forEach(t => {
      // ♪ state → SF mastering route (sfRouteGain/sfDryGain), live on existing nodes
      const sn = activeTrackNodesRef.current[t.id];
      if (sn && sn.sfRouteGain && sn.sfDryGain && (sn.sfRouteGain.gain.value > 0) !== !effMidiBypass(t)) {
        const _b = effMidiBypass(t);
        sn.sfRouteGain.gain.value = _b ? 0 : 1;
        sn.sfDryGain.gain.value = _b ? 1 : 0;
      }
      const sig = (t.muted ? '1' : '0') + (t.solo ? '1' : '0') + ':' + (t.volumeDb ?? 0);
      if (trackMuteSoloSigRef.current[t.id] === sig) return;
      trackMuteSoloSigRef.current[t.id] = sig;
      const audible = computeTrackAudibleGain(list, t) > 0;
      const node = activeTrackNodesRef.current[t.id];
      const audibleGain = computeTrackAudibleGain(list, t);
      if (node) setTrackNodeGain(node, audibleGain);
      if (node && node.sfEntry) {
        try {
          const g = (typeof audibleGain === 'number' && isFinite(audibleGain) && !isNaN(audibleGain)) ? audibleGain : 1.0;
          node.sfEntry.gain.setTargetAtTime(g, getAudioContext().currentTime, 0.02);
        } catch (e) {}
      }
      // MIDI tracks: mirror the gain decision onto the channel CC7 volume so
      // FluidSynth-rendered notes respect mute/solo too. CC7 = VOLUME track
      // (không chỉ 100/0) — volume slider MIDI tác dụng qua channel volume.
      if ((t.midiItems && t.midiItems.length > 0) || t.type === 'MIDI') {
        try {
          if (window.SonicSF && typeof window.SonicSF.controllerChange === 'function') {
            const ch = ensureTrackMidiChannel(t, list);
            var cc7 = 100;
            if (audible) {
              var volDb = (typeof t.volumeDb === 'number' && isFinite(t.volumeDb)) ? t.volumeDb : 0;
              var volLin = volDb <= -50 ? 0 : Math.pow(10, volDb / 20);
              cc7 = Math.max(1, Math.min(127, Math.round(volLin * 100)));
            } else {
              cc7 = 0;
            }
            window.SonicSF.controllerChange(ch, 7, cc7);
          }
        } catch (e) {}
      }
      trackAudibleRef.current[t.id] = audible;
    });
    updateSfRouting();
  }, [tracks, sessionTabs]);

  const prevActiveTabRef = useRef(activeTab);
  useEffect(() => {
    const prevTab = prevActiveTabRef.current;
    prevActiveTabRef.current = activeTab;
    updateSfRouting();
    // Log chẩn đoán zoom khi đổi tab (user: zoom reset khi chuyển tab)
    if (prevTab !== activeTab) {
      console.log('[Zoom] đổi tab', prevTab, '→', activeTab, '| zoom=', zoom, '| minZoom=', (minZoom || 0).toFixed(2));
    }
    // Tab DEACTIVE → stop âm của tab đó (user requirement): rời khỏi sub-tab
    // (PIANO_ROLL/section/audio tab) đang play → stop playback của tab đó.
    // Tab mới hoạt động bình thường; MAIN giữ hành vi cũ (mở piano-roll lúc
    // main play → main tiếp tục — handleEditMidiInTab đã xử lý).
    if (prevTab !== activeTab) {
      // Vào SECTION-TAB lúc MAIN đang play → CHỈ play items CỦA SECTION-TAB,
      // TẮT âm của MAIN items (user yêu cầu 06:40): restart play với
      // activeTracks = section clones (activeTab mới → activeTracksRef mới).
      if (prevTab === 'main' && activeTab && activeTab.startsWith('session_') && isPlaying) {
        try {
          const _ctx = getAudioContext();
          // Vị trí play → LOCAL của section tab (trừ secStart — vị trí section
          // trên main): section tab items dùng time local (0-based)
          let _secStart = 0;
          const _tab = sessionTabsRef.current.find(st => st.id === activeTab);
          if (_tab && _tab.sectionId) {
            (tracks || []).forEach(_t => (_t.sections || []).forEach(_s => {
              if (_s.sectionId === _tab.sectionId || _s.id === _tab.sectionId) _secStart = _s.start || 0;
            }));
          }
          const _pt = Math.max(0, startOffsetTimeRef.current + (_ctx.currentTime - startAudioTimeRef.current) - _secStart);
          stopAllPlayback();
          startOffsetTimeRef.current = _pt;
          startAudioTimeRef.current = _ctx.currentTime;
          startTrackPlayback(_pt);
          setIsPlaying(true);
          console.log('[SectionTab] play CHỈ section items tại local', _pt.toFixed(2), '(secStart', _secStart.toFixed(2) + ')');
        } catch (e) { console.warn('[SectionTab] restart error:', e); }
      }
      const prevSub = subTabsRef.current.find(s => s.id === prevTab);
      if (prevSub && prevSub.isPlaying) {
        // Chuyển sang SUB-TAB KHÁC (piano roll khác/audio tab) → STOP âm tab
        // cũ — play ĐÚNG nội dung TỪNG TAB (user 07:30: piano roll tab 1 →
        // tab 2 — không được nghe âm tab 1). Về MAIN/SECTION → giữ luật cũ
        // (04:15+ — âm tiếp/resume).
        const _newIsSub = activeTab && activeTab !== 'main' && !activeTab.startsWith('session_');
        if (prevSub.type !== 'PIANO_ROLL' || _newIsSub) {
          try { stopAllPlayback(); } catch (e) {}
        }
        setSubTabs(prev => prev.map(s => s.id === prevTab ? { ...s, isPlaying: false } : s));
        // Resume main/session play khi rời PIANO ROLL tab đã play (Space):
        // play piano roll → stopAllPlayback dừng main → quay lại MAIN/SECTION
        // bị CÂM. Resume startTrackPlayback(offset + elapsed thật) — chỉ khi
        // trước đó mở tab lúc main đang play (mainResumeRef đã set).
        const _resume = mainResumeRef.current;
        mainResumeRef.current = null;
        if (prevSub.type === 'PIANO_ROLL' && (activeTab === 'main' || (activeTab && activeTab.startsWith('session_')))) {
          // Resume section/main play tại vị trí TƯƠNG ỨNG với piano roll:
          // resumeAt = item.startTime + st.currentTime (vị trí TRONG item —
          // yêu cầu: play piano roll ở vị trí nào → về SECTION play đúng vị trí
          // đó). Ưu tiên item info; fallback mainResumeRef (offset+elapsed);
          // fallback cuối: setIsPlaying(true) — playhead chạy theo âm còn kêu.
          let _resumeAt = null;
          const _itemInfo = getPianoRollItemInfo(prevSub);
          if (_itemInfo) {
            _resumeAt = _itemInfo.startTime + (prevSub.currentTime || 0);
            // Về MAIN mà item thuộc SECTION: vị trí main = secStart + local
            if (activeTab === 'main' && prevSub.parent_tab_id && prevSub.parent_tab_id.startsWith('session_')) {
              const _pst = sessionTabsRef.current.find(s => s.id === prevSub.parent_tab_id);
              if (_pst && _pst.sectionId) {
                (activeTracksRef.current || []).forEach(_t => (_t.sections || []).forEach(_s => {
                  if (_s.sectionId === _pst.sectionId || _s.id === _pst.sectionId) _resumeAt += (_s.start || 0);
                }));
              }
            }
          } else if (_resume) {
            _resumeAt = _resume.offset + (getAudioContext().currentTime - _resume.audioTime);
          }
          if (_resumeAt !== null) {
            try {
              console.log('[Resume] play tại', _resumeAt.toFixed(2), 's (item.startTime=' + (_itemInfo ? _itemInfo.startTime.toFixed(2) : '?') + ' + st.currentTime=' + (prevSub.currentTime || 0).toFixed(2) + ')');
              stopAllPlayback();
              startOffsetTimeRef.current = _resumeAt;
              startAudioTimeRef.current = getAudioContext().currentTime;
              startTrackPlayback(_resumeAt);
              setIsPlaying(true);
            } catch (e) { console.warn('[Resume] error:', e); }
          } else {
            console.log('[Playhead] tiếp tục theo piano roll (không stop âm) — activeTab=' + activeTab);
            setIsPlaying(true);
          }
        }
      } else if (prevSub && prevSub.type === 'PIANO_ROLL') {
        // Rời PIANO ROLL tab KHÔNG play: main/section play ngầm vẫn chạy (âm
        // tiếp tục tự nhiên — không cần resume) → clear resume point.
        // ⚠️ KHÔNG clear khi prevTab = main/session (đang DI CHUYỂN VÀO piano
        // roll — mainResumeRef vừa set bởi handleEditMidiInTab — clear nhầm ở
        // đây làm resume chết → quay lại main/section bị CÂM).
        mainResumeRef.current = null;
      }
    }
  }, [activeTab]);

  const [selectionCleared, setSelectionCleared] = useState(false); // LOOP_EDITOR_2.md §4.2

  // Detect MAIN/SECTION play vừa DỪNG (hết notes/projectEnd/stop): nếu đang ở
  // PIANO ROLL tab (không play tab) → playhead về 0 — user: main play hết →
  // không còn midi note → piano roll phải về đầu (không giữ local cuối).
  const prevIsPlayingRef = useRef(false);
  useEffect(() => {
    const wasPlaying = prevIsPlayingRef.current;
    prevIsPlayingRef.current = isPlaying;
    if (wasPlaying && !isPlaying) {
      const _tabId = activeTabRef.current;
      if (_tabId && _tabId.startsWith('midi_')) {
        const _prSt = subTabsRef.current.find(s => s.id === _tabId);
        if (_prSt && !_prSt.isPlaying) {
          setSubTabs(prev => prev.map(s => s.id === _tabId ? { ...s, currentTime: 0 } : s));
        }
      }
    }
  }, [isPlaying]);

  const midiVuActivityRef = useRef({});
  // Đếm số note MIDI đang GIỮ per-track (ARM + MIDI keyboard live input).
  // VU tick dùng ref này: còn note giữ → giữ peak (không decay) → VU animate
  // ĐÚNG trường độ âm thanh (user bug: nhấn phím giữ âm còn kêu nhưng VU tắt
  // sau ~0.5s vì decay 0.75/frame). Hết note (note-off) → mới decay/tắt.
  const heldMidiNotesRef = useRef({});
  const triggerMidiVuActivity = (trackId, velocity) => {
    if (!trackId) return;
    const velFactor = typeof velocity === 'number' ? (velocity > 1 ? velocity / 127 : velocity) : 0.8;
    const peak = Math.min(1.0, Math.max(0.15, velFactor));
    midiVuActivityRef.current[trackId] = peak;
    // Log nguồn trigger (rate-limited 1/20 fire): xác định ai còn fire VU khi
    // âm đã ngừng (user: VU vẫn animation sau khi hết âm).
    try {
      const _tf = (window.__trigFrame = (window.__trigFrame || 0) + 1);
      if (_tf % 20 === 1) {
        console.log('[VUTrig]', trackId, 'vel=', velocity, 'peak=', peak.toFixed(2), 'play=', isPlaying, 'tab=', activeTabRef.current);
      }
    } catch (e) { }
  };
  window.triggerMidiVuActivity = triggerMidiVuActivity;


  // ── Temp Edit Tab (LOOP_EDITOR_2.md §1.2 Sub Tab) ──
  const [tempTabActive, setTempTabActive] = useState(false);
  const [tempTabBuffer, setTempTabBuffer] = useState(null);
  const [tempTabTrackId, setTempTabTrackId] = useState(null);
  const [tempTabOrigStart, setTempTabOrigStart] = useState(0);
  const [tempTabOrigEnd, setTempTabOrigEnd] = useState(0);
  const tempTabCanvasRef = useRef(null);

  // Effect parameters for temp tab
  const [tempTabEffects, setTempTabEffects] = useState({
    reverse: false,
    gainDb: 0,
    fadeInMs: 0,
    fadeOutMs: 0
  });

  // ── Auth / User State ──
  const [currentUser, setCurrentUser] = useState(null);
  const [authModalOpen, setAuthModalOpen] = useState(false);
  const [authMode, setAuthMode] = useState('login'); // 'login' | 'register' | 'force_change'
  const [isMandatoryLogin, setIsMandatoryLogin] = useState(false);
  const [profileModalOpen, setProfileModalOpen] = useState(false);
  const [systemManagerModalOpen, setSystemManagerModalOpen] = useState(false);
  const [aiConfigModalOpen, setAiConfigModalOpen] = useState(false);
  const [aiPresetModalOpen, setAiPresetModalOpen] = useState(false);
  const [aiPresetVersion, setAiPresetVersion] = useState(0);
  const [showMasteringModal, setShowMasteringModal] = useState(false);
  // ── Help / About / Preferences (menu Help + Tools) ──
  const [aboutModalOpen, setAboutModalOpen] = useState(false);
  const [helpModalOpen, setHelpModalOpen] = useState(false);
  const [preferencesModalOpen, setPreferencesModalOpen] = useState(false);
  const [prefs, setPrefs] = useState(() => {
    try {
      const saved = localStorage.getItem('sf_prefs');
      if (saved) return JSON.parse(saved);
    } catch (e) { }
    return { theme: 'dark', language: 'vi', buttonFontSize: 'md' };
  });
  // Áp dụng theme + font size lên <html> ngay khi đổi
  useEffect(() => {
    try {
      document.documentElement.setAttribute('data-theme', prefs.theme || 'dark');
      document.documentElement.setAttribute('data-btnfont', prefs.buttonFontSize || 'md');
      localStorage.setItem('sf_prefs', JSON.stringify(prefs));
    } catch (e) { }
  }, [prefs]);
  // Load preferences từ server (nếu có tài khoản) khi khởi động
  useEffect(() => {
    let cancelled = false;
    if (window.SonicAPI && window.SonicAPI.getPreferences) {
      window.SonicAPI.getPreferences().then(res => {
        if (cancelled || !res || !res.success || !res.preferences) return;
        const p = res.preferences;
        if (p.theme || p.language || p.buttonFontSize) {
          setPrefs(prev => ({
            theme: p.theme || prev.theme,
            language: p.language || prev.language,
            buttonFontSize: p.buttonFontSize || prev.buttonFontSize
          }));
        }
      }).catch(() => { });
    }
    return () => { cancelled = true; };
  }, []);
  // Lưu preferences lên server khi đổi
  const handlePrefsChange = (next) => {
    setPrefs(next);
    try { localStorage.setItem('sf_prefs', JSON.stringify(next)); } catch (e) { }
    if (window.SonicAPI && window.SonicAPI.savePreferences) {
      window.SonicAPI.savePreferences(next).catch(() => { });
    }
  };
  // Unified FX Rack target context (unified_fx_rack_panel.md): { trackId, trackName } | null
  const [fxRackTarget, setFxRackTarget] = useState(null);
  window.__openFxRack = (trackId, trackName) => setFxRackTarget({ trackId, trackName });
  const [masteringSettings, setMasteringSettings] = useState({
    masterConnected: true,  // ⚠️ FIX: bật ON mặc định — MIDI item/preview PHẢI được xử lí qua mastering FX ngay khi chạy app (không cần bật/tắt power)
    activeModule: 'eq',
    eqActive: true,
    imagerActive: true,
    maximizerActive: true,
    eqLowGain: 1.5,
    eqMid1Gain: -1.0,
    eqMid2Gain: 2.0,
    eqHighGain: 1.8,
    w1: 0,
    w2: 115,
    w3: 135,
    w4: 150,
    imagerScale: 'v2',
    maxGain: 5.4,
    maxUpward: 2.0,
    maxSoftClip: 15,
    maxTransient: 25,
    ceiling: -0.1,
    isBypassed: false,
    chain: DEFAULT_MASTER_CHAIN.map(m => ({ ...m })),
    compActive: false, compThreshold: -16, compRatio: 3, compMakeup: 0,
    limActive: false, limThreshold: -1.0,
    excActive: false, excDrive: 40,
    rebalActive: false, rebalMid: 0, rebalSide: 0,
  });

  useEffect(() => {
    window.currentMasteringSettings = masteringSettings;
    if (audioCtx && masterBus) {
      toggleMasteringOnMaster(masteringSettings.masterConnected, masteringSettings.isBypassed);
      applyMasteringSettings(masteringSettings);
      // Re-sync live track routes: mastering ON → mọi track qua chain (♪ bị
      // override bởi effMidiBypass/effAudioBypass) — nút PWR bật/tắt phải áp
      // ngay lên node đang phát (không chờ tracks effect).
      try {
        const _list = (activeTracksRef.current && activeTracksRef.current.length) ? activeTracksRef.current : tracks;
        _list.forEach(_t => {
          const _n = activeTrackNodesRef.current[_t.id];
          if (_n) {
            if (_n.sfRouteGain && _n.sfDryGain) {
              const _b = effMidiBypass(_t);
              _n.sfRouteGain.gain.value = _b ? 0 : 1;
              _n.sfDryGain.gain.value = _b ? 1 : 0;
            }
            if (_n.route && _n.route.routeGain && _n.route.dryGain) {
              const _ab = routeBypassFor(_t);
              _n.route.routeGain.gain.value = _ab ? 0 : 1;
              _n.route.dryGain.gain.value = _ab ? 1 : 0;
            }
          }
        });
        updateSfRouting();
      } catch (e) {}
    }
  }, [masteringSettings]);

  const [pluginManagerModalOpen, setPluginManagerModalOpen] = useState(false);
  const [pluginsData, setPluginsData] = useState(null);
  // D10: bridge indicator + "Ép dùng WASM" debug toggle.
  const [bridgeUi, setBridgeUi] = useState({ connected: false, forceWasm: false });
  React.useEffect(() => {
    if (!window.NativeBridgeService) return;
    setBridgeUi(s => ({ ...s, connected: !!window.NativeBridgeService.isBridgeConnected }));
    window.NativeBridgeService.onStatusChange(function (st) {
      // V8 bug 4: reset map NGAY DAU callback (ca connected lan !connected) —
      // Rust auto-restart KHONG emit bridge-down khi thanh cong → map cu true →
      // GUI click skip load → attach null → nen trang.
      window.__bridgeLoadedChannels = {};
      setBridgeUi(s => ({ ...s, connected: !!st.connected }));
      if (!st.connected) {
        // Bridge down/restart → C++ mất hết instrument → reset map để lần tới
        // load lại (nút GUI ensure-load trước khi open — tránh cửa sổ trắng).
        window.__bridgeLoadedChannels = {};
        return;
      }
      // Bridge (re)connect → load lại toàn bộ VST track (không mở GUI).
      try {
        const _tl = activeTracksRef.current || [];
        for (let i = 0; i < _tl.length; i++) {
          const _t = _tl[i];
          const _inst = _t.instrumentId || (_t.synth_engine && _t.synth_engine.plugin_id);
          if (!_inst || (typeof _inst === 'string' && _inst.startsWith('sf_'))) continue;
          loadVstToBridge(_t).catch(function () {});
        }
      } catch (e) { console.warn('[Bridge] reconnect reload fail:', e); }
    });
    return () => window.NativeBridgeService.onStatusChange(null);
  }, []);

  const loadAudioBuffersForTracks = async (tracksList) => {
    let hasLoadedAny = false;
    const loadBuffer = async (url) => {
      const res = await fetch(url);
      if (!res.ok) return null;
      const blob = await res.blob();
      return await window.SonicAudio.decodeAudioFile(blob);
    };
    const tryLoad = async (fileId) => {
      if (!fileId) return null;
      try {
        const result = await loadBuffer('/static/audio/uploads/' + fileId);
        if (result) return result;
      } catch (_) {}
      try {
        const result = await loadBuffer(`${API_AUDIO}/download/${fileId}`);
        if (result) return result;
      } catch (_) {}
      return null;
    };
    const updatedTracks = await Promise.all(tracksList.map(async t => {
      let trackBuffer = t.buffer;
      let trackChannelInfo = t.channelInfo;
      if (t.serverFileId && !trackBuffer) {
        const result = await tryLoad(t.serverFileId);
        if (result) {
          trackBuffer = result.audioBuffer;
          trackChannelInfo = result.channelInfo;
          hasLoadedAny = true;
        }
      }
      const updatedClips = await Promise.all((t.clips || []).map(async c => {
        let clipBuffer = c.buffer;
        const targetFileId = c.serverFileId || t.serverFileId;
        if (targetFileId && !clipBuffer) {
          const result = await tryLoad(targetFileId);
          if (result) {
            clipBuffer = result.audioBuffer;
            hasLoadedAny = true;
          }
        }
        return { ...c, buffer: clipBuffer };
      }));
      // Section items: nạp CẢ audioclip bên trong section (trước đây bỏ sót →
      // audioclip trong section hiện "(audio chưa được tải)" sau save+open).
      const updatedSections = await Promise.all((t.sections || []).map(async s => {
        const updatedSTracks = await Promise.all((s.tracks || []).map(async st => {
          const updatedSClips = await Promise.all((st.clips || []).map(async c => {
            let clipBuffer = c.buffer;
            const targetFileId = c.serverFileId || st.serverFileId;
            if (targetFileId && !clipBuffer) {
              const result = await tryLoad(targetFileId);
              if (result) {
                clipBuffer = result.audioBuffer;
                hasLoadedAny = true;
              }
            }
            return { ...c, buffer: clipBuffer };
          }));
          return { ...st, clips: updatedSClips };
        }));
        return { ...s, tracks: updatedSTracks };
      }));
      if (trackBuffer && updatedClips.length === 0) {
        const clipId = `default_${t.id}`;
        return { ...t, buffer: trackBuffer, channelInfo: trackChannelInfo,
          clips: [{ id: clipId, buffer: trackBuffer, startTime: t.startTime || 0, name: t.name, speed: 1.0 }],
          sections: updatedSections };
      }
      return { ...t, buffer: trackBuffer, channelInfo: trackChannelInfo, clips: updatedClips, sections: updatedSections };
    }));
    if (hasLoadedAny) {
      setTracks(prev => {
        // Merge by TRACK ID (not array index): if the state changed between the
        // fetch start and now (e.g. another project opened), index-based merging
        // would scramble tracks and drop items into the wrong track. updatedTracks
        // is authoritative; patch clip buffers from prev by matching clip ids.
        const prevById = new Map((prev || []).map(pt => [pt.id, pt]));
        return updatedTracks.map(ut => {
          const pt = prevById.get(ut.id);
          if (!pt) return ut;
          const ptClips = pt.clips || [];
          const utClips = ut.clips || [];
          const mergedClips = ptClips.map(pc => {
            const uc = utClips.find(c => c.id === pc.id);
            if (uc) return { ...pc, buffer: pc.buffer || uc.buffer };
            return pc;
          });
          return { ...ut, clips: mergedClips };
        });
      });
      // Merge cho SESSION TABS: track của session tab có id TRÙNG track MAIN
      // (vd id 1) — match vào allLoadedTracks (main đứng trước) → session tab
      // Track 01 bị THAY bằng track MAIN (chứa SECTION_ITEM + không có midi)
      // → block lồng + mất MIDI. FIX: session tab CHỈ match với INNER TRACKS
      // của section items (đúng tầng), KHÔNG match track main top-level.
      const sectionInnerTracks = [];
      updatedTracks.forEach(function (ut) {
        (ut.sections || []).forEach(function (s) {
          (s.tracks || []).forEach(function (st) { sectionInnerTracks.push(st); });
        });
      });
      setSessionTabs(prev => prev.map(st => ({
        ...st,
        tracks: (st.tracks || []).map(t => {
          const found = sectionInnerTracks.find(u => u.id === t.id);
          return found || t;
        })
      })));
    }
  };

  const restoreLastSessionProject = async () => {
    var pendingWasNull = !window.__pendingSfsProject;
    loadPendingSfsProject();
    if (!pendingWasNull) return;
    
    var lastId = localStorage.getItem('sonic_project_id');
    var lastName = localStorage.getItem('sonic_project_name') || 'Dự án';
    var parsed = null;
    if (!lastId) {
      // Máy mới / browser ẩn danh: localStorage TRỐNG (id/name không tồn tại
      // trên máy này) → tự mở project Cloud GẦN NHẤT của tài khoản đang đăng
      // nhập — project tạo trên máy khác vẫn mở được ngay.
      const prof = currentUser || (function () { try { return JSON.parse(localStorage.getItem('sonic_user') || 'null'); } catch (e) { return null; } })();
      if (prof && window.SonicAPI && window.SonicAPI.listCloudProjects) {
        try {
          const cloudList = await window.SonicAPI.listCloudProjects();
          if (cloudList && cloudList.length > 0) {
            const latest = cloudList[0]; // backend ORDER BY updated_at DESC
            lastId = latest.id;
            lastName = latest.name || lastName;
            const proj = await window.SonicAPI.getCloudProject(lastId);
            if (proj && proj.data_json) parsed = JSON.parse(proj.data_json);
          }
        } catch (e) { console.warn('restore cloud fallback error:', e); }
      }
    } else {
      try {
        if (lastId.startsWith('local_')) {
          var localData = localStorage.getItem('sonic_local_project_data');
          if (localData) parsed = JSON.parse(localData);
        } else {
          var proj = await window.SonicAPI.getCloudProject(lastId);
          if (proj) parsed = JSON.parse(proj.data_json);
        }
      } catch (e) { console.warn('restore load error:', e); }
    }
    try {
      if (!parsed) return;
      var restoredBpm = bpm;
      var restoredTracks = [];
      var restoredSessionTabs = [];
      var restoredSubTabs = [];
      if (parsed.main_session) {
        var result = deserializeProjectFromSchema(parsed);
        restoredTracks = result.tracks;
        restoredBpm = result.bpm;
        restoredSessionTabs = result.sessionTabs;
        restoredSubTabs = result.subTabs;
        // Bỏ qua zoom ≤ 1 (data cũ lưu 1.0 — lỗi __currentZoom của Media
        // Explorer) — áp dụng mới giữ zoom hiện tại/localStorage — hết thu nhỏ.
        if (result.zoom > 1 && setZoom) setZoom(result.zoom);
        if (result.masteringSettings) setMasteringSettings(result.masteringSettings);
      } else {
        restoredTracks = (parsed.tracks || []).map(function(t) {
          var rest = Object.assign({}, t); delete rest.height;
          return Object.assign({}, rest, { buffer: null, channelInfo: t.channelInfo || null, clips: t.clips || [], serverFileId: t.serverFileId || null });
        });
      }
      setTracks(restoredTracks);
      loadAudioBuffersForTracks(restoredTracks).catch(function(err) { console.warn('loadAudioBuffersForTracks error:', err); });
      trackMidiChannelsRef.current = {};
      preloadTrackInstruments(restoredTracks).catch(function(err) { console.warn('preloadTrackInstruments error:', err); });
      setBpm(restoredBpm.toString());
      setSelectedTrackId(restoredTracks[0]?.id || '1');
      setProjectName(lastName);
      setCurrentProjectId(lastId);
      localStorage.setItem('sonic_project_id', lastId);
      setSessionTabs(restoredSessionTabs);
      setSubTabs(restoredSubTabs);
      // DIAG section-tab block: log tracks + sections của section tab sau restore
      try {
        (restoredSessionTabs || []).forEach(function(st) {
          var secCount = 0, midiCount = 0, clipCount = 0;
          (st.tracks || []).forEach(function(tr) {
            secCount += (tr.sections || []).length;
            midiCount += (tr.midiItems || []).length;
            clipCount += (tr.clips || []).length;
          });
          console.log('[Restore] sessionTab', st.id, 'sectionId', st.sectionId, 'tracks', (st.tracks || []).length, 'sections', secCount, 'midi', midiCount, 'clips', clipCount);
        });
      } catch (e) {}
      var restoredItemCount = 0;
      restoredTracks.forEach(function(rt) {
        if (rt.clips) restoredItemCount += rt.clips.length;
        if (rt.midiItems) restoredItemCount += rt.midiItems.length;
        if (rt.sections) restoredItemCount += rt.sections.length;
      });
      showToast('Đã khôi phục dự án "' + lastName + '" (' + restoredItemCount + ' items).', 'info');
    } catch(e) {
      console.warn('restoreLastSessionProject failed:', e);
      // Stale session id (project deleted / DB reset): clear it so the error
      // does not repeat on every page load.
      localStorage.removeItem('sonic_project_id');
      localStorage.removeItem('sonic_project_name');
    }
  };

  useEffect(() => {
    const checkAuthStatus = async () => {
      const savedToken = localStorage.getItem('sonic_token');
      if (!savedToken) {
        setIsMandatoryLogin(true);
        setAuthMode('login');
        setAuthModalOpen(true);
        return;
      }
      try {
        const profile = await window.SonicAPI.getProfile();
        setCurrentUser(profile);
        localStorage.setItem('sonic_user', JSON.stringify(profile));
        if (profile.must_change_password) {
          setIsMandatoryLogin(true);
          setAuthMode('force_change');
          setAuthModalOpen(true);
        } else {
          setIsMandatoryLogin(false);
          setAuthModalOpen(false);
          restoreLastSessionProject();
        }
      } catch (err) {
        const cached = localStorage.getItem('sonic_user');
        if (cached) {
          try { setCurrentUser(JSON.parse(cached)); } catch (_) { }
          setIsMandatoryLogin(false);
          setAuthModalOpen(false);
          restoreLastSessionProject();
        } else {
          localStorage.removeItem('sonic_token');
          localStorage.removeItem('sonic_user');
          setCurrentUser(null);
          setIsMandatoryLogin(true);
          setAuthMode('login');
          setAuthModalOpen(true);
        }
      }
    };
    checkAuthStatus();
  }, []);

  const loadPendingSfsProject = () => {
    const proj = window.__pendingSfsProject;
    if (!proj) return;
    try {
      let restoredTracks = [];
      let restoredBpm = bpm;
      let restoredSessionTabs = [];
      let restoredSubTabs = [];
      if (proj.main_session) {
        const result = deserializeProjectFromSchema(proj);
        restoredTracks = result.tracks;
        restoredBpm = result.bpm;
        restoredSessionTabs = result.sessionTabs;
        restoredSubTabs = result.subTabs;
        if (result.masteringSettings) {
          setMasteringSettings(result.masteringSettings);
        } else {
          setMasteringSettings({
            masterConnected: false,
            activeModule: 'eq',
            eqActive: true,
            imagerActive: true,
            maximizerActive: true,
            eqLowGain: 1.5,
            eqMid1Gain: -1.0,
            eqMid2Gain: 2.0,
            eqHighGain: 1.8,
            w1: 0,
            w2: 115,
            w3: 135,
            w4: 150,
            imagerScale: 'v2',
            maxGain: 5.4,
            maxUpward: 2.0,
            maxSoftClip: 15,
            maxTransient: 25,
            ceiling: -0.1,
            isBypassed: false,
            chain: DEFAULT_MASTER_CHAIN.map(m => ({ ...m })),
            compActive: false, compThreshold: -16, compRatio: 3, compMakeup: 0,
            limActive: false, limThreshold: -1.0,
            excActive: false, excDrive: 40,
            rebalActive: false, rebalMid: 0, rebalSide: 0,
            carlaBridgeActive: false, carlaBridge: { plugin: '', plugin_path: '', connected: false },
          });
        }
      } else {
        restoredTracks = (proj.tracks || []).map(t => {
          const { height: _h, ...rest } = t;
          return {
            ...rest,
            buffer: null,
            channelInfo: t.channelInfo || null,
            clips: t.clips || [],
            serverFileId: t.serverFileId || null
          };
        });
      }
      if (restoredTracks.length > 0) {
        setTracks(restoredTracks);
        setBpm(restoredBpm.toString());
        if (restoredSessionTabs.length > 0) {
          setSessionTabs(restoredSessionTabs);
        }
        if (restoredSubTabs.length > 0) {
          setSubTabs(restoredSubTabs);
        }
        showToast(`Đã tải dự án "${proj.metadata?.title || proj.name || 'Dự án mới'}" từ liên kết .sfs thành công!`, "success");
      }
    } catch (e) {
      showToast("Lỗi tải dự án từ .sfs", "error");
    } finally {
      window.__pendingSfsProject = null;
    }
  };
  const handleAuthSuccess = user => {
    setCurrentUser(user);
    if (user.must_change_password) {
      setIsMandatoryLogin(true);
      setAuthMode('force_change');
      setAuthModalOpen(true);
    } else {
      setIsMandatoryLogin(false);
      setAuthModalOpen(false);
      restoreLastSessionProject();
      const loadPrefs = (p) => {
        if (!p) return;
        if (p.showAIPanel !== undefined) setShowAIPanel(p.showAIPanel);
        if (p.showExportPanel !== undefined) setShowExportPanel(p.showExportPanel);
        if (p.showSelectionPanel !== undefined) setShowSelectionPanel(p.showSelectionPanel);
        if (p.showPythonToolsPanel !== undefined) setShowPythonToolsPanel(p.showPythonToolsPanel);
        if (p.showMediaExplorer !== undefined) setShowMediaExplorer(p.showMediaExplorer);
        if (p.showFxRack !== undefined) setShowFxRack(p.showFxRack);
        if (p.showMidiEvents !== undefined) setShowMidiEvents(p.showMidiEvents);
        if (p.panelPositions) setPanelPositions(p.panelPositions);
        if (p.rightSidebarWidth) setRightSidebarWidth(p.rightSidebarWidth);
        if (p.mediaExplorerHeight) setMediaExplorerHeight(p.mediaExplorerHeight);
        if (p.selectedProviderId) setSelectedProviderId(p.selectedProviderId);
      };
      (async () => {
        try {
          const data = await window.SonicAPI.getPreferences();
          if (data && data.preferences) loadPrefs(data.preferences);
          else { const cached = localStorage.getItem('sonic_preferences'); if (cached) loadPrefs(JSON.parse(cached)); }
        } catch (e) { const cached = localStorage.getItem('sonic_preferences'); if (cached) loadPrefs(JSON.parse(cached)); }
        try {
          const data = await window.SonicAPI.getAIConfigs();
          if (data && data.providers) {
            setAiProviders(data.providers);
            const active = data.providers.find(p => p.is_active) || data.providers[0];
            if (active) setSelectedProviderId(active.id);
          }
        } catch (e) { }
        try {
          window.SonicAPI.getSoundfontCatalog().then(cat => {
            window.__soundfontCatalog = cat;
          }).catch(() => {});
        } catch (e) { }
        // Re-fetch instrument data after auth (useEffect on mount runs before token is set)
        try {
          window.SonicAPI.listPlugins().then(async data => {
            try {
              const catResp = await window.SonicAPI.getSoundfontCatalog?.() ?? await fetch('/api/v1/plugins/soundfonts/catalog').then(r => r.json());
              const catalog = catResp.full_catalog || {};
              data.soundfonts = (data.soundfonts || []).map(sf => {
                const sfId = sf.id.replace('sf_', '');
                const catEntry = catalog[sfId.toLowerCase()] || catalog[sfId];
                if (catEntry && catEntry.instruments) return { ...sf, presets: catEntry.instruments };
                return sf;
              });
            } catch (e) { console.warn('Catalog fetch error:', e); }
            setInstrumentSelectorData(data);
          }).catch(() => {});
        } catch (e) { }
        // ── Khôi phục dự án đang làm dở (temp autosave) khi load lại app ──
        // Yêu cầu: "Khi tắt ứng dụng → tự động lưu temp; khi load lại → tải lại
        // dự án đang làm dở." Chỉ restore khi app khởi động với project TRỐNG
        // (chưa có clip/midi/audio nào) — không đè lên dự án user đang mở.
        try {
          const tmp = await window.SonicAPI.getTempProject();
          if (tmp && tmp.has_temp && tmp.data_json) {
            let proj = tmp.data_json;
            if (typeof proj === 'string') { try { proj = JSON.parse(proj); } catch (e) { proj = null; } }
            if (proj && proj.main_session) {
              const freshStart = !(tracks && tracks.some(t => (t.clips && t.clips.length > 0) || (t.midiItems && t.midiItems.length > 0) || t.buffer));
              if (freshStart) {
                const result = deserializeProjectFromSchema(proj);
                if (result && result.tracks && result.tracks.length > 0) {
                  setTracks(result.tracks);
                  loadAudioBuffersForTracks(result.tracks);
                  setBpm((result.bpm || 120).toString());
                  if (result.sessionTabs && result.sessionTabs.length > 0) setSessionTabs(result.sessionTabs);
                  if (result.subTabs && result.subTabs.length > 0) setSubTabs(result.subTabs);
                  if (result.masteringSettings) setMasteringSettings(result.masteringSettings);
                  const tmpName = (proj.metadata && proj.metadata.title) || 'Dự án tạm';
                  setProjectName(tmpName);
                  localStorage.setItem('sonic_project_name', tmpName);
                  showToast('Đã khôi phục dự án đang làm dở từ bản lưu tạm', 'success');
                }
              }
            }
          }
        } catch (e) { console.warn('[TempRestore] error:', e); }
      })();
    }
  };
  const handleLogout = () => {
    localStorage.removeItem('sonic_token');
    localStorage.removeItem('sonic_user');
    setCurrentUser(null);
    setIsMandatoryLogin(true);
    setAuthMode('login');
    setAuthModalOpen(true);
  };

  // ── Temp project auto-save (local + server) ──
  useEffect(() => {
    const serializeSafe = arr => (arr || []).map(t => ({
      id: t.id,
      name: t.name,
      startTime: t.startTime,
      height: t.height,
      volumeDb: t.volumeDb,
      pan: t.pan,
      muted: t.muted,
      solo: t.solo,
      color: t.color,
      markers: t.markers || [],
      serverFileId: t.serverFileId || null,
      channelInfo: t.channelInfo ? {
        channels: t.channelInfo.channels,
        isStereo: t.channelInfo.isStereo,
        label: t.channelInfo.label
      } : null
    }));
    window.SonicStorage.scheduleTempAutoSave(() => {
      return serializeProjectToSchema(currentProjectId || 'temp_project', projectName || 'Dự án tạm chưa lưu', bpm, tracks, subTabs, sessionTabs, masteringSettings);
    });
  }, [tracks, subTabs, sessionTabs, masteringSettings]);

  // ── Temp autosave SERVER khi tắt ứng dụng (Bug: đóng app mất dự án làm dở) ──
  // Lưu temp vào thư mục temp của ứng dụng (storage/temp + DB) khi:
  //  1) đóng tab/window (beforeunload/pagehide — sendBeacon keepalive)
  //  2) định kỳ 30s khi có thay đổi (phòng Tauri kill engine ngay khi close)
  // Load lại app → khôi phục dự án đang làm dở (getTempProject ở mount).
  useEffect(() => {
    const buildTempPayload = () => {
      try {
        const schemaObj = serializeProjectToSchema(currentProjectId || 'temp_project', projectName || 'Dự án tạm chưa lưu', bpm, tracks, subTabs, sessionTabs, masteringSettings);
        const cid = (window.SonicStorage && window.SonicStorage.getClientId) ? window.SonicStorage.getClientId() : '';
        return JSON.stringify({ data_json: JSON.stringify(schemaObj), client_id: cid });
      } catch (e) { return null; }
    };
    const saveTempServer = () => {
      const payload = buildTempPayload();
      if (!payload) return;
      try {
        if (navigator.sendBeacon) {
          // sendBeacon không đặt header JSON được — backend đọc body JSON thuần
          const blob = new Blob([payload], { type: 'application/json' });
          navigator.sendBeacon(window.API_BASE_URL + '/api/v1/projects/temp', blob);
        } else {
          fetch(window.API_BASE_URL + '/api/v1/projects/temp', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: payload,
            keepalive: true
          }).catch(() => {});
        }
      } catch (e) { /* fire-and-forget */ }
    };
    const onUnload = (e) => {
      saveTempServer();
      // Để sendBeacon có cơ hội gửi trước khi WebView bị destroy
      try { navigator.sendBeacon && navigator.sendBeacon(window.API_BASE_URL + '/health', new Blob(['ping'], { type: 'text/plain' })); } catch (e2) {}
    };
    window.addEventListener('beforeunload', onUnload);
    window.addEventListener('pagehide', onUnload);
    const interval = setInterval(saveTempServer, 30 * 1000);
    return () => {
      window.removeEventListener('beforeunload', onUnload);
      window.removeEventListener('pagehide', onUnload);
      clearInterval(interval);
    };
  }, [currentProjectId, projectName, bpm, tracks, subTabs, sessionTabs, masteringSettings]);

  // ── Realtime sync giữa các client (LAN browser ↔ Tauri standalone) ──
  // Bug 3: 2 session cùng tài khoản không sync. Poll revision nhẹ 3s; khi
  // updated_at mới hơn bản đã apply + client_id KHÁC mình → tải full temp →
  // deserialize → apply. Skip nếu data_json giống hệt bản cuối (chống churn
  // khi 30s writer bump updated_at với nội dung không đổi). Ping-pong chặn
  // bởi: bỏ qua bản do mình ghi (client_id) + hash-skip khi autosave.
  useEffect(() => {
    if (!window.SonicAPI || !window.SonicAPI.getTempRevision) return;
    const myId = (window.SonicStorage && window.SonicStorage.getClientId) ? window.SonicStorage.getClientId() : '';
    let lastAppliedAt = 0;
    let lastAppliedJson = '';
    let timer = null;
    let stopped = false;
    const applyRemote = async () => {
      try {
        const rev = await window.SonicAPI.getTempRevision();
        if (!rev || !rev.updated_at) return;
        if (rev.client_id === myId) { lastAppliedAt = rev.updated_at; return; } // bản do mình ghi
        if (rev.updated_at <= lastAppliedAt) return; // không mới hơn bản đã apply
        const tmp = await window.SonicAPI.getTempProject();
        if (!tmp || !tmp.has_temp || !tmp.data_json) return;
        const projStr = typeof tmp.data_json === 'string' ? tmp.data_json : JSON.stringify(tmp.data_json);
        let proj = tmp.data_json;
        if (typeof proj === 'string') { try { proj = JSON.parse(proj); } catch (e) { proj = null; } }
        if (!proj || !proj.main_session) return;
        if (projStr === lastAppliedJson) { lastAppliedAt = rev.updated_at; return; } // nội dung không đổi
        const result = deserializeProjectFromSchema(proj);
        if (result && result.tracks && result.tracks.length > 0) {
          lastAppliedJson = projStr;
          lastAppliedAt = rev.updated_at;
          setTracks(result.tracks);
          loadAudioBuffersForTracks(result.tracks).catch(function (err) { console.warn('loadAudioBuffersForTracks (sync) error:', err); });
          setBpm((result.bpm || 120).toString());
          if (result.sessionTabs && result.sessionTabs.length > 0) setSessionTabs(result.sessionTabs);
          if (result.subTabs && result.subTabs.length > 0) setSubTabs(result.subTabs);
          if (result.masteringSettings) setMasteringSettings(result.masteringSettings);
          const tmpName = (proj.metadata && proj.metadata.title) || 'Dự án tạm';
          setProjectName(tmpName);
          localStorage.setItem('sonic_project_name', tmpName);
          showToast('Đã đồng bộ dự án từ thiết bị khác', 'info');
        }
      } catch (e) { console.warn('[Sync] error:', e); }
    };
    const tick = () => {
      if (stopped) return;
      applyRemote().finally(() => {
        if (!stopped) timer = setTimeout(tick, 3000);
      });
    };
    timer = setTimeout(tick, 4000); // sau mount + restore temp
    return () => { stopped = true; if (timer) clearTimeout(timer); };
  }, []);

  // ── Timer-based auto-save (5 min) + backup (30 min) ──
  useEffect(() => {
    const BACKUP_MAX_KEY = 'sonic_backup_max_count';
    if (!localStorage.getItem(BACKUP_MAX_KEY)) {
      localStorage.setItem(BACKUP_MAX_KEY, '10');
    }

    const interval5 = setInterval(() => {
      if (!currentProjectId) return;
      // Auto-save: giống handleSaveProject
      if (currentProjectId.startsWith('local_')) {
        const finalName = projectName || 'Dự án mới';
        const localId = currentProjectId;
        const schemaObj = serializeProjectToSchema(localId, finalName, bpm, tracks, subTabs, sessionTabs, masteringSettings);
        const dataStr = JSON.stringify(schemaObj);
        localStorage.setItem('sonic_local_project_data', dataStr);
        localStorage.setItem('sonic_project_id', localId);
        localStorage.setItem('sonic_project_name', finalName);
      } else if (currentUser && window.SonicAPI) {
        try {
          const schemaObj = serializeProjectToSchema(currentProjectId, projectName, bpm, tracks, subTabs, sessionTabs, masteringSettings);
          const dataJson = JSON.stringify(schemaObj);
          window.SonicAPI.updateCloudProject(currentProjectId, projectName || 'Dự án mới', dataJson).catch(() => {});
        } catch (e) {
          console.warn('Auto-save error:', e);
        }
      }
    }, 5 * 60 * 1000);

    const interval30 = setInterval(() => {
      if (!currentProjectId || currentProjectId.startsWith('local_') || !currentUser || !window.SonicAPI) return;
      try {
        window.SonicAPI.createBackup(currentProjectId).catch(() => {});
      } catch (e) {
        console.warn('Backup error:', e);
      }
    }, 30 * 60 * 1000);

    return () => {
      clearInterval(interval5);
      clearInterval(interval30);
    };
  }, [currentProjectId, projectName, currentUser, tracks, subTabs, sessionTabs, masteringSettings, bpm]);

  // Lucide icons initialization
  useEffect(() => {
    setTimeout(() => {
      if (window.lucide) {
        window.lucide.createIcons();
      }
    }, 50);
  }, [activeTool, activeTab, contextMenu, activeTracks]);
  const timelineWrapperRef = useRef(null);
  const [timelineWrapperNode, setTimelineWrapperNode] = useState(null);
  const handleTimelineWrapperRef = useCallback(node => {
    timelineWrapperRef.current = node;
    setTimelineWrapperNode(node);
  }, []);
  const tcpContainerRef = useRef(null);
  const [scrollLeft, setScrollLeft] = useState(0);
  const handleTimelineScroll = e => {
    if (tcpContainerRef.current) {
      tcpContainerRef.current.scrollTop = e.currentTarget.scrollTop;
    }
    setScrollLeft(e.currentTarget.scrollLeft);
  };
  const handleTCPScroll = e => {
    if (timelineWrapperRef.current) {
      timelineWrapperRef.current.scrollTop = e.currentTarget.scrollTop;
    }
  };
  const panelDropZoneRef = useRef(null);
  const startPanelDrag = (panelId, e) => {
    panelDragRef.current = {
      panelId,
      startX: e.clientX,
      startY: e.clientY
    };
    setPanelDropZone(null);
    setDragGhostPanel(panelId);
    setDragGhostPos({
      x: e.clientX - 120,
      y: e.clientY - 20
    });
    const onMove = ev => {
      if (!panelDragRef.current || !workspaceRef.current) return;
      const rect = workspaceRef.current.getBoundingClientRect();
      const x = ev.clientX - rect.left;
      const y = ev.clientY - rect.top;
      const w = rect.width;
      const h = rect.height;
      const margin = 60;
      let zone = null;
      if (x < margin && x > 10) zone = 'left'; else if (x > w - margin && x < w - 10) zone = 'right'; else if (y < margin && y > 10) zone = 'top'; else if (y > h - margin && y < h - 10) zone = 'bottom';
      panelDropZoneRef.current = zone;
      setPanelDropZone(zone);
      setDragGhostPos({
        x: ev.clientX - 120,
        y: ev.clientY - 20
      });
    };
    const onUp = () => {
      if (panelDragRef.current) {
        const pid = panelDragRef.current.panelId;
        const targetZone = panelDropZoneRef.current;
        if (targetZone && targetZone !== panelPositions[pid]) {
          setPanelPositions(prev => ({
            ...prev,
            [pid]: targetZone
          }));
        }
        panelDragRef.current = null;
        panelDropZoneRef.current = null;
        setPanelDropZone(null);
        setDragGhostPos(null);
        setDragGhostPanel(null);
      }
      document.removeEventListener('mousemove', onMove);
      document.removeEventListener('mouseup', onUp);
    };
    document.addEventListener('mousemove', onMove);
    document.addEventListener('mouseup', onUp);
  };

  // ── Right Sidebar Column Resizer ──
  const startColResize = e => {
    e.preventDefault();
    const startX = e.clientX;
    const startWidth = rightSidebarWidth;
    const onMove = ev => {
      const deltaX = startX - ev.clientX;
      const newWidth = Math.max(200, Math.min(600, startWidth + deltaX));
      setRightSidebarWidth(newWidth);
    };
    const onUp = () => {
      document.removeEventListener('mousemove', onMove);
      document.removeEventListener('mouseup', onUp);
    };
    document.addEventListener('mousemove', onMove);
    document.addEventListener('mouseup', onUp);
  };

  // ── TCP Resizer ──
  const startTcpResize = e => {
    e.preventDefault();
    const startX = e.clientX;
    const startWidth = tcpWidth;
    const onMove = ev => {
      const deltaX = ev.clientX - startX;
      const newWidth = Math.max(280, Math.min(600, startWidth + deltaX));
      setTcpWidth(newWidth);
    };
    const onUp = () => {
      document.removeEventListener('mousemove', onMove);
      document.removeEventListener('mouseup', onUp);
    };
    document.addEventListener('mousemove', onMove);
    document.addEventListener('mouseup', onUp);
  };

  // ── Right Sidebar Row Resizer (Media Explorer / AI Panel) ──
  const startRowResize = e => {
    e.preventDefault();
    const startY = e.clientY;
    const startHeight = mediaExplorerHeight;
    const sidebarEl = document.getElementById('right-sidebar');
    const sidebarHeight = sidebarEl ? sidebarEl.getBoundingClientRect().height : 400;
    const onMove = ev => {
      const deltaY = startY - ev.clientY;
      const pct = ((startHeight / 100 * sidebarHeight + deltaY) / sidebarHeight) * 100;
      const newPct = Math.max(20, Math.min(80, pct));
      setMediaExplorerHeight(newPct);
    };
    const onUp = () => {
      document.removeEventListener('mousemove', onMove);
      document.removeEventListener('mouseup', onUp);
    };
    document.addEventListener('mousemove', onMove);
    document.addEventListener('mouseup', onUp);
  };

  const rulerRef = useRef(null);
  const activeSourcesRef = useRef([]);
  const activeTrackNodesRef = useRef({}); // { [trackId]: { gainNode, pannerNode } }
  const startOffsetTimeRef = useRef(0);
  const startBufferOffsetRef = useRef(0);
  const startAudioTimeRef = useRef(0);
  const animationFrameIdRef = useRef(null);
  const toastTimeoutRef = useRef(null);
  const rulerDragStartRef = useRef(null);
  const rulerAnchorRef = useRef(null);
  const isDraggingRulerRef = useRef(false);
  const subTabDragStartRef = useRef(null);
  const isDraggingSubTabRef = useRef(false);
  const handlePlayPauseRef = useRef(null);
  const currentTimeRef = useRef(currentTime);
  // D9: playhead sample counter cho native bridge (A11/A13 dùng để đồng bộ
  // timeline sample-accurate); cập nhật trong updatePlayhead khi bridge active.
  const bridgePlayheadSampleRef = useRef(0);
  // Resume main/session play khi rời PIANO ROLL tab: mở piano roll lúc main
  // đang play → bấm Space (play piano roll) → stopAllPlayback dừng main →
  // quay lại MAIN/SECTION → CÂM. Lưu {offset, audioTime} lúc mở tab → khi
  // rời tab resume startTrackPlayback(offset + elapsed thật).
  const mainResumeRef = useRef(null);
  currentTimeRef.current = currentTime;

  // ── Keyboard Shortcuts ──
  const handleUndoRef = useRef(handleUndo);
  const handleRedoRef = useRef(handleRedo);
  handleUndoRef.current = handleUndo;
  handleRedoRef.current = handleRedo;
  const handleDeleteSelectedItemsRef = useRef(() => {});
  handleDeleteSelectedItemsRef.current = (idsToDelete) => {
    const count = idsToDelete.size;
    // Close piano roll tabs của midi items bị xóa (user 07:40 — áp cả
    // SECTION-TAB: xóa midi item → close tab đã mở của item đó)
    setSubTabs(prev => {
      const next = prev.filter(s => !(s.target_id && idsToDelete.has(s.target_id)));
      // CHỈ về main khi tab đang active là PIANO ROLL (midi_*) bị close —
      // KHÔNG đá SECTION-TAB về main (user 07:45)
      if (activeTabRef.current && activeTabRef.current.startsWith('midi_') && !next.some(s => s.id === activeTabRef.current)) {
        setActiveTab('main');
      }
      return next;
    });
    // Sync nội dung section tab → section item trên MAIN (canvas vẽ lại —
    // user 07:45) — setTimeout 0: chạy SAU updateActiveTracks (data mới)
    if (activeTabRef.current && activeTabRef.current.startsWith('session_')) {
      const _tabId = activeTabRef.current;
      setTimeout(() => { try { syncSectionTabToMain(_tabId); } catch (e) {} }, 0);
    }
    setSelectedItemIds(new Set());
    updateActiveTracks(prev => prev.map(t => {
      let changed = false;
      const sections = (t.sections || []).filter(s => {
        if (idsToDelete.has(s.id)) { changed = true; return false; }
        return true;
      });
      const midiItems = (t.midiItems || []).filter(m => {
        if (idsToDelete.has(m.id)) { changed = true; return false; }
        return true;
      });
      let clips = t.clips || [];
      const hasVirtualClip = !t.clips || t.clips.length === 0;
      if (hasVirtualClip && t.buffer) {
        const canonical = 'default_' + t.id;
        if (idsToDelete.has(canonical)) {
          changed = true;
          return {
            ...t,
            sections,
            midiItems,
            clips: [],
            buffer: null,
            startTime: 0
          };
        }
      }
      const updatedClips = clips.filter(c => {
        const canonical = c.id === 'default' ? 'default_' + t.id : c.id;
        if (idsToDelete.has(canonical)) { changed = true; return false; }
        return true;
      });
      if (!changed && sections.length === (t.sections || []).length && midiItems.length === (t.midiItems || []).length) {
        return t;
      }
      return {
        ...t,
        sections,
        midiItems,
        clips: updatedClips,
        buffer: updatedClips.length > 0 ? updatedClips[0].buffer : null,
        startTime: updatedClips.length > 0 ? updatedClips[0].startTime : 0,
        name: updatedClips.length > 0 ? updatedClips[0].name : t.name
      };
    }));
    showToast(count === 1 ? 'Đã xóa 1 item.' : `Đã xóa ${count} items.`, 'info');
  };
  const selectedClipIdRef = useRef(null);
  selectedClipIdRef.current = selectedClipId;
  const selectedTrackIdRef = useRef(selectedTrackId);
  selectedTrackIdRef.current = selectedTrackId;
  const handleDeleteTrackRef = useRef(() => {});
  const handleSplitTrackRef = useRef(() => {});
  const handleRecordClickRef = useRef(() => {});
  const activeTabRef = useRef(activeTab);
  activeTabRef.current = activeTab;
  const activePlaybackSpeedRef = useRef(1.0);
  const subTabsRef = useRef(subTabs);
  subTabsRef.current = subTabs;
  const subTabSelectedNodeTimeRef = useRef(null);
  subTabSelectedNodeTimeRef.current = subTabSelectedNodeTime;
  const handleSubTabNormalize = tabId => {
    setSubTabs(prev => prev.map(st => {
      if (st.id !== tabId || !st.buffer) return st;
      const ctx = getAudioContext();
      const data = st.buffer.getChannelData(0);
      const left = st.selectionStart !== null && st.selectionEnd !== null ? Math.min(st.selectionStart, st.selectionEnd) : 0;
      const right = st.selectionStart !== null && st.selectionEnd !== null ? Math.max(st.selectionStart, st.selectionEnd) : st.buffer.duration;
      const startSample = Math.floor(left * st.buffer.sampleRate);
      const endSample = Math.floor(right * st.buffer.sampleRate);
      let maxVal = 0;
      for (let i = startSample; i < endSample; i++) {
        const abs = Math.abs(data[i]);
        if (abs > maxVal) maxVal = abs;
      }
      if (maxVal === 0) return st;
      const scale = 1.0 / maxVal;
      const clonedBuffer = ctx.createBuffer(1, Math.max(1, data.length), st.buffer.sampleRate);
      const clonedData = clonedBuffer.getChannelData(0);
      clonedData.set(data);
      for (let i = startSample; i < endSample; i++) {
        clonedData[i] = Math.max(-1, Math.min(1, clonedData[i] * scale));
      }
      showToast('Đã Normalize vùng chọn.', 'success');
      return {
        ...st,
        buffer: clonedBuffer
      };
    }));
  };
  const handleSubTabGain = (tabId, gainDb) => {
    setSubTabs(prev => prev.map(st => {
      if (st.id !== tabId || !st.buffer) return st;
      const ctx = getAudioContext();
      const data = st.buffer.getChannelData(0);
      const left = st.selectionStart !== null && st.selectionEnd !== null ? Math.min(st.selectionStart, st.selectionEnd) : 0;
      const right = st.selectionStart !== null && st.selectionEnd !== null ? Math.max(st.selectionStart, st.selectionEnd) : st.buffer.duration;
      const startSample = Math.floor(left * st.buffer.sampleRate);
      const endSample = Math.floor(right * st.buffer.sampleRate);
      const scale = Math.pow(10, gainDb / 20);
      const clonedBuffer = ctx.createBuffer(1, Math.max(1, data.length), st.buffer.sampleRate);
      const clonedData = clonedBuffer.getChannelData(0);
      clonedData.set(data);
      for (let i = startSample; i < endSample; i++) {
        clonedData[i] = Math.max(-1, Math.min(1, clonedData[i] * scale));
      }
      showToast(`Đã điều chỉnh Gain: ${gainDb} dB.`, 'success');
      return {
        ...st,
        buffer: clonedBuffer
      };
    }));
  };
  const handleSubTabFade = (tabId, type) => {
    setSubTabs(prev => prev.map(st => {
      if (st.id !== tabId || !st.buffer) return st;
      const ctx = getAudioContext();
      const data = st.buffer.getChannelData(0);
      const left = st.selectionStart !== null && st.selectionEnd !== null ? Math.min(st.selectionStart, st.selectionEnd) : 0;
      const right = st.selectionStart !== null && st.selectionEnd !== null ? Math.max(st.selectionStart, st.selectionEnd) : st.buffer.duration;
      const startSample = Math.floor(left * st.buffer.sampleRate);
      const endSample = Math.floor(right * st.buffer.sampleRate);
      const durationSamples = endSample - startSample;
      if (durationSamples <= 0) return st;
      const clonedBuffer = ctx.createBuffer(1, Math.max(1, data.length), st.buffer.sampleRate);
      const clonedData = clonedBuffer.getChannelData(0);
      clonedData.set(data);
      if (type === 'in') {
        for (let i = 0; i < durationSamples; i++) {
          const alpha = i / durationSamples;
          clonedData[startSample + i] *= alpha;
        }
        showToast('Đã áp dụng Fade In.', 'success');
      } else if (type === 'out') {
        for (let i = 0; i < durationSamples; i++) {
          const alpha = (durationSamples - i) / durationSamples;
          clonedData[startSample + i] *= alpha;
        }
        showToast('Đã áp dụng Fade Out.', 'success');
      }
      return {
        ...st,
        buffer: clonedBuffer
      };
    }));
  };
  const handleSubTabCut = tabId => {
    const st = subTabsRef.current.find(s => s.id === tabId);
    if (!st || !st.buffer) return;
    const left = st.selectionStart !== null && st.selectionEnd !== null ? Math.min(st.selectionStart, st.selectionEnd) : null;
    const right = st.selectionStart !== null && st.selectionEnd !== null ? Math.max(st.selectionStart, st.selectionEnd) : null;
    if (left === null || right === null || left === right) {
      showToast('Vui lòng chọn vùng để Cut.', 'warning');
      return;
    }
    const ctx = getAudioContext();
    const sr = st.buffer.sampleRate;
    const data = st.buffer.getChannelData(0);
    const startSample = Math.floor(left * sr);
    const endSample = Math.floor(right * sr);
    const len = Math.max(1, endSample - startSample); // guard 0 frames → createBuffer crash
    const cutBuffer = ctx.createBuffer(1, len, sr);
    cutBuffer.copyToChannel(data.subarray(startSample, endSample), 0);
    clipboardRef.current = {
      buffer: cutBuffer,
      name: 'Subtab Clip'
    };
    const newBuffer = ctx.createBuffer(1, Math.max(1, data.length - len), sr);
    const newData = newBuffer.getChannelData(0);
    let idx = 0;
    for (let i = 0; i < startSample; i++) newData[idx++] = data[i];
    for (let i = endSample; i < data.length; i++) newData[idx++] = data[i];
    setSubTabs(prev => prev.map(s => s.id === tabId ? {
      ...s,
      buffer: newBuffer,
      currentTime: left,
      selectionStart: null,
      selectionEnd: null
    } : s));
    showToast('Đã Cut vùng chọn.', 'success');
  };
  const handleSubTabCopy = tabId => {
    const st = subTabsRef.current.find(s => s.id === tabId);
    if (!st || !st.buffer) return;
    const left = st.selectionStart !== null && st.selectionEnd !== null ? Math.min(st.selectionStart, st.selectionEnd) : null;
    const right = st.selectionStart !== null && st.selectionEnd !== null ? Math.max(st.selectionStart, st.selectionEnd) : null;
    if (left === null || right === null || left === right) {
      showToast('Vui lòng chọn vùng để Copy.', 'warning');
      return;
    }
    const ctx = getAudioContext();
    const sr = st.buffer.sampleRate;
    const data = st.buffer.getChannelData(0);
    const startSample = Math.floor(left * sr);
    const endSample = Math.floor(right * sr);
    const len = Math.max(1, endSample - startSample); // guard 0 frames → createBuffer crash
    const copyBuffer = ctx.createBuffer(1, len, sr);
    copyBuffer.copyToChannel(data.subarray(startSample, endSample), 0);
    clipboardRef.current = {
      buffer: copyBuffer,
      name: 'Subtab Clip',
      sampleRate: sr,
      channels: 1,
      speed: 1.0
    };
    showToast('Đã Copy vùng chọn.', 'success');
  };
  const handleSubTabPaste = tabId => {
    const st = subTabsRef.current.find(s => s.id === tabId);
    if (!st) return;

    // Check if Piano Roll Tab
    if (st.type === 'PIANO_ROLL') {
      const clip = clipboardRef.current || window.globalStudioClipboard;
      if (!clip || clip.type !== 'midi' || !clip.notes) {
        showToast('Clipboard không chứa dữ liệu MIDI.', 'warning');
        return;
      }
      const secondsPerBeat = 60 / (bpm || 120);
      const pasteBeat = (st.currentTime || 0) / secondsPerBeat;
      
      const newNotes = clip.notes.map(n => ({
        id: 'note_' + Math.random().toString(36).substr(2, 9),
        pitch: n.pitch,
        start_beat: pasteBeat + n.start_beat,
        duration_beats: n.duration_beats,
        velocity: n.velocity || 0.8
      }));
      
      setSubTabs(prev => prev.map(s => s.id === tabId ? {
        ...s,
        notes: [...(s.notes || []), ...newNotes]
      } : s));
      showToast('Đã dán note MIDI vào Piano Roll.', 'success');
      return;
    }

    // Default Waveform Audio Paste
    if (!st.buffer) return;
    const clip = clipboardRef.current || window.globalStudioClipboard;
    if (!clip || !clip.buffer) {
      showToast('Clipboard trống hoặc không chứa dữ liệu âm thanh.', 'warning');
      return;
    }
    const ctx = getAudioContext();
    const clipBuf = clip.buffer;
    const sr = st.buffer.sampleRate;
    const data = st.buffer.getChannelData(0);
    const insertTime = st.currentTime || 0;
    const insertSample = Math.floor(insertTime * sr);
    const newBuffer = ctx.createBuffer(1, Math.max(1, data.length + clipBuf.length), sr);
    const newData = newBuffer.getChannelData(0);
    let idx = 0;
    for (let i = 0; i < insertSample; i++) newData[idx++] = data[i];
    const clipData = clipBuf.getChannelData(0);
    for (let i = 0; i < clipBuf.length; i++) newData[idx++] = clipData[i];
    for (let i = insertSample; i < data.length; i++) newData[idx++] = data[i];
    setSubTabs(prev => prev.map(s => s.id === tabId ? {
      ...s,
      buffer: newBuffer,
      currentTime: insertTime + clipBuf.duration,
      selectionStart: null,
      selectionEnd: null
    } : s));
    showToast('Đã dán dữ liệu âm thanh.', 'success');
  };
  const handleSubTabDelete = tabId => {
    const st = subTabsRef.current.find(s => s.id === tabId);
    if (!st || !st.buffer) return;
    const left = st.selectionStart !== null && st.selectionEnd !== null ? Math.min(st.selectionStart, st.selectionEnd) : null;
    const right = st.selectionStart !== null && st.selectionEnd !== null ? Math.max(st.selectionStart, st.selectionEnd) : null;
    if (left === null || right === null || left === right) {
      showToast('Vui lòng chọn vùng để Delete.', 'warning');
      return;
    }
    const ctx = getAudioContext();
    const sr = st.buffer.sampleRate;
    const data = st.buffer.getChannelData(0);
    const startSample = Math.floor(left * sr);
    const endSample = Math.floor(right * sr);
    const len = endSample - startSample;
    const newBuffer = ctx.createBuffer(1, data.length - len, sr);
    const newData = newBuffer.getChannelData(0);
    let idx = 0;
    for (let i = 0; i < startSample; i++) newData[idx++] = data[i];
    for (let i = endSample; i < data.length; i++) newData[idx++] = data[i];
    setSubTabs(prev => prev.map(s => s.id === tabId ? {
      ...s,
      buffer: newBuffer,
      currentTime: left,
      selectionStart: null,
      selectionEnd: null
    } : s));
    showToast('Đã xóa vùng chọn.', 'success');
  };
  const handleSubTabLoop = (tabId, loopCount) => {
    const st = subTabsRef.current.find(s => s.id === tabId);
    if (!st || !st.buffer) return;
    const left = st.selectionStart !== null && st.selectionEnd !== null ? Math.min(st.selectionStart, st.selectionEnd) : null;
    const right = st.selectionStart !== null && st.selectionEnd !== null ? Math.max(st.selectionStart, st.selectionEnd) : null;
    if (left === null || right === null || left === right) {
      showToast('Vui lòng chọn vùng để Loop.', 'warning');
      return;
    }
    const ctx = getAudioContext();
    const sr = st.buffer.sampleRate;
    const data = st.buffer.getChannelData(0);
    const startSample = Math.floor(left * sr);
    const endSample = Math.floor(right * sr);
    const len = endSample - startSample;

    // Loop payload N times
    const segmentData = data.subarray(startSample, endSample);
    const addedSamples = len * (loopCount - 1);
    const newBuffer = ctx.createBuffer(1, data.length + addedSamples, sr);
    const newData = newBuffer.getChannelData(0);
    let idx = 0;
    for (let i = 0; i < endSample; i++) newData[idx++] = data[i];
    for (let n = 0; n < loopCount - 1; n++) {
      for (let i = 0; i < len; i++) newData[idx++] = segmentData[i];
    }
    for (let i = endSample; i < data.length; i++) newData[idx++] = data[i];
    setSubTabs(prev => prev.map(s => s.id === tabId ? {
      ...s,
      buffer: newBuffer,
      selectionStart: null,
      selectionEnd: null
    } : s));
    showToast(`Đã lặp vùng chọn ${loopCount} lần.`, 'success');
  };

  // ── Item copy/cut helpers (Ctrl+C/X — user 07:50: phải copy/cut ITEM được
  // chọn, không phải track) ──
  const findItemContext = (itemId) => {
    for (const t of (activeTracksRef.current || [])) {
      if ((t.midiItems || []).some(m => m.id === itemId)) return { trackId: t.id, itemType: 'midi' };
      if ((t.clips || []).some(c => c.id === itemId)) return { trackId: t.id, itemType: 'clip' };
    }
    return null;
  };
  const copyItemToClipboard = (trackId, itemId, itemType) => {
    // Dùng REF (không phải activeTracks closure cũ): Ctrl+C/X chạy trong keydown
    // effect deps [] → closure giữ activeTracks RENDER ĐẦU — item TẠO SAU phiên
    // (vd midi_1786099252336) không tồn tại trong closure cũ → copy fail → cắt
    // nhầm track (user 08:10 — log [CutItem] found= 2/midi nhưng không cắt).
    const trk = (activeTracksRef.current || []).find(t => t.id === trackId);
    if (!trk) return false;
    const isMidi = itemType === 'midi' || (!itemType && (trk.midiItems || []).some(m => m.id === itemId));
    if (isMidi) {
      const item = (trk.midiItems || []).find(m => m.id === itemId);
      if (!item) return false;
      // duration midiItem tính GIÂY (insertMidiItem = 4*spb — scheduling dùng
      // seconds) — KHÔNG nhân (60/bpm) — nhân làm duration gấp đôi → block
      // paste bị dài/ngắn sai (user 07:55).
      clipboardRef.current = {
        type: 'midi',
        notes: (item.notes || []).map(n => ({ ...n })),
        duration: item.duration || 4,
        name: item.name || 'MIDI Item',
        color: item.color || '#a855f7'
      };
      window.globalStudioClipboard = clipboardRef.current;
      return true;
    }
    const item = (trk.clips || []).find(c => c.id === itemId);
    if (item && item.buffer) {
      clipboardRef.current = {
        buffer: item.buffer,
        name: item.name || trk.name,
        volumeDb: trk.volumeDb,
        pan: trk.pan,
        color: item.color || trk.color,
        sampleRate: item.buffer.sampleRate,
        channels: item.buffer.numberOfChannels,
        speed: item.speed || 1.0
      };
      window.globalStudioClipboard = clipboardRef.current;
      return true;
    }
    return false;
  };

  useEffect(() => {
    const handler = e => {
      // Bypass global hotkeys when typing inside input/textarea/contentEditable elements
      if (e.target && (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.isContentEditable)) {
        // PIANO ROLL tab: Space LUÔN play/stop (user yêu cầu — transport phải
        // capture được kể cả khi có input đang focus trong tab) — các phím
        // khác vẫn bypass để gõ liệu bình thường.
        if (!(e.key === ' ' || e.code === 'Space') || !activeTabRef.current || !activeTabRef.current.startsWith('midi_')) {
          return;
        }
      }
      const ctrl = e.ctrlKey || e.metaKey;
      const alt = e.altKey;

      // Global space play/pause shortcut for transport
      if (e.key === ' ' || e.code === 'Space') {
        if (window.mediaExplorerActive) return;
        e.preventDefault();
        // Ngăn space kích hoạt BUTTON đang focus (tab header/control):
        // browser fire click tổng hợp ở KEYUP space → click tab header →
        // CHUYỂN TAB (nhìn như space không play/stop). Blur button trước khi
        // play/stop — keyup sau đó target body → không click.
        try {
          const _ae = document.activeElement;
          if (_ae && _ae.tagName === 'BUTTON') _ae.blur();
        } catch (err) { }
        if (recordingStateRef.current === 'RECORDING' || recordingStateRef.current === 'COUNT_IN') {
          handleRecordClickRef.current();
          return;
        }
        if (handlePlayPauseRef.current) handlePlayPauseRef.current();
        return;
      }
      if (activeTabRef.current !== 'main' && !activeTabRef.current.startsWith('session_')) {
        // Sub-Tab keyboard shortcuts mapping
        const curTabId = activeTabRef.current;
        if (ctrl && alt && e.key === 'n') {
          e.preventDefault();
          handleSubTabNormalize(curTabId);
          return;
        }
        if (e.key === 'f' || e.key === 'F') {
          e.preventDefault();
          handleSubTabFade(curTabId, 'in');
          return;
        }
        if (e.key === 'g' || e.key === 'G') {
          e.preventDefault();
          handleSubTabFade(curTabId, 'out');
          return;
        }
        if (ctrl && e.key === 'l') {
          e.preventDefault();
          handleSubTabLoop(curTabId, 4);
          return;
        }
        if (!ctrl && !e.metaKey && !e.altKey && (e.key === 'v' || e.key === 'V')) {
          e.preventDefault();
          const val = prompt("Nhập Gain điều chỉnh (dB):", "0");
          if (val) handleSubTabGain(curTabId, parseFloat(val) || 0);
          return;
        }
        if (ctrl && e.key === 'x') {
          e.preventDefault();
          handleSubTabCut(curTabId);
          return;
        }
        if (ctrl && e.key === 'c') {
          e.preventDefault();
          handleSubTabCopy(curTabId);
          return;
        }
        if (ctrl && e.key === 'v') {
          e.preventDefault();
          handleSubTabPaste(curTabId);
          return;
        }
        if (e.key === 'Delete' || e.key === 'Backspace' || e.key === 'Del') {
          e.preventDefault();
          if (subTabSelectedNodeTimeRef.current !== null) {
            const selTime = subTabSelectedNodeTimeRef.current;
            setSubTabs(prev => prev.map(s => {
              if (s.id !== curTabId) return s;
              const curNodes = s.graphMode === 'pan' ? s.panningNodes || [] : s.volumeNodes || [];
              const updated = curNodes.filter(n => n.time !== selTime);
              return {
                ...s,
                [s.graphMode === 'pan' ? 'panningNodes' : 'volumeNodes']: updated
              };
            }));
            setSubTabSelectedNodeTime(null);
          } else {
            handleSubTabDelete(curTabId);
          }
          return;
        }
        return;
      }
      if (ctrl && e.key === 'z' && !e.shiftKey) {
        const tag = document.activeElement?.tagName;
        if (tag === 'INPUT' || tag === 'TEXTAREA') return;
        e.preventDefault();
        handleUndoRef.current();
        return;
      }
      if (ctrl && (e.key === 'y' || e.key === 'z' && e.shiftKey)) {
        const tag = document.activeElement?.tagName;
        if (tag === 'INPUT' || tag === 'TEXTAREA') return;
        e.preventDefault();
        handleRedoRef.current();
        return;
      }
      if (ctrl && !alt && e.key === 'o') {
        e.preventDefault();
        handleImportSFS();
        return;
      }
      if (ctrl && !alt && e.key === 'n') {
        e.preventDefault();
        // New project: đóng hết tab + reset MỌI trạng thái về mặc định
        try { stopAllPlayback(); } catch (e) {}
        setSubTabs([]);
        setSessionTabs([]);
        setActiveTab('main');
        trackMidiChannelsRef.current = {};
        Object.keys(trackMidiBypassMap).forEach(function (k) { delete trackMidiBypassMap[k]; });
        Object.keys(trackAudioBypassMap).forEach(function (k) { delete trackAudioBypassMap[k]; });
        Object.keys(trackMasteringBypassMap).forEach(function (k) { delete trackMasteringBypassMap[k]; });
        setSelectedItemIds(new Set());
        setTracks([{
          id: '1',
          name: 'Track 01',
          buffer: null,
          startTime: 0,
          volumeDb: 0,
          pan: 0,
          muted: false,
          solo: false,
          color: '#0f766e',
          markers: [],
          serverFileId: null
        }, {
          id: '2',
          name: 'Track 02',
          buffer: null,
          startTime: 0,
          volumeDb: 0,
          pan: 0,
          muted: false,
          solo: false,
          color: '#1d4ed8',
          markers: [],
          serverFileId: null
        }]);
        setSelectedTrackId('1');
        setProjectName('');
        setCurrentProjectId(null);
        localStorage.removeItem('sonic_project_name');
        localStorage.removeItem('sonic_project_id');
        showToast('New project created', 'info');
        return;
      }
      if (ctrl && !alt && !e.shiftKey && e.key === 'a') {
        e.preventDefault();
        const curTab = activeTabRef.current;
        if (curTab !== 'main' && !curTab.startsWith('session_')) return;
        captureSelectionUndo();
        const allIds = new Set();
        (activeTracksRef.current || []).forEach(t => {
          (t.sections || []).forEach(s => allIds.add(s.id));
          (t.midiItems || []).forEach(m => allIds.add(m.id));
          const clips = t.clips && t.clips.length > 0 ? t.clips : t.buffer ? [{
            id: 'default',
            buffer: t.buffer,
            startTime: t.startTime || 0,
            name: t.name,
            speed: t.speed || 1.0
          }] : [];
          clips.forEach(c => allIds.add(c.id === 'default' ? 'default_' + t.id : c.id));
        });
        setSelectedItemIds(allIds);
        pushSelectionUndo();
        return;
      }
      if (ctrl && !alt && e.key === 's') {
        e.preventDefault();
        const curTab = activeTabRef.current;
        if (curTab && curTab.startsWith('session_')) {
          // SECTION-TAB: Ctrl+S = Lưu section (như nút Lưu section)
          handleSaveSectionTabRef.current(curTab);
          showToast('Đã lưu Section', 'success');
        } else {
          handleSaveProjectRef.current();
        }
        return;
      }
      if (ctrl && alt && e.key === 's' || ctrl && e.shiftKey && e.key === 's') {
        e.preventDefault();
        handleExportSFS();
        return;
      }
      if (ctrl && !alt && e.key === 'i') {
        e.preventDefault();
        addNewTrack();
        return;
      }
      if (ctrl && alt && e.key === 'i') {
        e.preventDefault();
        showToast('Import audio', 'info');
        return;
      }
      if (ctrl && !alt && e.key === 'e') {
        e.preventDefault();
        openTempTab();
        return;
      }
      if (ctrl && !alt && e.key === 'm') {
        e.preventDefault();
        handleMergeTracks();
        return;
      }
      if (ctrl && !alt && e.key === 'c') {
        e.preventDefault();
        const selItems = selectedItemIdsRef.current;
        if (selItems && selItems.size > 0) {
          // Copy ITEM đầu tiên được chọn (user 07:50 — Ctrl+C phải copy item)
          const firstId = selItems.values().next().value;
          const found = findItemContext(firstId);
          if (found && copyItemToClipboard(found.trackId, firstId, found.itemType)) {
            showToast('Đã sao chép item.', 'info');
          } else {
            handleCopyTrack();
          }
        } else {
          handleCopyTrack();
        }
        return;
      }
      if (ctrl && !alt && e.key === 'x') {
        e.preventDefault();
        e.stopPropagation(); // chặn browser cut (user 07:55 — Ctrl-X bị capture)
        const selItems = selectedItemIdsRef.current;
        if (selItems && selItems.size > 0) {
          // Cut ITEM đầu tiên được chọn: copy + xóa item (+ close piano roll
          // tab + sync section — handleDeleteSelectedItemsRef đã xử lý)
          const firstId = selItems.values().next().value;
          const found = findItemContext(firstId);
          console.log('[CutItem] Ctrl+X — itemId=', firstId, 'found=', found ? found.trackId + '/' + found.itemType : 'null', 'selSize=', selItems.size);
          if (found && copyItemToClipboard(found.trackId, firstId, found.itemType)) {
            handleDeleteSelectedItemsRef.current(new Set([firstId]));
            showToast('Đã cắt item.', 'info');
          } else {
            handleCutTrack();
          }
        } else {
          handleCutTrack();
        }
        return;
      }
      if (ctrl && !alt && e.key === 'v') {
        e.preventDefault();
        // Dùng REF — effect deps [] → handlePasteTrack closure CŨ (selectedTrackId
        // stale = '1' → paste sai track — user 08:05)
        handlePasteTrackRef.current();
        return;
      }
       if (e.key === 'Delete' || e.key === 'Backspace' || e.key === 'Del') {
         const selItems = selectedItemIdsRef.current;
         if (selItems.size > 0) {
           e.preventDefault();
           const idsToDelete = new Set(selItems);
           handleDeleteSelectedItemsRef.current(idsToDelete);
           return;
         }
         const selClip = selectedClipIdRef.current;
        if (selClip) {
          e.preventDefault();
          const {
            trackId,
            clipId
          } = selClip;
          setTracks(prev => {
            const track = prev.find(t => t.id === trackId);
            if (!track) return prev;
            const beforeSnap = captureTrackSnapshotRef.current ? captureTrackSnapshotRef.current(trackId) : null;
            const updatedClips = (track.clips || []).filter(c => c.id !== clipId);
            const updatedTracks = prev.map(t => {
              if (t.id === trackId) {
                return {
                  ...t,
                  clips: updatedClips,
                  buffer: updatedClips.length > 0 ? updatedClips[0].buffer : null,
                  startTime: updatedClips.length > 0 ? updatedClips[0].startTime : 0,
                  name: updatedClips.length > 0 ? updatedClips[0].name : `Track ${t.id}`
                };
              }
              return t;
            });
            setTimeout(() => {
              const afterSnap = captureTrackSnapshotRef.current ? captureTrackSnapshotRef.current(trackId) : null;
              pushAction('DELETE_CLIP', trackId, beforeSnap, afterSnap);
            }, 50);
            return updatedTracks;
          });
          setSelectedClipId(null);
          showToast('Đã xóa clip.', 'info');
          return;
        } else {
          e.preventDefault();
          handleDeleteTrackRef.current();
          return;
        }
      }
      if (ctrl && !alt && e.key === 's') {
        e.preventDefault();
        const curTab = activeTabRef.current;
        if (curTab === 'main' || (curTab && curTab.startsWith('session_'))) {
          // MAIN (hoặc SECTION): save project + save TẤT CẢ dirty sub-tabs
          // (piano roll/audio/section) — user 07:35: nhấn lưu ở MAIN phải lưu
          // cả dirty tab (trước đây nhánh main RỖNG — dirty piano roll bị mất).
          handleSaveProject();
          subTabsRef.current.filter(s => s.isDirty).forEach(st => {
            if (st.type === 'PIANO_ROLL') {
              handleSaveMidiNotes(st.id, st.trackId, st.target_id, st.notes || []);
            } else if (st.type === 'SECTION') {
              handleSaveSectionTab(st.id);
            } else if (st.buffer) {
              // Audio clip sub-tab: save buffer to track
              const subTrack = activeTracksRef.current.find(t => t.id === st.trackId);
              if (subTrack) {
                updateActiveTracks(prev => prev.map(t => t.id === st.trackId ? { ...t, buffer: st.buffer } : t));
              }
            }
          });
          if (curTab.startsWith('session_')) {
            // SECTION-TAB: cũng lưu section hiện tại
            handleSaveSectionTabRef.current(curTab);
          }
          showToast('Đã lưu dự án + các tab đã sửa', 'success');
        } else {
          // Sub-tab: save current tab
          const st = subTabsRef.current.find(s => s.id === curTab);
          if (st) {
            if (st.type === 'PIANO_ROLL') {
              handleSaveMidiNotes(st.id, st.trackId, st.target_id, st.notes || []);
              showToast('Đã lưu Piano Roll', 'success');
            } else if (st.type === 'SECTION') {
              handleSaveSectionTab(st.id);
              showToast('Đã lưu Section Tab', 'success');
            } else if (st.buffer) {
              const subTrack = activeTracksRef.current.find(t => t.id === st.trackId);
              if (subTrack) {
                updateActiveTracks(prev => prev.map(t => t.id === st.trackId ? { ...t, buffer: st.buffer } : t));
              }
              showToast('Đã lưu Audio Tab', 'success');
            }
          }
        }
        return;
      }
      if (!ctrl && !alt && e.key === 's') {
        e.preventDefault();
        handleSplitTrackRef.current(selectedTrackIdRef.current);
        return;
      }
      if (e.key === 'F7') {
        e.preventDefault();
        e.stopPropagation();
        window.__toggleMixerRef();
        return;
      }
      if (e.key === 'F6') {
        e.preventDefault();
        e.stopPropagation();
        window.__toggleMediaExplorerRef();
        return;
      }
    };
    window.addEventListener('keydown', handler, { capture: true });
    return () => window.removeEventListener('keydown', handler, { capture: true });
  }, []);

  // ── Temp Tab: draw isolated waveform ──
  useEffect(() => {
    if (!tempTabActive || !tempTabBuffer || !tempTabCanvasRef.current) return;
    const canvas = tempTabCanvasRef.current;
    const ctx = canvas.getContext('2d');
    const dpr = window.devicePixelRatio || 1;
    const rect = canvas.getBoundingClientRect();
    canvas.width = rect.width * dpr;
    canvas.height = rect.height * dpr;
    ctx.scale(dpr, dpr);
    const w = rect.width;
    const h = rect.height;
    ctx.fillStyle = '#181818';
    ctx.fillRect(0, 0, w, h);
    const data = tempTabBuffer.getChannelData(0);
    const sr = tempTabBuffer.sampleRate;
    const totalSamples = data.length;
    if (totalSamples === 0) return;
    ctx.strokeStyle = '#6ee7b7';
    ctx.lineWidth = 1;
    for (let px = 0; px < w; px++) {
      const startSample = Math.floor(px / w * totalSamples);
      const endSample = Math.floor((px + 1) / w * totalSamples);
      let maxVal = 0;
      for (let i = startSample; i < endSample && i < totalSamples; i++) {
        const abs = Math.abs(data[i]);
        if (abs > maxVal) maxVal = abs;
      }
      const mid = h / 2;
      const peakHeight = maxVal * (h * 0.4);
      ctx.beginPath();
      ctx.moveTo(px, mid - peakHeight);
      ctx.lineTo(px, mid + peakHeight);
      ctx.stroke();
    }
  }, [tempTabActive, tempTabBuffer]);

  // ── Sub Tab: open as new tab instead of modal (LOOP_EDITOR_2.md §1) ──
  const openTempTab = () => {
    const useLocal = selectionMode === 'local' && localSelectionTrackId;
    const trackId = useLocal ? localSelectionTrackId : selectedTrackId;
    const t = tracks.find(x => x.id === trackId);
    if (!t || !t.buffer) {
      showToast('Vui lòng chọn track có dữ liệu âm thanh.', 'warning');
      return;
    }
    if (selLeft === null || selRight === null || selRight <= selLeft) {
      showToast('Vui lòng chọn một khoảng thời gian trên sóng âm.', 'warning');
      return;
    }

    // Check if a subtab for this track+range already exists
    const existing = subTabs.find(s => s.trackId === trackId && s.startTime === selLeft && s.endTime === selRight);
    if (existing) {
      setActiveTab(existing.id);
      showToast(`Sub Tab already open.`, 'info');
      return;
    }
    const sr = t.buffer.sampleRate;
    const trackStart = t.startTime || 0;
    const relSelLeft = Math.max(0, selLeft - trackStart);
    const relSelRight = Math.max(0, selRight - trackStart);
    const startSample = Math.max(0, Math.floor(relSelLeft * sr));
    const endSample = Math.min(t.buffer.length, Math.floor(relSelRight * sr));
    const len = endSample - startSample;
    if (len < 100) {
      showToast('Khoảng chọn quá ngắn.', 'warning');
      return;
    }
    const ctx = getAudioContext();
    const numChannels = t.buffer.numberOfChannels || 1;
    const subBuffer = ctx.createBuffer(numChannels, len, sr);
    for (let c = 0; c < numChannels; c++) {
      subBuffer.copyToChannel(t.buffer.getChannelData(c).subarray(startSample, endSample), c);
    }
    const subChannelInfo = window.SonicAudio.analyzeAudioBufferChannels(subBuffer);
    const tabId = 'subtab_' + Date.now();
    const tabLabel = `Edit_${t.name.replace('.wav', '').slice(0, 10)}_${selLeft.toFixed(1)}s`;
    setSubTabs(prev => [...prev, {
      id: tabId,
      label: tabLabel,
      trackId: trackId,
      startTime: selLeft,
      endTime: selRight,
      buffer: subBuffer,
      channelInfo: subChannelInfo,
      effects: {
        reverse: false,
        gainDb: 0,
        fadeInMs: 0,
        fadeOutMs: 0,
        normalizeDb: 0,
        pitch: 0,
        speedStretch: 100
      },
      currentTime: 0,
      selectionStart: null,
      selectionEnd: null,
      isPlaying: false,
      speed: 1.0,
      volumeNodes: [],
      panningNodes: [],
      fadeInLen: 0,
      fadeOutLen: 0,
      graphMode: null,
      isLooping: false,
      loopCount: 0
    }]);
    setActiveTab(tabId);
  };
  const handleEditClipInSubTab = (trackId, clipId) => {
    const track = activeTracks.find(t => t.id === trackId);
    if (!track) return;
    const clips = track.clips && track.clips.length > 0 ? track.clips : track.buffer ? [{
      id: 'default_' + track.id,
      buffer: track.buffer,
      startTime: track.startTime || 0,
      name: track.name
    }] : [];
    const clip = clips.find(c => c.id === clipId || clipId === 'default' && c.id === 'default_' + trackId);
    if (!clip || !clip.buffer) return;
    const resolvedClipId = clip.id === 'default' ? 'default_' + trackId : clip.id;
    const existing = subTabs.find(s => s.clipId === resolvedClipId && s.trackId === trackId);
    if (existing) {
      setActiveTab(existing.id);
      showToast(`Sub Tab for "${clip.name}" already open.`, 'info');
      return;
    }
    const sr = clip.buffer.sampleRate;
    const len = clip.buffer.length;
    const numChannels = clip.buffer.numberOfChannels || 1;
    const ctx = getAudioContext();
    const subBuffer = ctx.createBuffer(numChannels, len, sr);
    for (let c = 0; c < numChannels; c++) {
      subBuffer.copyToChannel(clip.buffer.getChannelData(c), c);
    }
    const subChannelInfo = window.SonicAudio.analyzeAudioBufferChannels(subBuffer);
    const tabId = 'subtab_' + Date.now();
    const tabLabel = `Edit_${clip.name.replace('.wav', '').slice(0, 10)}`;
    setSubTabs(prev => [...prev, {
      id: tabId,
      label: tabLabel,
      trackId: trackId,
      clipId: resolvedClipId,
      startTime: clip.startTime,
      endTime: clip.startTime + clip.buffer.duration,
      buffer: subBuffer,
      channelInfo: subChannelInfo,
      effects: {
        reverse: false,
        gainDb: 0,
        fadeInMs: 0,
        fadeOutMs: 0,
        normalizeDb: 0,
        pitch: 0,
        speedStretch: 100
      },
      currentTime: 0,
      selectionStart: null,
      selectionEnd: null,
      isPlaying: false,
      speed: 1.0,
      volumeNodes: [],
      panningNodes: [],
      fadeInLen: 0,
      fadeOutLen: 0,
      graphMode: null,
      isLooping: false,
      loopCount: 0
    }]);
    setActiveTab(tabId);
  };

  const handleEditMidiInTab = (trackId, midiItemId) => {
    const track = activeTracks.find(t => t.id === trackId);
    if (!track) return;
    const midiItem = (track.midiItems || []).find(m => m.id === midiItemId);
    if (!midiItem) return;

    // Clean stop playback ONLY khi KHÔNG đang play main — nếu main đang play,
    // mở PIANO ROLL tab KHÔNG được dừng âm (user: dblclick mở tab lúc main
    // play → âm main phải tiếp tục). Nhánh play sub-tab tự stopAllPlayback
    // trước khi play nên không xung đột route.
    if (isPlaying) {
      // Lưu điểm resume: nếu user bấm Space trong piano roll (play piano roll
      // → stopAllPlayback dừng main) → khi quay lại MAIN/SECTION resume main
      // từ đúng vị trí âm (offset + elapsed thật) — hết câm.
      mainResumeRef.current = { offset: startOffsetTimeRef.current, audioTime: getAudioContext().currentTime };
    } else {
      mainResumeRef.current = null;
      stopAllPlayback();
    }

    const ctx = getAudioContext();
    const silentBuffer = ctx.createBuffer(1, 128, ctx.sampleRate);

    const existing = subTabs.find(s => s.type === 'PIANO_ROLL' && s.target_id === midiItemId);
    
    if (existing) {
      setSubTabs(prev => prev.map(s => s.id === existing.id ? {
        ...s,
        buffer: s.buffer || silentBuffer,
        instrumentProgram: track.instrumentProgram !== undefined ? track.instrumentProgram : (track.synth_engine ? track.synth_engine.soundfont_program : undefined),
        instrumentName: track.instrumentName,
        instrumentId: track.instrumentId,
        synth_engine: track.synth_engine,
      } : s));
      setActiveTab(existing.id);
      showToast('Piano Roll cho nốt MIDI đã được mở.', 'info');
      return;
    }
    const tabId = 'midi_' + Date.now();
    const tabLabel = `Piano Roll: ${midiItem.name || 'MIDI'}`;
    const newTab = {
      id: tabId,
      label: tabLabel,
      type: 'PIANO_ROLL',
      trackId: trackId,
      target_id: midiItemId,
      parent_tab_id: activeTab === 'main' ? null : activeTab,
      notes: midiItem.notes || [],
      duration: midiItem.duration || 4,
      buffer: silentBuffer,
      instrumentProgram: track.instrumentProgram !== undefined ? track.instrumentProgram : (track.synth_engine ? track.synth_engine.soundfont_program : undefined),
      instrumentName: track.instrumentName,
      instrumentId: track.instrumentId,
      synth_engine: track.synth_engine,
      currentTime: 0,
      isPlaying: false,
      isLooping: true,
      selectionStart: null,
      selectionEnd: null,
      viewport_start_bar: 0.0,
      viewport_bar_width: 8.0,
      scroll_y_pitch: 60,
      snap_resolution: snapValue || "1/16",
      note_selection: []
    };
    setSubTabs(prev => [...prev, newTab]);
    setActiveTab(tabId);
    // Đồng bộ routing SF + mastering ngay khi mở piano roll — preview keybed
    // phải đi qua mastering FX của main out (không cần bật/tắt power).
    setTimeout(() => { try { if (window.__ensureMasteringRouting) window.__ensureMasteringRouting(); } catch (e) {} }, 60);
    // KHÔNG loadSoundFont ở đây (kể cả nền): sfload SF mới bất đồng bộ làm
    // WASM heap 256MB đầy → FluidSynth stall → CÂM TOÀN CỤC (mọi âm thanh
    // chết sau khi mở tab). playNote TỰ load + retry đúng lúc note cần.
  };

  const handleUpdateMidiNotes = (tabId, notes) => {
    setSubTabs(prev => prev.map(s => s.id === tabId ? { ...s, notes: notes, isDirty: true } : s));
  };

  const handleSaveMidiNotes = (tabId, trackId, midiItemId, updatedNotes) => {
    const pianoRollTab = subTabs.find(s => s.id === tabId);
    const parentTabId = pianoRollTab ? pianoRollTab.parent_tab_id : null;
    if (parentTabId && parentTabId.startsWith('session_')) {
      setSessionTabs(prev => prev.map(s => {
        if (s.id !== parentTabId) return s;
        return {
          ...s,
          tracks: s.tracks.map(t => {
            if (t.id !== trackId) return t;
            return {
              ...t,
              midiItems: (t.midiItems || []).map(m => {
                if (m.id !== midiItemId) return m;
                return { ...m, notes: updatedNotes };
              })
            };
          })
        };
      }));
    } else {
      setTracks(prev => prev.map(t => {
        if (t.id !== trackId) return t;
        return {
          ...t,
          midiItems: (t.midiItems || []).map(m => {
            if (m.id !== midiItemId) return m;
            return { ...m, notes: updatedNotes };
          })
        };
      }));
    }
    setSubTabs(prev => prev.map(s => s.id === tabId ? { ...s, isDirty: false } : s));
    showToast('Đã lưu các chỉnh sửa nốt MIDI vào item cha!', 'success');
  };

  // Sync nội dung SECTION-TAB → section item trên MAIN (sec.tracks) — cập nhật
  // canvas section item ngay khi thay đổi nội dung tab (user 07:45 — xóa item
  // trong SECTION-TAB → canvas section item ở MAIN phải vẽ lại). KHÔNG đổi
  // duration (0505 — giữ kích thước user resize).
  const syncSectionTabToMain = (tabId) => {
    const tab = sessionTabsRef.current.find(s => s.id === tabId);
    if (!tab || !tab.sectionId) return;
    setTracks(prev => prev.map(t => ({
      ...t,
      sections: (t.sections || []).map(s => {
        if (s.sectionId !== tab.sectionId && s.id !== tab.sectionId) return s;
        return { ...s, tracks: tab.tracks };
      })
    })));
  };

  const handleSaveSectionTab = async (tabId) => {
    const tab = sessionTabs.find(s => s.id === tabId);
    if (!tab) return;
    // Upload buffer-only clips (chưa có serverFileId — upload sớm thất bại/
    // race) TRƯỚC khi ghi vào section item → serialized AUDIO_ITEM có file →
    // reload không mất audioclip.
    const ensureClipsUploaded = async (tracksArr) => {
      for (const tr of tracksArr || []) {
        for (const c of (tr.clips || [])) {
          if (!c.serverFileId && c.buffer && c.buffer.duration > 0.05) {
            try {
              const blob = encodeWavBlob(c.buffer);
              const up = await uploadToServer(new File([blob], (c.name || 'clip').replace(/\.[^.]+$/, '') + '.wav', { type: 'audio/wav' }), tr.id);
              if (up && up.file_id) { c.serverFileId = up.file_id; }
            } catch (err) { console.warn('section clip upload failed:', err); }
          }
        }
      }
    };

    // Gồm CẢ track buffer-only (audioclip vừa chèn, upload chưa tạo clip —
    // race: save trước khi upload xong → trước đây bị LỌC mất → audioclip
    // không lưu). Buffer-only → tạo default clip để serialize bắt được item.
    // LỌC BỎ section item TỰ TRỎ (sectionId/section item trỏ về CHÍNH section
    // đang lưu — insertSectionAtPlayhead tạo item thiếu sectionId → fallback
    // s.id → block lồng + ghi đè sectionStore → mất track MIDI).
    const contentTracks = tab.tracks ? tab.tracks
      .filter(tr => tr.clips?.length > 0 || tr.midiItems?.length > 0 || !!tr.buffer)
      .map(tr => {
        const nextTr = { ...tr };
        if (nextTr.sections && nextTr.sections.length > 0) {
          nextTr.sections = nextTr.sections.filter(s => (s.sectionId || s.id) !== tab.sectionId);
        }
        if (tr.buffer && (!tr.clips || tr.clips.length === 0)) {
          return { ...nextTr, clips: [{ id: 'default_' + tr.id, buffer: tr.buffer, startTime: tr.startTime || 0, name: tr.name, speed: 1.0 }] };
        }
        return nextTr;
      }) : [];
    // Đảm bảo mọi clip có serverFileId (upload buffer-only trước khi lưu)
    await ensureClipsUploaded(contentTracks);

    setTracks(prev => prev.map(t => {
      if (!t.sections || t.sections.length === 0) return t;
      return {
        ...t,
        sections: t.sections.map(s => {
          if (s.sectionId !== tab.sectionId && s.id !== tab.sectionId) return s;
          return {
            ...s,
            name: tab.name,
            // ⚠️ KHÔNG ghi đè duration: MAIN SESSION chỉ play ĐÚNG duration
            // của section item (user đã resize thủ công) — mặc kệ SECTION-TAB
            // dài bao nhiêu. Trước đây duration = durationSec (theo content
            // tab) → item tự giãn/thu lại mỗi lần lưu → mất kích thước user
            // set. Play MAIN đã clamp theo sec.duration (19154/19191).
            tracks: contentTracks
          };
        })
      };
    }));

    // Clear isDirty flag
    setSessionTabs(prev => prev.map(s => s.id === tabId ? { ...s, isDirty: false } : s));

    showToast(`Đã lưu nội dung Section "${tab.name}" vào Main Session!`, 'success');
  };

  // ── Double-click/Edit Section: open Main Session in new tab ──
  const handleEditSectionInTab = (trackId, sectionId) => {
    const track = tracks.find(t => t.id === trackId);
    if (!track) return;
    const section = (track.sections || []).find(s => s.id === sectionId);
    if (!section) return;
    const existing = sessionTabs.find(s => s.sectionId === sectionId);
    if (existing) { setActiveTab(existing.id); showToast(`Tab "${section.name}" already open.`, 'info'); return; }
    const tabId = 'session_' + Date.now();
    const tabName = section.name || 'Section';
    const clonedTracks = section.tracks ? section.tracks.map(t => {
      // Giữ CLIPS + gắn lại buffer THẬT (JSON.parse(JSON.stringify()) DROP
      // AudioBuffer → audioclip mất khỏi section editor). Trước đây clips:[]
      // → section item mở bằng nhấp đôi không chứa audioclip.
      const base = JSON.parse(JSON.stringify(t));
      const srcClips = (t.clips || []).map(c => ({ ...c, buffer: c.buffer || null }));
      return {
        ...base,
        clips: srcClips,
        sections: [],
        markers: [],
        isArmed: false,
        monitoringEnabled: true,
        instrumentId: t.instrumentId || null,
        instrumentProgram: t.instrumentProgram,
        instrumentName: t.instrumentName || null,
        // Reset bypass theo CONTEXT SECTION (KHÔNG kế thừa từ track gốc):
        // track gốc có thể audioByPass/masteringBypass=true (data lưu cũ) →
        // kế thừa làm track MIDI section bị kéo vào dry bus khi play (bypass
        // mastering dù ♪ tắt). Section default: audio raw (A sáng = bypass),
        // MIDI qua mastering (♪ tắt).
        masteringBypass: false,
        audioBypass: true,
        midiBypass: false,
        _isSectionClone: true
      };
    }) : tracks.filter(t => t.id === trackId).map(t => ({
      ...t,
      clips: [],
      sections: [],
      midiItems: [],
      markers: [],
      isArmed: false,
      monitoringEnabled: true,
      instrumentId: null,
      instrumentProgram: undefined,
      instrumentName: null,
      soundfont_id: null,
      soundfont_bank: undefined,
      soundfont_program: undefined,
      instrument_source: null,
      synth_engine: undefined,
      masteringBypass: false,
      audioBypass: true,
      midiBypass: false,
      _isSectionClone: true
    }));
    setSessionTabs(prev => [...prev, { id: tabId, name: tabName, sectionId: sectionId, tracks: clonedTracks, color: section.color || null }]);
    setActiveTab(tabId);
  };

  // ── Sub Tab: apply effects & merge back (LOOP_EDITOR_2.md §1.2) ──
  const applySubTab = tabId => {
    const subTab = subTabs.find(s => s.id === tabId);
    if (!subTab || !subTab.buffer) return;
    const track = activeTracks.find(t => t.id === subTab.trackId);
    if (!track || !track.buffer) return;
    const beforeSnap = captureTrackSnapshot(subTab.trackId);

    // Clone buffer and apply effects
    const ctx = getAudioContext();
    const sr = subTab.buffer.sampleRate;
    const eff = subTab.buffer.getChannelData(0);
    let resultBuffer = ctx.createBuffer(1, eff.length, sr);
    let resultData = resultBuffer.getChannelData(0);
    resultData.set(eff);

    // Apply effects inline
    const fx = subTab.effects || {};
    const applyResample = (data, ratio) => {
      const newLen = Math.round(data.length * ratio);
      const out = new Float32Array(newLen);
      for (let i = 0; i < newLen; i++) {
        const srcIdx = i / ratio;
        const idx0 = Math.floor(srcIdx);
        const idx1 = Math.min(idx0 + 1, data.length - 1);
        const frac = srcIdx - idx0;
        out[i] = data[idx0] * (1 - frac) + data[idx1] * frac;
      }
      return out;
    };
    if (fx.reverse) {
      const rev = new Float32Array(resultData);
      for (let i = 0; i < resultData.length; i++) rev[i] = resultData[resultData.length - 1 - i];
      resultData.set(rev);
    }
    if (fx.gainDb !== 0) {
      const gain = Math.pow(10, fx.gainDb / 20);
      for (let i = 0; i < resultData.length; i++) resultData[i] = Math.max(-1, Math.min(1, resultData[i] * gain));
    }
    if (fx.fadeInMs > 0) {
      const fadeSamples = Math.min(resultData.length, Math.floor(fx.fadeInMs / 1000 * sr));
      for (let i = 0; i < fadeSamples; i++) resultData[i] *= i / fadeSamples;
    }
    // Graph Editor fade curves (trigonometric, 15_GRAPH_EDIT.md §2.3)
    const gFadeIn = subTab.fadeInLen || 0;
    const gFadeOut = subTab.fadeOutLen || 0;
    if (gFadeIn > 0) {
      const fadeSamples = Math.min(resultData.length, Math.floor(gFadeIn * sr));
      for (let i = 0; i < fadeSamples; i++) {
        resultData[i] *= (1 - Math.cos(Math.PI * i / fadeSamples)) / 2;
      }
    }
    if (gFadeOut > 0) {
      const fadeSamples = Math.min(resultData.length, Math.floor(gFadeOut * sr));
      for (let i = resultData.length - fadeSamples; i < resultData.length; i++) {
        const t = i - (resultData.length - fadeSamples);
        resultData[i] *= (1 + Math.cos(Math.PI * t / fadeSamples)) / 2;
      }
    }

    // Graph Editor volume automation spline (Monotone Cubic Hermite Spline, 16_FIX_GRAPH.md §2.1)
    const volNodes = subTab.volumeNodes || [];
    if (volNodes.length > 0) {
      const sortedNodes = [...volNodes].sort((a, b) => a.time - b.time);
      const computeHermiteTangents = pts => {
        const n = pts.length;
        if (n < 2) return [];
        const m = new Array(n);
        for (let i = 1; i < n - 1; i++) {
          const hP = pts[i].time - pts[i - 1].time;
          const hN = pts[i + 1].time - pts[i].time;
          const sP = (pts[i].db - pts[i - 1].db) / hP;
          const sN = (pts[i + 1].db - pts[i].db) / hN;
          m[i] = (sP + sN) / 2;
        }
        m[0] = n > 1 ? (pts[1].db - pts[0].db) / (pts[1].time - pts[0].time) : 0;
        m[n - 1] = n > 1 ? (pts[n - 1].db - pts[n - 2].db) / (pts[n - 1].time - pts[n - 2].time) : 0;
        return m;
      };
      const tangents = computeHermiteTangents(sortedNodes);
      const getVolumeGainAtTime = t => {
        if (sortedNodes.length === 1) {
          return Math.pow(10, sortedNodes[0].db / 20);
        }
        if (t <= sortedNodes[0].time) {
          return Math.pow(10, sortedNodes[0].db / 20);
        }
        if (t >= sortedNodes[sortedNodes.length - 1].time) {
          return Math.pow(10, sortedNodes[sortedNodes.length - 1].db / 20);
        }
        for (let i = 0; i < sortedNodes.length - 1; i++) {
          const n1 = sortedNodes[i];
          const n2 = sortedNodes[i + 1];
          if (t >= n1.time && t <= n2.time) {
            const h = n2.time - n1.time;
            if (h <= 0) return Math.pow(10, n1.db / 20);
            const frac = (t - n1.time) / h;
            const frac2 = frac * frac,
              frac3 = frac2 * frac;
            const db = (2 * frac3 - 3 * frac2 + 1) * n1.db + (frac3 - 2 * frac2 + frac) * h * tangents[i] + (-2 * frac3 + 3 * frac2) * n2.db + (frac3 - frac2) * h * tangents[i + 1];
            return Math.pow(10, db / 20);
          }
        }
        return 1.0;
      };
      for (let i = 0; i < resultData.length; i++) {
        const t = i / sr;
        resultData[i] *= getVolumeGainAtTime(t);
      }
    }
    if (fx.normalizeDb !== 0) {
      let maxVal = 0;
      for (let i = 0; i < resultData.length; i++) {
        const abs = Math.abs(resultData[i]);
        if (abs > maxVal) maxVal = abs;
      }
      if (maxVal > 0) {
        const targetAmp = Math.pow(10, fx.normalizeDb / 20);
        const scale = targetAmp / maxVal;
        for (let i = 0; i < resultData.length; i++) resultData[i] = Math.max(-1, Math.min(1, resultData[i] * scale));
      }
    }
    if (fx.pitch !== 0) {
      const ratio = Math.pow(2, fx.pitch / 12);
      const newData = applyResample(resultData, 1 / ratio);
      resultBuffer = ctx.createBuffer(1, newData.length, sr);
      resultData = resultBuffer.getChannelData(0);
      resultData.set(newData);
    }
    const stretchSpeed = subTab.speed || 1.0;
    if (stretchSpeed !== 1.0) {
      const ratio = 1.0 / stretchSpeed;
      const newData = applyResample(resultData, ratio);
      resultBuffer = ctx.createBuffer(1, newData.length, sr);
      resultData = resultBuffer.getChannelData(0);
      resultData.set(newData);
    }
    if (fx.speedStretch !== 100) {
      const ratio = fx.speedStretch / 100;
      const newData = applyResample(resultData, ratio);
      resultBuffer = ctx.createBuffer(1, newData.length, sr);
      resultData = resultBuffer.getChannelData(0);
      resultData.set(newData);
    }
    const isSectionTrack = sessionTabs.some(s => s.tracks.some(t => t.id === subTab.trackId));
    if (isSectionTrack) {
      if (subTab.clipId) {
        setSessionTabs(prev => prev.map(s => {
          if (!s.tracks.some(t => t.id === subTab.trackId)) return s;
          return {
            ...s,
            tracks: s.tracks.map(t => {
              if (t.id !== subTab.trackId) return t;
              const updatedClips = (t.clips || []).map(c => {
                if (c.id === subTab.clipId) {
                  return {
                    ...c,
                    buffer: resultBuffer,
                    name: c.name.endsWith('(edited)') ? c.name : c.name + ' (edited)'
                  };
                }
                return c;
              });
              return {
                ...t,
                clips: updatedClips,
                buffer: updatedClips[0]?.buffer,
                startTime: updatedClips[0]?.startTime || 0,
                name: updatedClips[0]?.name || t.name
              };
            })
          };
        }));
      } else {
        // Crossfade merge into original track (§2.2)
        const sr = track.buffer.sampleRate;
        const origData = track.buffer.getChannelData(0);
        const trackStart = track.startTime || 0;
        const startSample = Math.floor((subTab.startTime - trackStart) * sr);
        const endSample = Math.floor((subTab.endTime - trackStart) * sr);
        const crossfadeLen = Math.min(100, Math.floor(0.01 * sr)); // 10ms

        const mergedBuffer = ctx.createBuffer(1, track.buffer.length, sr);
        const mergedData = mergedBuffer.getChannelData(0);
        for (let i = 0; i < startSample; i++) mergedData[i] = origData[i];
        for (let i = endSample; i < track.buffer.length; i++) mergedData[i] = origData[i];
        for (let i = 0; i < resultData.length; i++) {
          const globalIdx = startSample + i;
          let val = resultData[i];
          if (i < crossfadeLen) {
            const alpha = i / crossfadeLen;
            val = (1 - alpha) * (origData[globalIdx] || 0) + alpha * resultData[i];
          } else if (i > resultData.length - crossfadeLen) {
            const distFromEnd = resultData.length - 1 - i;
            const alpha = distFromEnd / crossfadeLen;
            const origEndIdx = endSample - (resultData.length - i);
            val = alpha * (origEndIdx >= 0 ? origData[origEndIdx] : 0) + (1 - alpha) * resultData[i];
          }
          mergedData[globalIdx] = val;
        }
        setSessionTabs(prev => prev.map(s => {
          if (!s.tracks.some(t => t.id === subTab.trackId)) return s;
          return {
            ...s,
            tracks: s.tracks.map(t => {
              if (t.id !== subTab.trackId) return t;
              return {
                ...t,
                buffer: mergedBuffer,
                name: t.name + ' (edited)'
              };
            })
          };
        }));
      }
    } else {
      if (subTab.clipId) {
        // Clip-based merge
        setTracks(prev => prev.map(t => {
          if (t.id !== subTab.trackId) return t;
          const updatedClips = (t.clips || []).map(c => {
            if (c.id === subTab.clipId) {
              return {
                ...c,
                buffer: resultBuffer,
                name: c.name.endsWith('(edited)') ? c.name : c.name + ' (edited)'
              };
            }
            return c;
          });
          return {
            ...t,
            clips: updatedClips,
            buffer: updatedClips[0]?.buffer,
            startTime: updatedClips[0]?.startTime || 0,
            name: updatedClips[0]?.name || t.name
          };
        }));
      } else {
        // Crossfade merge into original track (§2.2)
        const sr = track.buffer.sampleRate;
        const origData = track.buffer.getChannelData(0);
        const trackStart = track.startTime || 0;
        const startSample = Math.floor((subTab.startTime - trackStart) * sr);
        const endSample = Math.floor((subTab.endTime - trackStart) * sr);
        const crossfadeLen = Math.min(100, Math.floor(0.01 * sr)); // 10ms

        const mergedBuffer = ctx.createBuffer(1, track.buffer.length, sr);
        const mergedData = mergedBuffer.getChannelData(0);
        for (let i = 0; i < startSample; i++) mergedData[i] = origData[i];
        for (let i = endSample; i < track.buffer.length; i++) mergedData[i] = origData[i];
        for (let i = 0; i < resultData.length; i++) {
          const globalIdx = startSample + i;
          let val = resultData[i];
          if (i < crossfadeLen) {
            const alpha = i / crossfadeLen;
            val = (1 - alpha) * (origData[globalIdx] || 0) + alpha * resultData[i];
          } else if (i > resultData.length - crossfadeLen) {
            const distFromEnd = resultData.length - 1 - i;
            const alpha = distFromEnd / crossfadeLen;
            const origEndIdx = endSample - (resultData.length - i);
            val = alpha * (origEndIdx >= 0 ? origData[origEndIdx] : 0) + (1 - alpha) * resultData[i];
          }
          mergedData[globalIdx] = val;
        }
        setTracks(prev => prev.map(t => {
          if (t.id !== subTab.trackId) return t;
          return {
            ...t,
            buffer: mergedBuffer,
            name: t.name + ' (edited)'
          };
        }));
      }
    }
    const afterSnap = captureTrackSnapshot(subTab.trackId);
    pushAction('EDIT_TAB', subTab.trackId, beforeSnap, afterSnap);

    // Tab Lifetime: Engaging the Apply trigger propagates data back to the primary environment but does not close down the active sub-tab view.
    // closeSubTab(tabId);
    showToast('Đã áp dụng chỉnh sửa vào track chính.', 'success');
  };
  const applySubTabEffect = (tabId, effectType, value) => {
    const subTab = subTabs.find(s => s.id === tabId);
    if (!subTab || !subTab.buffer) return;
    const ctx = getAudioContext();
    const sr = subTab.buffer.sampleRate;
    const eff = subTab.buffer.getChannelData(0);

    // Clone buffer
    let resultBuffer = ctx.createBuffer(1, eff.length, sr);
    let resultData = resultBuffer.getChannelData(0);
    resultData.set(eff);

    // Calculate selection start/end in unstretched sample indices
    const speed = subTab.speed || 1.0;
    const hasSelection = subTab.selectionStart !== null && subTab.selectionEnd !== null && subTab.selectionStart !== subTab.selectionEnd;
    const tStart = hasSelection ? Math.min(subTab.selectionStart, subTab.selectionEnd) * speed : 0;
    const tEnd = hasSelection ? Math.max(subTab.selectionStart, subTab.selectionEnd) * speed : subTab.buffer.duration;
    const startSample = Math.max(0, Math.min(eff.length, Math.floor(tStart * sr)));
    const endSample = Math.max(0, Math.min(eff.length, Math.floor(tEnd * sr)));
    if (effectType === 'normalize') {
      let maxVal = 0;
      for (let i = startSample; i < endSample; i++) {
        const abs = Math.abs(resultData[i]);
        if (abs > maxVal) maxVal = abs;
      }
      if (maxVal > 0) {
        const targetAmp = Math.pow(10, value / 20);
        const scale = targetAmp / maxVal;
        for (let i = startSample; i < endSample; i++) {
          resultData[i] = Math.max(-1, Math.min(1, resultData[i] * scale));
        }
      }
    } else if (effectType === 'gain') {
      const gainScale = value / 100;
      for (let i = startSample; i < endSample; i++) {
        resultData[i] = Math.max(-1, Math.min(1, resultData[i] * gainScale));
      }
    } else if (effectType === 'pitch') {
      const ratio = Math.pow(2, value / 12);
      const applyResample = (data, r) => {
        const newLen = Math.round(data.length * r);
        const out = new Float32Array(newLen);
        for (let i = 0; i < newLen; i++) {
          const srcIdx = i / r;
          const idx0 = Math.floor(srcIdx);
          const idx1 = Math.min(idx0 + 1, data.length - 1);
          const frac = srcIdx - idx0;
          out[i] = data[idx0] * (1 - frac) + data[idx1] * frac;
        }
        return out;
      };
      const sub = resultData.slice(startSample, endSample);
      const subResampled = applyResample(sub, 1 / ratio);
      for (let i = 0; i < endSample - startSample; i++) {
        resultData[startSample + i] = i < subResampled.length ? subResampled[i] : 0.0;
      }
    } else if (effectType === 'invert_phase') {
      // DSP: đảo pha — nhân -1 toàn bộ vùng chọn/clip
      for (let i = startSample; i < endSample; i++) {
        resultData[i] = -resultData[i];
      }
    } else if (effectType === 'swap_channels') {
      // DSP: đảo kênh L/R — buffer 2 kênh (nếu có), hoán đổi dữ liệu
      const srcBuffer = subTab.buffer;
      if (srcBuffer.numberOfChannels >= 2) {
        const l = srcBuffer.getChannelData(0).slice();
        const r = srcBuffer.getChannelData(1).slice();
        const out = ctx.createBuffer(2, eff.length, sr);
        out.getChannelData(0).set(r);
        out.getChannelData(1).set(l);
        resultBuffer = out;
        resultData = resultBuffer.getChannelData(0);
      } else {
        // Mono → không đổi kênh được, giữ nguyên
        showToast('Buffer mono — không có kênh L/R để hoán đổi.', 'info');
        return;
      }
    } else if (effectType === 'reverse') {
      // DSP: đảo ngược thời gian vùng chọn/clip
      const seg = resultData.slice(startSample, endSample);
      for (let i = 0; i < seg.length; i++) {
        resultData[startSample + i] = seg[seg.length - 1 - i];
      }
    }

    // Update subTab buffer state
    setSubTabs(prev => prev.map(s => {
      if (s.id !== tabId) return s;
      return {
        ...s,
        buffer: resultBuffer,
        selectionStart: null,
        selectionEnd: null
      };
    }));
    const effectLabels = {
      normalize: 'Normalize', gain: 'Gain', pitch: 'Pitch',
      invert_phase: 'Phase Invert', swap_channels: 'Swap L/R', reverse: 'Reverse'
    };
    showToast(`Đã áp dụng ${effectLabels[effectType] || effectType} cho ${hasSelection ? 'vùng chọn' : 'toàn bộ clip'}.`, 'success');
  };
  const exportSubTabBuffer = async tabId => {
    const subTab = subTabs.find(s => s.id === tabId);
    if (!subTab || !subTab.buffer) return;
    try {
      const buffer = subTab.buffer;
      const sr = buffer.sampleRate;
      const monoData = buffer.getChannelData(0);
      const bufferLength = monoData.length;
      const bitDepth = 16;
      const bytesPerSample = 2;
      const headerSize = 44;
      const fileSizeBytes = headerSize + bufferLength * bytesPerSample;
      const fileBuffer = new ArrayBuffer(fileSizeBytes);
      const view = new DataView(fileBuffer);
      const writeString = (offset, string) => {
        for (let i = 0; i < string.length; i++) {
          view.setUint8(offset + i, string.charCodeAt(i));
        }
      };
      writeString(0, 'RIFF');
      view.setUint32(4, fileSizeBytes - 8, true);
      writeString(8, 'WAVE');
      writeString(12, 'fmt ');
      view.setUint32(16, 16, true);
      view.setUint16(20, 1, true);
      view.setUint16(22, 1, true);
      view.setUint32(24, sr, true);
      view.setUint32(28, sr * bytesPerSample, true);
      view.setUint16(32, bytesPerSample, true);
      view.setUint16(34, bitDepth, true);
      writeString(36, 'data');
      view.setUint32(40, bufferLength * bytesPerSample, true);
      let offset = 44;
      for (let i = 0; i < bufferLength; i++) {
        const sample = Math.max(-1, Math.min(1, monoData[i]));
        view.setInt16(offset, Math.floor(sample < 0 ? sample * 0x8000 : sample * 0x7FFF), true);
        offset += bytesPerSample;
      }
      const blob = new Blob([view], {
        type: 'audio/wav'
      });
      const url = URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.href = url;
      a.download = `${subTab.label || 'subtab-export'}.wav`;
      a.click();
      URL.revokeObjectURL(url);
      showToast("Xuất bản sub-tab hoàn tất!", "success");
    } catch (err) {
      showToast("Lỗi xuất âm thanh: " + err.message, "error");
    }
  };
  const closeSubTab = tabId => {
    const tab = subTabs.find(s => s.id === tabId);
    if (!tab) return;
    const performClose = () => {
      setSubTabs(prev => prev.filter(s => s.id !== tabId));
      if (activeTab === tabId) setActiveTab('main');
    };
    if (tab.isDirty) {
      setAppWarningModal({
        title: "Đóng Tab?",
        message: `Bạn chưa lưu các thay đổi của tab "${tab.label || 'MIDI'}". Bạn có chắc chắn muốn đóng tab này và mất hết thay đổi?`,
        isAlert: false,
        onConfirm: performClose
      });
    } else {
      performClose();
    }
  };
  const closeSessionTab = tabId => {
    const targetTab = sessionTabs.find(s => s.id === tabId);
    if (!targetTab) return;
    const activeChildTabs = subTabs.filter(s => s.parent_tab_id === tabId);
    if (activeChildTabs.length > 0) {
      setAppWarningModal({
        title: "Không thể đóng Tab Section",
        message: `Vui lòng đóng các Tab con biên tập trước khi đóng Tab Section này:\n\n` +
          activeChildTabs.map(t => `• ${t.label || t.name}`).join("\n"),
        isAlert: true
      });
      return;
    }
    const performClose = () => {
      setSessionTabs(prev => prev.filter(s => s.id !== tabId));
      if (activeTab === tabId) setActiveTab('main');
    };
    if (targetTab.isDirty) {
      setAppWarningModal({
        title: "Đóng Tab Section?",
        message: `Bạn chưa lưu các thay đổi của Section "${targetTab.name}". Bạn có chắc chắn muốn đóng tab này và mất hết thay đổi?`,
        isAlert: false,
        onConfirm: performClose
      });
    } else {
      performClose();
    }
  };
  const updateSubTabEffects = (tabId, effects) => {
    setSubTabs(prev => prev.map(s => s.id === tabId ? {
      ...s,
      effects: {
        ...s.effects,
        ...effects
      }
    } : s));
  };
  const handleSubTabNormalizeWithValue = (tabId, normalizeDb) => {
    updateSubTabEffects(tabId, {
      normalizeDb
    });
  };
  const handleSubTabGainWithValue = (tabId, gainDb) => {
    updateSubTabEffects(tabId, {
      gainDb
    });
  };
  const handleSubTabPitch = (tabId, pitch) => {
    updateSubTabEffects(tabId, {
      pitch
    });
  };
  const handleSubTabStretch = (tabId, speedStretch) => {
    updateSubTabEffects(tabId, {
      speedStretch
    });
  };
  const handleSubTabFadeState = (tabId, type) => {
    const st = subTabs.find(s => s.id === tabId);
    if (!st) return;
    const fx = st.effects || {};
    if (type === 'in') {
      updateSubTabEffects(tabId, {
        fadeInMs: (fx.fadeInMs || 0) + 10
      });
    } else if (type === 'out') {
      updateSubTabEffects(tabId, {
        fadeOutMs: (fx.fadeOutMs || 0) + 10
      });
    }
  };

  // ── Context Menu Handlers ──
  const handleContextMenu = (e, trackId, clickTime, sectionId, itemId, itemType) => {
    e.preventDefault();
    e.stopPropagation();
    setContextMenu({
      x: e.clientX,
      y: e.clientY,
      trackId,
      time: clickTime || currentTime,
      sectionId: sectionId || null,
      itemId: itemId || null,
      itemType: itemType || null
    });
  };
  const closeContextMenu = () => setContextMenu(null);

  const contextMenuEditSection = () => {
    if (contextMenu.sectionId) {
      handleEditSectionInTab(contextMenu.trackId, contextMenu.sectionId);
    }
    closeContextMenu();
  };

  // Close context menu on any click outside
  useEffect(() => {
    const handler = () => {
      if (contextMenu) closeContextMenu();
    };
    if (contextMenu) {
      window.addEventListener('click', handler);
      return () => window.removeEventListener('click', handler);
    }
  }, [contextMenu]);
  const contextMenuEdit = () => {
    const track = activeTracks.find(t => t.id === contextMenu.trackId);
    if (track) setSelectedTrackId(contextMenu.trackId);
    closeContextMenu();
    if (!track) return;

    const secondsPerBar = (60.0 / (parseInt(bpm) || 120)) * 4;

    // Check MIDI items first
    const midiItems = track.midiItems || [];
    const clickedMidi = midiItems.find(m => contextMenu.time >= m.startTime && contextMenu.time < m.startTime + (m.duration || 4) * secondsPerBar);

    if (clickedMidi) {
      handleEditMidiInTab(contextMenu.trackId, clickedMidi.id);
      return;
    }

    // Check Audio clips next
    const clips = track.clips && track.clips.length > 0 ? track.clips : track.buffer ? [{
      id: 'default',
      buffer: track.buffer,
      startTime: track.startTime || 0,
      name: track.name,
      speed: track.speed || 1.0
    }] : [];
    const clickedClip = clips.find(c => contextMenu.time >= c.startTime && contextMenu.time < c.startTime + c.buffer.duration / (c.speed || 1.0));
    if (clickedClip) {
      handleEditClipInSubTab(contextMenu.trackId, clickedClip.id);
    } else {
      openTempTab();
    }
  };
  const contextMenuSplit = () => {
    closeContextMenu();
    handleSplitTrack(contextMenu.trackId);
  };
  const contextMenuDelete = () => {
    const tid = contextMenu.trackId;
    const track = activeTracks.find(t => t.id === tid);
    if (!track) {
      closeContextMenu();
      return;
    }

    const secondsPerBeat = 60.0 / (parseInt(bpm) || 120);
    const secondsPerBar = secondsPerBeat * 4;
    const time = contextMenu.time;

    // Check for Section item under cursor
    const secList = track.sections || [];
    const clickedSec = secList.find(s => time >= s.start && time < s.start + s.duration);

    // Check for MIDI item under cursor
    const midiItems = track.midiItems || [];
    const clickedMidi = midiItems.find(m => time >= m.startTime && time < m.startTime + (m.duration || 4) * secondsPerBar);

    // Check for Audio clip under cursor
    const clips = track.clips && track.clips.length > 0 ? track.clips : track.buffer ? [{
      id: 'default',
      buffer: track.buffer,
      startTime: track.startTime || 0,
      name: track.name,
      speed: track.speed || 1.0
    }] : [];
    const clickedClip = clips.find(c => time >= c.startTime && time < c.startTime + c.buffer.duration / (c.speed || 1.0));

    if (clickedSec) {
      const beforeSnap = captureTrackSnapshot(tid);
      updateActiveTracks(prev => prev.map(t => {
        if (t.id !== tid) return t;
        return {
          ...t,
          sections: (t.sections || []).filter(s => s.id !== clickedSec.id)
        };
      }));
      const afterSnap = captureTrackSnapshot(tid);
      pushAction('DELETE_SECTION', tid, beforeSnap, afterSnap);
      closeContextMenu();
      // Sync section tab → section item trên MAIN (canvas vẽ lại — 07:45)
      if (activeTabRef.current && activeTabRef.current.startsWith('session_')) {
    setTimeout(() => { try { syncSectionTabToMain(activeTabRef.current); } catch (e) {} }, 0);
      }
      showToast('Đã xoá section item.', 'info');
      return;
    }

    if (clickedMidi) {
      const beforeSnap = captureTrackSnapshot(tid);
      // Close piano roll tab mở của midi item này (user 07:35)
      setSubTabs(prev => {
        const next = prev.filter(s => s.target_id !== clickedMidi.id);
        // CHỈ về main khi tab active là PIANO ROLL bị close — giữ SECTION-TAB
        if (activeTabRef.current && activeTabRef.current.startsWith('midi_') && !next.some(s => s.id === activeTabRef.current)) {
          setActiveTab('main');
        }
        return next;
      });
      // Sync section tab → section item trên MAIN (canvas vẽ lại — 07:45)
      if (activeTabRef.current && activeTabRef.current.startsWith('session_')) {
    setTimeout(() => { try { syncSectionTabToMain(activeTabRef.current); } catch (e) {} }, 0);
      }
      updateActiveTracks(prev => prev.map(t => {
        if (t.id !== tid) return t;
        return {
          ...t,
          midiItems: (t.midiItems || []).filter(m => m.id !== clickedMidi.id)
        };
      }));
      const afterSnap = captureTrackSnapshot(tid);
      pushAction('DELETE_MIDI', tid, beforeSnap, afterSnap);
      closeContextMenu();
      showToast('Đã xoá MIDI item.', 'info');
      return;
    }

    if (clickedClip) {
      const beforeSnap = captureTrackSnapshot(tid);
      updateActiveTracks(prev => prev.map(t => {
        if (t.id !== tid) return t;
        const updatedClips = (t.clips || []).filter(c => c.id !== clickedClip.id);
        return {
          ...t,
          clips: updatedClips,
          buffer: updatedClips[0]?.buffer || null,
          startTime: updatedClips[0]?.startTime || 0,
          name: updatedClips[0]?.name || t.name
        };
      }));
      const afterSnap = captureTrackSnapshot(tid);
      pushAction('DELETE_CLIP', tid, beforeSnap, afterSnap);
      closeContextMenu();
      // Sync section tab → section item trên MAIN (canvas vẽ lại — 07:45)
      if (activeTabRef.current && activeTabRef.current.startsWith('session_')) {
    setTimeout(() => { try { syncSectionTabToMain(activeTabRef.current); } catch (e) {} }, 0);
      }
      showToast('Đã xoá audio clip.', 'info');
      return;
    }

    // No item clicked under cursor -> Attempt to delete the track itself
    // Item LỖI/rỗng (clip không buffer — khung viền "audio chưa được tải",
    // midi không notes, section không nội dung) KHÔNG chặn xoá track.
    const hasClips = (track.clips && track.clips.some(c => c.buffer)) || !!track.buffer;
    const hasMidi = track.midiItems && track.midiItems.some(m => (m.notes && m.notes.length > 0));
    const hasSections = track.sections && track.sections.some(s => (s.tracks && s.tracks.some(st => (st.clips && st.clips.some(c => c.buffer)) || (st.midiItems && st.midiItems.some(m => m.notes && m.notes.length > 0)))));
    const isTrackEmpty = !hasClips && !hasMidi && !hasSections;

    if (!isTrackEmpty) {
      closeContextMenu();
      setAppWarningModal({
        title: 'Không thể xoá Track',
        message: 'Không thể xoá track chứa dữ liệu. Vui lòng xoá hết các item trước khi xoá track.',
        isAlert: true
      });
      return;
    }

    const sessionTab = sessionTabs.find(s => s.id === activeTab);
    if (sessionTab) {
      updateActiveTracks(prev => {
        const filtered = prev.filter(t => t.id !== tid);
        if (filtered.length > 0) setSelectedTrackId(filtered[0].id);
        return filtered;
      });
      closeContextMenu();
      showToast('Đã xoá track.', 'info');
    } else {
      const beforeSnap = captureTrackSnapshot(tid);
      setTracks(prev => {
        const filtered = prev.filter(t => t.id !== tid);
        if (filtered.length > 0) setSelectedTrackId(filtered[0].id || '1');
        return filtered;
      });
      const afterSnap = captureTrackSnapshot(tid);
      pushAction('DELETE', tid, beforeSnap, afterSnap);
      if (selectedTrackId === tid) setSelectedTrackId(tracks.filter(t => t.id !== tid)[0]?.id || '1');
      closeContextMenu();
      showToast('Đã xoá track.', 'info');
    }
  };
  const contextMenuCopy = () => {
    // ⚠️ Context menu trên ITEM → copy ĐÚNG item (user 07:25 — trước đây copy
    // nhầm track buffer → paste ra audio item dù cut/copy midi item).
    if (contextMenu.itemType === 'midi' && contextMenu.itemId) {
      const trk = activeTracks.find(t => t.id === contextMenu.trackId);
      const item = trk && (trk.midiItems || []).find(m => m.id === contextMenu.itemId);
      if (item) {
        clipboardRef.current = {
          type: 'midi',
          notes: (item.notes || []).map(n => ({ ...n })),
          duration: item.duration || 4,
          name: item.name || 'MIDI Item',
          color: item.color || '#a855f7'
        };
        window.globalStudioClipboard = clipboardRef.current;
        closeContextMenu();
        showToast(`Đã sao chép MIDI item "${item.name || ''}".`, 'info');
        return;
      }
    }
    if (contextMenu.itemType === 'clip' && contextMenu.itemId) {
      const trk = activeTracks.find(t => t.id === contextMenu.trackId);
      const item = trk && (trk.clips || []).find(c => c.id === contextMenu.itemId);
      if (item && item.buffer) {
        clipboardRef.current = {
          buffer: item.buffer,
          name: item.name || trk.name,
          volumeDb: trk.volumeDb,
          pan: trk.pan,
          color: item.color || trk.color,
          sampleRate: item.buffer.sampleRate,
          channels: item.buffer.numberOfChannels,
          speed: item.speed || 1.0
        };
        window.globalStudioClipboard = clipboardRef.current;
        closeContextMenu();
        showToast(`Đã sao chép clip "${item.name || ''}".`, 'info');
        return;
      }
    }
    const track = activeTracks.find(t => t.id === contextMenu.trackId);
    if (!track || !track.buffer) return;
    const sr = track.buffer.sampleRate;
    const data = track.buffer.getChannelData(0);

    // Copy selected region if selection exists
    if (selLeft !== null && selRight !== null && selRight > selLeft) {
      const trackStart = track.startTime || 0;
      const relSelLeft = Math.max(0, selLeft - trackStart);
      const relSelRight = Math.max(0, selRight - trackStart);
      const startSample = Math.floor(relSelLeft * sr);
      const endSample = Math.min(data.length, Math.floor(relSelRight * sr));
      const len = endSample - startSample;
      if (len > 0) {
        const ctx = getAudioContext();
        const numCh = track.buffer.numberOfChannels || 1;
        const clipBuffer = ctx.createBuffer(numCh, len, sr);
        for (let ch = 0; ch < numCh; ch++) {
          clipBuffer.copyToChannel(track.buffer.getChannelData(ch).subarray(startSample, endSample), ch);
        }
        clipboardRef.current = {
          buffer: clipBuffer,
          name: track.name,
          volumeDb: track.volumeDb,
          pan: track.pan,
          color: track.color,
          sampleRate: clipBuffer.sampleRate,
          channels: numCh,
          speed: track.speed || 1.0
        };
        closeContextMenu();
        showToast('Đã sao chép vùng chọn.', 'info');
        return;
      }
    }

    // No selection: copy entire track
    clipboardRef.current = {
      buffer: track.buffer,
      name: track.name,
      volumeDb: track.volumeDb,
      pan: track.pan,
      color: track.color,
      sampleRate: track.buffer.sampleRate,
      channels: track.buffer.numberOfChannels,
      speed: track.speed || 1.0
    };
    closeContextMenu();
    showToast('Đã sao chép toàn bộ track.', 'info');
  };
  const contextMenuCut = () => {
    // ⚠️ Context menu trên ITEM → cut ĐÚNG item (copy + xóa item — trước đây
    // cut nhầm track buffer/audio — user 07:25).
    if (contextMenu.itemType === 'midi' && contextMenu.itemId) {
      const trk = activeTracks.find(t => t.id === contextMenu.trackId);
      const item = trk && (trk.midiItems || []).find(m => m.id === contextMenu.itemId);
      if (item) {
        clipboardRef.current = {
          type: 'midi',
          notes: (item.notes || []).map(n => ({ ...n })),
          duration: item.duration || 4,
          name: item.name || 'MIDI Item',
          color: item.color || '#a855f7'
        };
        window.globalStudioClipboard = clipboardRef.current;
        // Close piano roll tab mở của midi item này (user 07:35)
        setSubTabs(prev => {
          const next = prev.filter(s => s.target_id !== contextMenu.itemId);
          // CHỈ về main khi tab active là PIANO ROLL bị close — giữ SECTION-TAB
          if (activeTabRef.current && activeTabRef.current.startsWith('midi_') && !next.some(s => s.id === activeTabRef.current)) {
            setActiveTab('main');
          }
          return next;
        });
        // Sync section tab → section item trên MAIN (canvas vẽ lại — 07:45)
        if (activeTabRef.current && activeTabRef.current.startsWith('session_')) {
      setTimeout(() => { try { syncSectionTabToMain(activeTabRef.current); } catch (e) {} }, 0);
        }
        updateActiveTracks(prev => prev.map(t => t.id === contextMenu.trackId ? {
          ...t,
          midiItems: (t.midiItems || []).filter(m => m.id !== contextMenu.itemId)
        } : t));
        closeContextMenu();
        showToast(`Đã cắt MIDI item "${item.name || ''}".`, 'info');
        return;
      }
    }
    if (contextMenu.itemType === 'clip' && contextMenu.itemId) {
      const trk = activeTracks.find(t => t.id === contextMenu.trackId);
      const item = trk && (trk.clips || []).find(c => c.id === contextMenu.itemId);
      if (item && item.buffer) {
        clipboardRef.current = {
          buffer: item.buffer,
          name: item.name || trk.name,
          volumeDb: trk.volumeDb,
          pan: trk.pan,
          color: item.color || trk.color,
          sampleRate: item.buffer.sampleRate,
          channels: item.buffer.numberOfChannels,
          speed: item.speed || 1.0
        };
        window.globalStudioClipboard = clipboardRef.current;
        updateActiveTracks(prev => prev.map(t => t.id === contextMenu.trackId ? {
          ...t,
          clips: (t.clips || []).filter(c => c.id !== contextMenu.itemId)
        } : t));
        closeContextMenu();
        showToast(`Đã cắt clip "${item.name || ''}".`, 'info');
        return;
      }
    }
    const t = tracks.find(x => x.id === contextMenu.trackId);
    if (!t || !t.buffer) {
      contextMenuDelete();
      return;
    }
    const beforeSnap = captureTrackSnapshot(contextMenu.trackId);
    const sr = t.buffer.sampleRate;
    const data = t.buffer.getChannelData(0);
    if (selLeft !== null && selRight !== null && selRight > selLeft) {
      const trackStart = t.startTime || 0;
      const relSelLeft = Math.max(0, selLeft - trackStart);
      const relSelRight = Math.max(0, selRight - trackStart);
      const startSample = Math.floor(relSelLeft * sr);
      const endSample = Math.min(data.length, Math.floor(relSelRight * sr));
      const len = endSample - startSample;
      if (len > 0) {
        const ctx = getAudioContext();
        const numCh = t.buffer.numberOfChannels || 1;
        const clipBuffer = ctx.createBuffer(numCh, len, sr);
        for (let ch = 0; ch < numCh; ch++) {
          clipBuffer.copyToChannel(t.buffer.getChannelData(ch).subarray(startSample, endSample), ch);
        }
        clipboardRef.current = {
          buffer: clipBuffer,
          name: t.name,
          volumeDb: t.volumeDb,
          pan: t.pan,
          color: t.color,
          sampleRate: sr,
          channels: numCh,
          speed: t.speed || 1.0
        };
        const newLen = data.length - len;
        const newBuffer = ctx.createBuffer(numCh, newLen, sr);
        for (let ch = 0; ch < numCh; ch++) {
          const src = t.buffer.getChannelData(ch);
          const dst = newBuffer.getChannelData(ch);
          let idx = 0;
          for (let i = 0; i < startSample; i++) dst[idx++] = src[i];
          for (let i = endSample; i < src.length; i++) dst[idx++] = src[i];
        }
        setTracks(p => p.map(tr => tr.id === contextMenu.trackId ? {
          ...tr,
          buffer: newBuffer
        } : tr));
        const afterSnap = captureTrackSnapshot(contextMenu.trackId);
        pushAction('CUT', contextMenu.trackId, beforeSnap, afterSnap);
        closeContextMenu();
        showToast('Đã cắt vùng chọn vào clipboard.', 'info');
        return;
      }
    }
    contextMenuCopy();
    contextMenuDelete();
  };
  const doPaste = (targetTrackId, pasteTime) => {
    const clip = clipboardRef.current || window.globalStudioClipboard;
    if (!clip) {
      showToast('Clipboard trống.', 'warning');
      return null;
    }
    
    // Support MIDI Clip Paste
    if (clip.type === 'midi') {
      const { notes, name, duration, color } = clip;
      const targetTrack = activeTracks.find(t => t.id === targetTrackId);
      const newMidiItem = {
        id: 'midi_' + Date.now(),
        startTime: pasteTime,
        duration: duration || 4,
        name: (name || 'Pasted MIDI').replace(/\.\w+$/, '') + ' (Pasted)',
        notes: notes.map(n => ({
          id: 'note_' + Math.random().toString(36).substr(2, 9),
          pitch: n.pitch,
          start_beat: n.start_beat,
          duration_beats: n.duration_beats,
          velocity: n.velocity || 0.8
        }))
      };
      
      if (targetTrack) {
        updateActiveTracks(p => p.map(t => {
          if (t.id === targetTrackId) {
            const existingMidi = t.midiItems || [];
            return {
              ...t,
              midiItems: [...existingMidi, newMidiItem]
            };
          }
          return t;
        }));
        setCurrentTime(pasteTime);
        showToast('Đã dán MIDI vào track.', 'success');
        return targetTrackId;
      } else {
        const colors = ['#0f766e', '#1d4ed8', '#701a75', '#a21caf', '#b45309'];
        const rearrangeNewId = 'track_pasted_midi_' + Date.now();
        updateActiveTracks(prev => [...prev, {
          id: rearrangeNewId,
          name: `Pasted_${name || 'MIDI'}`,
          buffer: null,
          startTime: 0,
          clips: [],
          midiItems: [newMidiItem],
          volumeDb: 0,
          pan: 0,
          muted: false,
          solo: false,
          color: color || colors[prev.length % colors.length],
          markers: [],
          serverFileId: null
        }]);
        setSelectedTrackId(rearrangeNewId);
        setCurrentTime(pasteTime);
        showToast('Đã dán track MIDI mới từ clipboard.', 'success');
        return rearrangeNewId;
      }
    }
    
    // Default Audio Clip Paste
    if (!clip.buffer) {
      showToast('Clipboard không chứa dữ liệu âm thanh hợp lệ.', 'warning');
      return null;
    }
    const {
      buffer: clipBuffer,
      name,
      volumeDb,
      pan,
      color,
      sampleRate,
      channels,
      speed
    } = clip;
    const ctx = getAudioContext();
    const targetTrack = activeTracks.find(t => t.id === targetTrackId);
    const newClip = {
      id: nextClipId(),
      startTime: pasteTime,
      buffer: clipBuffer,
      name: (name || 'Pasted Clip').replace(/\.\w+$/, '') + ' (Pasted)',
      ...(volumeDb !== undefined ? { volumeDb } : {}),
      ...(pan !== undefined ? { pan } : {}),
      ...(color ? { color } : {}),
      ...(speed !== undefined ? { speed } : {})
    };
    if (targetTrack) {
      updateActiveTracks(p => p.map(t => {
        if (t.id === targetTrackId) {
          const existingClips = t.clips && t.clips.length > 0 ? t.clips : t.buffer ? [{
            id: 'default_' + t.id,
            buffer: t.buffer,
            startTime: t.startTime || 0,
            name: t.name
          }] : [];
          const updatedClips = [...existingClips, newClip];
          return {
            ...t,
            clips: updatedClips,
            buffer: updatedClips[0].buffer,
            startTime: updatedClips[0].startTime,
            name: name || t.name,
            volumeDb: volumeDb ?? t.volumeDb,
            pan: pan ?? t.pan,
            color: color || t.color
          };
        }
        return t;
      }));
      setCurrentTime(pasteTime);
      showToast('Đã dán clip vào track.', 'success');
      return targetTrackId;
    }

    // No matching track — create a new one
    const colors = ['#0f766e', '#1d4ed8', '#701a75', '#a21caf', '#b45309'];
    const rearrangeNewId = 'track_pasted_' + Date.now();
    updateActiveTracks(prev => [...prev, {
      id: rearrangeNewId,
      name: `Pasted_${name || 'track'}`,
      buffer: clipBuffer,
      startTime: pasteTime,
      clips: [newClip],
      volumeDb: volumeDb ?? 0,
      pan: pan ?? 0,
      muted: false,
      solo: false,
      color: color || colors[prev.length % colors.length],
      markers: [],
      serverFileId: null
    }]);
    setSelectedTrackId(rearrangeNewId);
    setCurrentTime(pasteTime);
    showToast('Đã dán track mới từ clipboard.', 'success');
    return rearrangeNewId;
  };
  const handlePasteTrack = () => {
    // Paste vào TRACK của ITEM đang chọn (nếu có) — không phải track 1 mặc
    // định (user 07:55: Ctrl+V dán vào track 1 dù track khác được chọn item).
    let targetId = selectedTrackId;
    try {
      const selItems = selectedItemIdsRef.current;
      if (selItems && selItems.size > 0) {
        const firstId = selItems.values().next().value;
        const found = findItemContext(firstId);
        if (found) targetId = found.trackId;
      }
    } catch (e) {}
    // Paste tại vị trí CLICK (bên phải playhead) — không phải playhead;
    // click bên TRÁI playhead → paste tại playhead (user 08:05)
    const _clickT = lastClickTimeRef.current;
    const pasteTime = (_clickT != null && _clickT > currentTime) ? _clickT : currentTime;
    doPaste(targetId, pasteTime);
  };
  // Ref cho keydown handler (effect deps [] — closure STALE: selectedTrackId
  // render đầu = '1' → Ctrl+V luôn paste track 1 — user 08:05)
  const handlePasteTrackRef = useRef(handlePasteTrack);
  handlePasteTrackRef.current = handlePasteTrack;
  const contextMenuPaste = () => {
    const result = doPaste(contextMenu.trackId, contextMenu.time || currentTime);
    closeContextMenu();
  };
  const contextMenuMerge = () => {
    const activeTracks = tracks.filter(t => t.buffer && !t.muted);
    if (activeTracks.length < 2) {
      showToast('Cần ít nhất 2 track có dữ liệu để merge.', 'warning');
      closeContextMenu();
      return;
    }
    const ctx = getAudioContext();
    const maxDur = Math.max(...activeTracks.map(t => (t.startTime || 0) + t.buffer.duration));
    const sr = activeTracks[0].buffer.sampleRate;
    const merged = ctx.createBuffer(1, Math.ceil(maxDur * sr), sr);
    const mergedData = merged.getChannelData(0);
    activeTracks.forEach(t => {
      const data = t.buffer.getChannelData(0);
      const startSample = Math.floor((t.startTime || 0) * sr);
      const volLinear = (t.volumeDb ?? 0) <= -50 ? 0 : Math.pow(10, (t.volumeDb ?? 0) / 20);
      for (let i = 0; i < data.length; i++) {
        if (startSample + i < mergedData.length) {
          mergedData[startSample + i] += data[i] * volLinear;
        }
      }
    });
    let maxPeak = 0;
    for (let i = 0; i < mergedData.length; i++) {
      const abs = Math.abs(mergedData[i]);
      if (abs > maxPeak) maxPeak = abs;
    }
    if (maxPeak > 1.0) {
      for (let i = 0; i < mergedData.length; i++) mergedData[i] /= maxPeak;
    }
    const colors = ['#0f766e', '#1d4ed8', '#701a75', '#a21caf', '#b45309'];
    const rearrangeNewId = 'track_merged_' + Date.now();
    const names = activeTracks.map(t => t.name).join('+').slice(0, 30);
    setTracks(prev => [...prev, {
      id: rearrangeNewId,
      name: `Merged_${names}.wav`,
      buffer: merged,
      startTime: 0,
      volumeDb: 0,
      pan: 0,
      muted: false,
      solo: false,
      color: colors[prev.length % colors.length],
      markers: [],
      serverFileId: null
    }]);
    setSelectedTrackId(rearrangeNewId);
    closeContextMenu();
    showToast(`Đã merge ${activeTracks.length} tracks.`, 'success');
  };

  // Menu bar direct handlers (don't rely on contextMenu state)
  const handleMergeTracks = () => {
    const at = tracks.filter(t => t.buffer && !t.muted);
    if (at.length < 2) {
      showToast('Cần 2+ tracks để merge.', 'warning');
      return;
    }
    const actx = getAudioContext();
    const maxDur = Math.max(...at.map(t => (t.startTime || 0) + t.buffer.duration));
    const sr = at[0].buffer.sampleRate;
    const mb = actx.createBuffer(1, Math.ceil(maxDur * sr), sr);
    const mdata = mb.getChannelData(0);
    at.forEach(t => {
      const d = t.buffer.getChannelData(0);
      const startSample = Math.floor((t.startTime || 0) * sr);
      const volLinear = (t.volumeDb ?? 0) <= -50 ? 0 : Math.pow(10, (t.volumeDb ?? 0) / 20);
      for (let i = 0; i < d.length; i++) {
        if (startSample + i < mdata.length) {
          mdata[startSample + i] += d[i] * volLinear;
        }
      }
    });
    let mp = 0;
    for (let i = 0; i < mdata.length; i++) {
      const a = Math.abs(mdata[i]);
      if (a > mp) mp = a;
    }
    if (mp > 1.0) for (let i = 0; i < mdata.length; i++) mdata[i] /= mp;
    setTracks(p => [...p, {
      id: 'merged_' + Date.now(),
      name: 'Merged_mix.wav',
      buffer: mb,
      startTime: 0,
      volumeDb: 0,
      pan: 0,
      muted: false,
      solo: false,
      color: ['#0f766e', '#1d4ed8'][p.length % 2],
      markers: [],
      serverFileId: null
    }]);
    showToast('Merged all unmuted tracks.', 'success');
  };
  const handleCopyTrack = () => {
    const t = activeTracks.find(x => x.id === selectedTrackId);
    if (!t || !t.buffer) return;
    const sr = t.buffer.sampleRate;
    const data = t.buffer.getChannelData(0);
    // If selection exists, copy only the selected region
    if (selLeft !== null && selRight !== null && selRight > selLeft) {
      const trackStart = t.startTime || 0;
      const relSelLeft = Math.max(0, selLeft - trackStart);
      const relSelRight = Math.max(0, selRight - trackStart);
      const startSample = Math.floor(relSelLeft * sr);
      const endSample = Math.min(data.length, Math.floor(relSelRight * sr));
      const len = endSample - startSample;
      if (len > 0) {
        const ctx = getAudioContext();
        const clipBuffer = ctx.createBuffer(1, len, sr);
        clipBuffer.copyToChannel(data.subarray(startSample, endSample), 0);
        clipboardRef.current = {
          buffer: clipBuffer,
          name: t.name,
          volumeDb: t.volumeDb,
          color: t.color
        };
        showToast('Copied selection to clipboard.', 'info');
        return;
      }
    }
    // No selection: copy entire track
    clipboardRef.current = {
      buffer: t.buffer,
      name: t.name,
      volumeDb: t.volumeDb,
      pan: t.pan,
      color: t.color,
      sampleRate: t.buffer.sampleRate,
      channels: t.buffer.numberOfChannels,
      speed: t.speed || 1.0
    };
    showToast('Copied track to clipboard.', 'info');
  };
  const handleCutTrack = () => {
    const t = activeTracks.find(x => x.id === selectedTrackId);
    if (!t || !t.buffer) return;
    const beforeSnap = captureTrackSnapshot(selectedTrackId);
    const sr = t.buffer.sampleRate;
    const data = t.buffer.getChannelData(0);
    // If selection exists, cut only the selected region
    if (selLeft !== null && selRight !== null && selRight > selLeft) {
      const trackStart = t.startTime || 0;
      const relSelLeft = Math.max(0, selLeft - trackStart);
      const relSelRight = Math.max(0, selRight - trackStart);
      const startSample = Math.floor(relSelLeft * sr);
      const endSample = Math.min(data.length, Math.floor(relSelRight * sr));
      const len = endSample - startSample;
      if (len > 0) {
        // Copy selection to clipboard
        const ctx = getAudioContext();
        const clipBuffer = ctx.createBuffer(1, len, sr);
        clipBuffer.copyToChannel(data.subarray(startSample, endSample), 0);
        clipboardRef.current = {
          buffer: clipBuffer,
          name: t.name,
          volumeDb: t.volumeDb,
          color: t.color
        };
        // Remove selection from track, glue the two remaining parts
        const newLen = data.length - len;
        const newBuffer = ctx.createBuffer(1, newLen, sr);
        const newData = newBuffer.getChannelData(0);
        let idx = 0;
        for (let i = 0; i < startSample; i++) newData[idx++] = data[i];
        for (let i = endSample; i < data.length; i++) newData[idx++] = data[i];
        updateActiveTracks(p => p.map(tr => tr.id === selectedTrackId ? {
          ...tr,
          buffer: newBuffer
        } : tr));
        const afterSnap = captureTrackSnapshot(selectedTrackId);
        pushAction('CUT', selectedTrackId, beforeSnap, afterSnap);
        showToast('Cut selection to clipboard.', 'info');
        return;
      }
    }
    // No selection: cut entire track (copy + delete)
    handleCopyTrack();
    handleDeleteTrack();
  };
  const handleDeleteTrack = () => {
    const tid = selectedTrackId;
    const sessionTab = sessionTabs.find(s => s.id === activeTab);
    if (sessionTab) {
      const trackData = activeTracks.find(t => t.id === tid);
      if (trackData) deleteTrackWithUndo(tid, JSON.parse(JSON.stringify(trackData)));
      updateActiveTracks(prev => prev.filter(t => t.id !== tid));
      setSelectedTrackId(activeTracks.filter(t => t.id !== tid)[0]?.id || '1');
    } else {
      const curTracks = tracks;
      const track = curTracks.find(t => t.id === tid);
      if (track && track.sections && track.sections.length > 0) {
        showToast('Không thể xoá track chứa Section item.', 'warning');
        return;
      }
      if (track) deleteTrackWithUndo(tid, JSON.parse(JSON.stringify(track)));
      setTracks(p => p.filter(t => t.id !== tid));
      setSelectedTrackId(curTracks.filter(t => t.id !== tid)[0]?.id || '1');
    }
    showToast('Deleted track.', 'info');
  };
  handleDeleteTrackRef.current = handleDeleteTrack;

  // ── Server Health Check ──
  const [viewportWidth, setViewportWidth] = useState(1200);
  useEffect(() => {
    if (!timelineWrapperNode) return;
    const observer = new ResizeObserver(entries => {
      for (let entry of entries) {
        setViewportWidth(entry.contentRect.width);
      }
    });
    observer.observe(timelineWrapperNode);
    return () => observer.disconnect();
  }, [timelineWrapperNode]);
  useEffect(() => {
    fetch(API_BASE_URL).then(r => {
      if (r.ok) setServerStatus('connected'); else setServerStatus('error');
    }).catch(() => setServerStatus('offline'));
  }, []);

  // ── Computed Values ──
  const leadInMargin = 0;
  const leadInMarginRef = useRef(0);
  leadInMarginRef.current = 0;
  const maxDuration = useMemo(() => {
    const secPerBar = (60.0 / (parseInt(bpm) || 120)) * 4;
    // MAIN SESSION: endtime của items trên track MAIN (mặc kệ SECTION-TAB dài bao nhiêu —
    // sub-track items bị clamp trong section bounds khi play trong main session).
    // SECTION-TAB: endtime của items trên track CỦA TAB (tính theo vị trí section trên main).
    let max;
    if (activeTab === 'main') {
      max = computeMainSessionEndTime(activeTracks);
    } else {
      const tab = sessionTabs.find(st => st.id === activeTab);
      const tabMax = tab ? computeMainSessionEndTime(tab.tracks || []) : 0;
      let secStart = 0;
      const secRef = tab ? tab.sectionId : null;
      if (secRef) {
        activeTracks.forEach(t => (t.sections || []).forEach(s => {
          if (s.sectionId === secRef || s.id === secRef) secStart = s.start || 0;
        }));
      }
      max = secStart + tabMax;
    }
    if (recordingState === 'RECORDING' || recordingState === 'COUNT_IN') {
      max = Math.max(max, currentTime + 60);
    }
    max += secPerBar * 12 + scrollBufferExtra;
    return max;
  }, [activeTab, activeTracks, sessionTabs, recordingState, currentTime, bpm, scrollBufferExtra]);
  const maxDurationRef = useRef(maxDuration);
  maxDurationRef.current = maxDuration;
  // ── Project REAL end-time (KHÔNG buffer) ──
  // maxDuration ở trên cộng 12 bars + scrollBuffer cho vùng SCROLL/ZOOM; nếu
  // dùng nó làm điểm dừng LOOP thì loop kéo dài quá duration thật. projectEnd
  // = endtime của session (main: items trên track main; section-tab: secStart +
  // items của tab) — dùng cho updatePlayhead dừng/loop lại đúng cuối bài.
  const projectEnd = useMemo(() => {
    let max;
    if (activeTab === 'main') {
      max = computeMainSessionEndTime(activeTracks);
      // Content-min CHỈ khi nút LOOP BẬT (user: projectEnd chỉ kích hoạt khi
      // LOOP bật — play loop liên tục tại content end): content NGẮN hơn item
      // → loop ngắn đúng content; content DÀI hơn item → loop tại item end.
      // LOOP TẮT → play 1 lần tới END ITEMS (computeMainSessionEndTime —
      // không cắt sớm theo content).
      if (isLoopingSelection) {
        (activeTracks || []).forEach(_t => (_t.sections || []).forEach(_s => {
          const _tab = sessionTabs.find(st => st.sectionId === _s.sectionId || st.sectionId === _s.id);
          let _contentEnd = 0;
          if (_tab) {
            (_tab.tracks || []).forEach(_tr => {
              (_tr.clips || []).forEach(_c => {
                const _e = (_c.startTime || 0) + (_c.buffer ? _c.buffer.duration / (_c.speed || 1.0) : 4);
                if (_e > _contentEnd) _contentEnd = _e;
              });
              (_tr.midiItems || []).forEach(_m => {
                const _e = (_m.startTime || 0) + (_m.duration || 4);
                if (_e > _contentEnd) _contentEnd = _e;
              });
            });
          }
          if (_contentEnd > 0) {
            const _itemEnd = (_s.start || 0) + (_s.duration || 0);
            const _realEnd = (_s.start || 0) + Math.min(_s.duration || 0, _contentEnd);
            if (_realEnd < _itemEnd && max <= _itemEnd + 0.001) max = _realEnd;
          }
        }));
      }
    } else {
      const tab = sessionTabs.find(st => st.id === activeTab);
      const tabMax = tab ? computeMainSessionEndTime(tab.tracks || []) : 0;
      let secStart = 0;
      const secRef = tab ? tab.sectionId : null;
      if (secRef) {
        activeTracks.forEach(t => (t.sections || []).forEach(s => {
          if (s.sectionId === secRef || s.id === secRef) secStart = s.start || 0;
        }));
      }
      max = secStart + tabMax;
    }
    if (recordingState === 'RECORDING' || recordingState === 'COUNT_IN') {
      max = Math.max(max, currentTime + 60);
    }
    return Math.max(1, max);
  }, [activeTab, activeTracks, sessionTabs, recordingState, currentTime, isLoopingSelection]);
  const projectEndRef = useRef(projectEnd);
  projectEndRef.current = projectEnd;
  const minZoom = useMemo(() => {
    return viewportWidth / maxDuration;
  }, [viewportWidth, maxDuration]);
  const timelineWidth = useMemo(() => {
    return Math.max(zoom * (maxDuration + leadInMargin), viewportWidth);
  }, [zoom, maxDuration, viewportWidth, leadInMargin]);
  useEffect(() => {
    // ⚠️ KHÔNG ép minZoom khi activeTab là SUB-TAB (PIANO ROLL/audio):
    // maxDuration tính theo activeTab=sub-tab → chỉ còn 12 bars buffer →
    // minZoom LỚN → ép zoom lên (log: vào midi_ → ép 32.78→37.54) → quay lại
    // MAIN/SECTION zoom giữ giá trị bị ép (KHÔNG khôi phục) = "zoom reset khi
    // chuyển tab". Timeline không hiển thị ở sub-tab → ép vô nghĩa.
    const _tabNow = activeTabRef.current;
    if (_tabNow !== 'main' && !(_tabNow && _tabNow.startsWith('session_'))) return;
    if (zoom < minZoom) {
      console.log('[Zoom] ép lên minZoom=', minZoom.toFixed(2), '(zoom=', zoom.toFixed(2), ') tab=', activeTab, 'maxDuration=', maxDuration.toFixed(2));
      setZoom(minZoom);
    }
  }, [minZoom]);
  const playheadLeftPos = useMemo(() => (currentTime + leadInMargin) * zoom, [currentTime, zoom, leadInMargin]);
  const selLeft = useMemo(() => {
    if (selectionMode === 'local' && localSelectionStart !== null && localSelectionEnd !== null) {
      return Math.max(0, Math.min(localSelectionStart, localSelectionEnd));
    }
    if (selectionStart === null || selectionEnd === null) return null;
    return Math.max(0, Math.min(selectionStart, selectionEnd));
  }, [selectionStart, selectionEnd, selectionMode, localSelectionStart, localSelectionEnd]);
  const selRight = useMemo(() => {
    if (selectionMode === 'local' && localSelectionStart !== null && localSelectionEnd !== null) {
      return Math.max(0, Math.max(localSelectionStart, localSelectionEnd));
    }
    if (selectionStart === null || selectionEnd === null) return null;
    return Math.max(0, Math.max(selectionStart, selectionEnd));
  }, [selectionStart, selectionEnd, selectionMode, localSelectionStart, localSelectionEnd]);

  const dspSelectionStats = useMemo(() => {
    const track = tracks.find(t => t.id === selectedTrackId);
    if (!track) return null;
    const numChannels = track.buffer ? (track.buffer.numberOfChannels || 1) : 0;
    if (selLeft === null || selRight === null || selRight <= selLeft || !track.buffer) {
      return {
        trackName: track.name,
        channels: numChannels,
        timeRange: 'Chưa chọn vùng',
        peakVolume: 'N/A'
      };
    }
    const buffer = track.buffer;
    const sampleRate = buffer.sampleRate;
    const startSample = Math.max(0, Math.min(buffer.length - 1, Math.floor(selLeft * sampleRate)));
    const endSample = Math.max(0, Math.min(buffer.length, Math.floor(selRight * sampleRate)));
    let maxVal = 0;
    for (let c = 0; c < numChannels; c++) {
      const data = buffer.getChannelData(c);
      for (let i = startSample; i < endSample; i++) {
        const val = Math.abs(data[i]);
        if (val > maxVal) maxVal = val;
      }
    }
    let peakDb = 'N/A';
    if (maxVal > 0) {
      const db = 20 * Math.log10(maxVal);
      peakDb = db.toFixed(2) + ' dB';
    } else {
      peakDb = '-∞ dB';
    }
    return {
      trackName: track.name,
      channels: numChannels,
      timeRange: `${selLeft.toFixed(2)}s - ${selRight.toFixed(2)}s (${(selRight - selLeft).toFixed(2)}s)`,
      peakVolume: peakDb
    };
  }, [tracks, selectedTrackId, selLeft, selRight]);

  // ── Toast helper ──
  const showToast = (text, type = 'info', actionText = null, onActionClick = null) => {
    if (toastTimeoutRef.current) clearTimeout(toastTimeoutRef.current);
    setToastMessage({
      text,
      type,
      actionText,
      onActionClick
    });
    toastTimeoutRef.current = setTimeout(() => setToastMessage(null), actionText ? 8000 : 3500);
  };
  window.showToast = showToast;

  // ── Server-side upload ──
  // WAV encoder tối thiểu — upload buffer-only clip khi LƯU (nếu chưa có
  // serverFileId — upload sớm thất bại/race → clip mất sau reload).
  const encodeWavBlob = (audioBuffer) => {
    const numCh = Math.max(1, audioBuffer.numberOfChannels || 1);
    const sr = audioBuffer.sampleRate || 44100;
    const len = audioBuffer.length;
    const interleaved = new Float32Array(len * numCh);
    for (let ch = 0; ch < numCh; ch++) {
      const data = audioBuffer.getChannelData(ch);
      for (let i = 0; i < len; i++) interleaved[i * numCh + ch] = data[i];
    }
    const buffer = new ArrayBuffer(44 + interleaved.length * 2);
    const view = new DataView(buffer);
    const writeStr = (off, s) => { for (let i = 0; i < s.length; i++) view.setUint8(off + i, s.charCodeAt(i)); };
    writeStr(0, 'RIFF'); view.setUint32(4, 36 + interleaved.length * 2, true); writeStr(8, 'WAVE');
    writeStr(12, 'fmt '); view.setUint32(16, 16, true); view.setUint16(20, 1, true);
    view.setUint16(22, numCh, true); view.setUint32(24, sr, true);
    view.setUint32(28, sr * numCh * 2, true); view.setUint16(32, numCh * 2, true); view.setUint16(34, 16, true);
    writeStr(36, 'data'); view.setUint32(40, interleaved.length * 2, true);
    for (let i = 0; i < interleaved.length; i++) {
      const s = Math.max(-1, Math.min(1, interleaved[i]));
      view.setInt16(44 + i * 2, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
    }
    return new Blob([buffer], { type: 'audio/wav' });
  };
  const uploadToServer = async (file, trackId) => {
    const formData = new FormData();
    formData.append('file', file);
    try {
      const resp = await fetch(`${API_AUDIO}/upload`, {
        method: 'POST',
        body: formData
      });
      if (!resp.ok) throw new Error(`Upload failed: ${resp.status}`);
      const data = await resp.json();
      serverFileIdMap[trackId] = data.file_id;
      return data;
    } catch (err) {
      console.warn('Server upload failed, using client-side only:', err.message);
      return null;
    }
  };

  // ── Server-side waveform loading ──
  const loadServerWaveform = async fileId => {
    try {
      const resp = await fetch(`${API_AUDIO}/waveform/${fileId}?num_peaks=800`);
      if (!resp.ok) return null;
      return await resp.json();
    } catch {
      return null;
    }
  };

  // ── Check Celery task result ──
  const pollTaskResult = async (taskId, maxPoll = 10) => {
    for (let i = 0; i < maxPoll; i++) {
      await new Promise(r => setTimeout(r, 1500));
      try {
        const resp = await fetch(`${API_TASKS}/${taskId}`);
        if (!resp.ok) continue;
        const data = await resp.json();
        if (data.status === 'SUCCESS') return data.result;
        if (data.status === 'FAILURE') throw new Error(data.error || 'Task failed');
      } catch (err) {
        throw err;
      }
    }
    throw new Error('Task polling timeout');
  };

  // ── Wheel Zoom ──
  useEffect(() => {
    const timeline = timelineWrapperRef.current;
    if (!timeline) return;
    const handleWheel = e => {
      if (e.ctrlKey || e.metaKey) {
        e.preventDefault();
        const zoomFactor = e.deltaY > 0 ? 0.9 : 1.1;
        setZoom(prevZoom => {
          let newZoom = prevZoom * zoomFactor;
          if (newZoom < minZoom) newZoom = minZoom;
          if (newZoom > 50000) newZoom = 50000;
          requestAnimationFrame(() => {
            const centerTime = currentTimeRef.current;
            const newCenterX = centerTime * newZoom;
            const viewportWidth = timeline.clientWidth;
            timeline.scrollLeft = newCenterX - viewportWidth / 2;
          });
          return newZoom;
        });
      } else if (e.shiftKey) {
        e.preventDefault();
        timeline.scrollLeft += e.deltaY;
      }
    };
    timeline.addEventListener('wheel', handleWheel, {
      passive: false
    });
    return () => timeline.removeEventListener('wheel', handleWheel);
  }, [minZoom]);

  // ── Update Playhead ──
  const startSubTabPlayback = (st, offsetWallTime) => {
    const context = getAudioContext();
    if (!st.buffer) {
      st.buffer = context.createBuffer(1, 128, context.sampleRate);
    }
    const speed = st.speed || 1.0;
    const offsetBuffer = offsetWallTime * speed;
    const eff = st.buffer.getChannelData(0);
    const edBuffer = context.createBuffer(1, eff.length, st.buffer.sampleRate);
    const edData = edBuffer.getChannelData(0);
    edData.set(eff);
    const fx = st.effects || {};
    if (fx.reverse) {
      const reversed = new Float32Array(edData);
      for (let i = 0; i < edData.length; i++) reversed[i] = edData[edData.length - 1 - i];
      edBuffer.copyToChannel(reversed, 0);
    }
    if (fx.gainDb !== 0) {
      const gain = Math.pow(10, fx.gainDb / 20);
      for (let i = 0; i < edData.length; i++) edData[i] = Math.max(-1, Math.min(1, edData[i] * gain));
    }
    if (fx.fadeInMs > 0) {
      const sr = edBuffer.sampleRate;
      const fadeSamples = Math.min(edData.length, Math.floor(fx.fadeInMs / 1000 * sr));
      for (let i = 0; i < fadeSamples; i++) edData[i] = edData[i] * (i / fadeSamples);
    }
    if (fx.fadeOutMs > 0) {
      const sr = edBuffer.sampleRate;
      const fadeSamples = Math.min(edData.length, Math.floor(fx.fadeOutMs / 1000 * sr));
      for (let i = edData.length - fadeSamples; i < edData.length; i++) {
        edData[i] = edData[i] * ((edData.length - 1 - i) / fadeSamples);
      }
    }
    const source = context.createBufferSource();
    source.buffer = edBuffer;
    source.playbackRate.value = speed;
    activePlaybackSpeedRef.current = speed;

    // Graph Editor Automation Node Chain (15_GRAPH_EDIT.md §3)
    // source → volumeGainNode → pannerNode → fadeGainNode → destination

    const volumeGainNode = context.createGain();
    volumeGainNode.gain.setValueAtTime(1.0, context.currentTime);
    const pannerNode = context.createStereoPanner();
    pannerNode.pan.setValueAtTime(0.0, context.currentTime);
    const fadeGainNode = context.createGain();
    fadeGainNode.gain.setValueAtTime(1.0, context.currentTime);

    // Schedule volume automation nodes
    const volNodes = st.volumeNodes || [];
    if (volNodes.length > 0) {
      volumeGainNode.gain.cancelScheduledValues(context.currentTime);
      volNodes.forEach((n, i) => {
        const t = context.currentTime + n.time / speed;
        const linearGain = Math.pow(10, n.db / 20);
        if (i === 0) volumeGainNode.gain.setValueAtTime(linearGain, t); else volumeGainNode.gain.linearRampToValueAtTime(linearGain, t);
      });
    }

    // Schedule panning automation nodes
    const panNodes = st.panningNodes || [];
    if (panNodes.length > 0) {
      pannerNode.pan.cancelScheduledValues(context.currentTime);
      panNodes.forEach((n, i) => {
        const t = context.currentTime + n.time / speed;
        const clamped = Math.max(-1, Math.min(1, n.pan));
        if (i === 0) pannerNode.pan.setValueAtTime(clamped, t); else pannerNode.pan.linearRampToValueAtTime(clamped, t);
      });
    }

    // Schedule fade curves
    const duration = st.buffer.duration;
    const fIn = st.fadeInLen || 0;
    const fOut = st.fadeOutLen || 0;
    if (fIn > 0) {
      fadeGainNode.gain.setValueAtTime(0.0, context.currentTime);
      fadeGainNode.gain.linearRampToValueAtTime(1.0, context.currentTime + fIn / speed);
    }
    if (fOut > 0) {
      const fadeOutStart = (duration - fOut) / speed;
      fadeGainNode.gain.setValueAtTime(1.0, context.currentTime + Math.max(0, fadeOutStart));
      fadeGainNode.gain.linearRampToValueAtTime(0.0, context.currentTime + duration / speed);
    }
    source.connect(volumeGainNode);
    volumeGainNode.connect(pannerNode);
    pannerNode.connect(fadeGainNode);
    // Route through mastering chain unless this track has mastering bypass ON.
    const route = createMasteringRoute(context, { masteringBypass: !!trackAudioBypassMap[st.trackId] }, masterBus);
    fadeGainNode.connect(route.routeGain);
    fadeGainNode.connect(route.dryGain);
    source.start(context.currentTime, offsetBuffer);
    activeSourcesRef.current = [source];
    // PIANO_ROLL: GIỮ track node thật trong ref (SF notes đã schedule tới nó —
    // ghi đè bằng silent node làm mất mastering route/updateSfRouting đúng).
    if (st.type !== 'PIANO_ROLL') {
      activeTrackNodesRef.current[st.trackId] = {
        gainNode: volumeGainNode,
        pannerNode,
        source
      };
    }
    // Realtime mute/solo for the newly created playback chain.
    if (window.__applyTrackMuteSolo) window.__applyTrackMuteSolo(st.trackId, null);
    startOffsetTimeRef.current = offsetWallTime;
    startBufferOffsetRef.current = offsetBuffer;
    startAudioTimeRef.current = context.currentTime;
  };
  const playMetronomeClick = (time, isDownbeat = false) => {
    try {
      const ctx = getAudioContext();
      const osc = ctx.createOscillator();
      const gainNode = ctx.createGain();
      osc.connect(gainNode);
      gainNode.connect(masterBus ? masterBus.input : ctx.destination);

      osc.frequency.setValueAtTime(isDownbeat ? 1000 : 800, time);
      gainNode.gain.setValueAtTime(0.08, time);
      gainNode.gain.exponentialRampToValueAtTime(0.001, time + 0.08);

      osc.start(time);
      osc.stop(time + 0.1);
    } catch (e) {
      console.log('Metronome click play error:', e);
    }
  };

  // Realtime re-schedule tracking (loop play cập nhật khi items đổi vị trí)
  const scheduledItemsSigRef = React.useRef('');
  const playheadFrameCountRef = React.useRef(0);
  const pendingRescheduleRef = React.useRef(null);
  // Master-silence watchdog counter: đếm frame im lặng liên tục khi đang play
  const masterSilenceFramesRef = React.useRef(0);
  // Cooldown rebuild (ms) — phòng watchdog trigger lặp trong đoạn im tự nhiên
  const lastMasterRebuildTimeRef = React.useRef(0);
  const lastRescheduleTimeRef = React.useRef(0);
  // Re-schedule NGAY (reset cooldown) sau khi THẢ chuột — check kế tiếp trong
  // updatePlayhead (~100ms) re-schedule luôn.
  const triggerRescheduleNow = () => {
    try {
      lastRescheduleTimeRef.current = 0;
    } catch (e) { }
  };

  const updatePlayhead = () => {
    // D9: playhead sample counter cho bridge — C++ A11/A13 dùng để đồng bộ
    // timeline sample-accurate (scheduling theo sampleOffset).
    if (window.NativeBridgeService && window.NativeBridgeService.isBridgeConnected && (isPlaying || isPlayingRef.current)) {
      try {
        const _pctx = getAudioContext();
        const _elapsed = _pctx.currentTime - (startAudioTimeRef.current || _pctx.currentTime);
        bridgePlayheadSampleRef.current = Math.max(0, Math.floor((startOffsetTimeRef.current + _elapsed) * (_pctx.sampleRate || 44100)));
        // set_position ~mỗi giây: C++ A13 cập nhật timeline anchor, tối ưu cho
        // seek/loop (Rust pump + C++ không tự biết vị trí; JS là nguồn duy nhất).
        try {
          const _sr = _pctx.sampleRate || 44100;
          if (!window.__bridgeLastPosSent) window.__bridgeLastPosSent = 0;
          if (bridgePlayheadSampleRef.current - window.__bridgeLastPosSent > _sr) {
            window.__bridgeLastPosSent = bridgePlayheadSampleRef.current;
            window.NativeBridgeService.transport('set_position', bridgePlayheadSampleRef.current);
          }
        } catch (e2) {}
      } catch (e) {}
    }
    // Realtime re-schedule: khi đang play (loop play) mà items (vị trí/speed/
    // duration — kể cả nội dung section tab) thay đổi → dừng + schedule lại từ
    // playhead hiện tại để item mới phát đúng vị trí mới (không phát nội dung
    // cũ ở vị trí cũ). Kiểm tra ~10fps (mỗi 6 frame) để kéo item cập nhật gần
    // như realtime mà không tốn CPU mỗi frame.
    if (isPlaying && activeTabRef.current === 'main' && recordingStateRef.current !== 'RECORDING') {
      playheadFrameCountRef.current++;
      if (playheadFrameCountRef.current % 6 === 0) {
        // ⚠️ Dùng REFS (không phải state closure) — rAF loop giữ updatePlayhead
        // của render cũ nên activeTracks/sessionTabs trong closure là STALE →
        // signature không bao giờ đổi khi kéo item → không re-schedule.
        const sig = buildItemsSignature(activeTracksRef.current, sessionTabsRef.current);
        if (sig !== scheduledItemsSigRef.current) {
          // THROTTLE ~300ms: re-schedule định kỳ NGAY CẢ TRONG LÚC KÉO (clip
          // kéo đi → âm thanh cũ dừng ≤300ms, clip mới phát khi playhead tới
          // vị trí mới — realtime) mà không stop/start mỗi frame (giật). Sau
          // khi thả chuột triggerRescheduleNow reset cooldown → re-schedule
          // ở check kế tiếp (~100ms).
          const nowT = performance.now();
          if (nowT - (lastRescheduleTimeRef.current || 0) > 300) {
            lastRescheduleTimeRef.current = nowT;
            scheduledItemsSigRef.current = sig;
            const pt = currentTimeRef.current;
            stopAllPlayback();
            setIsPlaying(true);
            // Re-schedule theo ĐÚNG chế độ play hiện tại: loop local / solo chỉ
            // phát track liên quan (không phát nhầm track khác); ngược lại play
            // toàn session. Cả 2 hàm đều tính playOffset = pt − clip.startTime
            // → clip vừa kéo tới đúng playhead phát TỪ ĐẦU clip (realtime).
            const curTracks = activeTracksRef.current || activeTracks;
            const soloed = curTracks.some(t => t.solo);
            if (soloed) {
              curTracks.filter(t => t.solo).forEach(t => startLocalTrackPlayback(t.id, pt));
            } else if (selectionMode === 'local' && localSelectionTrackId) {
              startLocalTrackPlayback(localSelectionTrackId, pt);
            } else {
              startTrackPlayback(pt);
            }
          }
        } else {
          pendingRescheduleRef.current = null;
        }
      }
    }

    // Master-silence watchdog: đang play + có source ĐANG TRONG KHOẢNG PHÁT
    // (theo lịch) nhưng master output im lặng liên tục ~750ms → graph bị hỏng
    // (BiquadFilter "state is bad" — node cache dính) → rebuild nodes.
    // Áp cho main + sub-tab AUDIO. PIANO_ROLL tab KHÔNG có nguồn liên tục
    // (chỉ notes MIDI schedule rời rạc) → rest/im >750ms là TỰ NHIÊN, watchdog
    // sẽ rebuild loop + panic hủy notes chờ → CÂM TOÀN CỤC → exempt hoàn toàn
    // (các fix setValueAtTime/NaN guard đã hết "state is bad" — watchdog chỉ
    // còn là lớp cứu cuối cho main/audio-tab).
    // PIANO_ROLL: watchdog CHỈ rebuild khi output chứa NaN (chain chết —
    // state-bad). KHÔNG rebuild khi im lặng thường (rests tự nhiên giữa các
    // note — false-positive = stopAllPlayback + reschedule = glitch).
    const _anySubPlaying = subTabsRef.current.some(s => s.isPlaying);
    const activeSub = subTabsRef.current.find(s => s.id === activeTabRef.current);
    const isPianoRoll = activeSub && activeSub.type === 'PIANO_ROLL';
    // SECTION-TAB cũng chỉ rebuild khi NaN (20:00): audioclip mất/rest tự
    // nhiên → im lặng > 750ms là BÌNH THƯỜNG — pk<0.001 → recovery hủy play +
    // playhead về đầu track (đúng lỗi user: "playhead về đầu + không play").
    const isSectionTab = activeTabRef.current && activeTabRef.current.startsWith('session_');
    if ((isPlaying || _anySubPlaying) && masterBus && masterBus.analyser && (isPianoRoll || activeSourcesRef.current.length > 0)) {
      try {
        // "Đáng lẽ đang có âm" — quyết định watchdog có được rebuild không:
        // - Main / sub-tab audio: source thật đang trong khoảng phát.
        // - PIANO_ROLL: tab đang play (notes đã schedule) = đáng lẽ có âm.
        const ctxNow = getAudioContext().currentTime;
        let anyPlaying = false;
        if (isPianoRoll) {
          anyPlaying = _anySubPlaying;
        } else if (_anySubPlaying) {
          const _subs = subTabsRef.current || [];
          for (let _si = 0; _si < _subs.length; _si++) {
            const s = _subs[_si];
            if (!s.isPlaying) continue;
            if (s.buffer) {
              if (activeSourcesRef.current.some(src => typeof src.startTime === 'number' && ctxNow >= src.startTime && ctxNow <= src.startTime + (src.buffer ? src.buffer.duration : 0) + 0.1)) { anyPlaying = true; break; }
            }
          }
        } else {
          anyPlaying = activeSourcesRef.current.some(s => typeof s.startTime === 'number' && ctxNow >= s.startTime && ctxNow <= s.startTime + (s.buffer ? s.buffer.duration : 0) + 0.1);
        }
        if (anyPlaying) {
          const d = new Uint8Array(128);
          masterBus.analyser.getByteTimeDomainData(d);
          let pk = 0;
          for (let i = 0; i < d.length; i++) { const v = Math.abs(d[i] - 128) / 128; if (v > pk) pk = v; }
          // PIANO_ROLL: chain chết xuất NaN → getByteTimeDomainData đọc NaN →
          // byte 0/128 → pk CAO → mù. Check float data (chỉ piano roll — rests
          // tự nhiên thì KHÔNG rebuild).
          let nanOut = false;
          if (isPianoRoll) {
            try {
              const f = new Float32Array(128);
              masterBus.analyser.getFloatTimeDomainData(f);
              for (let i = 0; i < f.length; i++) { if (!isFinite(f[i])) { nanOut = true; break; } }
            } catch (e) {}
          }
          // PIANO_ROLL: CHỈ rebuild khi output NaN (chain chết — state-bad).
          // pk<0.001 (im lặng) KHÔNG trigger cho piano roll — rests tự nhiên
          // giữa các note > 750ms là BÌNH THƯỜNG → false-positive = recovery
          // hủy play + restart notes (glitch) — đúng chuỗi log recovery trước.
          if ((isPianoRoll || isSectionTab ? nanOut : (pk < 0.001 || nanOut))) {
            masterSilenceFramesRef.current++;
            const sinceRebuild = performance.now() - (lastMasterRebuildTimeRef.current || 0);
            // NaN = chain CHẾT chắc chắn → rebuild NGAY (3 frame ≈ 50ms).
            // pk<0.001 (im lặng nghi ngờ — main) giữ 45 frame (750ms) để loại
            // transient gap false-positive. Cooldown 3000 chống rebuild-loop.
            if (masterSilenceFramesRef.current > (nanOut ? 3 : 45) && sinceRebuild > 3000) {
              masterSilenceFramesRef.current = 0;
              lastMasterRebuildTimeRef.current = performance.now();
              console.warn('[Recovery] Master silent while sources active — rebuilding audio graph');
              try {
                const pt = currentTimeRef.current;
                stopAllPlayback();
                Object.keys(activeTrackNodesRef.current).forEach(k => { const n = activeTrackNodesRef.current[k]; try { if (n && n.gainNode && n.gainNode.disconnect) n.gainNode.disconnect(); } catch (e) {} });
                activeTrackNodesRef.current = {};
                // Tháo chain cũ khỏi destination rồi rebuild THẬT — initMasterBus
                // early-return khi masterBus còn tồn tại → recovery trước đây là
                // no-op → chain "state is bad" bị STUCK vĩnh viễn.
                try {
                  if (masterBus) {
                    try { if (masterBus.analyser) masterBus.analyser.disconnect(); } catch (e) {}
                    try { if (masterBus.output) masterBus.output.disconnect(); } catch (e) {}
                    try { if (masterBus.dryOutput) masterBus.dryOutput.disconnect(); } catch (e) {}
                  }
                  masterBus = null;
                  initMasterBus(getAudioContext());
                } catch (e) { console.warn('[Recovery] initMasterBus error:', e); }
                // Resume ĐÚNG chế độ play hiện tại (main hoặc sub-tab piano roll)
                const curTab = activeTabRef.current;
                const subSt = subTabsRef.current.find(s => s.id === curTab);
                if (subSt && (subSt.type === 'PIANO_ROLL' || subSt.buffer)) {
                  const resumeAt = subSt.currentTime || 0;
                  if (subSt.type === 'PIANO_ROLL') schedulePianoRollMidi(subSt, resumeAt);
                  startSubTabPlayback(subSt, resumeAt);
                  setSubTabs(prev => prev.map(s => s.id === curTab ? {
                    ...s,
                    buffer: s.buffer || getAudioContext().createBuffer(1, 128, getAudioContext().sampleRate),
                    isPlaying: true,
                    currentTime: resumeAt
                  } : s));
                  animationFrameIdRef.current = requestAnimationFrame(updatePlayhead);
                } else {
                  setIsPlaying(true);
                  startTrackPlayback(pt);
                }
              } catch (e) { console.warn('[Recovery] rebuild error:', e); }
            }
          } else {
            masterSilenceFramesRef.current = 0;
          }
        } else {
          masterSilenceFramesRef.current = 0;
        }
      } catch (e) { masterSilenceFramesRef.current = 0; }
    } else {
      masterSilenceFramesRef.current = 0;
    }

    if (recordingStateRef.current === 'RECORDING') {
      const audioCtx = getAudioContext();
      const lookahead = 0.1; // 100ms
      const secondsPerBeat = 60.0 / (parseInt(bpmRef.current) || 120);

      // Metronome Click Scheduler
      while (true) {
        const beatNum = nextMetronomeBeatRef.current;
        const elapsedBeats = beatNum - (recordingStartTimeRef.current / secondsPerBeat);
        const beatTime = startAudioTimeRef.current + elapsedBeats * secondsPerBeat;

        if (beatTime < audioCtx.currentTime + lookahead) {
          const isDownbeat = (beatNum % 4 === 0);
          playMetronomeClick(beatTime, isDownbeat);
          nextMetronomeBeatRef.current++;
        } else {
          break;
        }
      }

      // Throttled Live Preview compilation (every 200ms)
      const now = Date.now();
      if (now - lastTempCompileTimeRef.current > 200) {
        lastTempCompileTimeRef.current = now;

        // Audio preview
        for (let trackId in recordingPCMDataRef.current) {
          const data = recordingPCMDataRef.current[trackId];
          if (data && data.length > 0) {
            const tempBuf = audioCtx.createBuffer(1, data.length, audioCtx.sampleRate);
            tempBuf.getChannelData(0).set(data);
            setRecTempAudioBuffer(tempBuf);
          }
        }

        // MIDI preview
        let combinedNotes = [];
        const armedTracks = activeTracksRef.current.filter(t => t.isArmed);
        for (let track of armedTracks) {
          const midiRec = activeMIDIRecordersRef.current[track.id];
          if (midiRec) {
            const currentBeat = (audioCtx.currentTime - midiRec.recStartAudioTime - midiRec.latencyCompSec) / secondsPerBeat;
            const notes = [...midiRec.recordedNotes, ...Array.from(midiRec.activeNotes.values()).map(n => ({
              ...n,
              duration_beats: currentBeat - n.start_beat
            }))];
            combinedNotes = combinedNotes.concat(notes);
          }
        }
        if (combinedNotes.length > 0 || armedTracks.some(t => activeMIDIRecordersRef.current[t.id])) {
          setRecTempMidiNotes(combinedNotes);
          setCanvasRedrawCount(n => n + 1);
        }
      }
    }

    const isSubTab = subTabsRef.current.some(sub => sub.id === activeTabRef.current);
    if (isSubTab) {
      const st = subTabsRef.current.find(s => s.id === activeTabRef.current);
      if (!st) return;
      // PIANO ROLL KHÔNG cần buffer: tab restored từ phiên trước — AudioBuffer
      // KHÔNG serialize → st.buffer undefined → return cũ làm playhead ĐỨNG IM
      // (user bug 06:15 — mở lại piano roll đã mở phiên trước khi đang play).
      // startSubTabPlayback tự tạo silent buffer khi play (18103).
      if (!st.buffer && st.type !== 'PIANO_ROLL') return;
      if (!st.isPlaying) {
        // PIANO ROLL / sub-tab KHÔNG play: playhead theo MAIN/SECTION nếu đang
        // play — ĐỒNG BỘ 2 CHIỀU: vị trí play (section/main) → vị trí TRONG
        // item (st.currentTime = playPos - item.startTime); playhead vượt ra
        // ngoài duration item → bắt đầu từ đầu (0). Không có play nào → đứng
        // yên (chế độ edit). Loop luôn tiếp tục (rAF).
        if (isPlaying) {
          const context = getAudioContext();
          const _ph = startOffsetTimeRef.current + (context.currentTime - startAudioTimeRef.current);
          let _local = _ph;
          const _itemInfo = getPianoRollItemInfo(st);
          if (_itemInfo) {
            if (_ph < _itemInfo.startTime) _local = 0;
            else if (_ph >= _itemInfo.startTime + _itemInfo.duration) _local = 0; // vượt duration item → đầu
            else _local = _ph - _itemInfo.startTime;
          }
          // Log chẩn đoán (rate-limited ~2 lần/giây): xác nhận đồng bộ MAIN/
          // SECTION ↔ PIANO ROLL — ph (vị trí play) → local (trong item).
          try {
            const _sf = (window.__syncFrame = (window.__syncFrame || 0) + 1);
            if (_sf % 45 === 0) {
              console.log('[Sync] PR', st.id, 'ph=', _ph.toFixed(2), 'item.start=', _itemInfo ? _itemInfo.startTime.toFixed(2) : '-', 'dur=', _itemInfo ? _itemInfo.duration.toFixed(2) : '-', '→ local=', _local.toFixed(2), 'tab=', activeTabRef.current);
            }
          } catch (e) { }
          setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, currentTime: _local } : s));
        }
        animationFrameIdRef.current = requestAnimationFrame(updatePlayhead);
        return;
      }
      const context = getAudioContext();
      const speedFactor = st.speed || 1.0;
      const elapsed = context.currentTime - startAudioTimeRef.current;
      const wallTime = startOffsetTimeRef.current + elapsed;
      const bufferPos = startBufferOffsetRef.current + elapsed * speedFactor;

      // Loop sub-tab selection (bufferPos is buffer-time)
      if (st.selectionStart !== null && st.selectionEnd !== null && st.selectionStart !== st.selectionEnd && st.isLooping) {
        const start = Math.min(st.selectionStart, st.selectionEnd);
        const end = Math.max(st.selectionStart, st.selectionEnd);
        if (bufferPos >= end) {
          stopAllPlayback();
          setSubTabs(prev => prev.map(s => s.id === activeTabRef.current ? {
            ...s,
            buffer: s.buffer || getAudioContext().createBuffer(1, 128, getAudioContext().sampleRate),
            currentTime: start,
            isPlaying: true
          } : s));
          schedulePianoRollMidi(st, start);
          startSubTabPlayback(st, start);
          animationFrameIdRef.current = requestAnimationFrame(updatePlayhead);
          return;
        }
      }
      const effectiveDuration = st.type === 'PIANO_ROLL' ? (() => {
        const notes = st.notes || [];
        const bpmVal = parseInt(bpmRef?.current || bpm) || 120;
        const beatSec = 60.0 / bpmVal;
        if (recordingStateRef.current === 'RECORDING') return 600.0; // 10 min during recording
        let maxEnd = 0;
        notes.forEach(n => {
          const end = (n.start_beat || 0) + (n.duration_beats || 1);
          if (end > maxEnd) maxEnd = end;
        });
        return Math.max(maxEnd * beatSec, 16 * beatSec * 4) + 1.0;
      })() : st.buffer.duration;
      if (bufferPos >= effectiveDuration) {
        stopAllPlayback();
        if (st.isLooping) {
          setSubTabs(prev => prev.map(s => s.id === activeTabRef.current ? {
            ...s,
            buffer: s.buffer || getAudioContext().createBuffer(1, 128, getAudioContext().sampleRate),
            currentTime: 0,
            isPlaying: true
          } : s));
          // Loop lại: phải schedule notes MỚI (âm piano roll mất ở lần loop 2
          // nếu chỉ chạy silent buffer).
          if (st.type === 'PIANO_ROLL') schedulePianoRollMidi(st, 0);
          startSubTabPlayback(st, 0);
          animationFrameIdRef.current = requestAnimationFrame(updatePlayhead);
        } else {
          setSubTabs(prev => prev.map(s => s.id === activeTabRef.current ? {
            ...s,
            currentTime: 0,
            isPlaying: false
          } : s));
        }
        return;
      }
      setSubTabs(prev => prev.map(s => s.id === activeTabRef.current ? {
        ...s,
        currentTime: wallTime
      } : s));
      animationFrameIdRef.current = requestAnimationFrame(updatePlayhead);
      return;
    }
    if (!isPlaying) return;
    const context = getAudioContext();
    const elapsed = context.currentTime - startAudioTimeRef.current;
    const updatedTime = startOffsetTimeRef.current + elapsed;

    // Selection Loop - LOOP_MAKER.md + LOOP_EDITOR_2.md §4.2
    // If selection cleared by user, play linearly (don't loop)
    if (!selectionCleared && isLoopingSelection && selLeft !== null && selRight !== null) {
      if (selRight > selLeft && updatedTime >= selRight) {
        if (hasAnySolo || selectionMode === 'local') {
          stopAllPlayback();
          startOffsetTimeRef.current = selLeft;
          startAudioTimeRef.current = context.currentTime;
          if (hasAnySolo) {
            const soloed = tracks.filter(t => t.solo);
            soloed.forEach(t => startLocalTrackPlayback(t.id, selLeft));
          } else {
            startLocalTrackPlayback(localSelectionTrackId, selLeft);
          }
          setCurrentTime(selLeft);
          setIsPlaying(true);
        } else {
          stopAllPlayback();
          startOffsetTimeRef.current = selLeft;
          startAudioTimeRef.current = context.currentTime;
          startTrackPlayback(selLeft);
          setCurrentTime(selLeft);
          setIsPlaying(true);
        }
        animationFrameIdRef.current = requestAnimationFrame(updatePlayhead);
        return;
      }
    }
    // Hết bài: dừng/loop lại tại ENDTIME THẬT của session (projectEnd — không
    // buffer 12 bars như maxDuration dùng cho scroll/zoom).
    if (updatedTime >= projectEndRef.current) {
      console.log('[Stop] projectEnd reached — updatedTime=', updatedTime.toFixed(2), 'projectEnd=', projectEndRef.current.toFixed(2), 'isLoopingSelection=', isLoopingSelection);
      if (recordingStateRef.current === 'RECORDING') {
        setCurrentTime(updatedTime);
        animationFrameIdRef.current = requestAnimationFrame(updatePlayhead);
        return;
      }
      if (isLoopingSelection) {
        stopAllPlayback();
        startOffsetTimeRef.current = 0;
        startAudioTimeRef.current = context.currentTime;
        startTrackPlayback(0);
        setCurrentTime(0);
        setIsPlaying(true);
        animationFrameIdRef.current = requestAnimationFrame(updatePlayhead);
        return;
      }
      stopAllPlayback();
      setCurrentTime(0);
      return;
    }
    setCurrentTime(updatedTime);
    animationFrameIdRef.current = requestAnimationFrame(updatePlayhead);
  };
  useEffect(() => {
    if (isPlaying || subTabs.some(s => s.isPlaying)) {
      animationFrameIdRef.current = requestAnimationFrame(updatePlayhead);
    } else {
      cancelAnimationFrame(animationFrameIdRef.current);
    }
    return () => cancelAnimationFrame(animationFrameIdRef.current);
  }, [isPlaying, subTabs, isLoopingSelection, selLeft, selRight, selectionMode, localSelectionTrackId, selectionCleared, activeTab]);

  // ── FX Nodes ──
  const createChorusNode = (context, inputNode, outputNode) => {
    const dryGain = context.createGain();
    dryGain.gain.value = 0.6;
    const wetGain = context.createGain();
    wetGain.gain.value = 0.5;
    const delayNode = context.createDelay();
    delayNode.delayTime.value = 0.02;
    const lfo = context.createOscillator();
    lfo.type = 'sine';
    lfo.frequency.value = 1.5;
    const lfoGain = context.createGain();
    lfoGain.gain.value = 0.002;
    lfo.connect(lfoGain);
    lfoGain.connect(delayNode.delayTime);
    lfo.start();
    inputNode.connect(dryGain);
    inputNode.connect(delayNode);
    delayNode.connect(wetGain);
    dryGain.connect(outputNode);
    wetGain.connect(outputNode);
    return { stop: () => { try { lfo.stop(); } catch(e) {} } };
  };
  const createReverbNode = (context, inputNode, outputNode) => {
    const dryGain = context.createGain();
    dryGain.gain.value = 0.6;
    const wetGain = context.createGain();
    wetGain.gain.value = 0.4;
    const convolver = context.createConvolver();
    const rate = context.sampleRate;
    const len = rate * 2.0;
    const impulse = context.createBuffer(2, len, rate);
    const left = impulse.getChannelData(0);
    const right = impulse.getChannelData(1);
    for (let i = 0; i < len; i++) {
      const decay = Math.exp(-i / (rate * 0.5));
      left[i] = (Math.random() * 2 - 1) * decay;
      right[i] = (Math.random() * 2 - 1) * decay;
    }
    convolver.buffer = impulse;
    inputNode.connect(dryGain);
    inputNode.connect(convolver);
    convolver.connect(wetGain);
    dryGain.connect(outputNode);
    wetGain.connect(outputNode);
  };

  // ── Playback ──
  const getOrCreateTrackNode = (track, context) => {
    if (!track) return null;
    let node = activeTrackNodesRef.current[track.id];
    if (!node) {
      const gainNode = context.createGain();
      const volDb = track.volumeDb ?? 0;
      const volLinear = volDb <= -50 ? 0 : Math.pow(10, volDb / 20);
      gainNode.gain.setValueAtTime(volLinear, context.currentTime);
      const pannerNode = context.createStereoPanner();
      pannerNode.pan.setValueAtTime((track.pan ?? 0) / 100, context.currentTime);
      // Ensure master bus is initialized for MAIN OUT routing
      if (!masterBus) initMasterBus(context);
      
      const analyserNode = context.createAnalyser();
      analyserNode.fftSize = 2048;
      pannerNode.connect(analyserNode);
      // Wave Observer scope analysers (unified_fx_rack_panel_update.md §III.3):
      // L/R split at the context OUTPUT (post-FX, after panner) so the rack's
      // oscilloscope can render Stereo/Left/Right/Mid/Side channels.
      const scopeSplitter = context.createChannelSplitter(2);
      const scopeAnalyserL = context.createAnalyser();
      const scopeAnalyserR = context.createAnalyser();
      scopeAnalyserL.fftSize = 2048;
      scopeAnalyserR.fftSize = 2048;
      pannerNode.connect(scopeSplitter);
      scopeSplitter.connect(scopeAnalyserL, 0);
      scopeSplitter.connect(scopeAnalyserR, 1);
      // Dual mastering route: routeGain -> mastering chain (normal), dryGain ->
      // dry bus (skip mastering ONLY). BOTH tap POST-FX (analyser) so the A
      // button bypasses JUST the Mastering FX Chain — the track FX Rack chain
      // (fxEntry → modules) is controlled ONLY by the PWR button (fxActive).
      const route = createMasteringRoute(context, track, masterBus);
      analyserNode.connect(route.routeGain);
      analyserNode.connect(route.dryGain);

      let fxStopFn;
      // Track FX chain (unified_fx_rack_panel.md §II): reusable module instances
      // in series between fxEntry (from gainNode) and fxLegacyIn (into the
      // legacy chorus/reverb path). fxEntry/fxLegacyIn let rebuildTrackFxGraph
      // re-route THIS track's chain without touching other tracks or the bus.
      const fxEntry = context.createGain();
      const fxLegacyIn = context.createGain();
      gainNode.connect(fxEntry);
      const fxChain = track.fxChain || [];
      // FX Power (nút PWR): ON = mọi items con (midi/section/audioclip) qua FX
      // Rack Panel chain; OFF = bỏ toàn bộ track FX (fxChain + chorus/reverb).
      const fxEnabled = track.fxActive !== false;
      const chainMods = fxEnabled ? fxChain.filter(m => m && m.active !== false).map(m => {
        try { return createTrackFxModule(m.type || m, context, m.params); } catch (e) { return null; }
      }).filter(Boolean) : [];
      let fxChainTail = fxEntry;
      chainMods.forEach(mod => {
        fxChainTail.connect(mod.input);
        fxChainTail = mod.output;
      });
      fxChainTail.connect(fxLegacyIn);
      if (fxEnabled && track.fxType === 'chorus') {
        const fxInput = context.createGain();
        fxLegacyIn.connect(fxInput);
        const chorus = createChorusNode(context, fxInput, pannerNode);
        fxStopFn = chorus.stop;
      } else if (fxEnabled && track.fxType === 'reverb') {
        const fxInput = context.createGain();
        fxLegacyIn.connect(fxInput);
        createReverbNode(context, fxInput, pannerNode);
        fxStopFn = null;
      } else {
        fxLegacyIn.connect(pannerNode);
      }
      // Independent MIDI/soundfont FX path (user request): SF audio flows
      // gainNode → sfEntry → [own module instances] → sfOut → pan → master bus,
      // fully separate from the audio-clips chain — so the A (audio) bypass
      // NEVER affects the soundfont instrument, and the ♪ bypass never touches
      // audio clips/sections.
      let sfEntry = null, sfOut = null, sfPan = null, sfAnalyser = null, sfRouteGain = null, sfDryGain = null, sfMods = [];
      const isMidiTrack = (track.midiItems && track.midiItems.length > 0) || track.type === 'MIDI' || !!track.instrumentId;
      if (isMidiTrack) {
        sfEntry = context.createGain();
        sfOut = context.createGain();
        sfPan = context.createStereoPanner();
        sfPan.pan.setValueAtTime((track.pan ?? 0) / 100, context.currentTime);
        sfAnalyser = context.createAnalyser();
        sfAnalyser.fftSize = 2048;
        // NOTE: sfEntry is NOT fed from gainNode — the FluidSynth output is
        // routed into sfEntry directly by updateSfRouting(). Connecting
        // gainNode → sfEntry here would LEAK every audio clip/section into the
        // SF chain → masterBus.input → mastering even when bypassed (double
        // audio + still processed). sfEntry.gain mirrors the track's audible
        // gain (volume/mute/solo) instead.
        sfEntry.gain.value = 1;
        sfOut.connect(sfPan);
        // Feed the soundfont into the shared scope analysers too, so the Wave
        // Observer shows the FULL post-FX track signal: audioclips + section
        // items (pannerNode) AND midi items (sfOut) mixed.
        sfOut.connect(scopeSplitter);
        // ♪ button: bypass Mastering FX Chain for the soundfont ONLY (track FX
        // Rack modules are still applied — PWR controls those). sfRouteGain →
        // masterBus.input (mastering), sfDryGain → dry bus (skip mastering).
        const sfBypass = effMidiBypass(track);
        sfRouteGain = context.createGain();
        sfDryGain = context.createGain();
        sfRouteGain.gain.value = sfBypass ? 0 : 1;
        sfDryGain.gain.value = sfBypass ? 1 : 0;
        console.log('[Bypass] sfEntry created track', track.id, 'midiBypass=' + sfBypass, sfBypass ? 'DRY' : 'MASTERING', '| midiItems:', (track.midiItems || []).length);
        sfPan.connect(sfRouteGain);
        sfPan.connect(sfDryGain);
        sfRouteGain.connect(masterBus.input);
        sfDryGain.connect(masterBus.dryInput);
        sfOut.connect(sfAnalyser);
        const sfModsBuilt = fxEnabled ? fxChain.filter(m => m && m.active !== false).map(m => {
          try { return createTrackFxModule(m.type || m, context, m.params); } catch (e) { return null; }
        }).filter(Boolean) : [];
        sfMods = sfModsBuilt;
        let sfTail = sfEntry;
        sfMods.forEach(mod => {
          sfTail.connect(mod.input);
          sfTail = mod.output;
        });
        sfTail.connect(sfOut);
      }
      node = { gainNode, pannerNode, fxStopFn, analyserNode, route, fxEntry, fxLegacyIn, scopeAnalyserL, scopeAnalyserR, sfEntry, sfOut, sfPan, sfAnalyser, sfRouteGain, sfDryGain, fxMods: chainMods, sfMods };
      // Realtime mute/solo: apply the track's current mute/solo/volume state to
      // the fresh node so items of muted/soloed tracks start correctly.
      const trackList = (activeTracksRef.current && activeTracksRef.current.length) ? activeTracksRef.current : [track];
      const nodeAudibleGain = computeTrackAudibleGain(trackList, track);
      setTrackNodeGain(node, nodeAudibleGain);
      if (node.sfEntry) {
        try {
          const g = (typeof nodeAudibleGain === 'number' && isFinite(nodeAudibleGain) && !isNaN(nodeAudibleGain)) ? nodeAudibleGain : 1.0;
          node.sfEntry.gain.setTargetAtTime(g, context.currentTime, 0.02);
        } catch (e) {}
      }
      activeTrackNodesRef.current[track.id] = node;
      console.log('[Bypass] node created track', track.id, 'initial audioBypass=', !!trackAudioBypassMap[track.id], 'routeGain=', node.route.routeGain.gain.value, 'dryGain=', node.route.dryGain.gain.value);
      updateSfRouting();
    }
    return node.gainNode;
  };
  // Per-context audio graph reconstruction (unified_fx_rack_panel.md §III.2):
  // re-routes ONLY this track's fx chain (fxEntry → active modules → fxLegacyIn),
  // leaving other tracks and the master bus untouched. Called by the FX Rack
  // panel on add/remove/reorder/toggle/param change — live while playing.
  const rebuildTrackFxGraph = (trackId) => {
    const node = activeTrackNodesRef.current[trackId];
    if (!node || !node.fxEntry || !node.fxLegacyIn) return;
    try {
      const list = (activeTracksRef.current && activeTracksRef.current.length) ? activeTracksRef.current : tracks;
      const track = list.find(t => t.id === trackId);
      if (!track) return;
      node.fxEntry.disconnect();
      const fxEnabled = track.fxActive !== false;
      const chainMods = fxEnabled ? (track.fxChain || []).filter(m => m && m.active !== false).map(m => {
        try { return createTrackFxModule(m.type || m, getAudioContext(), m.params); } catch (e) { return null; }
      }).filter(Boolean) : [];
      let tail = node.fxEntry;
      chainMods.forEach(mod => {
        tail.connect(mod.input);
        tail = mod.output;
      });
      tail.connect(node.fxLegacyIn);
      node.fxMods = chainMods;
      // Rebuild the independent SF chain too (same modules, separate instances)
      if (node.sfEntry && node.sfOut) {
        node.sfEntry.disconnect();
        const sfMods = fxEnabled ? (track.fxChain || []).filter(m => m && m.active !== false).map(m => {
          try { return createTrackFxModule(m.type || m, getAudioContext(), m.params); } catch (e) { return null; }
        }).filter(Boolean) : [];
        node.sfMods = sfMods;
        let sfTail = node.sfEntry;
        sfMods.forEach(mod => {
          sfTail.connect(mod.input);
          sfTail = mod.output;
        });
        sfTail.connect(node.sfOut);
      }
      if (node.sfPan && getAudioContext()) {
        node.sfPan.pan.setTargetAtTime((track.pan ?? 0) / 100, getAudioContext().currentTime, 0.02);
      }
    } catch (e) {
      console.warn('rebuildTrackFxGraph error:', e);
    }
  };
  const rebuildTrackFxGraphRef = useRef(null);
  rebuildTrackFxGraphRef.current = rebuildTrackFxGraph;
  window.__rebuildTrackFxGraph = rebuildTrackFxGraph;

  // Route the SHARED FluidSynth output through the gainNode of the single
  // audible MIDI track so its FX Rack chain / fader / pan affect the soundfont
  // instrument (unified_fx_rack_panel.md + user request). With more than one
  // audible MIDI track the shared worklet cannot split per channel, so we fall
  // back to the master bus (FX not applied — CC7 still mutes correctly).
  const updateSfRouting = () => {
    try {
      const list = (activeTracksRef.current && activeTracksRef.current.length) ? activeTracksRef.current : tracks;
      // PIANO_ROLL tab đang active → ưu tiên route FluidSynth tới NODE CỦA
      // TRACK đang edit (sfEntry → FX Rack riêng, fallback gainNode) để fader/
      // pan/FX áp đúng cho notes đang nghe — kể cả khi project có >1 track MIDI
      // audible (bình thường rơi về masterBus.input, mất FX track).
      const _activeTabId = activeTabRef.current;
      const _activeSub = subTabsRef.current ? subTabsRef.current.find(s => s.id === _activeTabId) : null;
      // MAIN/SECTION đang play + đang ở SUB-TAB (piano roll/audio): KHÔNG đổi
      // SF destination — âm đang phát thuộc MAIN context (route đã set ĐÚNG
      // lúc schedule — masterBus.input — qua mastering). Đổi đích khi âm đang
      // phát gây: (a) swap _gainNode → LAG ~1s (bug 06:20); (b) route vào node
      // MAIN trùng id (midiAudible nhánh) → sfEntry/sfRouteGain theo ♪ main →
      // âm dry NHỎ/không rõ (bug 06:25 — fix 0620). Giữ nguyên route hiện tại.
      if (isPlayingRef.current && _activeTabId && _activeTabId !== 'main' && !_activeTabId.startsWith('session_')) {
        return;
      }
      // MAIN/SECTION đang play (isPlayingRef) → BỎ QUA nhánh PIANO_ROLL:
      // đổi SF destination (setOutputDestination — disconnect/reconnect
      // _gainNode FluidSynth) khi âm đang phát → âm MIDI bị NGẮT ~1s (user
      // bug 06:20 — mở lại piano roll tab restored lúc main play → lag 1s
      // rồi mới phát tiếp). Chỉ route vào node piano roll khi KHÔNG play
      // (preview tab) — handlePlayPause sub-tab tự stopAllPlayback trước.
      if (_activeSub && _activeSub.type === 'PIANO_ROLL' && !isPlayingRef.current) {
        // PIANO ROLL của track TRONG SECTION (parent_tab_id = session_*): id
        // track trùng main track id → lookup key = id trúng node MAIN (route
        // theo ♪ main — sai context → MIDI bị kéo dry). ƯU TIÊN SUB-NODE
        // (key <parent>_sub_<id> — route theo ♪ section + thẳng masterBus);
        // không có sub-node → null (masterBus.input — qua mastering).
        let _prNode = activeTrackNodesRef.current[_activeSub.trackId];
        if (_activeSub.parent_tab_id && _activeSub.parent_tab_id.startsWith('session_')) {
          const _prSubKeys = Object.keys(activeTrackNodesRef.current).filter(k => k.endsWith('_sub_' + _activeSub.trackId));
          _prNode = _prSubKeys.length ? activeTrackNodesRef.current[_prSubKeys[0]] : null;
        }
        // D5: bridge active → nối BridgeAudioNode vào node track (sfEntry →
        // FX chain → mastering) thay vì SonicSF.setOutputDestination.
        if (_prNode && window.AudioRoutingEngine && window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive() && window.BridgeAudioNode) {
          window.AudioRoutingEngine.connect(window.BridgeAudioNode, _prNode, _activeSub.trackId);
          return;
        }
        if (_prNode && window.SonicSF && window.SonicSF.setOutputDestination) {
          if (_prNode.sfEntry) {
            window.SonicSF.setOutputDestination(_prNode.sfEntry);
            // Ép route qua mastering chain NGAY tại thời điểm routing (node có
            // thể tạo khi mastering OFF → route dry — sửa ngay nếu chain ON).
            if (_prNode.sfRouteGain && _prNode.sfDryGain) {
              const _prTrk = list.find(t => t.id === _activeSub.trackId);
              const _b = effMidiBypass(_prTrk || { id: _activeSub.trackId });
              _prNode.sfRouteGain.gain.value = _b ? 0 : 1;
              _prNode.sfDryGain.gain.value = _b ? 1 : 0;
            }
            return;
          }
          if (_prNode.gainNode) {
            window.SonicSF.setOutputDestination(_prNode.gainNode);
            // ⚠️ FIX: sync route gains NGAY tại thời điểm routing — trước đây
            // bỏ qua → node giữ bypass cũ (tạo khi mastering OFF / audioBypass
            // true) → preview âm soundfont KHÔNG qua mastering cho tới khi bật/
            // tắt power (effect re-sync). MIDI preview route theo ♪ (midiBypass).
            if (_prNode.route && _prNode.route.routeGain && _prNode.route.dryGain) {
              const _prTrk2 = list.find(t => t.id === _activeSub.trackId);
              const _b2 = effMidiBypass(_prTrk2 || { id: _activeSub.trackId });
              _prNode.route.routeGain.gain.value = _b2 ? 0 : 1;
              _prNode.route.dryGain.gain.value = _b2 ? 1 : 0;
            }
            return;
          }
        }
      }
      const midiAudible = list.filter(t => ((t.midiItems && t.midiItems.length > 0) || t.isArmed || t.type === 'MIDI' || !!t.instrumentId) && computeTrackAudibleGain(list, t) > 0);
      console.log('[SFRoute] activeTab=' + _activeTabId, 'midiAudible=' + midiAudible.map(t => t.id + '(midi' + (t.midiItems || []).length + ')').join(',') || 'none');
      if (midiAudible.length === 1) {
        const t = midiAudible[0];
        // SECTION-TAB: track id của section clone TRÙNG main track id → node
        // chính (key = id) là node MAIN (route theo ♪ main — SAI context, MIDI
        // section bị kéo vào dry). ƯU TIÊN SUB-NODE (key <parent>_sub_<id> —
        // route theo ♪ section + thẳng masterBus.input) — không có sub-node →
        // null (SF về masterBus.input — qua mastering, KHÔNG bypass).
        let node = activeTrackNodesRef.current[t.id];
        if (_activeTabId && _activeTabId.startsWith('session_')) {
          const subKeys = Object.keys(activeTrackNodesRef.current).filter(k => k.endsWith('_sub_' + t.id));
          node = subKeys.length ? activeTrackNodesRef.current[subKeys[0]] : null;
        }
        // SF ALWAYS enters the track's own FX chain (sfEntry → sfModules when
        // PWR ON). The ♪ button only switches the post-FX route (sfRouteGain /
        // sfDryGain) to skip or include the Mastering FX Chain.
        // D5: bridge active → nối vào node track (sfEntry → FX chain).
        if (node && window.AudioRoutingEngine && window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive() && window.BridgeAudioNode) {
          window.AudioRoutingEngine.connect(window.BridgeAudioNode, node, t.id);
          return;
        }
        if (node && node.sfEntry && window.SonicSF && window.SonicSF.setOutputDestination) {
          console.log('[SFRoute] dest=sfEntry node', t.id, 'sfRouteGain=' + (node.sfRouteGain ? node.sfRouteGain.gain.value : 'MISSING'), 'sfDryGain=' + (node.sfDryGain ? node.sfDryGain.gain.value : 'MISSING'));
          window.SonicSF.setOutputDestination(node.sfEntry);
          // Ép route qua mastering chain NGAY tại thời điểm routing.
          if (node.sfRouteGain && node.sfDryGain) {
            // ⚠️ FIX: truyền track OBJECT (t) — trước đây truyền t.id (string)
            // → effMidiBypass đọc track.id=undefined → map lookup sai → _b luôn
            // false → sfRouteGain bị ép 1 (không bypass) — nút ♪ vô hiệu qua
            // đường này.
            const _b = effMidiBypass(t);
            node.sfRouteGain.gain.value = _b ? 0 : 1;
            node.sfDryGain.gain.value = _b ? 1 : 0;
            console.log('[Bypass] sf route sync track', t.id, 'midiBypass=' + _b, _b ? 'DRY' : 'MASTERING');
          }
          return;
        }
        if (node && node.gainNode && window.SonicSF && window.SonicSF.setOutputDestination) {
          console.log('[SFRoute] dest=gainNode node', t.id, '(no sfEntry) — route.routeGain=' + (node.route ? node.route.routeGain.gain.value : 'MISSING'), 'route.dryGain=' + (node.route ? node.route.dryGain.gain.value : 'MISSING'));
          window.SonicSF.setOutputDestination(node.gainNode);
          // ⚠️ FIX: sync route gains — node giữ bypass cũ → preview không qua
          // mastering cho tới khi bật/tắt power. MIDI preview theo ♪.
          if (node.route && node.route.routeGain && node.route.dryGain) {
            const _b3 = effMidiBypass(t);
            node.route.routeGain.gain.value = _b3 ? 0 : 1;
            node.route.dryGain.gain.value = _b3 ? 1 : 0;
          }
          return;
        }
      }
      // D5: bridge active + không track nào audible → ngắt khỏi graph (về
      // masterBus qua updateSfRouting lần sau khi có track).
      if (window.AudioRoutingEngine && window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive() && window.BridgeAudioNode) {
        window.AudioRoutingEngine.connect(window.BridgeAudioNode, null, null);
      } else if (window.SonicSF && window.SonicSF.setOutputDestination) {
        window.SonicSF.setOutputDestination(null);
      }
    } catch (e) {
      console.warn('updateSfRouting error:', e);
    }
  };
  const updateSfRoutingRef = useRef(null);
  // ── Đồng bộ mastering + routing NGAY TRƯỚC KHI PREVIEW (keybed/draw/MIDI file) ──
  // Bug: preview instrument soundfont với MIDI keyboard KHÔNG tự động đi qua
  // mastering FX của main out — phải bật/tắt nút power mới có tác dụng. Nguyên
  // nhân: routing SF (setOutputDestination + sfRouteGain/sfDryGain) và graph
  // mastering chỉ được đồng bộ khi masteringSettings ĐỔI (toggle power), không
  // chạy khi bắt đầu preview. Hàm này ép đồng bộ 3 lớp (graph theo trạng thái
  // hiện tại — early-return nếu đã đúng; tham số module; route SF + route
  // gains) — gọi tại mọi điểm preview → âm preview LUÔN qua mastering khi bật.
  const ensureMasteringRouting = () => {
    try {
      const _s = window.currentMasteringSettings;
      if (!_s) return;
      // Đảm bảo audioCtx + masterBus đã tồn tại trước khi sync (lần preview
      // đầu tiên audio chưa init → getAudioContext tạo ngay)
      getAudioContext();
      if (!masterBus || !audioCtx) return;
      toggleMasteringOnMaster(!!_s.masterConnected, !!_s.isBypassed);
      applyMasteringSettings(_s);
      updateSfRouting();
      // Force-sync MỌI node đang tồn tại (route + sf route) theo trạng thái
      // hiện tại — phòng node tạo từ trước với bypass cũ (mastering OFF lúc
      // tạo node / audioBypass default true của section) → preview LUÔN qua
      // mastering khi chain bật, không cần bật/tắt power.
      try {
        const _list2 = (activeTracksRef.current && activeTracksRef.current.length) ? activeTracksRef.current : tracks;
        _list2.forEach(_t => {
          const _n = activeTrackNodesRef.current[_t.id];
          if (!_n) return;
          if (_n.sfRouteGain && _n.sfDryGain) {
            const _mb = effMidiBypass(_t);
            _n.sfRouteGain.gain.value = _mb ? 0 : 1;
            _n.sfDryGain.gain.value = _mb ? 1 : 0;
          }
          if (_n.route && _n.route.routeGain && _n.route.dryGain) {
            const _ab = routeBypassFor(_t);
            _n.route.routeGain.gain.value = _ab ? 0 : 1;
            _n.route.dryGain.gain.value = _ab ? 1 : 0;
          }
        });
      } catch (e2) {}
    } catch (e) {
      console.warn('ensureMasteringRouting error:', e);
    }
  };
  const ensureMasteringRoutingRef = useRef(null);
  ensureMasteringRoutingRef.current = ensureMasteringRouting;
  window.__ensureMasteringRouting = ensureMasteringRouting;
  // ── MIDI preview cache (bounce nhanh offline) ──
  // Khi preview: capture tiếng đàn (SF output — pre track-FX) vào cache theo
  // track. Khi export: nếu cache đủ độ dài → render OFFLINE NHANH bằng cache
  // (qua FX Rack + Mastering như playback) — không cần chờ bounce realtime.
  const midiCacheRef = useRef({});           // trackId -> { chunks:[Float32Array], duration, sampleRate }
  const midiCaptureNodesRef = useRef({});    // trackId -> { script, node }
  const ensureMidiCapture = (track, node) => {
    if (!track || !node || !node.sfEntry) return;
    const id = track.id;
    const audioCtx = getAudioContext();
    // Play lại từ đầu → reset cache để ghi bản mới nhất
    if (currentTimeRef.current <= 0.05) {
      midiCacheRef.current[id] = { chunks: [], duration: 0, sampleRate: audioCtx.sampleRate };
    }
    if (!midiCacheRef.current[id]) midiCacheRef.current[id] = { chunks: [], duration: 0, sampleRate: audioCtx.sampleRate };
    if (midiCaptureNodesRef.current[id]) return; // đã capture
    try {
      const script = audioCtx.createScriptProcessor(4096, 2, 2);
      script.onaudioprocess = (e) => {
        const c = midiCacheRef.current[id];
        if (!c) return;
        const L = e.inputBuffer.getChannelData(0);
        const R = e.inputBuffer.getChannelData(1);
        const n = L.length;
        const chunk = new Float32Array(n * 2);
        for (let i = 0; i < n; i++) { chunk[i * 2] = L[i]; chunk[i * 2 + 1] = R[i]; }
        c.chunks.push(chunk);
        c.duration += n / c.sampleRate;
      };
      node.sfEntry.connect(script);
      script.connect(audioCtx.destination);
      midiCaptureNodesRef.current[id] = { script, node };
    } catch (e) { console.warn('midi capture err', e); }
  };
  const stopMidiCapture = () => {
    Object.keys(midiCaptureNodesRef.current).forEach(id => {
      const cap = midiCaptureNodesRef.current[id];
      try { cap.script.disconnect(); } catch (e) { }
      try { if (cap.node && cap.node.sfEntry) cap.node.sfEntry.disconnect(cap.script); } catch (e) { }
    });
    midiCaptureNodesRef.current = {};
  };
  // Build an AudioBuffer from a captured track cache (interleaved stereo).
  const midiCacheToBuffer = (trackId) => {
    const c = midiCacheRef.current[trackId];
    if (!c || !c.chunks.length) return null;
    const total = c.chunks.reduce((a, ch) => a + ch.length / 2, 0);
    if (total < 100) return null;
    const audioCtx = getAudioContext();
    const buf = audioCtx.createBuffer(2, total, c.sampleRate || audioCtx.sampleRate);
    const L = buf.getChannelData(0), R = buf.getChannelData(1);
    let off = 0;
    c.chunks.forEach(ch => {
      for (let i = 0; i < ch.length; i += 2) { L[off] = ch[i]; R[off] = ch[i + 1]; off++; }
    });
    return buf;
  };
  updateSfRoutingRef.current = updateSfRouting;
  window.__updateSfRouting = updateSfRouting;
  window.__getTrackScopeAnalysers = (trackId) => {
    const node = activeTrackNodesRef.current[trackId];
    if (!node) return null;
    // The shared scope splitter carries the FULL post-FX track signal:
    // pannerNode (audioclips + section items) mixed with sfOut (midi items).
    if (!node.scopeAnalyserL || !node.scopeAnalyserR) return null;
    return { L: node.scopeAnalyserL, R: node.scopeAnalyserR, sr: getAudioContext().sampleRate };
  };
  // UI helper: fetch a live module instance (e.g. 'eqpro') from the active track
  // node so interactive panels can drive its DSP params in realtime.
  window.__getTrackFxModule = (trackId, type) => {
    const node = activeTrackNodesRef.current[trackId];
    if (!node) return null;
    const found = (node.fxMods || []).find(m => m && m.type === type);
    return found || null;
  };
  // Same for the soundfont (MIDI) module instances — lets the EQ PRO panel drive
  // BOTH the audio-clip chain and the SF chain in realtime.
  window.__getTrackSfFxModule = (trackId, type) => {
    const node = activeTrackNodesRef.current[trackId];
    if (!node) return null;
    const found = (node.sfMods || []).find(m => m && m.type === type);
    return found || null;
  };
  const getOrCreateSubTrackNode = (track, subTrack, context) => {
    if (!track || !subTrack) return null;
    const subKey = track.id + '_sub_' + subTrack.id;
    let node = activeTrackNodesRef.current[subKey];
    if (!node) {
      const gainNode = context.createGain();
      const volDb = subTrack.volumeDb ?? 0;
      const volLinear = volDb <= -50 ? 0 : Math.pow(10, volDb / 20);
      gainNode.gain.setValueAtTime(volLinear, context.currentTime);
      const parentNode = getOrCreateTrackNode(track, context);
      // Route RIÊNG cho sub-track (nút A/♪ trên section strip): mastering path →
      // parent; dry path → masterBus.dryInput — bypass áp đúng track section.
      // Dùng map THẬT (trackAudioBypassMap/trackMidiBypassMap) — KHÔNG qua
      // effAudioBypass (override mastering-ON ép false → route luôn no-bypass
      // lúc tạo node → nút A phải bấm 2 lần; stop+play → node tái tạo → mất).
      // RULES: sub-track có CLIPS → nút A (audioBypass); có MIDI → nút ♪ (midiBypass).
      const subHasClips = (subTrack.clips && subTrack.clips.length > 0) || !!subTrack.buffer;
      const subBypass = subHasClips
        ? (subTrack.audioBypass !== undefined ? !!subTrack.audioBypass : (trackAudioBypassMap[subTrack.id] !== undefined ? !!trackAudioBypassMap[subTrack.id] : false))
        : (subTrack.midiBypass !== undefined ? !!subTrack.midiBypass : (trackMidiBypassMap[subTrack.id] !== undefined ? !!trackMidiBypassMap[subTrack.id] : false));
      const routeGain = context.createGain();
      const dryGain = context.createGain();
      routeGain.gain.value = subBypass ? 0 : 1;
      dryGain.gain.value = subBypass ? 1 : 0;
      // analyserNode cho VU meter của SECTION-TAB track strip (VU tick đọc
      // node.analyserNode — sub-node trước đây không có → VU không animation).
      const subAnalyser = context.createAnalyser();
      subAnalyser.fftSize = 256;
      subAnalyser.smoothingTimeConstant = 0.4;
      gainNode.connect(subAnalyser);
      subAnalyser.connect(routeGain);
      subAnalyser.connect(dryGain);
      // Mastering path → masterBus.input TRỰC TIẾP (không qua parent node:
      // parent có route riêng với bypass riêng — parent audio_bypass=true →
      // nối qua parent làm MIDI/audio section bị kéo vào dry bus = bypass
      // mastering dù sub-track ♪/A đang tắt).
      if (window.masterBus && window.masterBus.input) {
        routeGain.connect(window.masterBus.input);
      } else {
        routeGain.connect(parentNode);
      }
      if (window.masterBus && window.masterBus.dryInput) {
        dryGain.connect(window.masterBus.dryInput);
      } else {
        dryGain.connect(parentNode);
      }
      node = { gainNode, pannerNode: null, analyserNode: subAnalyser, route: { routeGain: routeGain, dryGain: dryGain, _trackId: subTrack.id } };
      activeTrackNodesRef.current[subKey] = node;
    }
    return node.gainNode;
  };

  const playMidiPreviewNote = (pitch, velocity = 0.8, durationMs = 500) => {
    if (!window.SonicSF) return;
    const context = getAudioContext();
    const tab = subTabs.find(s => s.id === activeTabRef.current);
    const trackId = tab ? tab.trackId : selectedTrackId;
    const track = activeTracks.find(t => t.id === trackId);
    const destNode = getOrCreateTrackNode(track, context);
    const program = track ? track.instrumentProgram : undefined;
    var prevCh = track ? assignTrackMidiChannel(track, activeTracks) : 0;
    const routeCarla = shouldRouteCarla(track && track.synth_engine);
    // D7: bridge active → preview qua bridge (channel router allocate — đúng
    // instrument đã load khi chèn vào track).
    if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) {
      const isPerc = !!(track && track.synth_engine && track.synth_engine.soundfont_bank === 128);
      const tch = track ? track.id : 0;
      try { if (window.__ensureBridgeProgram) window.__ensureBridgeProgram(tch, program, track && track.synth_engine); } catch (e) {}
      try { window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_ON', channel: tch, pitch: pitch || 60, velocity: velocity, percussion: isPerc }); } catch (e) {}
      setTimeout(function () {
        try { window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_OFF', channel: tch, pitch: pitch || 60, velocity: 0, percussion: isPerc }); } catch (e) {}
      }, (durationMs || 500) + 30);
      return;
    }
    if (isStandaloneSf() && !routeCarla && isSfTrackEngine(track && track.synth_engine)) {
      playNativeSfNote(track, pitch, velocity, durationMs, null, track ? track.id : 'global');
      return;
    }
    window.SonicSF.playNote(
      pitch,
      velocity,
      durationMs,
      context.currentTime,
      program,
      destNode,
      prevCh,
      track ? track.synth_engine : undefined
    );
  };

  // D2: timeline MIDI note -> native bridge (khi bridge active) thay vì
  // SonicSF (FluidSynth WASM). Giữ setTimeout scheduling (như SonicSF cũ);
  // bridge tự render note-off sau duration. A11 (sample-accurate) sẽ thay
  // setTimeout bằng sampleOffset đẩy thẳng vào SHM.
  const scheduleMidiNoteDispatch = (track, pitch, velocity, durMs, startTime, program, destNode, ch, synthEngine) => {
    if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) {
      const ctx = getAudioContext();
      const startAt = startTime || ctx.currentTime; // undefined/null = phát ngay
      const delayMs = Math.max(0, (startAt - ctx.currentTime) * 1000);
      // Guard chống note-on trễ sau Stop: CHỈ khi là timeline (startTime thật).
      const guardPlay = startTime != null ? function () { return isPlayingRef.current || (subTabsRef.current && subTabsRef.current.some(s => s.isPlaying)); } : function () { return true; };
      const trkId = track ? track.id : 0;
      // Percussion (bank 128) → router allocateChannel ch 9; melodic → round-robin.
      const isPerc = !!(synthEngine && synthEngine.soundfont_bank === 128);
      // D8: track đổi instrument → gửi CC0/CC32 (bank THẬT từ synthEngine) +
      // program change qua bridge (A12) — helper dedupe theo track+bank+program.
      if (program !== undefined && program !== null) {
        try { if (window.__ensureBridgeProgram) window.__ensureBridgeProgram(trkId, program, synthEngine); } catch (e) {}
      }
      setTimeout(function () {
        if (!guardPlay()) return;
        try { window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_ON', channel: trkId, pitch: pitch || 60, velocity: velocity || 0.8, percussion: isPerc }); } catch (e) {}
      }, delayMs);
      setTimeout(function () {
        if (!guardPlay()) return;
        try { window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_OFF', channel: trkId, pitch: pitch || 60, velocity: 0, percussion: isPerc }); } catch (e) {}
      }, delayMs + (durMs || 1000) + 30);
      return;
    }
    window.SonicSF.playNote(pitch, velocity, durMs, startTime, program, destNode, ch, synthEngine);
  };

  const startTrackPlayback = offsetTime => {
    const context = getAudioContext();
    // D2: bridge active -> báo transport PLAY (bridge flush note-off cũ + đồng
    // bộ timeline; A13 C++ xử lý arg1=playheadSamples sau này).
    if (window.NativeBridgeService && window.NativeBridgeService.isBridgeConnected) {
      try { window.NativeBridgeService.transport('play'); } catch (e) {}
    }
    // ⚠️ FIX: đồng bộ mastering + Carla status NGAY khi play — MIDI item phải
    // qua mastering FX (khi bật) và qua Carla bridge (khi VSTi loaded).
    try { if (window.__ensureMasteringRouting) window.__ensureMasteringRouting(); } catch (e) {}
    refreshCarlaStatus();
    const allPlayTracks = activeTracksRef.current && activeTracksRef.current.length ? activeTracksRef.current : activeTracks;
    // Capture items signature at schedule time — updatePlayhead so sánh với
    // signature này để phát hiện items đổi vị trí giữa lúc play (re-schedule).
    scheduledItemsSigRef.current = buildItemsSignature(allPlayTracks, sessionTabs);
    console.log('[Play] offset=' + offsetTime + ' tracks=' + allPlayTracks.length + ' masterBus=' + !!masterBus + ' dest=' + (context.destination ? 'ok' : 'MISSING') + ' projectEnd=' + (projectEndRef.current || 0).toFixed(2));
    const hasSolo = allPlayTracks.some(t => t.solo);
    allPlayTracks.forEach(track => {
      const isPlayable = hasSolo ? track.solo : !track.muted;
      if (!isPlayable) return;

      const gainNode = getOrCreateTrackNode(track, context);
      const pannerNode = activeTrackNodesRef.current[track.id].pannerNode;
      // Re-apply route gains theo content-type NGAY tại thời điểm play — node
      // có thể STALE (tạo từ MAIN play với track cùng id — route theo data cũ)
      // → first play sau double-click vào SECTION-TAB bị bypass. Re-apply
      // routeBypassFor(track) mỗi lần schedule sẽ hết chu kỳ "stop+play mới đúng".
      const playNode = activeTrackNodesRef.current[track.id];
      if (playNode && playNode.route && playNode.route.routeGain) {
        const rb = routeBypassFor(track);
        playNode.route.routeGain.gain.value = rb ? 0 : 1;
        playNode.route.dryGain.gain.value = rb ? 1 : 0;
        console.log('[Bypass] reapply track', track.id, 'contentType=' + (routeBypassFor._lastType ? 'midi' : 'audio'), 'bypass=' + rb, '→ routeGain=' + playNode.route.routeGain.gain.value + '/dryGain=' + playNode.route.dryGain.gain.value);
      }
      console.log('[Play] track', track.id, track.name, 'node ok:', !!(gainNode && pannerNode), 'muted:', track.muted);
      const clips = track.clips && track.clips.length > 0 ? track.clips : track.buffer ? [{
        id: 'default',
        buffer: track.buffer,
        startTime: track.startTime || 0,
        name: track.name,
        speed: track.speed || 1.0
      }] : [];
      clips.forEach(clip => {
        if (!clip.buffer) return;
        const source = context.createBufferSource();
        source.buffer = clip.buffer;
        source.playbackRate.value = clip.speed || 1.0;
        source.connect(gainNode);
        const clipStart = clip.startTime || 0;
        const clipDuration = clip.buffer.duration / (clip.speed || 1.0);
        const clipEnd = clipStart + clipDuration;
        if (offsetTime < clipStart) {
          const delay = clipStart - offsetTime;
          source.start(context.currentTime + delay, 0);
          activeSourcesRef.current.push(source);
        } else if (offsetTime < clipEnd) {
          const playOffset = offsetTime - clipStart;
          source.start(context.currentTime, playOffset * (clip.speed || 1.0));
          activeSourcesRef.current.push(source);
        }
      });

      // MIDI items playback
      const midiItems = track.midiItems || [];
      const routeCarla = shouldRouteCarla(track.synth_engine);
      const nativeSf = isStandaloneSf() && !routeCarla && isSfTrackEngine(track.synth_engine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive());
      if ((track.type === 'MIDI' || midiItems.length > 0) && (window.SonicSF || routeCarla || nativeSf)) {
        if (nativeSf) {
          midiItems.forEach(item => scheduleNativeSfItem(track, item, offsetTime, context, gainNode, bpm, { sources: activeSourcesRef.current, isActive: () => !!isPlayingRef.current }));
        } else {
        // Preview cache: capture the soundfont (pre track-FX) for fast offline export
        ensureMidiCapture(track, activeTrackNodesRef.current[track.id]);
        var trkCh = assignTrackMidiChannel(track, allPlayTracks);
        // Ensure instrument is loaded in FluidSynth
        if (window.SonicSF && track.synth_engine && track.synth_engine.type === 'soundfont' && track.synth_engine.soundfont_id) {
          window.SonicSF.selectInstrument(trkCh, track.synth_engine.soundfont_bank || 0, track.synth_engine.soundfont_program || 0, track.synth_engine.soundfont_id);
        }
        const bpmVal = parseInt(bpm) || 120;
        const secondsPerBeat = 60.0 / bpmVal;
        midiItems.forEach(item => {
          const itemEndSec = item.startTime + (item.duration || 4);
          const notes = item.notes || [];
          notes.forEach(note => {
            // note start/duration is in beats (for MIDI items)
            const noteStartSec = item.startTime + (note.start_beat || 0) * secondsPerBeat;
            if (noteStartSec >= itemEndSec) return; // skip notes outside item's visual duration
            const noteDurSec = (note.duration_beats || 1) * secondsPerBeat;
            const noteEndSec = noteStartSec + noteDurSec;
            if (offsetTime < noteEndSec) {
              const durationMs = noteDurSec * 1000;
              const program = track._isSectionClone ? track.instrumentProgram : (track.instrumentProgram !== undefined ? track.instrumentProgram : 0);
              // Track VSTi + Carla local → MIDI item PHẢI play qua Carla bridge
              // (chỉ route Carla — KHÔNG play qua FluidSynth GM nữa, tránh âm
              // sai instrument chồng lên). Không có Carla → FluidSynth như cũ.
              // ⚠️ FIX: item route giống hệt keybed (không gate __carlaRunning — Carla mở
              // thủ công/cũ vẫn phải route; gate cũ làm "keybed kêu, item câm").
              const routeToCarla = !!(window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(track.synth_engine));
              if (offsetTime < noteStartSec) {
                const delay = noteStartSec - offsetTime;
                const startTime = context.currentTime + delay;
                if (!routeToCarla && window.SonicSF) {
                  scheduleMidiNoteDispatch(
                    track, note.pitch || 60,
                    note.velocity || 0.8,
                    durationMs,
                    startTime,
                    program,
                    gainNode,
                    trkCh,
                    track.synth_engine
                  );
                }
                // MIDI items → Carla bridge (track VSTi + Carla local): phát VSTi
                // realtime — yêu cầu: MIDI item PHẢI play qua Carla khi VSTi loaded.
                if (routeToCarla) scheduleCarlaNote(track.synth_engine, trkCh, note.pitch || 60, note.velocity || 0.8, startTime, durationMs);
                // Trigger VU meter flash when the note starts playing
                setTimeout(() => {
                  // ⚠️ Guard: stop → setTimeout sót không được fire (VU nhảy
                  // sau khi âm đã dừng — user bug 05:45).
                  if (!isPlayingRef.current) return;
                  if (window.triggerMidiVuActivity) {
                    // VU key theo CONTEXT (tách MAIN vs SECTION — user 07:20)
                    window.triggerMidiVuActivity((activeTab && activeTab.startsWith('session_') ? '_sess_' : '') + track.id, note.velocity || 0.8);
                  }
                }, delay * 1000);
              } else {
                const playOffset = offsetTime - noteStartSec;
                const remainingDurMs = (noteEndSec - offsetTime) * 1000;
                if (!routeToCarla && window.SonicSF) {
                  scheduleMidiNoteDispatch(
                    track, note.pitch || 60,
                    note.velocity || 0.8,
                    remainingDurMs,
                    context.currentTime,
                    program,
                    gainNode,
                    trkCh,
                    track.synth_engine
                  );
                }
                // MIDI items → Carla bridge (track VSTi + Carla local)
                if (routeToCarla) scheduleCarlaNote(track.synth_engine, trkCh, note.pitch || 60, note.velocity || 0.8, context.currentTime, remainingDurMs);
                // Trigger VU meter flash instantly
                if (window.triggerMidiVuActivity) {
                  window.triggerMidiVuActivity((activeTab && activeTab.startsWith('session_') ? '_sess_' : '') + track.id, note.velocity || 0.8);
                }
              }
            }
          });
        });
        }
      }
      // If track has instrumentId set but no MIDI items, create scheduled oscillators
      if (track.instrumentId && midiItems.length === 0 && track.buffer) {
        // Play audio normally (the buffer has the audio data)
        // This block is intentionally empty - audio clips already play above
      }

      // SECTION items playback
      const sections = track.sections || [];
      if (activeTab === 'main' && sections.length > 0) {
        const bpmVal = parseInt(bpm) || 120;
        const secondsPerBeat = 60.0 / bpmVal;
        const secondsPerBar = secondsPerBeat * 4;

        sections.forEach(sec => {
          const secStart = sec.start || 0;
          const secEnd = secStart + (sec.duration || 0);

          const openTab = sessionTabsRef.current ? sessionTabsRef.current.find(st => st.sectionId === sec.sectionId || st.sectionId === sec.id) : null;
          const secTracks = openTab ? openTab.tracks : (sec.tracks || []);
          const hasSubSolo = secTracks.some(st => st.solo);

          secTracks.forEach((subTrack, subIdx) => {
            const isSubPlayable = hasSubSolo ? subTrack.solo : !subTrack.muted;
            if (!isSubPlayable) return;

            const subNode = getOrCreateSubTrackNode(track, subTrack, context);
            if (!subNode) return;
            // Re-apply route sub-node theo content-type mỗi lần schedule —
            // sub-node có thể STALE (route theo trạng thái cũ) → midi section
            // bị kéo vào dry. routeBypassFor: MIDI-only → ♪, clips → A.
            const subNodeObj = activeTrackNodesRef.current[track.id + '_sub_' + subTrack.id];
            if (subNodeObj && subNodeObj.route && subNodeObj.route.routeGain) {
              const srb = routeBypassFor(subTrack);
              subNodeObj.route.routeGain.gain.value = srb ? 0 : 1;
              subNodeObj.route.dryGain.gain.value = srb ? 1 : 0;
            }

            // 1. Play clips in subTrack
            const subClips = subTrack.clips || [];
            subClips.forEach(clip => {
              if (!clip.buffer) return;

              const clipStartLocal = clip.startTime || 0;
              const clipDurLocal = clip.buffer.duration / (clip.speed || 1.0);

              const clipStartMain = secStart + clipStartLocal;
              const clipEndMain = clipStartMain + clipDurLocal;

              // Only play within the bounds of [secStart, secEnd]
              const activeStart = Math.max(clipStartMain, secStart);
              const activeEnd = Math.min(clipEndMain, secEnd);

              if (activeStart < activeEnd && offsetTime < activeEnd) {
                const source = context.createBufferSource();
                source.buffer = clip.buffer;
                source.playbackRate.value = clip.speed || 1.0;
                source.connect(subNode);

                if (offsetTime < activeStart) {
                  const delay = activeStart - offsetTime;
                  const playOffset = activeStart - clipStartMain;
                  const playDuration = activeEnd - activeStart;
                  source.start(context.currentTime + delay, playOffset * (clip.speed || 1.0), playDuration);
                  activeSourcesRef.current.push(source);
                } else {
                  const playOffset = offsetTime - clipStartMain;
                  const playDuration = activeEnd - offsetTime;
                  source.start(context.currentTime, playOffset * (clip.speed || 1.0), playDuration);
                  activeSourcesRef.current.push(source);
                }
              }
            });

            // 2. Play MIDI items in subTrack
            const subMidiItems = subTrack.midiItems || [];
            const subRouteCarla = shouldRouteCarla(subTrack.synth_engine);
            const subNativeSf = isStandaloneSf() && !subRouteCarla && isSfTrackEngine(subTrack.synth_engine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive());
            if (subMidiItems.length > 0 && (window.SonicSF || subRouteCarla || subNativeSf)) {
              if (subNativeSf) {
                subMidiItems.forEach(item => scheduleNativeSfItem(subTrack, item, offsetTime, context, subNode, bpm, { baseOffsetSec: secStart, limitSec: secEnd, sources: activeSourcesRef.current, isActive: () => !!isPlayingRef.current }));
                return; // native render → audio buffer (không qua WASM/Carla per-note)
              }
              subMidiItems.forEach(item => {
                const notes = item.notes || [];
                notes.forEach(note => {
                  const noteStartSecLocal = item.startTime + (note.start_beat || 0) * secondsPerBeat;
                  const noteDurSec = (note.duration_beats || 1) * secondsPerBeat;

                  const noteStartMain = secStart + noteStartSecLocal;
                  const noteEndMain = noteStartMain + noteDurSec;

                  // Only play if the note starts inside the Section item bounds and hasn't finished yet
                  if (noteStartMain >= secStart && noteStartMain < secEnd && offsetTime < noteEndMain) {
                    const notePlayEndMain = Math.min(noteEndMain, secEnd);
                    const program = subTrack.instrumentProgram;
                    var subCh = assignTrackMidiChannel(subTrack, secTracks);
                    // MIDI item trong SECTION → Carla bridge khi VSTi loaded
                    // (chỉ route Carla — không play FluidSynth GM sai âm)
                    const subRouteCarla = !!(window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(subTrack.synth_engine));

                    if (offsetTime < noteStartMain) {
                      const delay = noteStartMain - offsetTime;
                      const startTime = context.currentTime + delay;
                      const playDurMs = (notePlayEndMain - noteStartMain) * 1000;
                      if (!subRouteCarla && window.SonicSF) {
                        scheduleMidiNoteDispatch(
                          subTrack, note.pitch || 60,
                          note.velocity || 0.8,
                          playDurMs,
                          startTime,
                          program,
                          subNode,
                          subCh,
                          subTrack.synth_engine
                        );
                      }
                      if (subRouteCarla) scheduleCarlaNote(subTrack.synth_engine, subCh, note.pitch || 60, note.velocity || 0.8, startTime, playDurMs);
                      // Trigger VU meter flash đúng nhịp (cả subTrack — VU
                      // section tab + parent track — VU MAIN): MIDI section đi
                      // SF → masterBus.input (không qua sub-node analyser) →
                      // VU track chỉ nhảy qua midiVuActivity. (Bị mất khi
                      // commit gộp — khôi phục fix 00:30/02:00.)
                      setTimeout(() => {
                        // ⚠️ Guard: stop → setTimeout sót không được fire (VU
                        // nhảy sau khi âm đã dừng — user bug 05:45).
                        if (!isPlayingRef.current) return;
                        if (window.triggerMidiVuActivity) {
                          // VU key theo CONTEXT (tách MAIN vs SECTION — 07:20)
                          window.triggerMidiVuActivity((activeTab && activeTab.startsWith('session_') ? '_sess_' : '') + subTrack.id, note.velocity || 0.8);
                          window.triggerMidiVuActivity((activeTab && activeTab.startsWith('session_') ? '_sess_' : '') + track.id, note.velocity || 0.8);
                        }
                      }, delay * 1000);
                    } else {
                      const remainingDurMs = (notePlayEndMain - offsetTime) * 1000;
                      if (!subRouteCarla && window.SonicSF) {
                        scheduleMidiNoteDispatch(
                          subTrack, note.pitch || 60,
                          note.velocity || 0.8,
                          remainingDurMs,
                          context.currentTime,
                          program,
                          subNode,
                          subCh,
                          subTrack.synth_engine
                        );
                      }
                      if (subRouteCarla) scheduleCarlaNote(subTrack.synth_engine, subCh, note.pitch || 60, note.velocity || 0.8, context.currentTime, remainingDurMs);
                      if (window.triggerMidiVuActivity) {
                        // VU key theo CONTEXT (tách MAIN vs SECTION — 07:20)
                        window.triggerMidiVuActivity((activeTab && activeTab.startsWith('session_') ? '_sess_' : '') + subTrack.id, note.velocity || 0.8);
                        window.triggerMidiVuActivity((activeTab && activeTab.startsWith('session_') ? '_sess_' : '') + track.id, note.velocity || 0.8);
                      }
                    }
                  }
                });
              });
            }
          });
        });
      }
    });
    updateSfRouting();
  };

  // Solo playback for Local Selection Loop (LOOP_MAKER.md §2.2)
  const startLocalTrackPlayback = (trackId, offsetTime) => {
    const context = getAudioContext();
    try { if (window.__ensureMasteringRouting) window.__ensureMasteringRouting(); } catch (e) {}
    refreshCarlaStatus();
    const track = tracks.find(t => t.id === trackId);
    if (!track) return;
    const clips = track.clips && track.clips.length > 0 ? track.clips : track.buffer ? [{
      id: 'default',
      buffer: track.buffer,
      startTime: track.startTime || 0,
      name: track.name,
      speed: track.speed || 1.0
    }] : [];

    // Get or create persistent gain & panner for real-time control
    const gainNode = getOrCreateTrackNode(track, context);
    const pannerNode = activeTrackNodesRef.current[track.id].pannerNode;
    clips.forEach(clip => {
      if (!clip.buffer) return;
      const source = context.createBufferSource();
      source.buffer = clip.buffer;
      source.playbackRate.value = clip.speed || 1.0;
      source.connect(gainNode);
      // NOTE: do NOT connect gainNode → pannerNode directly here — the track
      // node already wires gainNode → fxEntry → [FX modules] → fxLegacyIn →
      // chorus/reverb → pannerNode. A direct tap would BYPASS the FX Rack
      // chain for audio clips (double path: with + without FX).
      const clipStart = clip.startTime || 0;
      const clipDuration = clip.buffer.duration / (clip.speed || 1.0);
      const clipEnd = clipStart + clipDuration;
      if (offsetTime < clipStart) {
        const delay = clipStart - offsetTime;
        source.start(context.currentTime + delay, 0);
        activeSourcesRef.current.push(source);
      } else if (offsetTime < clipEnd) {
        const playOffset = offsetTime - clipStart;
        source.start(context.currentTime, playOffset * (clip.speed || 1.0));
        activeSourcesRef.current.push(source);
      }
    });

    // MIDI items playback
    const midiItems = track.midiItems || [];
    const routeCarla = shouldRouteCarla(track.synth_engine);
    const nativeSf = isStandaloneSf() && !routeCarla && isSfTrackEngine(track.synth_engine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive());
    if ((track.type === 'MIDI' || midiItems.length > 0) && (window.SonicSF || routeCarla || nativeSf)) {
      if (nativeSf) {
        midiItems.forEach(item => scheduleNativeSfItem(track, item, offsetTime, context, gainNode, bpm, { sources: activeSourcesRef.current, isActive: () => !!isPlayingRef.current }));
        return; // native render → audio buffer (không qua WASM/Carla per-note)
      }
      var lcCh = assignTrackMidiChannel(track, tracks);
      const bpmVal = parseInt(bpm) || 120;
      const secondsPerBeat = 60.0 / bpmVal;
      midiItems.forEach(item => {
        const notes = item.notes || [];
        notes.forEach(note => {
          const noteStartSec = item.startTime + (note.start_beat || 0) * secondsPerBeat;
          const noteDurSec = (note.duration_beats || 1) * secondsPerBeat;
          const noteEndSec = noteStartSec + noteDurSec;
          if (offsetTime < noteEndSec) {
            const durationMs = noteDurSec * 1000;
            const program = track.instrumentProgram !== undefined ? track.instrumentProgram : 0;
            // MIDI item → Carla bridge khi VSTi loaded (chỉ route Carla — không
            // play FluidSynth GM sai âm chồng lên)
            const routeToCarla = !!(window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(track.synth_engine));
            if (offsetTime < noteStartSec) {
              const delay = noteStartSec - offsetTime;
              const startTime = context.currentTime + delay;
              if (!routeToCarla && window.SonicSF) {
                scheduleMidiNoteDispatch(
                  track, note.pitch || 60,
                  note.velocity || 0.8,
                  durationMs,
                  startTime,
                  program,
                  gainNode,
                  lcCh,
                  track.synth_engine
                );
              }
              // MIDI items → Carla bridge (track VSTi + Carla local)
              if (routeToCarla) scheduleCarlaNote(track.synth_engine, lcCh, note.pitch || 60, note.velocity || 0.8, startTime, durationMs);
            } else {
              const remainingDurMs = (noteEndSec - offsetTime) * 1000;
              if (!routeToCarla && window.SonicSF) {
                scheduleMidiNoteDispatch(
                  track, note.pitch || 60,
                  note.velocity || 0.8,
                  remainingDurMs,
                  context.currentTime,
                  program,
                  gainNode,
                  lcCh,
                  track.synth_engine
                );
              }
              // MIDI items → Carla bridge (track VSTi + Carla local)
              if (routeToCarla) scheduleCarlaNote(track.synth_engine, lcCh, note.pitch || 60, note.velocity || 0.8, context.currentTime, remainingDurMs);
            }
          }
        });
      });
    }
  };
  const schedulePianoRollMidi = (st, offsetSeconds, notesOverride) => {
    if (st.type !== 'PIANO_ROLL') return;
    const context = getAudioContext();
    try { if (window.__ensureMasteringRouting) window.__ensureMasteringRouting(); } catch (e) {}
    refreshCarlaStatus();
    const midiNotes = notesOverride || st.notes || [];
    const bpmVal = parseInt(bpm) || 120;
    const secondsPerBeat = 60.0 / bpmVal;
    const startWallTime = context.currentTime;
    const track = activeTracksRef.current ? activeTracksRef.current.find(t => t.id === st.trackId) : null;
    if (!track) { console.warn('[Play] Piano Roll track not found:', st.trackId); return; }
    const destNode = getOrCreateTrackNode(track, context);
    // Instrument context resolve từ TRACK live (nguồn duy nhất) — track "đã
    // loaded instrument" (instrumentProgram GM hoặc synth_engine soundfont)
    // phải chơi ĐÚNG instrument đó. ensureSonicInstrument select channel đúng
    // trước khi notes bắn (fire-and-forget — playNote tự load+retry nếu SF
    // chưa xong).
    const instCtx = resolveTrackInstrumentCtx(track, activeTracksRef.current || []);
    const instrumentProgram = instCtx.program;
    const synthEngine = instCtx.synthEngine;
    const mainCh = instCtx.ch;
    var allTracks = activeTracksRef.current || [];
    ensureSonicInstrument(instCtx);
    console.log('[Play] PianoRoll schedule:', st.id, '| notes=', midiNotes.length, '| track=', st.trackId, '| program=', instrumentProgram, '| sf=', instCtx.sfId ? instCtx.sfId : '-', '| dest=', destNode ? 'ok' : 'null', '| ch=', mainCh, '| sfEngine=', synthEngine ? 'yes' : 'no', '| bypassA=', !!trackAudioBypassMap[st.trackId], '| bypassMidi=', !!trackMidiBypassMap[st.trackId]);
    // 0 = item start. Notes must be scheduled relative to the item too, otherwise
    // items placed later in the project play `item.startTime` seconds in the
    // future (silence when pressing play). Ghost notes are already relative to
    // the item window, so no absolute-session offset is applied anywhere here.
    const routeCarla = shouldRouteCarla(synthEngine);
    if (isStandaloneSf() && !routeCarla && isSfTrackEngine(synthEngine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive())) {
      scheduleNativeSfItem(track, { startTime: 0, notes: midiNotes }, offsetSeconds, context, destNode, bpm, { sources: activeSourcesRef.current, isActive: () => !!isPlayingRef.current });
      return; // native render cả tab → audio buffer (không per-note WASM/Carla)
    }
    midiNotes.forEach(note => {
      const noteOnBeat = note.start_beat || 0;
      const noteDurBeat = note.duration_beats || 1;
      const noteStartSec = noteOnBeat * secondsPerBeat;
      const noteDurSec = noteDurBeat * secondsPerBeat;
      if (noteStartSec + noteDurSec > offsetSeconds) {
        const effectiveStart = Math.max(0, noteStartSec - offsetSeconds);
        const effectiveDur = noteDurSec - Math.max(0, offsetSeconds - noteStartSec);
        const scheduledTime = startWallTime + effectiveStart;
        const durMs = effectiveDur * 1000;
        // Track VSTi + Carla local → note PHẢI play qua Carla bridge (chỉ route
        // Carla — không play FluidSynth GM sai âm chồng lên)
        const routeToCarla = !!(window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(synthEngine));
        if (!routeToCarla && window.SonicSF) {
          scheduleMidiNoteDispatch(track, note.pitch || 60, note.velocity || 0.8, durMs, scheduledTime, instrumentProgram, destNode, mainCh, synthEngine);
        }
        // MIDI items → Carla (track VSTi + Carla local): phát VSTi realtime
        // (schedule theo audio clock bằng setTimeout — preview, timing gần đúng).
        if (routeToCarla) {
          const carlaDelay = Math.max(0, (scheduledTime - context.currentTime) * 1000);
          const carlaVel = Math.round((note.velocity || 0.8) * 127);
          const carlaCh = (synthEngine && synthEngine.midi_channel !== undefined) ? synthEngine.midi_channel : mainCh;
          const carlaPitch = note.pitch || 60;
          setTimeout(function () { window.SonicCarlaMidi.noteOn(carlaCh, carlaPitch, carlaVel); }, carlaDelay);
          setTimeout(function () { window.SonicCarlaMidi.noteOff(carlaCh, carlaPitch); }, carlaDelay + durMs + 30);
        }
      }
    });
    // Play ghost notes from active tracks
    var ghostLayers = st.ghostPlayLayers || [];
    ghostLayers.forEach(function(layer) {
      var ghostTrack = allTracks.find(function(t) { return t.id === layer.trackId; });
      var ghostProg = layer.instrumentProgram !== undefined ? layer.instrumentProgram : (ghostTrack ? ghostTrack.instrumentProgram : undefined);
      var ghostSynth = layer.synthEngine || (ghostTrack ? ghostTrack.synth_engine : undefined);
      if (ghostProg === undefined && !ghostSynth) return;
      var ghostDest = getOrCreateTrackNode(ghostTrack, context);
      var ghostCh = ghostTrack ? assignTrackMidiChannel(ghostTrack, allTracks) : 0;
      layer.notes.forEach(function(note) {
        var beat = note.start_beat || 0;
        var dur = note.duration_beats || 1;
        var startSec = beat * secondsPerBeat;
        var durSec = dur * secondsPerBeat;
        if (startSec + durSec > offsetSeconds) {
          var effStart = Math.max(0, startSec - offsetSeconds);
          var effDur = durSec - Math.max(0, offsetSeconds - startSec);
          var schedTime = startWallTime + effStart;
          var durMs = effDur * 1000;
          // Ghost note → Carla bridge khi ghost track VSTi (chỉ route Carla)
          var gRouteCarla = !!(window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(ghostSynth));
          if (!gRouteCarla && window.SonicSF) {
            scheduleMidiNoteDispatch(ghostTrack, note.pitch || 60, note.velocity || 0.8, durMs, schedTime, ghostProg, ghostDest, ghostCh, ghostSynth);
          }
          if (gRouteCarla) scheduleCarlaNote(ghostSynth, ghostCh, note.pitch || 60, note.velocity || 0.8, schedTime, durMs);
        }
      });
    });
  };
  const handlePlayPause = () => {
    console.log('[Play] click activeTab=', activeTab, 'isPlaying=', isPlaying, 'subPlaying=', subTabs.filter(s => s.isPlaying).length);
    if (activeTab !== 'main' && !activeTab.startsWith('session_')) {
      // Sub-tab playback transport
      const st = subTabs.find(s => s.id === activeTab);
      if (!st || (!st.buffer && st.type !== 'PIANO_ROLL')) return;
      // MAIN/SECTION đang play (ngầm — MIDI section/main còn kêu sau khi mở
      // tab): Space phải STOP trước — không được play sub-tab (user: trong
      // PIANO ROLL không phát hiện MIDI đang play → space 2 lần mới stop).
      if (isPlaying) {
        stopAllPlayback();
        setSubTabs(prev => prev.map(s => s.isPlaying ? { ...s, isPlaying: false } : s));
        setIsPlaying(false);
        return;
      }
      if (st.isPlaying) {
        stopAllPlayback();
        setSubTabs(prev => prev.map(s => s.id === activeTab ? {
          ...s,
          isPlaying: false
        } : s));
      } else {
        try {
          stopAllPlayback();
          const startOffset = st.currentTime || 0;
          if (st.type === 'PIANO_ROLL') {
            schedulePianoRollMidi(st, startOffset);
            startSubTabPlayback(st, startOffset);
          } else {
            startSubTabPlayback(st, startOffset);
          }
          setSubTabs(prev => prev.map(s => s.id === activeTab ? {
            ...s,
            buffer: s.buffer || getAudioContext().createBuffer(1, 128, getAudioContext().sampleRate),
            isPlaying: true,
            currentTime: startOffset
          } : s));
          // BẮT BUỘC start rAF loop updatePlayhead — nếu không, playhead sub-tab
          // không di chuyển (và loop/stop không bao giờ chạy).
          animationFrameIdRef.current = requestAnimationFrame(updatePlayhead);
        } catch (e) {
          console.error('[Play] Piano Roll play error:', e);
          showToast('Lỗi phát Piano Roll: ' + e.message, 'error');
        }
      }
      return;
    }
    const context = getAudioContext();
    if (isPlaying) {
      stopAllPlayback();
    } else {
      startOffsetTimeRef.current = currentTime;
      startAudioTimeRef.current = context.currentTime;
      startTrackPlayback(currentTime);
      setIsPlaying(true);
    }
  };
  handlePlayPauseRef.current = handlePlayPause;
  const handlePianoRollRealtimePlay = (trackIds) => {
    const prSt = subTabs.find(s => s.id === activeTab && s.type === 'PIANO_ROLL');
    if (!prSt) return;
    var prLayers = [];
    var ghostData = (window.SonicGhost && window.SonicGhost.extractGhostLayers) ? window.SonicGhost.extractGhostLayers(activeTracks, prSt.trackId, prSt.target_id, parseInt(bpm) || 120) : [];
    (ghostData || []).forEach(function(layer) {
      if (!trackIds || trackIds.indexOf(layer.track_id) === -1) return;
      var ltrk = (activeTracks || []).find(function(t) { return t.id === layer.track_id; });
      prLayers.push({
        trackId: layer.track_id,
        notes: layer.notes.map(function(n) { return { pitch: n.pitch, start_beat: n.relative_start_beat, duration_beats: n.duration_beats, velocity: n.velocity || 0.8 }; }),
        instrumentProgram: ltrk ? ltrk.instrumentProgram : undefined,
        instrumentName: ltrk ? ltrk.instrumentName : undefined,
        synthEngine: ltrk ? ltrk.synth_engine : undefined
      });
    });
    stopAllPlayback();
    const prCtx = getAudioContext();
    const prTNode = activeTrackNodesRef.current[prSt.trackId];
    if (prTNode && prTNode.gainNode) {
      prTNode.gainNode.gain.setValueAtTime(prTNode.gainNode.gain.value || 1, prCtx.currentTime);
      prTNode.gainNode.gain.linearRampToValueAtTime(0.001, prCtx.currentTime + 0.04);
    }
    setTimeout(() => {
      window.SonicSF.stopAll();
      stopAllNativeSfNotes();
      if (prTNode && prTNode.gainNode) {
        const prTrackData = activeTracksRef.current ? activeTracksRef.current.find(t => t.id === prSt.trackId) : null;
        const prVolDb = prTrackData ? (prTrackData.volumeDb ?? 0) : 0;
        const prVolLinear = prVolDb <= -50 ? 0 : Math.pow(10, prVolDb / 20);
        prTNode.gainNode.gain.setValueAtTime(0.001, prCtx.currentTime);
        prTNode.gainNode.gain.linearRampToValueAtTime(prVolLinear || 0.8, prCtx.currentTime + 0.015);
      }
      const prOffset = prSt.currentTime || 0;
      startOffsetTimeRef.current = prOffset;
      startAudioTimeRef.current = prCtx.currentTime;
      startBufferOffsetRef.current = prOffset * (prSt.speed || 1.0);
      const playTab = Object.assign({}, prSt, { ghostPlayLayers: prLayers });
      schedulePianoRollMidi(playTab, prOffset);
      setSubTabs(prev => prev.map(s => s.id === prSt.id ? Object.assign({}, s, { ghostPlayLayers: prLayers, isPlaying: true, currentTime: prOffset }) : s));
    }, 60);
  };
  const handlePause = () => {
    if (isPlaying || subTabs.some(s => s.isPlaying)) stopAllPlayback();
  };
  const stopAllPlayback = () => {
    try {
    isPlayingRef.current = false;
    try { if (subTabsRef.current) subTabsRef.current = subTabsRef.current.map(s => ({ ...s, isPlaying: false })); } catch (e) {}
    activeSourcesRef.current.forEach(src => {
      try {
        src.stop();
      } catch (e) { }
    });
    activeSourcesRef.current = [];
    Object.values(activeTrackNodesRef.current).forEach(n => {
      if (n.fxStopFn) { try { n.fxStopFn(); } catch (e) {} }
    });
    activeTrackNodesRef.current = {};
    // Clear VU activity NGAY: hết âm → VU tắt tức thì (không decay 0.35s từ
    // âm cũ — user 06:45: tắt âm MAIN items khi vào SECTION-TAB → VU main
    // items phải tắt theo, không "diễn" tiếp).
    try { midiVuActivityRef.current = {}; } catch (e) { }
    // Clear luôn counter note đang giữ — nếu không, sau stop (âm đã dừng)
    // tick vẫn thấy heldCnt > 0 → giữ peak → VU dính mãi (user bug 08:08).
    try { heldMidiNotesRef.current = {}; } catch (e) { }
    stopMidiCapture();
    stopAllNativeSfNotes();
    // D2/D6: bridge active -> transport STOP (bridge flush toàn pitch, hết âm
    // ngân tức thì; A13 C++ flush note-off).
    if (window.NativeBridgeService && window.NativeBridgeService.isBridgeConnected) {
      try { window.NativeBridgeService.transport('stop'); } catch (e) {}
      // V8 bug 3: transport stop thu 2 sau 150ms — note-on timer guard sync qua
      // React effect (cham) van bay toi bridge sau STOP → retrigger am treo.
      setTimeout(() => { try { window.NativeBridgeService.transport('stop'); } catch (e) {} }, 150);
    }
    if (window.SonicSF) {
      try { window.SonicSF.stopAll(); } catch (e) { console.warn('[Stop] stopAll error:', e); }
      // Dừng triệt để: noteoff từng note + hủy scheduled note-on (hết âm stuck)
      if (window.SonicSF.panic) { try { window.SonicSF.panic(); } catch (e) { console.warn('[Stop] panic error:', e); } }
    }
    } catch (e) {
      console.warn('[Stop] stopAllPlayback error:', e);
    }
    setIsPlaying(false);
    setSubTabs(prev => prev.map(s => ({
      ...s,
      isPlaying: false
    })));
    updateSfRouting();
  };
  const seekPlaybackTo = (time) => {
    const isSubTab = subTabs.some(sub => sub.id === activeTab);
    if (isSubTab) {
      const st = subTabs.find(s => s.id === activeTab);
      if (!st) return;
      if (st.isPlaying) {
        stopAllPlayback();
        window.SonicSF.stopAll();
        stopAllNativeSfNotes();
        setSubTabs(prev => prev.map(s => s.id === activeTab ? {
          ...s,
          currentTime: time,
          isPlaying: true
        } : s));
        const ctx = getAudioContext();
        startOffsetTimeRef.current = time;
        startAudioTimeRef.current = ctx.currentTime;
        startBufferOffsetRef.current = time * (st.speed || 1.0);
        if (st.type === 'PIANO_ROLL') {
          schedulePianoRollMidi(st, time);
        }
        startSubTabPlayback(st, time);
      } else {
        setSubTabs(prev => prev.map(s => s.id === activeTab ? {
          ...s,
          currentTime: time
        } : s));
      }
    } else {
      if (isPlaying) {
        stopAllPlayback();
        setCurrentTime(time);
        startOffsetTimeRef.current = time;
        startAudioTimeRef.current = getAudioContext().currentTime;
        startTrackPlayback(time);
        setIsPlaying(true);
      } else {
        setCurrentTime(time);
      }
    }
  };
  const handleStop = () => {
    if (recordingStateRef.current === 'RECORDING' || recordingStateRef.current === 'COUNT_IN') {
      stopRecordingTake();
      return;
    }
    stopAllPlayback();
    if (activeTab !== 'main' && !activeTab.startsWith('session_')) {
      setSubTabs(prev => prev.map(s => s.id === activeTab ? {
        ...s,
        currentTime: 0
      } : s));
    } else {
      setCurrentTime(0);
    }
  };

  const drawVuMeter = (canvas, db) => {
    if (!canvas) return;
    const ctx = canvas.getContext('2d');
    if (!ctx) return;

    const w = canvas.width;
    const h = canvas.height;
    ctx.clearRect(0, 0, w, h);

    const minDb = -60;
    const maxDb = 0;
    const frac = Math.max(0, Math.min(1, (db - minDb) / (maxDb - minDb)));

    ctx.fillStyle = '#18181b';
    ctx.fillRect(0, 0, w, h);

    const grad = ctx.createLinearGradient(0, 0, w, 0);
    grad.addColorStop(0, '#10b981');
    grad.addColorStop(0.7, '#eab308');
    grad.addColorStop(0.95, '#ef4444');

    ctx.fillStyle = grad;
    ctx.fillRect(0, 0, w * frac, h);

    if (db >= -0.5) {
      ctx.fillStyle = '#ff0000';
      ctx.fillRect(w - 6, 0, 6, h);
    }
  };

  const handleRecordClick = async () => {
    if (recordingState === 'RECORDING' || recordingState === 'COUNT_IN') {
      await stopRecordingTake();
      return;
    }

    // Check if piano roll tab is active and armed
    const activePianoRoll = subTabs.find(s => s.id === activeTab && s.type === 'PIANO_ROLL' && s.isArmed);
    if (activePianoRoll && selectedMidiInputId) {
      setRecordingState('COUNT_IN');
      const secondsPerBeat = 60.0 / (parseInt(bpm) || 120);
      const countInDuration = secondsPerBeat * 4;
      const audioCtx = getAudioContext();
      const now = audioCtx.currentTime;
      showToast('Metronome Count-in: 4... 3... 2... 1... (MIDI Piano Roll)', 'info');
      for (let i = 0; i < 4; i++) {
        playMetronomeClick(now + i * secondsPerBeat, i === 0);
      }
      setTimeout(() => {
        startPianoRollRecording(activePianoRoll);
      }, countInDuration * 1000);
      return;
    }

    const armed = activeTracks.filter(t => t.isArmed && t.inputSource?.deviceType && t.inputSource.deviceType !== 'NONE');
    if (armed.length === 0) {
      showToast('Vui lòng Arm (R) ít nhất một track và chọn cổng Input để ghi âm.', 'warning');
      return;
    }

    setRecordingState('COUNT_IN');
    const secondsPerBeat = 60.0 / (parseInt(bpm) || 120);
    const countInDuration = secondsPerBeat * 4;
    const audioCtx = getAudioContext();
    const now = audioCtx.currentTime;

    showToast('Metronome Count-in: 4... 3... 2... 1...', 'info');

    for (let i = 0; i < 4; i++) {
      playMetronomeClick(now + i * secondsPerBeat, i === 0);
    }

    setTimeout(() => {
      startRecordingTake(armed);
    }, countInDuration * 1000);
  };

  const startPianoRollRecording = (tab) => {
    try {
      const context = getAudioContext();
      const secondsPerBeat = 60.0 / (parseInt(bpm) || 120);
      const startTime = currentTime;
      const startBeat = startTime / secondsPerBeat;
      nextMetronomeBeatRef.current = Math.ceil(startBeat);
      const midiRec = new ClientMIDIRecorder(context, parseInt(bpm) || 120, 4);
      midiRec.tempTabId = tab.id;
      midiRec.selectedMidiInputId = selectedMidiInputId || 'ALL';
      midiRec.start(startTime / secondsPerBeat, midiRec.selectedMidiInputId);
      pianoRollRecorderRef.current = midiRec;
      activeMIDIRecordersRef.current['piano_roll'] = midiRec;
      setRecStartTimelineTime(startTime);
      recordingStartTimeRef.current = startTime;
      setRecordingState('RECORDING');
      setRecTempMidiNotes([]);
      const ctx = getAudioContext();
      const silentBuf = ctx.createBuffer(1, 128, ctx.sampleRate);
      setSubTabs(prev => prev.map(s => s.id === tab.id ? { ...s, buffer: silentBuf, isPlaying: true } : s));
      startSubTabPlayback({ ...tab, buffer: silentBuf }, startTime);
      startOffsetTimeRef.current = startTime;
      startAudioTimeRef.current = context.currentTime;
      setIsPlaying(true);
      midiRec.onNoteOn = (pitch, currentBeat) => {
        const elapsedBeats = Math.max(0, currentBeat);
        const sec = elapsedBeats * (60.0 / (parseInt(bpm) || 120));
        const activeNotes = Array.from(midiRec.activeNotes.values()).map(n => ({
          id: 'rec_' + n.pitch + '_' + currentBeat,
          pitch: n.pitch,
          start_beat: n.start_beat,
          duration_beats: Math.max(0.125, currentBeat - n.start_beat),
          velocity: Math.min(1, (n.velocity || 0.8)),
          pan: 0.0
        }));
        const rec = midiRec.recordedNotes.map((n, i) => ({
          id: 'rec_' + Date.now() + '_' + i,
          pitch: n.pitch,
          start_beat: n.start_beat,
          duration_beats: n.duration_beats,
          velocity: Math.min(1, (n.velocity || 0.8)),
          pan: 0.0
        }));
        const allNotes = [...rec, ...activeNotes];
        setRecTempMidiNotes(allNotes);
        setCanvasRedrawCount(n => n + 1);
        setSubTabs(prev => prev.map(s =>
          s.id === tab.id ? { ...s, currentTime: startTime + sec, isDirty: true } : s
        ));
      };
      if (!tab._recordingStarted) {
        tab._recordingStarted = true;
      }
      showToast('Recording MIDI to Piano Roll...', 'info');
    } catch (err) {
      console.error('startPianoRollRecording error:', err);
      showToast('Lỗi khi bắt đầu ghi âm Piano Roll: ' + err.message, 'warning');
      setRecordingState('IDLE');
    }
  };
  handleRecordClickRef.current = handleRecordClick;

  const startRecordingTake = async (armedTracks) => {
    const context = getAudioContext();
    if (context.state === 'suspended') {
      await context.resume();
    }

    setRecordingState('RECORDING');
    setRecTempMidiNotes([]);
    setRecTempAudioBuffer(null);

    const startTimelineTime = currentTime;
    setRecStartTimelineTime(startTimelineTime);
    recordingStartTimeRef.current = startTimelineTime;

    const secondsPerBeat = 60.0 / (parseInt(bpm) || 120);
    const startBeat = startTimelineTime / secondsPerBeat;
    nextMetronomeBeatRef.current = Math.ceil(startBeat);

    activeMIDIRecordersRef.current = {};
    activeAudioRecordersRef.current = {};
    recordingPCMDataRef.current = {};

    startOffsetTimeRef.current = startTimelineTime;
    startAudioTimeRef.current = context.currentTime;
    startTrackPlayback(startTimelineTime);
    setIsPlaying(true);

    let midiRecList = [];

    for (let track of armedTracks) {
      if (track.inputSource.deviceType === 'MIDI_KEYBOARD') {
        const midiRec = new ClientMIDIRecorder(context, parseInt(bpm) || 120, 4);
        const tempMidiItemId = 'midi_rec_' + Date.now() + '_' + track.id;
        midiRec.tempMidiItemId = tempMidiItemId;

        updateActiveTracks(prev => prev.map(t => {
          if (t.id !== track.id) return t;
          return {
            ...t,
            midiItems: [...(t.midiItems || []), {
              id: tempMidiItemId,
              name: 'Recording...',
              startTime: startTimelineTime,
              duration: 4 * (secondsPerBeat * 4),
              notes: []
            }]
          };
        }));

        midiRec.onNoteOn = (pitch, currentBeat) => {
          const canvas = trackVuRefs.current[track.id];
          if (canvas) {
            drawVuMeter(canvas, 0);
            setTimeout(() => drawVuMeter(canvas, -60), 100);
          }
          const elapsedBeats = Math.max(0, currentBeat);
          const elapsedSec = elapsedBeats * secondsPerBeat;
          const durationSec = Math.max(4 * (secondsPerBeat * 4), elapsedSec);
          const allNotes = [...midiRec.recordedNotes, ...Array.from(midiRec.activeNotes.values()).map(n => ({
            ...n,
            duration_beats: currentBeat - n.start_beat
          }))];
          setRecTempMidiNotes(allNotes);
          setCanvasRedrawCount(n => n + 1);
          if (midiRec.tempMidiItemId) {
            updateActiveTracks(prev => prev.map(t => {
              if (t.id !== track.id) return t;
              return {
                ...t,
                midiItems: t.midiItems.map(m => m.id === midiRec.tempMidiItemId ? { ...m, notes: allNotes, duration: durationSec } : m)
              };
            }));
          }
        };

        midiRec.onNoteOff = () => {
          const currentBeat = (context.currentTime - midiRec.recStartAudioTime - midiRec.latencyCompSec) / (60.0 / midiRec.bpm);

          const elapsedBeats = Math.max(0, currentBeat);
          const elapsedSec = elapsedBeats * secondsPerBeat;
          const durationSec = Math.max(4 * (secondsPerBeat * 4), elapsedSec);
          const activeNotesArray = Array.from(midiRec.activeNotes.values()).map(n => ({
            ...n,
            duration_beats: currentBeat - n.start_beat
          }));
          const allNotes = [...midiRec.recordedNotes, ...activeNotesArray];
          setRecTempMidiNotes(allNotes);
          setCanvasRedrawCount(n => n + 1);
          if (midiRec.tempMidiItemId) {
            updateActiveTracks(prev => prev.map(t => {
              if (t.id !== track.id) return t;
              return {
                ...t,
                midiItems: t.midiItems.map(m => m.id === midiRec.tempMidiItemId ? { ...m, notes: allNotes, duration: durationSec } : m)
              };
            }));
          }
        };

        midiRec.start(startTimelineTime / (secondsPerBeat * 4), track.inputSource.deviceId);
        activeMIDIRecordersRef.current[track.id] = midiRec;
        midiRecList.push({ trackId: track.id, midiRec });

      } else if (track.inputSource.deviceType === 'MICROPHONE') {
        const audioRec = new ClientAudioRecorder(context);
        try {
          await audioRec.initializeInput(track.inputSource.deviceId);

          recordingPCMDataRef.current[track.id] = [];

          audioRec.onLevelUpdate = (db) => {
            const canvas = trackVuRefs.current[track.id];
            if (canvas) {
              drawVuMeter(canvas, db);
            }
          };

          audioRec.onPCMChunk = (chunk) => {
            if (recordingPCMDataRef.current[track.id]) {
              const currentData = recordingPCMDataRef.current[track.id];
              const newData = new Float32Array(currentData.length + chunk.length);
              newData.set(currentData);
              newData.set(chunk, currentData.length);
              recordingPCMDataRef.current[track.id] = newData;
            }
          };

          const monitorGain = track.monitoringEnabled ? (masterBus ? masterBus.input : context.destination) : null;
          await audioRec.start(monitorGain, track.monitoringEnabled);
          activeAudioRecordersRef.current[track.id] = audioRec;
        } catch (err) {
          console.error('Failed to initialize microphone:', err);
          showToast('Không khởi động được micro: ' + err.message, 'warning');
        }
      }
    }

    recordingSyncRef.current = setInterval(() => {
      const secondsPerBeatInt = 60.0 / (parseInt(bpm) || 120);
      for (let { trackId, midiRec } of midiRecList) {
        if (!midiRec.isRecording || !midiRec.tempMidiItemId) continue;
        const currentTimeSec = Math.max(0, getAudioContext().currentTime - midiRec.recStartAudioTime - midiRec.latencyCompSec);
        const currentBeat = currentTimeSec / secondsPerBeatInt;
        const allNotes = [...midiRec.recordedNotes, ...Array.from(midiRec.activeNotes.values()).map(n => ({
          ...n,
          duration_beats: currentBeat - n.start_beat
        }))];
        const durationSec = Math.max(4 * (secondsPerBeatInt * 4), currentTimeSec);
        if (Math.random() < 0.2) { // Throttle log to prevent flooding (approx 2 logs/sec)
          console.log(`[DevLog] [MIDI Rec Sync] Temp Item ID: ${midiRec.tempMidiItemId}, Duration: ${durationSec.toFixed(2)}s, ActiveNotes: ${midiRec.activeNotes.size}, RecordedNotes: ${midiRec.recordedNotes.length}`);
        }
        setRecTempMidiNotes(allNotes);
        setCanvasRedrawCount(n => n + 1);
        updateActiveTracks(prev => prev.map(t => {
          if (t.id !== trackId) return t;
          return {
            ...t,
            midiItems: t.midiItems.map(m => m.id === midiRec.tempMidiItemId ? { ...m, notes: allNotes, duration: durationSec } : m)
          };
        }));
      }
    }, 100);

    showToast('Đang ghi âm...', 'info');
  };

  const stopRecordingTake = async () => {
    setRecordingState('IDLE');
    stopAllPlayback();

    const context = getAudioContext();
    const secondsPerBeat = 60.0 / (parseInt(bpm) || 120);
    const secondsPerBar = secondsPerBeat * 4;

    const midiRecorders = activeMIDIRecordersRef.current;
    const audioRecorders = activeAudioRecordersRef.current;

    Object.keys(trackVuRefs.current).forEach(tid => {
      const canvas = trackVuRefs.current[tid];
      if (canvas) drawVuMeter(canvas, -60);
    });

    let hasRecordedAnything = false;

    for (let trackId in midiRecorders) {
      const midiRec = midiRecorders[trackId];
      const recordedNotes = midiRec.stop();

      if (midiRec.tempTabId) {
        if (recordedNotes.length > 0) {
          const newNotes = recordedNotes.map((n, i) => ({
            id: 'rec_' + Date.now() + '_' + i,
            pitch: n.pitch,
            start_beat: Math.max(0, n.start_beat || 0),
            duration_beats: Math.max(0.125, n.duration_beats || 0.25),
            velocity: Math.min(1, (n.velocity || 0.8)),
            pan: 0.0
          }));
          setSubTabs(prev => prev.map(s => s.id === midiRec.tempTabId ? { ...s, notes: [...(s.notes || []), ...newNotes], isDirty: true } : s));
          setCanvasRedrawCount(n => n + 1);
          showToast(`Đã ghi ${recordedNotes.length} notes vào Piano Roll.`, 'success');
        }
        hasRecordedAnything = true;
      } else if (midiRec.tempMidiItemId) {
        const recCurrentTimeSec = Math.max(0, context.currentTime - midiRec.recStartAudioTime - midiRec.latencyCompSec);
        const recElapsedBeats = recCurrentTimeSec / (60.0 / midiRec.bpm);
        const totalDurationBeats = Math.max(4.0, recordedNotes.length > 0 ? Math.max(recElapsedBeats, ...recordedNotes.map(n => n.start_beat + n.duration_beats)) : recElapsedBeats);

        updateActiveTracks(prev => prev.map(t => {
          if (t.id !== trackId) return t;
          const itemIndex = (t.midiItems || []).findIndex(m => m.id === midiRec.tempMidiItemId);
          if (itemIndex >= 0) {
            const updatedItems = [...t.midiItems];
            updatedItems[itemIndex] = {
              ...updatedItems[itemIndex],
              name: recordedNotes.length > 0 ? 'Recorded MIDI' : 'Empty MIDI',
              notes: recordedNotes,
              duration: Math.ceil(totalDurationBeats / 4) * secondsPerBar
            };
            return { ...t, midiItems: updatedItems };
          }
        const newMidiItem = {
            id: 'midi_rec_' + Date.now(),
            name: 'Recorded MIDI',
            parent_track_id: t.id,
            startTime: recordingStartTimeRef.current,
            duration: Math.ceil(totalDurationBeats / 4) * secondsPerBar,
            length_bars: Math.ceil(totalDurationBeats / 4),
            notes: recordedNotes
        };
        return {
            ...t,
            midiItems: [...(t.midiItems || []), newMidiItem]
        };
        }));

        if (recordedNotes.length > 0) {
        hasRecordedAnything = true;
        }
    } else if (recordedNotes.length > 0) {
        hasRecordedAnything = true;
        const totalDurationBeats = Math.max(4.0, ...recordedNotes.map(n => n.start_beat + n.duration_beats));
        const newMidiItem = {
            id: 'midi_rec_' + Date.now(),
            name: 'Recorded MIDI',
            parent_track_id: trackId,
            startTime: recordingStartTimeRef.current,
            duration: Math.ceil(totalDurationBeats / 4) * secondsPerBar,
            length_bars: Math.ceil(totalDurationBeats / 4),
            notes: recordedNotes
        };
        updateActiveTracks(prev => prev.map(t => {
          if (t.id !== trackId) return t;
          return {
            ...t,
            midiItems: [...(t.midiItems || []), newMidiItem]
          };
        }));
      }
    }

    for (let trackId in audioRecorders) {
      const audioRec = audioRecorders[trackId];
      const audioBuffer = await audioRec.stop();
      if (audioBuffer && audioBuffer.duration > 0.05) {
        hasRecordedAnything = true;

        const newClip = {
          id: 'clip_rec_' + Date.now(),
          name: 'Recorded Audio.wav',
          buffer: audioBuffer,
          startTime: recordingStartTimeRef.current,
          speed: 1.0
        };

        updateActiveTracks(prev => prev.map(t => {
          if (t.id !== trackId) return t;
          const updatedClips = [...(t.clips || []), newClip];
          return {
            ...t,
            clips: updatedClips,
            buffer: updatedClips[0]?.buffer || null,
            startTime: updatedClips[0]?.startTime || 0,
            name: updatedClips[0]?.name || t.name
          };
        }));
      }
    }

    if (recordingSyncRef.current) {
      clearInterval(recordingSyncRef.current);
      recordingSyncRef.current = null;
    }
    activeMIDIRecordersRef.current = {};
    activeAudioRecordersRef.current = {};
    recordingPCMDataRef.current = {};

    setRecTempMidiNotes([]);
    setRecTempAudioBuffer(null);

    if (hasRecordedAnything) {
      showToast('Đã thu và lưu bản ghi vào timeline.', 'success');
    } else {
      showToast('Đã dừng ghi âm (không phát hiện tín hiệu đầu vào).', 'info');
    }
  };
  const handleSubTabResizeMouseDown = e => {
    e.preventDefault();
    e.stopPropagation();
    const startY = e.clientY;
    const startHeight = subTabHeight;
    const handleMouseMove = moveEvent => {
      const deltaY = moveEvent.clientY - startY;
      const newHeight = Math.max(48, Math.min(400, startHeight + deltaY));
      setSubTabHeight(newHeight);
    };
    const handleMouseUp = () => {
      document.removeEventListener('mousemove', handleMouseMove);
      document.removeEventListener('mouseup', handleMouseUp);
    };
    document.addEventListener('mousemove', handleMouseMove);
    document.addEventListener('mouseup', handleMouseUp);
  };

  // ── Playhead set with seek+play ──
  // Vị trí click gần nhất trên timeline — paste dùng vị trí CLICK (bên phải
  // playhead) thay vì playhead (user 08:05: paste tại con trỏ click; click bên
  // trái playhead → paste tại playhead)
  const lastClickTimeRef = useRef(null);
  const handlePlayheadSet = (time, shiftKey) => {
    lastClickTimeRef.current = time;
    setPlayheadWithUndo(time);
  };
  const clearLocalSelection = () => {
    setSelectionMode(null);
    setLocalSelectionTrackId(null);
    setLocalSelectionStart(null);
    setLocalSelectionEnd(null);
  };
  const handleRulerMouseDown = e => {
    if (e.ctrlKey) {
      e.preventDefault();
      e.stopPropagation();
      clearLocalSelection();
      const wrapper = timelineWrapperRef.current;
      if (wrapper) {
        const rect = wrapper.getBoundingClientRect();
        const scrollLeft = wrapper.scrollLeft;
        const rawTime = Math.max(0, (e.clientX - rect.left + scrollLeft) / zoom - leadInMargin);
        const time = snapValue !== 'free' ? snapTime(rawTime, snapValue, bpm) : rawTime;
        rulerDragStartRef.current = time;
        rulerAnchorRef.current = time;
        isDraggingRulerRef.current = true;
        captureSelectionUndo();
        setSelectionMode('global');
        setSelectionStart(time);
        setSelectionEnd(time);
      }
      return;
    }
    const wrapper = timelineWrapperRef.current;
    if (!wrapper) return;
    const rect = wrapper.getBoundingClientRect();
    const scrollLeft = wrapper.scrollLeft;
    const mouseX = e.clientX - rect.left + scrollLeft;
    const rawTime = Math.max(0, mouseX / zoom - leadInMargin);
    const time = snapValue !== 'free' ? snapTime(rawTime, snapValue, bpm) : rawTime;
    clearLocalSelection();
    captureSelectionUndo();
    setSelectionMode('global');
    rulerDragStartRef.current = time;
    isDraggingRulerRef.current = true;
    if (e.shiftKey) {
      e.preventDefault();
      e.stopPropagation();
      const anchor = rulerAnchorRef.current !== null && rulerAnchorRef.current !== undefined ? rulerAnchorRef.current : selectionStart !== null && selectionStart !== undefined ? selectionStart : currentTime;
      const selS = Math.max(0, Math.min(anchor, time));
      const selE = Math.max(0, Math.max(anchor, time));
      setSelectionStart(selS);
      setSelectionEnd(selE);
    } else {
      rulerAnchorRef.current = Math.max(0, time);
    }
    handlePlayheadSet(time);
  };

  // Global Ruler mousemove is tracked via document listener set up in useEffect
  useEffect(() => {
    const handleMouseMove = e => {
      if (!isDraggingRulerRef.current) return;
      const wrapper = timelineWrapperRef.current;
      if (!wrapper) return;
      const rect = wrapper.getBoundingClientRect();
      const scrollLeft = wrapper.scrollLeft;
      const mouseX = e.clientX - rect.left + scrollLeft;
      const rawTime = Math.max(0, Math.min(maxDuration, mouseX / zoom - leadInMarginRef.current));
      const time = snapValueRef.current !== 'free' ? snapTime(rawTime, snapValueRef.current, bpmRef.current) : rawTime;
      const anchor = rulerAnchorRef.current ?? rulerDragStartRef.current ?? time;
      setSelectionStart(Math.min(anchor, time));
      setSelectionEnd(Math.max(anchor, time));
    };
    const handleMouseUp = () => {
      if (isDraggingRulerRef.current) {
        isDraggingRulerRef.current = false;
        rulerDragStartRef.current = null;
        pushSelectionUndo();
      }
    };
    document.addEventListener('mousemove', handleMouseMove);
    document.addEventListener('mouseup', handleMouseUp);
    // Sub-tab ruler drag (section/editor tabs)
    const handleSubTabMove = e => {
      if (!isDraggingSubTabRef.current) return;
      const wrapper = timelineWrapperRef.current;
      if (!wrapper) return;
      const rect = wrapper.getBoundingClientRect();
      const sl = wrapper.scrollLeft;
      const raw = Math.max(0, (e.clientX - rect.left + sl) / zoom);
      const time = snapValueRef.current !== 'free' ? snapTime(raw, snapValueRef.current, bpmRef.current) : raw;
      const anchor = subTabDragStartRef.current ?? 0;
      setSubTabs(prev => prev.map(s => s.id === activeTabRef.current ? { ...s, selectionStart: Math.min(anchor, time), selectionEnd: Math.max(anchor, time) } : s));
    };
    const handleSubTabUp = () => {
      if (isDraggingSubTabRef.current) { isDraggingSubTabRef.current = false; subTabDragStartRef.current = null; }
    };
    document.addEventListener('mousemove', handleSubTabMove);
    document.addEventListener('mouseup', handleSubTabUp);
    return () => {
      document.removeEventListener('mousemove', handleMouseMove);
      document.removeEventListener('mouseup', handleMouseUp);
      document.removeEventListener('mousemove', handleSubTabMove);
      document.removeEventListener('mouseup', handleSubTabUp);
    };
  }, [zoom, maxDuration]);

  // ── Track Lane Local Selection Drag ──
  const localDragInProgressRef = useRef(false);
  const localDragTrackRef = useRef(null);
  const localDragStartTimeRef = useRef(0);
  const localSelectionAnchorRef = useRef(null);
  const handleTrackLaneMouseDown = (trackId, time) => {
    setSelectedTrackId(trackId);
    captureSelectionUndo();
    clearLocalSelection();
    // A fresh region selection re-enables selection-looping (a previous
    // Ctrl+click "clear selection" must not keep blocking the new region).
    setSelectionCleared(false);
    localSelectionAnchorRef.current = time;
    setSelectionMode('local');
    setLocalSelectionTrackId(trackId);
    setLocalSelectionStart(time);
    setLocalSelectionEnd(time);
    setSelectionStart(time);
    setSelectionEnd(time);
    localDragInProgressRef.current = true;
    localDragTrackRef.current = trackId;
    localDragStartTimeRef.current = time;
  };

  // Document-level mousemove/mouseup for local selection drag
  useEffect(() => {
    const handleMouseMove = e => {
      if (!localDragInProgressRef.current) return;
      const wrapper = timelineWrapperRef.current;
      if (!wrapper) return;
      const rect = wrapper.getBoundingClientRect();
      const scrollLeft = wrapper.scrollLeft;
      const mouseX = e.clientX - rect.left + scrollLeft;
      const time = Math.max(0, Math.min(maxDuration, mouseX / zoom));
      const anchor = localSelectionAnchorRef.current ?? localDragStartTimeRef.current;
      const selS = Math.min(anchor, time);
      const selE = Math.max(anchor, time);
      setLocalSelectionStart(selS);
      setLocalSelectionEnd(selE);
      setSelectionStart(selS);
      setSelectionEnd(selE);
    };
    const handleMouseUp = () => {
      if (localDragInProgressRef.current) {
        localDragInProgressRef.current = false;
        localDragTrackRef.current = null;
        localDragStartTimeRef.current = 0;
        pushSelectionUndo();
      }
    };
    document.addEventListener('mousemove', handleMouseMove);
    document.addEventListener('mouseup', handleMouseUp);
    return () => {
      document.removeEventListener('mousemove', handleMouseMove);
      document.removeEventListener('mouseup', handleMouseUp);
    };
  }, [zoom, maxDuration]);
  const draggedClipRef = useRef(null);
  draggedClipRef.current = draggedClip;
  const hoveredTrackIdRef = useRef(null);
  hoveredTrackIdRef.current = hoveredTrackId;
  const captureTrackSnapshotRef = useRef(null);
  captureTrackSnapshotRef.current = captureTrackSnapshot;
  const draggedSectionItemRef = useRef(null);
  draggedSectionItemRef.current = draggedSectionItem;
  const resizedSectionItemRef = useRef(null);
  resizedSectionItemRef.current = resizedSectionItem;
  const selectionUndoRef = useRef(null);
  const pushSelectionUndo = () => {
    var cur = selectionRef.current;
    var before = selectionUndoRef.current;
    if (!before || (before.start === cur.start && before.end === cur.end && before.mode === cur.mode)) return;
    var entry = {
      type: 'SELECTION',
      scope: 'global',
      label: 'Selection',
      before: before,
      after: { start: cur.start, end: cur.end, mode: cur.mode },
      undo: (e) => { setSelectionStart(e.before.start); setSelectionEnd(e.before.end); setSelectionMode(e.before.mode); showToast('Undo: Selection', 'info'); },
      redo: (e) => { setSelectionStart(e.after.start); setSelectionEnd(e.after.end); setSelectionMode(e.after.mode); showToast('Redo: Selection', 'info'); }
    };
    if (window.UndoRedoEngine) window.UndoRedoEngine.execute(entry);
    selectionUndoRef.current = null;
  };
  const captureSelectionUndo = () => {
    if (selectionUndoRef.current !== null) return;
    selectionUndoRef.current = { start: selectionRef.current.start, end: selectionRef.current.end, mode: selectionMode };
  };
  let clipSeqCounter = 0;
  const nextClipId = () => `clip_${Date.now()}_${++clipSeqCounter}`;

  const handleClipDragStart = (trackId, clipId, clickOffset, isDuplicate = false) => {
    const curTracks = activeTracksRef.current || activeTracks;
    const track = curTracks.find(t => t.id === trackId);
    if (!track) return;
    const existingClips = track.clips && track.clips.length > 0 ? track.clips : track.buffer ? [{
      id: 'default_' + track.id,
      buffer: track.buffer,
      startTime: track.startTime || 0,
      name: track.name
    }] : [];
    const clip = existingClips.find(c => c.id === clipId || clipId === 'default' && c.id === 'default_' + track.id);
    if (!clip) return;
    const beforeSnap = captureTrackSnapshot(trackId);
    if (isDuplicate) {
      const cloneId = nextClipId();
      const clone = { ...clip, id: cloneId, startTime: clip.startTime || 0, name: clip.name + ' (Copy)' };
      updateActiveTracks(prev => prev.map(t => {
        if (t.id === trackId) {
          const newClips = [...existingClips, clone];
          return { ...t, clips: newClips, buffer: newClips[0].buffer, startTime: newClips[0].startTime, name: newClips[0].name };
        }
        return t;
      }));
      setDraggedClip({
        trackId, clipId: cloneId, clickOffset, buffer: clip.buffer,
        name: clip.name + ' (Copy)', beforeSnap, isDuplicate: false
      });
      return;
    }
    if (!track.clips || track.clips.length === 0) {
      updateActiveTracks(prev => prev.map(t => {
        if (t.id === trackId) {
          return { ...t, clips: existingClips };
        }
        return t;
      }));
    }
    setDraggedClip({
      trackId, clipId: clip.id, clickOffset,
      buffer: clip.buffer, name: clip.name, beforeSnap,
      isDuplicate: false
    });
  };
  const handleClipDragStartRef = useRef(null);
  handleClipDragStartRef.current = handleClipDragStart;
  const stretchedClipRef = useRef(null);
  stretchedClipRef.current = stretchedClip;
  const handleClipStretchStart = (trackId, clipId, clickTime) => {
    const curTracks = activeTracksRef.current || activeTracks;
    const track = curTracks.find(t => t.id === trackId);
    if (!track) return;
    const clips = track.clips && track.clips.length > 0 ? track.clips : track.buffer ? [{
      id: 'default_' + track.id,
      buffer: track.buffer,
      startTime: track.startTime || 0,
      name: track.name,
      speed: track.speed || 1.0
    }] : [];
    const clip = clips.find(c => c.id === clipId || clipId === 'default' && c.id === 'default_' + trackId);
    if (!clip || !clip.buffer) return;
    const beforeSnap = captureTrackSnapshot(trackId);
    if (!track.clips || track.clips.length === 0) {
      updateActiveTracks(prev => prev.map(t => t.id === trackId ? {
        ...t,
        clips
      } : t));
    }
    setStretchedClip({
      trackId,
      clipId: clip.id === 'default' ? 'default_' + trackId : clip.id,
      originalDuration: clip.buffer.duration,
      startTime: clip.startTime,
      originalSpeed: clip.speed || 1.0,
      beforeSnap
    });
  };
  const handleSelectionEdgeDragStart = (e, trackId, side) => {
    const startX = e.clientX;
    const initialLeft = Math.min(localSelectionStart, localSelectionEnd);
    const initialRight = Math.max(localSelectionStart, localSelectionEnd);
    const handleMouseMove = moveEvent => {
      const deltaX = moveEvent.clientX - startX;
      const deltaSec = deltaX / zoom;
      if (side === 'left') {
        const newLeft = Math.max(0, Math.min(initialRight - 0.05, initialLeft + deltaSec));
        setLocalSelectionStart(newLeft);
        setLocalSelectionEnd(initialRight);
      } else {
        const newRight = Math.max(initialLeft + 0.05, Math.min(maxDuration, initialRight + deltaSec));
        setLocalSelectionStart(initialLeft);
        setLocalSelectionEnd(newRight);
      }
    };
    const handleMouseUp = () => {
      document.removeEventListener('mousemove', handleMouseMove);
      document.removeEventListener('mouseup', handleMouseUp);
    };
    document.addEventListener('mousemove', handleMouseMove);
    document.addEventListener('mouseup', handleMouseUp);
  };
  const handleTrackResizeMouseDown = (e, trackId) => {
    e.preventDefault();
    e.stopPropagation();
    const startY = e.clientY;
    const track = tracks.find(t => t.id === trackId);
    const startHeight = track ? track.height || 140 : 140;
    const handleMouseMove = moveEvent => {
      const deltaY = moveEvent.clientY - startY;
      const newHeight = Math.max(110, Math.min(300, startHeight + deltaY));
      setTracks(prev => prev.map(t => t.id === trackId ? {
        ...t,
        height: newHeight
      } : t));
    };
    const handleMouseUp = () => {
      document.removeEventListener('mousemove', handleMouseMove);
      document.removeEventListener('mouseup', handleMouseUp);
    };
    document.addEventListener('mousemove', handleMouseMove);
    document.addEventListener('mouseup', handleMouseUp);
  };
  const deleteTrack = trackId => {
    const sessionTab = sessionTabs.find(s => s.id === activeTab);
    const trackList = sessionTab ? sessionTab.tracks : tracks;
    const track = trackList.find(t => t.id === trackId);
    if (!track) return;

    const hasClips = (track.clips && track.clips.some(c => c.buffer)) || !!track.buffer;
    const hasMidi = track.midiItems && track.midiItems.some(m => (m.notes && m.notes.length > 0));
    const hasSections = track.sections && track.sections.some(s => (s.tracks && s.tracks.some(st => (st.clips && st.clips.some(c => c.buffer)) || (st.midiItems && st.midiItems.some(m => m.notes && m.notes.length > 0)))));
    const isTrackEmpty = !hasClips && !hasMidi && !hasSections;

    if (!isTrackEmpty) {
      showToast('Không thể xoá track chứa dữ liệu. Vui lòng xoá hết các item trước khi xoá track.', 'warning');
      return;
    }

    if (sessionTab) {
      updateActiveTracks(prev => {
        const filtered = prev.filter(t => t.id !== trackId);
        if (filtered.length > 0) setSelectedTrackId(filtered[0].id);
        return filtered;
      });
    } else {
      const beforeSnap = captureTrackSnapshot(trackId);
      setTracks(prev => {
        const filtered = prev.filter(t => t.id !== trackId);
        if (filtered.length > 0) setSelectedTrackId(filtered[0].id);
        return filtered;
      });
    }
    showToast('Đã xóa track.', 'info');
  };
  // Shared auto-scroll: when mouse near right edge, scroll container right
  const autoScrollTimeline = (clientX) => {
    const wrapper = timelineWrapperRef.current;
    if (!wrapper) return;
    const wr = wrapper.getBoundingClientRect();
    const margin = 200;
    if (clientX > wr.right - margin) {
      const depth = (clientX - (wr.right - margin)) / margin;
      const speed = Math.round(5 + depth * depth * 40);
      wrapper.scrollLeft += speed;
      if (wrapper.scrollLeft > wrapper.scrollWidth - wrapper.clientWidth - 100) {
        const secPerBar = (60.0 / (parseInt(bpm) || 120)) * 4;
        setScrollBufferExtra(prev => prev + secPerBar * 4);
      }
    } else if (clientX < wr.left + margin) {
      const depth = ((wr.left + margin) - clientX) / margin;
      const speed = Math.round(5 + depth * depth * 40);
      wrapper.scrollLeft = Math.max(0, wrapper.scrollLeft - speed);
    }
  };

  useEffect(() => {
    const handleMouseMove = e => {
      const drag = draggedClipRef.current;
      if (!drag) return;
      const wrapper = timelineWrapperRef.current;
      if (!wrapper) return;
      autoScrollTimeline(e.clientX);
      const rect = wrapper.getBoundingClientRect();
      const scrollLeft = wrapper.scrollLeft;
      const mouseX = e.clientX - rect.left + scrollLeft;
      const time = Math.max(0, mouseX / zoom - leadInMarginRef.current);
      const beatSec = (60.0 / (parseInt(bpmRef.current) || 120));
      const rawStart = Math.max(0, time - drag.clickOffset);
      const secPerBar = beatSec * 4;
      const marginBar = maxDurationRef.current - secPerBar;
      const clampedStart = Math.min(rawStart, marginBar);
      const newStart = snapTime(Math.max(0, clampedStart), snapValueRef.current, bpmRef.current);
      const itemPx = newStart * zoom;
      const keepMargin = 80;
      if (itemPx > scrollLeft + rect.width - keepMargin) {
        wrapper.scrollLeft = itemPx - rect.width + keepMargin;
        setCanvasRedrawCount(n => n + 1);
      } else if (itemPx < scrollLeft + keepMargin) {
        wrapper.scrollLeft = Math.max(0, itemPx - keepMargin);
        setCanvasRedrawCount(n => n + 1);
      }
      const targetTrackId = hoveredTrackIdRef.current || drag.trackId;
      updateActiveTracks(prev => prev.map(t => {
        // Clear the clip from its previous track if it moved to a new track
        if (t.id === drag.trackId && drag.trackId !== targetTrackId) {
          const updatedClips = (t.clips || []).filter(c => c.id !== drag.clipId);
          return {
            ...t,
            clips: updatedClips,
            buffer: updatedClips.length > 0 ? updatedClips[0].buffer : null,
            startTime: updatedClips.length > 0 ? updatedClips[0].startTime : 0,
            name: updatedClips.length > 0 ? updatedClips[0].name : `Track ${t.id}`
          };
        }
        // Update/set clip on target track
        if (t.id === targetTrackId) {
          const existingClips = t.clips && t.clips.length > 0 ? t.clips : t.buffer ? [{
            id: 'default_' + t.id,
            buffer: t.buffer,
            startTime: t.startTime || 0,
            name: t.name
          }] : [];
          const hasClip = existingClips.some(c => c.id === drag.clipId);
          let updatedClips;
          if (hasClip) {
            updatedClips = existingClips.map(c => c.id === drag.clipId ? {
              ...c,
              startTime: newStart
            } : c);
          } else {
            updatedClips = [...existingClips, {
              id: drag.clipId,
              buffer: drag.buffer,
              startTime: newStart,
              name: drag.name
            }];
          }
          return {
            ...t,
            clips: updatedClips,
            buffer: updatedClips[0].buffer,
            startTime: updatedClips[0].startTime,
            name: updatedClips[0].name
          };
        }
        return t;
      }));
      if (drag.trackId !== targetTrackId) {
        setDraggedClip(prev => ({
          ...prev,
          trackId: targetTrackId
        }));
      }
    };
    const handleMouseUp = () => {
      const drag = draggedClipRef.current;
      if (!drag) return;
      const afterSnap = captureTrackSnapshotRef.current(drag.trackId);
      pushAction('MOVE_CLIP', drag.trackId, drag.beforeSnap, afterSnap);
      setDraggedClip(null);
      showToast('Đã di chuyển clip.', 'success');
      // Realtime: clip vừa thả — re-schedule NGAY để phát theo vị trí mới
      // (kéo clip về đúng playhead → phát ngay, không chờ debounce).
      triggerRescheduleNow();
    };
    document.addEventListener('mousemove', handleMouseMove);
    document.addEventListener('mouseup', handleMouseUp);
    return () => {
      document.removeEventListener('mousemove', handleMouseMove);
      document.removeEventListener('mouseup', handleMouseUp);
    };
  }, [zoom]);

  // Document-level mousemove/mouseup for clip stretching
  useEffect(() => {
    const handleMouseMove = e => {
      const stretch = stretchedClipRef.current;
      if (!stretch) return;
      autoScrollTimeline(e.clientX);
      const wrapper = timelineWrapperRef.current;
      if (!wrapper) return;
      const rect = wrapper.getBoundingClientRect();
      const scrollLeft = wrapper.scrollLeft;
      const mouseX = e.clientX - rect.left + scrollLeft;
      const time = mouseX / zoom;
      const secPerBar = (60.0 / (parseInt(bpmRef.current) || 120)) * 4;
      const marginBar = maxDurationRef.current - secPerBar;
      const maxEnd = Math.min(time, marginBar);
      const newDuration = Math.max(0.1, maxEnd - stretch.startTime);
      const speedRatio = stretch.originalDuration / newDuration;
      updateActiveTracks(prev => prev.map(t => {
        if (t.id === stretch.trackId) {
          const updatedClips = (t.clips || []).map(c => {
            if (c.id === stretch.clipId) {
              return {
                ...c,
                speed: speedRatio
              };
            }
            return c;
          });
          return {
            ...t,
            clips: updatedClips,
            buffer: updatedClips[0]?.buffer,
            startTime: updatedClips[0]?.startTime || 0,
            speed: updatedClips[0]?.speed || 1.0
          };
        }
        return t;
      }));
    };
    const handleMouseUp = () => {
      const stretch = stretchedClipRef.current;
      if (!stretch) return;
      const afterSnap = captureTrackSnapshotRef.current(stretch.trackId);
      pushAction('STRETCH_CLIP', stretch.trackId, stretch.beforeSnap, afterSnap);
      setStretchedClip(null);
      showToast('Đã giãn thời gian clip.', 'success');
    };
    document.addEventListener('mousemove', handleMouseMove);
    document.addEventListener('mouseup', handleMouseUp);
    return () => {
      document.removeEventListener('mousemove', handleMouseMove);
      document.removeEventListener('mouseup', handleMouseUp);
    };
  }, [zoom]);

  // ── Deselect single item ──
  const handleDeselectItem = itemId => {
    captureSelectionUndo();
    setSelectedItemIds(prev => {
      var next = new Set(prev);
      next.delete(itemId);
      return next;
    });
  };

  // ── Add item to selectedItemIds ──
  const handleSaveSectionTabRef = useRef(handleSaveSectionTab);
  handleSaveSectionTabRef.current = handleSaveSectionTab;
  const handleAddToSelection = itemId => {
    captureSelectionUndo();
    setSelectedItemIds(prev => {
      var next = new Set(prev);
      next.add(itemId);
      return next;
    });
  };
  const handleSetPendingDrag = (trackId, itemType, itemId, clickOffset, e, preToggleSnapshot) => {
    // Compute selectedIds AFTER the toggle that just happened:
    // - If itemId was in snapshot (was selected) → toggle removed it → delete
    // - If itemId was not in snapshot (was not selected) → toggle added it → add
    var ids = preToggleSnapshot ? new Set(preToggleSnapshot) : new Set(selectedItemIds);
    if (preToggleSnapshot && preToggleSnapshot.has(itemId)) {
      ids.delete(itemId);
    } else {
      ids.add(itemId);
    }
    pendingDragRef.current = { trackId, itemType, itemId, clickOffset, startX: e.clientX, startY: e.clientY, selectedIds: ids, duplicate: true };
  };

  // Plain click on an item: select only; the real drag (move) starts after a
  // 5px movement threshold. Unifies section/MIDI/clip behavior and prevents
  // accidental moves from mouse jitter.
  const handleSetPendingDragMove = (trackId, itemType, itemId, clickOffset, e, dragIds) => {
    pendingDragRef.current = { trackId, itemType, itemId, clickOffset, startX: e.clientX, startY: e.clientY, selectedIds: dragIds, duplicate: false };
  };

  // ── Sweep Select ──
  const handleSweepSelectStart = (trackId, startTime, startY) => {
    isSweepingRef.current = true;
    sweepStartRef.current = startTime;
    sweepTrackIdRef.current = trackId;
    sweepStartYRef.current = startY || 0;
    sweepEndYRef.current = startY || 0;
    var init = { startTime, endTime: startTime };
    sweepSelectRef.current = init;
    setSweepSelect(init);
    // Do NOT clear selection here – wait until mouseup.
    // Small movement → deselect all; large movement → marquee toggle.
  };

  // ── Section / MIDI Item Drag Start ──
  const handleSectionItemDragStart = (trackId, itemType, itemId, clickOffset, isDuplicate, pendingSelectedIds) => {
    var curTracks = activeTracksRef.current || activeTracks;
    var multiIds = null;
    var selIds = pendingSelectedIds || selectedItemIds;
    if (selIds && selIds.size > 0 && selIds.has(itemId)) {
      var selArr = Array.from(selIds);
      var originals = {};
      curTracks.forEach(function(t) {
      (t.sections || []).forEach(function(s) {
        if (selArr.indexOf(s.id) >= 0) originals[s.id] = { type: 'section', start: s.start, trackId: t.id };
      });
      (t.midiItems || []).forEach(function(m) {
        if (selArr.indexOf(m.id) >= 0) originals[m.id] = { type: 'midiItem', start: m.startTime, trackId: t.id };
      });
      (t.clips || []).forEach(function(c) {
        var cid = c.id === 'default' ? 'default_' + t.id : c.id;
        if (selArr.indexOf(cid) >= 0) originals[cid] = { type: 'clip', start: c.startTime, trackId: t.id };
      });
      });
      if (Object.keys(originals).length > 0) multiIds = originals;
    }
    if (isDuplicate) {
      var track = curTracks.find(function(t) { return t.id === trackId; });
      if (!track) return;
      if (multiIds) {
        var newOriginals = {};
        var dupBeforeSnap = captureAllTracksSnapshot();
        updateActiveTracks(function(prev) {
          return prev.map(function(t) {
            var updatedSections = t.sections ? t.sections.slice() : [];
            var updatedMidi = t.midiItems ? t.midiItems.slice() : [];
            var updatedClips = t.clips ? t.clips.slice() : [];
            Object.keys(multiIds).forEach(function(oid) {
              var info = multiIds[oid];
              if (info.type === 'section') {
                var sec = (t.sections || []).find(function(s) { return s.id === oid; });
                if (sec) {
                  var rearrangeNewId = 'sec_dup_' + Date.now() + '_' + Math.random().toString(36).substr(2, 5);
                  updatedSections.push({ ...sec, id: rearrangeNewId, name: sec.name + ' (Copy)' });
                  newOriginals[rearrangeNewId] = { type: 'section', start: sec.start };
                }
              } else if (info.type === 'midiItem') {
                var mid = (t.midiItems || []).find(function(m) { return m.id === oid; });
                if (mid) {
                  var rearrangeNewId = 'midi_dup_' + Date.now() + '_' + Math.random().toString(36).substr(2, 5);
                  updatedMidi.push({ ...mid, id: rearrangeNewId, name: mid.name + ' (Copy)' });
                  newOriginals[rearrangeNewId] = { type: 'midiItem', start: mid.startTime };
                }
              } else if (info.type === 'clip') {
                var clip = (t.clips || []).find(function(c) { return c.id === oid || 'default_' + t.id === oid; });
                if (clip) {
                  var rearrangeNewId = 'clip_dup_' + Date.now() + '_' + Math.random().toString(36).substr(2, 5);
                  updatedClips.push({ ...clip, id: rearrangeNewId, startTime: clip.startTime, name: clip.name + ' (Copy)' });
                  newOriginals[rearrangeNewId] = { type: 'clip', start: clip.startTime };
                }
              }
            });
            return { ...t, sections: updatedSections, midiItems: updatedMidi, clips: updatedClips };
          });
        });
    var newItemId = Object.keys(newOriginals)[0] || itemId;
    var origPos = newOriginals[newItemId] || { start: 0 };
    setDraggedSectionItem({ trackId, itemType, itemId: newItemId, clickOffset, multiIds: newOriginals, originalPositions: { [newItemId]: origPos.start }, beforeSnap: dupBeforeSnap });
      } else {
        var items = itemType === 'section' ? (track.sections || []) : (track.midiItems || []);
        var item = items.find(function(it) { return it.id === itemId; });
        if (!item) return;
        var rearrangeNewId = itemType + '_dup_' + Date.now();
        var newItem = { ...item, id: rearrangeNewId, name: item.name + ' (Copy)' };
        var singleDupBeforeSnap = captureAllTracksSnapshot();
        updateActiveTracks(function(prev) {
          return prev.map(function(t) {
            if (t.id !== trackId) return t;
            var updated = itemType === 'section' ? [...(t.sections || []), newItem] : [...(t.midiItems || []), newItem];
            return itemType === 'section' ? { ...t, sections: updated } : { ...t, midiItems: updated };
          });
        });
        setDraggedSectionItem({ trackId, itemType, itemId: rearrangeNewId, clickOffset, isDuplicate: false, originalPositions: { [rearrangeNewId]: itemType === 'section' ? item.start : item.startTime }, beforeSnap: singleDupBeforeSnap });
      }
    } else {
      var curTrk = activeTracksRef.current ? activeTracksRef.current.find(t => t.id === trackId) : null;
      var its = itemType === 'section' ? (curTrk?.sections || []) : (itemType === 'clip' ? (curTrk?.clips || []) : (curTrk?.midiItems || []));
      var it = its.find(function(x) { return x.id === itemId || (itemType === 'clip' && itemId === 'default_' + curTrk?.id && x.id === 'default'); });
      var origPos = it ? (itemType === 'section' ? it.start : it.startTime) : 0;
      var beforeSnap = captureAllTracksSnapshot();
      setDraggedSectionItem({ trackId, itemType, itemId, clickOffset, multiIds, originalPositions: { [itemId]: origPos }, beforeSnap });
    }
  };
  handleSectionItemDragStartRef.current = handleSectionItemDragStart;

  // ── Section / MIDI Item Resize Start ──
  const handleSectionItemResizeStart = (trackId, itemType, itemId, side, clickTime) => {
    const curTracks = activeTracks;
    const track = curTracks.find(t => t.id === trackId);
    if (!track) return;
    const items = itemType === 'section' ? track.sections : track.midiItems;
    const item = (items || []).find(it => it.id === itemId);
    if (!item) return;
    const start = itemType === 'section' ? item.start : item.startTime;
    setResizedSectionItem({ trackId, itemType, itemId, side, originalStart: start, originalDuration: item.duration });
  };

  // ── Document-level mousemove/mouseup for Section/MIDI item drag ──
  useEffect(() => {
    const handleMouseMove = e => {
      const drag = draggedSectionItemRef.current;
      if (!drag) return;
      const wrapper = timelineWrapperRef.current;
      if (!wrapper) return;
      autoScrollTimeline(e.clientX);
      const rect = wrapper.getBoundingClientRect();
      const scrollLeft = wrapper.scrollLeft;
      const mouseX = e.clientX - rect.left + scrollLeft;
      const time = Math.max(0, mouseX / zoom - leadInMarginRef.current);
      const beatSec = (60.0 / (parseInt(bpm) || 120));
      const secondsPerBar = beatSec * 4;
      const marginBar = maxDurationRef.current - secondsPerBar;
      const rawStart = Math.max(0, Math.min(time - drag.clickOffset, marginBar));
      const newStart = snapTime(rawStart, snapValueRef.current, bpmRef.current);
      const itemPx = newStart * zoom;
      const keepMargin = 80;
      if (itemPx > scrollLeft + rect.width - keepMargin) {
        wrapper.scrollLeft = itemPx - rect.width + keepMargin;
        setCanvasRedrawCount(n => n + 1);
      } else if (itemPx < scrollLeft + keepMargin) {
        wrapper.scrollLeft = Math.max(0, itemPx - keepMargin);
        setCanvasRedrawCount(n => n + 1);
      }
      // Compute target track from mouse Y position
      var allTrks = activeTracksRef.current || [];
      var mouseY = e.clientY - rect.top;
      var trackTop = 0;
      var targetIdx = -1;
      for (var ti = 0; ti < allTrks.length; ti++) {
        var tH = allTrks[ti].height || (allTrks[ti].isArmed ? 164 : 140);
        if (mouseY >= trackTop && mouseY < trackTop + tH) { targetIdx = ti; break; }
        trackTop += tH;
      }
      if (targetIdx < 0) targetIdx = mouseY < trackTop ? 0 : Math.max(0, allTrks.length - 1);
      var targetTrackId = allTrks[targetIdx] ? allTrks[targetIdx].id : drag.trackId;
      // Use ORIGINAL trackId from drag start for crossOffset math (never changes)
      var dragBaseTrackId = drag.multiIds && drag.multiIds[drag.itemId] ? drag.multiIds[drag.itemId].trackId : drag.trackId;
      // Pre-compute crossOffset from current allTrks (stable per mousemove)
      var baseIdx = allTrks.findIndex(function(tr) { return tr.id === dragBaseTrackId; });
      if (baseIdx < 0) baseIdx = 0;
      var trgIdx = allTrks.findIndex(function(tr) { return tr.id === targetTrackId; });
      if (trgIdx < 0) trgIdx = allTrks.length - 1;
      if (trgIdx < 0) trgIdx = 0;
      var crossOffset = trgIdx - baseIdx;
      // Special path for multi-select drag: handle items from multiple tracks
      if (drag.multiIds) {
        var dragOrigStart = drag.multiIds[drag.itemId] ? drag.multiIds[drag.itemId].start : 0;
        var delta = newStart - dragOrigStart;
        // Extend activeTracks if target track index exceeds track count
        var outTracks = [];
        Object.keys(drag.multiIds).forEach(function(mid) {
          var inf = drag.multiIds[mid];
          var oIdx = allTrks.findIndex(function(tr) { return tr.id === inf.trackId; });
          if (oIdx < 0) oIdx = baseIdx;
          var needIdx = oIdx + crossOffset;
          if (needIdx >= allTrks.length) {
            for (var ai = allTrks.length; ai <= needIdx; ai++) {
              var exists = outTracks.some(function(ot) { return ot.id === (ai + 1).toString(); });
              if (!exists) {
                var colors = ['#0f766e', '#1d4ed8', '#701a75', '#a21caf', '#b45309'];
                outTracks.push({ id: (ai + 1).toString(), name: 'Track ' + (ai + 1), buffer: null, startTime: 0, volumeDb: 0, pan: 0, muted: false, solo: false, color: colors[ai % colors.length], clips: [], midiItems: [], sections: [], markers: [], isArmed: false, height: 140 });
              }
            }
          }
        });
        updateActiveTracks(function(prev) {
          var trkIds = prev.map(function(tr) { return tr.id; });
          var merged = outTracks.length > 0 ? prev.concat(outTracks.filter(function(ot) { return !trkIds.includes(ot.id); })) : prev;
          var allIds = merged.map(function(tr) { return tr.id; });
          return merged.map(function(t) {
            var resS = (t.sections || []).slice();
            var resM = (t.midiItems || []).slice();
            var resC = (t.clips || []).slice();
            Object.keys(drag.multiIds).forEach(function(mid) {
              var inf = drag.multiIds[mid];
              var newVal = inf.start + delta;
              var oIdx = allIds.indexOf(inf.trackId);
              if (oIdx < 0) oIdx = baseIdx;
              var needIdx = Math.max(0, oIdx + crossOffset);
              var targetTid = needIdx < merged.length ? merged[needIdx].id : merged[merged.length - 1].id;
              if (inf.type === 'section') {
                resS = resS.filter(function(s) { return s.id !== mid; });
                if (t.id === targetTid) {
                  var src = null;
                  for (var p = 0; p < prev.length; p++) { src = (prev[p].sections || []).find(function(s) { return s.id === mid; }); if (src) break; }
                  if (src) resS.push({ ...src, start: Math.max(0, newVal) });
                }
              } else if (inf.type === 'midiItem') {
                resM = resM.filter(function(mx) { return mx.id !== mid; });
                if (t.id === targetTid) {
                  var src = null;
                  for (var p = 0; p < prev.length; p++) { src = (prev[p].midiItems || []).find(function(mx) { return mx.id === mid; }); if (src) break; }
                  if (src) resM.push({ ...src, startTime: Math.max(0, newVal) });
                }
              } else if (inf.type === 'clip') {
                var cid3 = 'default_' + t.id;
                resC = resC.filter(function(cx) { return cx.id !== mid && cx.id !== cid3; });
                if (t.id === targetTid) {
                  var src = null;
                  for (var p = 0; p < prev.length; p++) { var c2 = (prev[p].clips || []).find(function(cx) { return cx.id === mid || 'default_' + prev[p].id === mid; }); if (c2) { src = c2; break; } }
                  if (src) resC.push({ ...src, startTime: Math.max(0, newVal) });
                }
              }
            });
            return { ...t, sections: resS, midiItems: resM, clips: resC };
          });
        });
      } else {
        // Single item drag path
        updateActiveTracks(function(prev) {
          let movedItem = null;
          for (let track of prev) {
            const sec = (track.sections || []).find(function(it) { return it.id === drag.itemId; });
            const mid = (track.midiItems || []).find(function(it) { return it.id === drag.itemId; });
            if (sec || mid) { movedItem = sec || mid; break; }
          }
          return prev.map(function(tr) {
            var its = drag.itemType === 'section'
              ? (tr.sections || []).filter(function(it) { return it.id !== drag.itemId; })
              : (tr.midiItems || []).filter(function(it) { return it.id !== drag.itemId; });
            if (tr.id === targetTrackId && movedItem) {
              its.push(drag.itemType === 'section'
                ? { ...movedItem, start: newStart }
                : { ...movedItem, startTime: newStart });
            }
            return drag.itemType === 'section' ? { ...tr, sections: its } : { ...tr, midiItems: its };
          });
        });
      }

    };
    const handleMouseUp = () => {
      const drag = draggedSectionItemRef.current;
      if (!drag) return;
      var changed = false;
      var trackId = drag.trackId;
      var beforeSnap = drag.beforeSnap;
      var origs = drag.originalPositions || {};
      updateActiveTracks(function(prev) {
        return prev.map(function(t) {
          var allItemIds = Object.keys(origs);
          var updatedSections = (t.sections || []).slice();
          var updatedMidi = (t.midiItems || []).slice();
          var hasChange = false;
          for (var i = 0; i < allItemIds.length; i++) {
            var oid = allItemIds[i];
            var origStart = origs[oid];
            if (t.id === trackId || drag.multiIds && Object.values(drag.multiIds).some(function(v) { return v.trackId === t.id && v.type === (oid.startsWith('sec_') ? 'section' : oid.startsWith('midi_') ? 'midiItem' : 'clip'); })) {
              var sec = updatedSections.find(function(s) { return s.id === oid; });
              var mid = updatedMidi.find(function(m) { return m.id === oid; });
              var item = sec || mid;
              if (item) {
                var curStart = sec ? item.start : item.startTime;
                if (Math.abs(curStart - origStart) > 0.001) {
                  hasChange = true;
                  changed = true;
                }
              }
            }
          }
          if (!hasChange) return t;
          return { ...t, sections: updatedSections, midiItems: updatedMidi };
        });
      });
      if (changed || drag.beforeSnap) {
        var afterSnap = captureAllTracksSnapshot();
        pushAction('MOVE_ITEM', 'ALL_TRACKS', drag.beforeSnap || beforeSnap, afterSnap);
      }
      // Realtime: items vừa thả — re-schedule NGAY theo vị trí mới.
      triggerRescheduleNow();
      if (drag.itemType === 'midiItem') {
        let finalTrackId = null;
        const curTrks = activeTracksRef.current || activeTracks;
        for (const t of curTrks) {
          if ((t.midiItems || []).some(m => m.id === drag.itemId)) {
            finalTrackId = t.id;
            break;
          }
        }
        if (finalTrackId) {
          const targetTrack = curTrks.find(t => t.id === finalTrackId);
          if (targetTrack) {
            setSubTabs(prev => prev.map(st => {
              if (st.target_id === drag.itemId) {
                return {
                  ...st,
                  trackId: finalTrackId,
                  instrumentProgram: targetTrack.instrumentProgram !== undefined ? targetTrack.instrumentProgram : st.instrumentProgram,
                  instrumentName: targetTrack.instrumentName || st.instrumentName
                };
              }
              return st;
            }));
          }
        }
      }
      setDraggedSectionItem(null);
      // Sync section tab → section item trên MAIN sau khi MOVE item (user
      // 08:15 — vị trí item đổi → canvas section item ở MAIN phải theo)
      if (activeTabRef.current && activeTabRef.current.startsWith('session_')) {
        const _tabId = activeTabRef.current;
        setTimeout(() => { try { syncSectionTabToMain(_tabId); } catch (e) {} }, 0);
      }
      showToast('Đã di chuyển ' + (drag.itemType === 'section' ? 'section' : 'MIDI item') + '.', 'success');
    };
    document.addEventListener('mousemove', handleMouseMove);
    document.addEventListener('mouseup', handleMouseUp);
    return () => {
      document.removeEventListener('mousemove', handleMouseMove);
      document.removeEventListener('mouseup', handleMouseUp);
    };
  }, [zoom, activeTab, sessionTabs]);

  // ── Pending drag: click selects; mousemove > 5px threshold starts the real drag ──
  useEffect(() => {
    const handleMouseMove = e => {
      var pd = pendingDragRef.current;
      if (!pd) return;
      var dx = e.clientX - pd.startX;
      if (Math.abs(dx) > 5) {
        var pdSnap = pendingDragRef.current;
        pendingDragRef.current = null;
        if (pdSnap.itemType === 'clip' && !pdSnap.duplicate && (!pdSnap.selectedIds || pdSnap.selectedIds.size <= 1)) {
          // Single clip move → clip drag machinery
          if (handleClipDragStartRef.current) handleClipDragStartRef.current(pdSnap.trackId, pdSnap.itemId, pdSnap.clickOffset, false);
        } else if (handleSectionItemDragStartRef.current) {
          handleSectionItemDragStartRef.current(pdSnap.trackId, pdSnap.itemType, pdSnap.itemId, pdSnap.clickOffset, !!pdSnap.duplicate, pdSnap.selectedIds);
        }
      }
    };
    document.addEventListener('mousemove', handleMouseMove);
    var handleMouseUp = function() { pendingDragRef.current = null; };
    document.addEventListener('mouseup', handleMouseUp);
    return function() {
      document.removeEventListener('mousemove', handleMouseMove);
      document.removeEventListener('mouseup', handleMouseUp);
      pendingDragRef.current = null;
    };
  }, []);

  // ── Document-level mousemove/mouseup for Section/MIDI item resize ──
  useEffect(() => {
    const handleMouseMove = e => {
      const resize = resizedSectionItemRef.current;
      if (!resize) return;
      const wrapper = timelineWrapperRef.current;
      if (!wrapper) return;
      autoScrollTimeline(e.clientX);
      const rect = wrapper.getBoundingClientRect();
      const scrollLeft = wrapper.scrollLeft;
      const mouseX = e.clientX - rect.left + scrollLeft;
      const time = Math.max(0, mouseX / zoom - leadInMarginRef.current);
      updateActiveTracks(prev => prev.map(t => {
        if (t.id !== resize.trackId) return t;
        const items = resize.itemType === 'section' ? [...(t.sections || [])] : [...(t.midiItems || [])];
        const idx = items.findIndex(it => it.id === resize.itemId);
        if (idx === -1) return t;
        const item = items[idx];
        if (resize.side === 'left') {
          const beatSec = (60.0 / (parseInt(bpm) || 120));
          const newStart = Math.max(0, Math.min(time, resize.originalStart + resize.originalDuration - 0.1));
          const end = resize.originalStart + resize.originalDuration;
          const newDuration = end - newStart;
          if (newDuration < 0.1) return t;
          items[idx] = resize.itemType === 'section'
            ? { ...item, start: newStart, duration: newDuration }
            : { ...item, startTime: newStart, duration: newDuration };
        } else {
          const secondsPerBar = (60.0 / (parseInt(bpm) || 120)) * 4;
          const marginBar = maxDurationRef.current - secondsPerBar;
          const clampedTime = Math.min(time, marginBar);
          const snappedDuration = snapValueRef.current !== 'free' ? snapTime(clampedTime - resize.originalStart, snapValueRef.current, bpm) : clampedTime - resize.originalStart;
          const newDuration = Math.max(0.1, snappedDuration);
          items[idx] = { ...item, duration: newDuration };
        }
        return resize.itemType === 'section'
          ? { ...t, sections: items }
          : { ...t, midiItems: items };
      }));
      setCanvasRedrawCount(n => n + 1);
      const edgePx = (resize.side === 'left' ? Math.max(0, time) : time) * zoom;
      const keepMargin = 80;
      if (edgePx > scrollLeft + rect.width - keepMargin) {
        wrapper.scrollLeft = edgePx - rect.width + keepMargin;
        setCanvasRedrawCount(n => n + 1);
      } else if (edgePx < scrollLeft + keepMargin) {
        wrapper.scrollLeft = Math.max(0, edgePx - keepMargin);
        setCanvasRedrawCount(n => n + 1);
      }
    };
    const handleMouseUp = () => {
      const resize = resizedSectionItemRef.current;
      if (!resize) return;
      setResizedSectionItem(null);
      // Sync nội dung section tab → section item trên MAIN sau khi RESIZE item
      // (user 08:15 — kéo ngắn midi item trong SECTION-TAB → canvas section
      // item ở MAIN phải theo đúng duration mới — trước đây chỉ sync khi XÓA)
      if (activeTabRef.current && activeTabRef.current.startsWith('session_')) {
        const _tabId = activeTabRef.current;
        setTimeout(() => { try { syncSectionTabToMain(_tabId); } catch (e) {} }, 0);
      }
    };
    document.addEventListener('mousemove', handleMouseMove);
    document.addEventListener('mouseup', handleMouseUp);
    return () => {
      document.removeEventListener('mousemove', handleMouseMove);
      document.removeEventListener('mouseup', handleMouseUp);
    };
  }, [zoom, activeTab, sessionTabs]);

  // ── Sweep Select mousemove/mouseup ──
  useEffect(() => {
    const handleMouseMove = e => {
      if (!isSweepingRef.current) return;
      const wrapper = timelineWrapperRef.current;
      if (!wrapper) return;
      const rect = wrapper.getBoundingClientRect();
      const scrollLeft = wrapper.scrollLeft;
      const mouseX = e.clientX - rect.left + scrollLeft;
      const time = Math.max(0, mouseX / zoom - leadInMarginRef.current);
      setSweepSelect(prev => {
        var updated = prev ? { ...prev, endTime: time } : null;
        sweepSelectRef.current = updated;
        return updated;
      });
    };
    const handleMouseUp = () => {
      if (!isSweepingRef.current) return;
      isSweepingRef.current = false;
      sweepTrackIdRef.current = null;
      const sweep = sweepSelectRef.current;
      sweepSelectRef.current = null;
      captureSelectionUndo();
      if (sweep) {
        const start = Math.min(sweep.startTime, sweep.endTime);
        const end = Math.max(sweep.startTime, sweep.endTime);
        if (Math.abs(end - start) < 0.02) {
          setSelectedItemIds(new Set());
          setSweepSelect(null);
          pushSelectionUndo();
          return;
        }
        const curTracks = activeTracksRef.current || [];
        const found = new Set();
        curTracks.forEach(t => {
          (t.midiItems || []).forEach(m => {
            if (m.startTime < end && m.startTime + m.duration > start) {
              found.add(m.id);
            }
          });
          (t.sections || []).forEach(s => {
            if (s.start < end && s.start + s.duration > start) {
              found.add(s.id);
            }
          });
          (t.clips || []).forEach(c => {
            const dur = c.buffer ? c.buffer.duration / (c.speed || 1.0) : 4;
            if (c.startTime < end && c.startTime + dur > start) {
              const cid = c.id === 'default' ? 'default_' + t.id : c.id;
              found.add(cid);
            }
          });
        });
        setSelectedItemIds(prev => {
          const next = new Set(prev);
          found.forEach(id => {
            if (next.has(id)) next.delete(id);
            else next.add(id);
          });
          return next;
        });
        setSweepSelect(null);
        sweepTrackIdRef.current = null;
        pushSelectionUndo();
      }
    };
    document.addEventListener('mousemove', handleMouseMove);
    document.addEventListener('mouseup', handleMouseUp);
    return () => {
      document.removeEventListener('mousemove', handleMouseMove);
      document.removeEventListener('mouseup', handleMouseUp);
    };
  }, [zoom]);

  const handleSelectRange = (start, end, reset) => {
    captureSelectionUndo();
    const maxLen = maxDuration;
    const cleanStart = Math.max(0, Math.min(maxLen, start));
    const cleanEnd = Math.max(0, Math.min(maxLen, end));
    if (reset) {
      setSelectionStart(cleanStart);
      setSelectionEnd(cleanEnd);
    } else {
      setSelectionEnd(cleanEnd);
    }
    setSelectionCleared(false);
    pushSelectionUndo();
  };
  const handleSelectionInputChange = (field, val) => {
    captureSelectionUndo();
    const numericVal = Math.max(0, parseFloat(val) || 0);
    if (selectionMode === 'local') {
      if (field === 'start') {
        setLocalSelectionStart(numericVal);
      } else {
        setLocalSelectionEnd(numericVal);
      }
    } else {
      if (field === 'start') {
        setSelectionStart(numericVal);
      } else {
        setSelectionEnd(numericVal);
      }
    }
    pushSelectionUndo();
  };
  const selectionStats = useMemo(() => {
    if (selLeft === null || selRight === null) {
      return {
        start: 0,
        end: 0,
        length: 0
      };
    }
    const s = Math.min(selLeft, selRight);
    const e = Math.max(selLeft, selRight);
    return {
      start: parseFloat(s.toFixed(3)),
      end: parseFloat(e.toFixed(3)),
      length: parseFloat((e - s).toFixed(3))
    };
  }, [selLeft, selRight]);

  // ── Handle Drag (selection resize) ──
  const handleHandleDragStart = (e, side) => {
    e.preventDefault();
    e.stopPropagation();
    const startX = e.clientX;
    const useLocal = selectionMode === 'local';
    const currentStart = useLocal ? localSelectionStart : selectionStart;
    const currentEnd = useLocal ? localSelectionEnd : selectionEnd;
    const initialLeft = Math.min(currentStart, currentEnd);
    const initialRight = Math.max(currentStart, currentEnd);
    const setStart = useLocal ? setLocalSelectionStart : setSelectionStart;
    const setEnd = useLocal ? setLocalSelectionEnd : setSelectionEnd;
    const handleMouseMove = moveEvent => {
      const deltaX = moveEvent.clientX - startX;
      const deltaSec = deltaX / zoom;
      if (side === 'left') {
        const newLeft = Math.max(0, Math.min(initialRight - 0.05, initialLeft + deltaSec));
        setStart(newLeft);
        setEnd(initialRight);
      } else {
        const newRight = Math.max(initialLeft + 0.05, Math.min(maxDuration, initialRight + deltaSec));
        setStart(initialLeft);
        setEnd(newRight);
      }
    };
    const handleMouseUp = () => {
      document.removeEventListener('mousemove', handleMouseMove);
      document.removeEventListener('mouseup', handleMouseUp);
    };
    document.addEventListener('mousemove', handleMouseMove);
    document.addEventListener('mouseup', handleMouseUp);
  };
  const handleSelectionBodyDragStart = e => {
    e.preventDefault();
    e.stopPropagation();
    const startX = e.clientX;
    const useLocal = selectionMode === 'local';
    const currentStart = useLocal ? localSelectionStart : selectionStart;
    const currentEnd = useLocal ? localSelectionEnd : selectionEnd;
    const initialLeft = Math.min(currentStart, currentEnd);
    const initialRight = Math.max(currentStart, currentEnd);
    const widthSec = initialRight - initialLeft;
    const setStart = useLocal ? setLocalSelectionStart : setSelectionStart;
    const setEnd = useLocal ? setLocalSelectionEnd : setSelectionEnd;
    const handleMouseMove = moveEvent => {
      const deltaX = moveEvent.clientX - startX;
      const deltaSec = deltaX / zoom;
      let newLeft = initialLeft + deltaSec;
      let newRight = initialRight + deltaSec;
      if (newLeft < 0) {
        newLeft = 0;
        newRight = widthSec;
      }
      if (newRight > maxDuration) {
        newRight = maxDuration;
        newLeft = maxDuration - widthSec;
      }
      setStart(newLeft);
      setEnd(newRight);
    };
    const handleMouseUp = () => {
      document.removeEventListener('mousemove', handleMouseMove);
      document.removeEventListener('mouseup', handleMouseUp);
    };
    document.addEventListener('mousemove', handleMouseMove);
    document.addEventListener('mouseup', handleMouseUp);
  };

  // ── Track Controls ──
  const toggleTrackSoloEvaluate = trackId => {
    const wasPlaying = isPlaying;
    const playingSubTab = subTabs.find(s => s.isPlaying && s.type === 'PIANO_ROLL');
    stopAllPlayback();
    updateActiveTracks(prev => prev.map(t =>
      t.id === trackId ? { ...t, solo: !t.solo, muted: false } : t
    ));
    setTimeout(() => {
      const soloCtx = getAudioContext();
      if (wasPlaying) {
        startOffsetTimeRef.current = currentTime;
        startAudioTimeRef.current = soloCtx.currentTime;
        startBufferOffsetRef.current = currentTime;
        setIsPlaying(true);
        startTrackPlayback(currentTime);
      }
      if (playingSubTab) {
        const prSt = subTabs.find(s => s.id === playingSubTab.id);
        if (prSt) {
          const prOffset = prSt.currentTime || 0;
          startOffsetTimeRef.current = prOffset;
          startAudioTimeRef.current = soloCtx.currentTime;
          startBufferOffsetRef.current = prOffset * (prSt.speed || 1.0);
          schedulePianoRollMidi(prSt, prOffset);
          startSubTabPlayback(prSt, prOffset);
          setSubTabs(prev => prev.map(s => s.id === prSt.id ? Object.assign({}, s, { isPlaying: true, currentTime: prOffset }) : s));
        }
      }
    }, 60);
    setTimeout(() => lucide.createIcons(), 50);
  };
  const toggleTrackMute = trackId => {
    const beforeSnap = captureTrackSnapshot(trackId);
    updateActiveTracks(prev => prev.map(t => t.id === trackId ? {
      ...t,
      muted: !t.muted
    } : t));
    setUndoStack(prev => {
      const next = [...prev, {
        action_type: 'MUTE',
        track_id: trackId,
        timestamp: Date.now(),
        before_state: beforeSnap,
        after_state: captureTrackSnapshot(trackId)
      }];
      if (next.length > MAX_UNDO) next.shift();
      return next;
    });
    setTimeout(() => lucide.createIcons(), 50);
  };
  const updateTrackProp = (trackId, props) => {
    updateActiveTracks(prev => prev.map(t => t.id === trackId ? { ...t, ...props } : t));
    // Mixer fader dùng updateTrackProp({volumeDb}) — áp gain realtime cho node
    // thường + sub-node SECTION (key <parent>_sub_<trackId>)
    if (props && props.volumeDb !== undefined) {
      const volLinear = props.volumeDb <= -50 ? 0 : Math.pow(10, props.volumeDb / 20);
      const ctx = getAudioContext();
      const nodes = activeTrackNodesRef.current[trackId];
      if (nodes) nodes.gainNode.gain.setValueAtTime(volLinear, ctx.currentTime);
      Object.keys(activeTrackNodesRef.current).forEach(function(k) {
        if (k.endsWith('_sub_' + trackId)) {
          const sn = activeTrackNodesRef.current[k];
          if (sn && sn.gainNode) sn.gainNode.gain.setValueAtTime(volLinear, ctx.currentTime);
        }
      });
    }
    setTimeout(() => lucide.createIcons(), 50);
  };
  const toggleTrackDrum = trackId => {
    var mt = tracks || [];
    var tidx = mt.findIndex(function(tr) { return tr.id === trackId; });
    if (tidx < 0) tidx = 0;
    setTracks(prev => prev.map(t => {
      if (t.id !== trackId) return t;
      const becomingPercussion = !t.is_percussion;
      let newBank = becomingPercussion ? 128 : (t._saved_sf_bank !== undefined ? t._saved_sf_bank : 0);
      let newProgram = becomingPercussion ? 0 : (t._saved_sf_program !== undefined ? t._saved_sf_program : 0);
      let newCh = becomingPercussion ? 9 : (t._saved_sf_channel !== undefined ? t._saved_sf_channel : (tidx % 16));
      
      return {
        ...t,
        is_percussion: becomingPercussion,
        soundfont_bank: newBank,
        instrumentProgram: newProgram,
        midiChannel: newCh,
        _saved_sf_bank: becomingPercussion ? t.soundfont_bank : t._saved_sf_bank,
        _saved_sf_program: becomingPercussion ? t.instrumentProgram : t._saved_sf_program,
        _saved_sf_channel: becomingPercussion ? t.midiChannel : t._saved_sf_channel,
        synth_engine: t.synth_engine ? {
          ...t.synth_engine,
          soundfont_bank: newBank,
          soundfont_program: newProgram
        } : t.synth_engine
      };
    }));
    setTimeout(() => lucide.createIcons(), 50);
  };
  const updateTrackVolumeDb = (trackId, val) => {
    const beforeSnap = captureTrackSnapshot(trackId);
    updateActiveTracks(prev => prev.map(t => t.id === trackId ? {
      ...t,
      volumeDb: val
    } : t));
    setUndoStack(prev => {
      const next = [...prev, {
        action_type: 'VOLUME_CHANGE',
        track_id: trackId,
        timestamp: Date.now(),
        before_state: beforeSnap,
        after_state: captureTrackSnapshot(trackId)
      }];
      if (next.length > MAX_UNDO) next.shift();
      return next;
    });
    // Real-time update during playback
    const volLinear = val <= -50 ? 0 : Math.pow(10, val / 20);
    const ctx = getAudioContext();
    const nodes = activeTrackNodesRef.current[trackId];
    if (nodes) {
      nodes.gainNode.gain.setValueAtTime(volLinear, ctx.currentTime);
    }
    // Section sub-nodes (key: <parent>_sub_<trackId>) — volume slider của
    // SECTION-TAB phải đổi âm section ngay lập tức
    Object.keys(activeTrackNodesRef.current).forEach(function(k) {
      if (k.endsWith('_sub_' + trackId)) {
        const sn = activeTrackNodesRef.current[k];
        if (sn && sn.gainNode) sn.gainNode.gain.setValueAtTime(volLinear, ctx.currentTime);
      }
    });
  };
  const updateTrackPan = (trackId, val) => {
    const beforeSnap = captureTrackSnapshot(trackId);
    updateActiveTracks(prev => prev.map(t => t.id === trackId ? {
      ...t,
      pan: val
    } : t));
    setUndoStack(prev => {
      const next = [...prev, {
        action_type: 'PAN_CHANGE',
        track_id: trackId,
        timestamp: Date.now(),
        before_state: beforeSnap,
        after_state: captureTrackSnapshot(trackId)
      }];
      if (next.length > MAX_UNDO) next.shift();
      return next;
    });
    // Real-time update during playback
    const nodes = activeTrackNodesRef.current[trackId];
    if (nodes) {
      nodes.pannerNode.pan.setValueAtTime(val / 100, getAudioContext().currentTime);
    }
  };
  const updateTrackName = (trackId, name) => {
    const beforeSnap = captureTrackSnapshot(trackId);
    setTracks(prev => prev.map(t => t.id === trackId ? {
      ...t,
      name
    } : t));
    setUndoStack(prev => {
      const next = [...prev, {
        action_type: 'RENAME',
        track_id: trackId,
        timestamp: Date.now(),
        before_state: beforeSnap,
        after_state: captureTrackSnapshot(trackId)
      }];
      if (next.length > MAX_UNDO) next.shift();
      return next;
    });
  };
  const updateTrackColor = (trackId, color) => {
    const beforeSnap = captureTrackSnapshot(trackId);
    setTracks(prev => prev.map(t => t.id === trackId ? {
      ...t,
      color
    } : t));
    // Lưu màu NGAY (flush — không chờ debounce 2s — tránh mất màu khi reload nhanh)
    try {
      if (window.SonicStorage) {
        const nextTracks = (tracks || []).map(t => t.id === trackId ? { ...t, color } : t);
        window.SonicStorage.scheduleTempAutoSave(() => serializeProjectToSchema(currentProjectId || 'temp_project', projectName || 'Dự án tạm chưa lưu', bpm, nextTracks, subTabs, sessionTabs, masteringSettings));
        if (window.SonicStorage.flushTempAutoSave) window.SonicStorage.flushTempAutoSave();
      }
    } catch (e) {}
    setUndoStack(prev => {
      const next = [...prev, {
        action_type: 'RECOLOR',
        track_id: trackId,
        timestamp: Date.now(),
        before_state: beforeSnap,
        after_state: captureTrackSnapshot(trackId)
      }];
      if (next.length > MAX_UNDO) next.shift();
      return next;
    });
  };
  const updateClipName = (trackId, clipId, newName) => {
    setTracks(prev => prev.map(t => {
      if (t.id !== trackId) return t;
      const clips = t.clips && t.clips.length > 0 ? t.clips : [];
      return { ...t, clips: clips.map(c => c.id === clipId ? { ...c, name: newName } : c) };
    }));
  };

  // ── MIDI .mid file parser ──
  const parseMidiFile = (arrayBuffer) => {
    const data = new Uint8Array(arrayBuffer);
    if (data.length < 14) return null;
    var pos = 0;
    var read32 = function() { var v = (data[pos] << 24) | (data[pos+1] << 16) | (data[pos+2] << 8) | data[pos+3]; pos += 4; return v; };
    var read16 = function() { var v = (data[pos] << 8) | data[pos+1]; pos += 2; return v; };
    var readVLQ = function() { var v = 0, b; do { b = data[pos++]; v = (v << 7) | (b & 0x7f); } while (b & 0x80); return v; };

    var header = String.fromCharCode(data[0], data[1], data[2], data[3]);
    if (header !== 'MThd') return null;
    pos = 8; var fmt = read16(); var numTracks = read16(); var division = read16();
    var ticksPerBeat = (division & 0x8000) ? 480 : (division || 480);
    var bpm = 120;
    var result = [];
    for (var t = 0; t < numTracks; t++) {
      if (pos + 8 > data.length) break;
      var trkId = String.fromCharCode(data[pos], data[pos+1], data[pos+2], data[pos+3]);
      pos += 4; var trkLen = read32(); var endPos = Math.min(pos + trkLen, data.length);
      if (trkId !== 'MTrk') {
        pos = endPos;
        continue;
      }
      var absTicks = 0; var runningStatus = 0; var trackName = 'MIDI Track ' + (t + 1);
      var midiNotes = []; var pendingNotes = {};
      var maxAbsTick = 0;
      while (pos < endPos) {
        try {
          var delta = readVLQ(); absTicks += delta;
          var status = data[pos];
          if (status >= 0x80) {
            if (status < 0xf0) {
              runningStatus = status;
            }
            pos++;
          } else {
            status = runningStatus;
          }
          var cmd = status >> 4;
          if (cmd === 0x9 || cmd === 0x8) {
            var chan = status & 0x0F;
            var pitch = data[pos++]; var vel = pos < endPos ? data[pos++] : 0;
            var noteKey = chan + '_' + pitch;
            if (cmd === 0x9 && vel > 0) {
              pendingNotes[noteKey] = { tick: absTicks, vel: vel };
              if (absTicks > maxAbsTick) maxAbsTick = absTicks;
            } else {
              var pn = pendingNotes[noteKey];
              if (pn) {
                var durTicks = absTicks - pn.tick;
                if (durTicks <= 0) durTicks = 240;
                midiNotes.push({ id: 'mn_' + t + '_' + pitch + '_' + pn.tick, pitch: pitch, start_beat: pn.tick / ticksPerBeat, duration_beats: durTicks / ticksPerBeat, velocity: Math.min(1, pn.vel / 127) });
                delete pendingNotes[noteKey];
              }
            }
          } else if (status >= 0xc0 && status < 0xe0) { if (pos < endPos) pos += 1; }
          else if (status >= 0xe0 && status < 0xf0) { if (pos + 2 <= endPos) pos += 2; }
          else if (status >= 0xa0 && status < 0xc0) { if (pos + 2 <= endPos) pos += 2; }
          else if (status >= 0xf0 && status < 0xf8) { if (pos < endPos) { var sl = readVLQ(); pos += sl; } }
          else if (status === 0xff) {
            if (pos >= endPos) break;
            var metaType = data[pos++]; var metaLen = readVLQ();
            if (metaType === 0x03) { try { trackName = String.fromCharCode.apply(null, Array.from(data.subarray(pos, pos + metaLen))); } catch(e) {} }
            else if (metaType === 0x51 && pos + 3 <= endPos) { bpm = Math.round(60000000 / ((data[pos] << 16) | (data[pos+1] << 8) | data[pos+2])); }
            pos += Math.min(metaLen, endPos - pos);
          }
          else { if (pos + 2 <= endPos) pos += 2; }
        } catch(e) { pos = endPos; }
      }
      Object.keys(pendingNotes).forEach(function(k) {
        var pn = pendingNotes[k];
        var parts = k.split('_');
        var p = parseInt(parts[1]);
        var dur = Math.max(240, maxAbsTick - pn.tick);
        midiNotes.push({ id: 'mn_' + t + '_' + p + '_' + pn.tick, pitch: p, start_beat: pn.tick / ticksPerBeat, duration_beats: dur / ticksPerBeat, velocity: Math.min(1, pn.vel / 127) });
      });
      if (midiNotes.length > 0) {
        var lastEnd = 0; var maxEndBeat = 0;
        midiNotes.forEach(function(n) {
          var e = (n.start_beat + n.duration_beats) * 60 / bpm;
          if (e > lastEnd) lastEnd = e;
          var eb = n.start_beat + n.duration_beats;
          if (eb > maxEndBeat) maxEndBeat = eb;
        });
        result.push({
          name: trackName, notes: midiNotes, duration: lastEnd || 4, startTime: 0,
          id: 'midi_' + t + '_' + Date.now(),
          totalBeats: maxEndBeat || 16,
          bars: Math.max(1, Math.ceil((maxEndBeat || 16) / 4)),
          bpm: bpm,
          ticksPerBeat: ticksPerBeat
        });
      }
      pos = endPos;
    }
    return result.length > 0 ? result : null;
  };
  window.parseMidiFile = parseMidiFile;

  // ── Load File on Track (with server upload) ──
  // ── Resolve Media Explorer drag file → real File (for timeline drop) ──
  const resolveMediaExplorerDropFile = async () => {
    const mef = window.__mediaExplorerDragFile;
    if (!mef) return null;
    try {
      if (mef.handle && typeof mef.handle.getFile === 'function') {
        const f = await mef.handle.getFile();
        return new File([f], f.name || mef.name, { type: f.type || 'application/octet-stream' });
      }
      if (mef.file_id || mef.fileId) {
        const fid = mef.file_id || mef.fileId;
        const resp = await fetch(`${API_BASE_URL}/api/v1/audio/download/${fid}`);
        const blob = await resp.blob();
        return new File([blob], mef.name || mef.original_name || fid, { type: blob.type || 'audio/wav' });
      }
      if (mef.path) {
        const resp = await fetch(`${API_BASE_URL}/api/v1/media/file?path=${encodeURIComponent(mef.path)}`);
        const blob = await resp.blob();
        return new File([blob], mef.name || mef.path.split(/[\\/]/).pop(), { type: blob.type || 'application/octet-stream' });
      }
      // Media Library samples: only metadata (name/kind) — real bytes served
      // from /static/midi (generated demo loops, see app/static/midi).
      if (mef.kind === 'midi' && mef.name) {
        const resp = await fetch(`${API_BASE_URL}/static/midi/${encodeURIComponent(mef.name)}`);
        const blob = await resp.blob();
        return new File([blob], mef.name, { type: 'audio/midi' });
      }
    } catch (e) {
      showToast('Không thể nạp file từ Media Explorer: ' + e.message, 'error');
    }
    return null;
  };

  const loadFileOnTrack = async (trackId, file) => {
    if (!file) return;
    var fileName = file.name || '';
    var isMidi = /\.mid$|\.midi$/i.test(fileName);
    showToast(`Đang nạp file ${fileName}...`, 'info');
    try {
      if (isMidi) {
        var arrayBuffer = await file.arrayBuffer();
        var midiResult = parseMidiFile(arrayBuffer);
        if (!midiResult || midiResult.length === 0) {
          showToast('Không tìm thấy nốt nhạc trong file MIDI.', 'error');
          return;
        }
        if (midiResult.length === 1) {
          updateActiveTracks(prev => prev.map(function(t) {
            if (t.id !== trackId) return t;
            return { ...t, name: fileName, midiItems: [midiResult[0]] };
          }));
          showToast('Đã tải MIDI: ' + fileName, 'success');
        } else {
          var colors = ['#0f766e', '#1d4ed8', '#701a75', '#a21caf', '#b45309', '#ca8a04', '#dc2626', '#0891b2'];
          var firstDone = false;
          midiResult.forEach(function(midiItem, idx) {
            if (!firstDone) {
              updateActiveTracks(prev => prev.map(function(t) {
                if (t.id !== trackId) return t;
                return { ...t, name: midiItem.name || fileName, midiItems: [midiItem] };
              }));
              firstDone = true;
            } else {
              var newId = 'midi_track_' + Date.now() + '_' + idx;
              updateActiveTracks(function(prev) {
                var curLen = prev.length;
                return prev.concat([{
                  id: newId, name: midiItem.name || 'MIDI Track ' + (idx + 1),
                  buffer: null, startTime: 0, volumeDb: 0, pan: 0,
                  muted: false, solo: false, color: colors[idx % colors.length],
                  markers: [], serverFileId: null, clips: [], sections: [],
                  midiItems: [midiItem],
                  isArmed: false, monitoringEnabled: true,
                  inputSource: { deviceType: 'NONE', deviceId: '' }
                }]);
              });
            }
          });
          showToast('Đã tải MIDI: ' + midiResult.length + ' tracks from ' + fileName, 'success');
        }
        return;
      }
      // Upload to server
      uploadToServer(file, trackId);

      // Decode locally for playback + analyze channels (stereo/mono)
      const {
        audioBuffer: decodedBuffer,
        channelInfo
      } = await window.SonicAudio.decodeAudioFile(file);
      setTracks(prev => prev.map(t => t.id === trackId ? {
        ...t,
        name: fileName,
        buffer: decodedBuffer,
        channelInfo: channelInfo
      } : t));
      showToast(`Nạp file thành công: ${fileName} (${channelInfo.label})`, 'success');
    } catch (err) {
      showToast(isMidi ? "Lỗi giải mã MIDI." : "Lỗi giải mã âm thanh. Định dạng file không tương thích.", 'error');
    }
  };

  // ── Drop MIDI file → create new track(s) ──
  const handleDropMidiToNewTracks = async (file) => {
    if (!file) return;
    showToast('Đang nạp MIDI: ' + file.name + '...', 'info');
    try {
      var arrayBuffer = await file.arrayBuffer();
      var midiResult = parseMidiFile(arrayBuffer);
      if (!midiResult || midiResult.length === 0) {
        showToast('Không tìm thấy nốt nhạc trong file MIDI.', 'error');
        return;
      }
      var colors = ['#0f766e', '#1d4ed8', '#701a75', '#a21caf', '#b45309', '#ca8a04', '#dc2626', '#0891b2'];
      var now = Date.now();
      var newTracks = midiResult.map(function(midiItem, idx) {
        return {
          id: 'midi_track_' + now + '_' + idx,
          name: midiItem.name || (midiResult.length > 1 ? 'MIDI Track ' + (idx + 1) : (file.name || 'MIDI').replace(/\.midi?$/i, '')),
          buffer: null, startTime: 0, volumeDb: 0, pan: 0,
          muted: false, solo: false, masteringBypass: false, audioBypass: false, midiBypass: false, fxActive: true, color: colors[idx % colors.length],
          markers: [], serverFileId: null, clips: [], sections: [],
          midiItems: [midiItem],
          isArmed: false, monitoringEnabled: true,
          inputSource: { deviceType: 'NONE', deviceId: '' }
        };
      });
      updateActiveTracks(function(prev) { return prev.concat(newTracks); });
      if (newTracks.length > 0) setSelectedTrackId(newTracks[0].id);
      showToast('Đã tải MIDI: ' + newTracks.length + ' track(s) từ ' + file.name, 'success');
    } catch (err) {
      showToast('Lỗi giải mã MIDI.', 'error');
    }
  };

  // ── Synth Generators ──
  const generateSynthToTrack = (trackId, type) => {
    const sampleRate = 44100;
    const duration = 12.0;
    const context = getAudioContext();
    const frameCount = sampleRate * duration;
    const newBuffer = context.createBuffer(1, frameCount, sampleRate);
    const channelData = newBuffer.getChannelData(0);
    if (type === 'kick') {
      for (let i = 0; i < frameCount; i++) {
        const t = i / sampleRate;
        const beatTime = t % 0.5;
        const freq = 120 * Math.exp(-35 * beatTime);
        channelData[i] = Math.sin(2 * Math.PI * freq * beatTime) * Math.exp(-6 * beatTime);
      }
    } else {
      const notes = [220.00, 261.63, 293.66, 329.63, 392.00];
      for (let i = 0; i < frameCount; i++) {
        const t = i / sampleRate;
        const noteIdx = Math.floor(t * 2) % notes.length;
        const freq = notes[noteIdx];
        channelData[i] = Math.sin(2 * Math.PI * freq * t) * 0.25 * (1.0 - t % 0.5 / 0.5);
      }
    }
    updateActiveTracks(prev => prev.map(t => t.id === trackId ? {
      ...t,
      name: `Demo_${type.toUpperCase()}.wav`,
      buffer: newBuffer
    } : t));
    showToast(`Đã nạp sóng âm tổng hợp: ${type.toUpperCase()}`, 'success');
  };

  // ── Add Track ──
  const addNewTrack = () => {
    const curTracks = activeTracks;
    const rearrangeNewId = (curTracks.length + 1).toString();
    const colors = ['#0f766e', '#1d4ed8', '#701a75', '#a21caf', '#b45309'];
    const selectColor = colors[curTracks.length % colors.length];
    // SECTION-TAB: audioclip item mặc định BYPASS mastering (raw) — nút A xám;
    // MAIN session: qua mastering (A sáng). User requirement 15:00.
    const isSectionCtx = activeTabRef.current && activeTabRef.current.startsWith('session_');
    updateActiveTracks(prev => [...prev, {
      id: rearrangeNewId,
      name: `Track ${rearrangeNewId}`,
      buffer: null,
      startTime: 0,
      volumeDb: 0,
      pan: 0,
      muted: false,
      solo: false,
      masteringBypass: false,
      audioBypass: isSectionCtx ? true : false,
      midiBypass: false,
      fxActive: true,
      color: selectColor,
      markers: [],
      serverFileId: null,
      clips: [],
      sections: [],
      midiItems: [],
      isArmed: false,
      monitoringEnabled: true,
      inputSource: { deviceType: 'NONE', deviceId: '' }
    }]);
    showToast(`Đã thêm Track ${rearrangeNewId}.`, 'info');
    setTimeout(() => lucide.createIcons(), 200);
    return rearrangeNewId;
  };

  // ── Update tracks in active context (main session or section tab) ──
  const updateActiveTracksRef = useCallback(updater => {
    const currentSessionTabs = sessionTabsRef.current;
    const currentActiveTab = activeTabRef.current;
    var st = currentSessionTabs.find(s => s.id === currentActiveTab);
    if (st) {
      setSessionTabs(prev => prev.map(s => s.id === currentActiveTab ? { ...s, tracks: updater(s.tracks), isDirty: true } : s));
      return;
    }
    var pr = subTabsRef.current ? subTabsRef.current.find(s => s.id === currentActiveTab && s.type === 'PIANO_ROLL') : null;
    if (pr && pr.parent_tab_id && pr.parent_tab_id.startsWith('session_')) {
      var parentSt = currentSessionTabs.find(s => s.id === pr.parent_tab_id);
      if (parentSt) {
        setSessionTabs(prev => prev.map(s => s.id === pr.parent_tab_id ? { ...s, tracks: updater(s.tracks), isDirty: true } : s));
        return;
      }
    }
    setTracks(updater);
  }, []);
  const updateActiveTracks = updateActiveTracksRef;

  const toggleTrackArm = trackId => {
    updateActiveTracks(prev => prev.map(t => {
      if (t.id !== trackId) return t;
      return { ...t, isArmed: !t.isArmed };
    }));
    setTimeout(() => lucide.createIcons(), 50);
  };

  const toggleTrackMonitor = trackId => {
    updateActiveTracks(prev => prev.map(t => {
      if (t.id !== trackId) return t;
      return { ...t, monitoringEnabled: !t.monitoringEnabled };
    }));
    setTimeout(() => lucide.createIcons(), 50);
  };

  const updateTrackInputSource = (trackId, type, deviceId) => {
    updateActiveTracks(prev => prev.map(t => {
      if (t.id !== trackId) return t;
      return { ...t, inputSource: { deviceType: type, deviceId } };
    }));
  };
  
  // ── MIDI Export: tạo file MIDI từ tất cả tracks ──
  const triggerMidiExport = () => {
    const bpmNum = parseFloat(bpm) || 120;
    const ppq = 480; // Pulses Per Quarter Note
    const ticksPerBeat = ppq;
    const beatDuration = 60 / bpmNum;
    
    // Build MIDI tracks
    let midiTracks = [];
    let currentTrackNum = 0;
    
    tracks.forEach(track => {
      currentTrackNum++;
      const events = [];
      let hasNotes = false;
      
      // Collect MIDI events from midiItems
      const midiItems = track.midiItems || [];
      midiItems.forEach(item => {
        const notes = item.notes || [];
        notes.forEach(note => {
          hasNotes = true;
          const startTick = Math.round(note.startBeat * ticksPerBeat);
          const durTick = Math.round(note.durationBeats * ticksPerBeat);
          const velocity = Math.round((note.velocity || 0.8) * 100);
          const pitch = note.pitch || 60;
          events.push({ tick: startTick, type: 'note_on', pitch, velocity });
          events.push({ tick: startTick + durTick, type: 'note_off', pitch, velocity: 0 });
        });
      });
      
      // If track has audio buffer but no MIDI, create a "rest" track with one silent note
      if (!hasNotes && track.buffer) {
        const durSec = track.buffer.duration;
        const durBeats = durSec / beatDuration;
        const durTicks = Math.round(durBeats * ticksPerBeat);
        // Use a C-2 (pitch 0 = rest note) indicator
        events.push({ tick: 0, type: 'note_on', pitch: 0, velocity: 1 });
        events.push({ tick: durTicks, type: 'note_off', pitch: 0, velocity: 0 });
      }
      
      if (events.length === 0 && !track.buffer) return; // Skip empty tracks
      
      // Sort events by tick
      events.sort((a, b) => a.tick - b.tick);
      
      // MIDI track header bytes
      const trackBytes = [];
      // Track name
      const nameStr = (track.name || ('Track ' + currentTrackNum)).slice(0, 255);
      trackBytes.push(0xFF, 0x03, nameStr.length);
      for (let i = 0; i < nameStr.length; i++) trackBytes.push(nameStr.charCodeAt(i));
      
      // End of track marker will be calculated later
      let lastTick = 0;
      events.forEach(ev => {
        const delta = ev.tick - lastTick;
        lastTick = ev.tick;
        // Delta time as variable-length quantity
        writeVLQ(trackBytes, delta);
        if (ev.type === 'note_on') {
          trackBytes.push(0x90, ev.pitch, ev.velocity);
        } else {
          trackBytes.push(0x80, ev.pitch, 0);
        }
      });
      
      // End of track
      writeVLQ(trackBytes, 0);
      trackBytes.push(0xFF, 0x2F, 0x00);
      
      // Track chunk: "MTrk" + length + data
      const trackData = [0x4D, 0x54, 0x72, 0x6B]; // "MTrk"
      const len = trackBytes.length;
      trackData.push((len >> 24) & 0xFF, (len >> 16) & 0xFF, (len >> 8) & 0xFF, len & 0xFF);
      trackData.push(...trackBytes);
      midiTracks.push(trackData);
    });
    
    if (midiTracks.length === 0) {
      showToast("Không có dữ liệu MIDI nào để xuất.", "warning");
      return;
    }
    
    // Header: "MThd" + length(6) + format(1) + tracks + division
    const header = [0x4D, 0x54, 0x68, 0x64, 0x00, 0x00, 0x00, 0x06, 0x00, 0x01, (midiTracks.length >> 8) & 0xFF, midiTracks.length & 0xFF, (ppq >> 8) & 0xFF, ppq & 0xFF];
    
    const allBytes = header.concat(...midiTracks.flat());
    const uint8 = new Uint8Array(allBytes);
    const blob = new Blob([uint8], { type: 'audio/midi' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = (projectName || 'Project') + '.mid';
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    URL.revokeObjectURL(url);
    showToast(`Đã xuất file MIDI với ${midiTracks.length} tracks!`, "success");
  };
  
  function writeVLQ(bytes, value) {
    if (value < 0) value = 0;
    const buf = [];
    buf.push(value & 0x7F);
    while (value > 0x7F) {
      value >>= 7;
      buf.push(0x80 | (value & 0x7F));
    }
    buf.reverse();
    buf.forEach(b => bytes.push(b));
  }

  // ── Insert Track Below Selected ──
  const insertTrackBelow = () => {
    const curTracks = activeTracks;
    const rearrangeNewId = `t${Date.now()}`;
    const colors = ['#0f766e', '#1d4ed8', '#701a75', '#a21caf', '#b45309'];
    const selectColor = colors[(curTracks.length) % colors.length];
    const newTrack = {
      id: rearrangeNewId,
      name: `Track ${rearrangeNewId}`,
      buffer: null,
      startTime: 0,
      volumeDb: 0,
      pan: 0,
      muted: false,
      solo: false,
      masteringBypass: false,
      audioBypass: false,
      midiBypass: false,
      fxActive: true,
      color: selectColor,
      markers: [],
      serverFileId: null,
      clips: [],
      sections: [],
      midiItems: []
    };
    updateActiveTracks(prev => {
      const idx = prev.findIndex(t => t.id === selectedTrackId);
      if (idx === -1) return [...prev, newTrack];
      const copy = [...prev];
      copy.splice(idx + 1, 0, newTrack);
      return copy;
    });
    setSelectedTrackId(rearrangeNewId);
    showToast(`Đã thêm Track ${rearrangeNewId}.`, 'info');
    setTimeout(() => lucide.createIcons(), 200);
  };

  // ── Insert Section at Playhead ──
  const insertSectionAtPlayhead = () => {
    const curTracks = activeTracks;
    const track = curTracks.find(t => t.id === selectedTrackId);
    if (!track) { showToast('Chọn track trước', 'warning'); return; }
    const secondsPerBar = (60.0 / (parseInt(bpm) || 120)) * 4;
    const section = {
      id: `sec_${Date.now()}`,
      name: 'Section',
      start: currentTime,
      duration: 4 * secondsPerBar,
      length_bars: 4,
      color: track.color || '#06b6d4'
    };
    createSectionWithUndo(selectedTrackId, section);
    updateActiveTracks(prev => prev.map(t => t.id === selectedTrackId ? {
      ...t,
      sections: [...(t.sections || []), section]
    } : t));
    showToast(`Đã thêm Section tại ${currentTime.toFixed(2)}s`, 'success');
  };

  // ── Insert MIDI Item at Playhead ──
  const insertMidiItemAtPlayhead = () => {
    const curTracks = activeTracks;
    const track = curTracks.find(t => t.id === selectedTrackId);
    if (!track) { showToast('Chọn track trước', 'warning'); return; }
    const secondsPerBar = (60.0 / (parseInt(bpm) || 120)) * 4;
    const midiItem = {
      id: `midi_${Date.now()}`,
      name: 'MIDI Item',
      startTime: currentTime,
      duration: 4 * secondsPerBar,
      length_bars: 4,
      notes: [],
      color: '#a78bfa'
    };
    createMidiWithUndo(selectedTrackId, midiItem);
    updateActiveTracks(prev => prev.map(t => t.id === selectedTrackId ? {
      ...t,
      midiItems: [...(t.midiItems || []), midiItem]
    } : t));
    showToast(`Đã thêm MIDI item tại ${currentTime.toFixed(2)}s`, 'success');
  };

  // ── Insert Sound Clip at Cursor ──
  const insertSoundClipAtCursor = () => {
    const curTracks = activeTracks;
    const track = curTracks.find(t => t.id === selectedTrackId);
    if (!track) { showToast('Chọn track trước', 'warning'); return; }
    const input = document.createElement('input');
    input.type = 'file';
    input.accept = 'audio/*';
    input.multiple = true;
    input.onchange = async e => {
      const files = Array.from(e.target.files || []);
      if (!files.length) return;
      let offset = currentTime;
      let count = 0;
      for (const file of files) {
        try {
          const uploadResult = await uploadToServer(file, track.id);
          const fileId = uploadResult?.file_id || null;
          const { audioBuffer: decodedBuffer, channelInfo } = await window.SonicAudio.decodeAudioFile(file);
          const clipId = `clip_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
          const newClip = { id: clipId, buffer: decodedBuffer, startTime: offset, name: file.name, speed: 1.0, serverFileId: fileId };
          createClipWithUndo(selectedTrackId, newClip);
          updateActiveTracks(prev => prev.map(t => t.id === selectedTrackId ? {
            ...t,
            serverFileId: t.serverFileId || fileId,
            clips: [...(t.clips || []), newClip]
          } : t));
          offset += decodedBuffer.duration;
          count++;
        } catch (err) {
          showToast(`Lỗi nạp ${file.name}`, 'error');
        }
      }
      if (count > 0) showToast(`Đã chèn ${count} file âm thanh`, 'success');
    };
    input.click();
  };

  // ── Server-side Export ──
  const triggerWavExport = async () => {
    // TỰ ĐỘNG bounce realtime nếu project có MIDI items: FluidSynth (ScriptProcessor)
    // không render được offline, nên bounce là cách duy nhất giữ trung thực
    // MIDI + FX Rack + Mastering Chain trong file xuất.
    const allT = (activeTracksRef.current && activeTracksRef.current.length) ? activeTracksRef.current : tracks;
    const hasMidiItems = allT.some(t => t.midiItems && t.midiItems.length > 0)
      || (sessionTabs || []).some(s => (s.tracks || []).some(t => t.midiItems && t.midiItems.length > 0));
    if (hasMidiItems) {
      // Nếu preview đã cache đủ độ dài cho MỌI track MIDI → export OFFLINE NHANH
      // bằng cache (vẫn qua FX Rack + Mastering như playback). Ngược lại →
      // bounce realtime để đảm bảo trung thực.
      const midiTracks = allT.filter(t => t.midiItems && t.midiItems.length > 0);
      let needDur = 1.0;
      midiTracks.forEach(t => {
        (t.midiItems || []).forEach(m => {
          if (m.endTime) needDur = Math.max(needDur, m.endTime);
          else if (m.startTime) needDur = Math.max(needDur, m.startTime + (m.duration || 8));
        });
      });
      const cacheOK = midiTracks.length > 0 && midiTracks.every(t => {
        const c = midiCacheRef.current[t.id];
        return c && c.chunks.length > 0 && c.duration >= needDur - 0.3;
      });
      if (cacheOK) {
        showToast("Dùng MIDI cache từ preview — export OFFLINE NHANH (vẫn đủ FX Rack + Mastering Chain).", "info");
        clientSideExport(allT);
        return;
      }
      showToast("Chưa có MIDI cache đầy đủ — bounce realtime để đảm bảo trung thực.", "info");
      triggerBounceExport();
      return;
    }
    let exportTracks;
    let clipStart = 0;
    let clipEnd = 0;
    const src = exportSettings.source;
    if (src === 'active_clip' || src === 'clip_selection') {
      const selTrack = selectedTrackId ? tracks.find(t => t.id === selectedTrackId) : null;
      if (!selTrack || !selTrack.buffer) { showToast("Không có clip nào được chọn.", "warning"); return; }
      let rangeStart = selectionStart;
      let rangeEnd = selectionEnd;
      if (rangeStart === null || rangeEnd === null || rangeEnd <= rangeStart) {
        rangeStart = 0;
        rangeEnd = selTrack.buffer.duration;
      }
      const ctx = getAudioContext();
      const numCh = selTrack.buffer.numberOfChannels || 1;
      const sr = selTrack.buffer.sampleRate;
      const startSample = Math.max(0, Math.floor(rangeStart * sr));
      const endSample = Math.min(selTrack.buffer.length, Math.floor(rangeEnd * sr));
      const len = endSample - startSample;
      if (len <= 100) { showToast("Vùng chọn quá ngắn hoặc không có dữ liệu.", "warning"); return; }
      const clipBuf = ctx.createBuffer(numCh, len, sr);
      for (let ch = 0; ch < numCh; ch++) {
        clipBuf.copyToChannel(selTrack.buffer.getChannelData(ch).subarray(startSample, endSample), ch);
      }
      exportTracks = [{ ...selTrack, buffer: clipBuf, startTime: 0, clips: [{ id: 'export_clip', buffer: clipBuf, startTime: 0, name: selTrack.name }] }];
    } else if (src === 'track_mix') {
      const sel = tracks.filter(t => t.buffer && !t.muted);
      const selTrk = selectedTrackId ? sel.filter(t => t.id === selectedTrackId) : sel;
      if (selTrk.length === 0) { showToast("Track được chọn không có dữ liệu.", "warning"); return; }
      exportTracks = selTrk;
    } else {
      exportTracks = tracks.filter(t => t.buffer && !t.muted);
    }
    if (exportTracks.length === 0) {
      showToast("Không tìm thấy dữ liệu âm thanh hợp lệ để xuất.", "warning");
      return;
    }

    // Check if all active tracks have server file IDs
    const allOnServer = exportTracks.every(t => serverFileIdMap[t.id]);
    if (allOnServer && serverStatus === 'connected') {
      // Use server-side export
      setIsExporting(true);
      showToast("Đang gửi yêu cầu xuất âm thanh đến máy chủ...", "info");
      try {
        const sessionId = `session_${Date.now()}`;
        const tracksMeta = exportTracks.map(t => ({
          track_id: t.id,
          file_id: serverFileIdMap[t.id],
          volume_db: t.volumeDb,
          muted: false,
          clips: [{
            clip_id: `clip_${t.id}`,
            start_time_seconds: t.startTime || 0,
            end_time_seconds: (t.startTime || 0) + t.buffer.duration,
            loop_count: 1,
            apply_zero_crossing: true,
            fade_in_ms: 0,
            fade_out_ms: 0
          }]
        }));
        const resp = await fetch(`${API_MULTITRACK}/mix`, {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json'
          },
          body: JSON.stringify({
            session_id: sessionId,
            export_settings: {
              sample_rate: parseInt(exportSettings.sampleRate),
              bit_depth: parseInt(exportSettings.bitDepth),
              format: exportSettings.format,
              channels: exportSettings.channels
            },
            tracks: tracksMeta
          })
        });
        if (!resp.ok) throw new Error(`Server export failed: ${resp.status}`);
        const data = await resp.json();
        showToast("Đang xử lý trên máy chủ...", "info");

        // Poll for result
        const result = await pollTaskResult(data.task_id, 20);
        if (result.success) {
          // Get the uploaded file from server
          const fileId = result.output_file_id || result.output_path?.split('/').pop();
          if (fileId) {
            const downloadUrl = `${API_AUDIO}/download/${fileId}`;
            showToast("Xuất bản âm thanh từ máy chủ hoàn tất!", "success", "Tải về", () => {
              const a = document.createElement('a');
              a.href = downloadUrl;
              a.download = fileId;
              a.click();
            });
          }
        } else {
          throw new Error(result.error || 'Server processing failed');
        }
      } catch (err) {
        showToast("Lỗi xuất máy chủ: " + err.message + ". Chuyển sang xuất client.", "warning");
        // Fall back to client-side export
        clientSideExport(exportTracks);
      } finally {
        setIsExporting(false);
      }
    } else {
      // Client-side export (existing working code)
      clientSideExport(exportTracks);
    }
  };

  // ── Realtime Bounce Export (đầy đủ MIDI + FX Rack + Mastering) ──
  // MIDI/soundfont chạy qua FluidSynth (ScriptProcessor) — không render được
  // trong OfflineAudioContext. Vì vậy bounce REALTIME: chạy lại toàn bộ project
  // (audio + MIDI + sections qua FX racks + mastering chain, đúng như nghe) và
  // capture PCM từ master bus, sau đó encode WAV trung thực.
  const triggerBounceExport = async () => {
    if (!masterBus) { showToast("Hãy phát thử một lần để khởi tạo audio engine trước.", "warning"); return; }
    if (isExporting) return;
    setIsExporting(true);
    const audioCtx = getAudioContext();
    try {
      const allTracks = (activeTracksRef.current && activeTracksRef.current.length) ? activeTracksRef.current : tracks;
      // MAIN SESSION end time (items trên track main — sections tính theo bounds,
      // không kéo dài theo nội dung SECTION-TAB) — bounce không cắt sớm/cắt thiếu.
      let durationLimit = Math.max(1.0, computeMainSessionEndTime(allTracks));
      durationLimit += 1.2; // FX/mastering tail

      const captureRate = audioCtx.sampleRate || 44100;
      const bitDepth = parseInt(exportSettings.bitDepth) || 16;
      const outChannels = exportSettings.channels === 'mono' ? 1 : 2;
      showToast(`Bounce realtime ~${Math.round(durationLimit)}s — giữ nguyên âm thanh đang phát...`, "info");

      // Capture tap on the master bus output (post-FX + post-mastering)
      const chunks = [];
      const script = audioCtx.createScriptProcessor(4096, 2, 2);
      script.onaudioprocess = (e) => {
        const L = e.inputBuffer.getChannelData(0);
        const R = e.inputBuffer.getChannelData(1);
        const n = L.length;
        const chunk = new Float32Array(n * 2);
        for (let i = 0; i < n; i++) { chunk[i * 2] = L[i]; chunk[i * 2 + 1] = R[i]; }
        chunks.push(chunk);
      };
      masterBus.output.connect(script);
      script.connect(audioCtx.destination);

      // Rewind + play the FULL project from the start (FluidSynth + FX racks +
      // mastering chain — exactly what you hear).
      stopAllPlayback();
      try { if (window.SonicSF && window.SonicSF.stopAllNotes) window.SonicSF.stopAllNotes(); } catch (e) { }
      startTrackPlayback(0);

      // Realtime wait, then stop & detach
      await new Promise(res => setTimeout(res, Math.ceil(durationLimit * 1000) + 900));
      stopAllPlayback();
      try { if (window.SonicSF && window.SonicSF.stopAllNotes) window.SonicSF.stopAllNotes(); } catch (e) { }
      try { script.disconnect(); } catch (e) { }
      try { masterBus.output.disconnect(script); } catch (e) { }

      // Encode interleaved PCM chunks → WAV
      const totalFrames = chunks.reduce((a, c) => a + c.length / 2, 0);
      const stereoBuf = new Float32Array(totalFrames * 2);
      let off = 0;
      chunks.forEach(c => { stereoBuf.set(c, off); off += c.length; });
      const frames = totalFrames;
      const bytesPerSample = bitDepth / 8;
      const headerSize = 44;
      const fileSizeBytes = headerSize + frames * bytesPerSample * outChannels;
      const view = new DataView(new ArrayBuffer(fileSizeBytes));
      const writeString = (o, s) => { for (let i = 0; i < s.length; i++) view.setUint8(o + i, s.charCodeAt(i)); };
      writeString(0, 'RIFF'); view.setUint32(4, fileSizeBytes - 8, true); writeString(8, 'WAVE');
      writeString(12, 'fmt '); view.setUint32(16, 16, true); view.setUint16(20, 1, true);
      view.setUint16(22, outChannels, true); view.setUint32(24, captureRate, true);
      view.setUint32(28, captureRate * bytesPerSample * outChannels, true);
      view.setUint16(32, bytesPerSample, true); view.setUint16(34, bitDepth, true);
      writeString(36, 'data'); view.setUint32(40, frames * bytesPerSample * outChannels, true);
      let o = 44;
      for (let i = 0; i < frames; i++) {
        for (let ch = 0; ch < outChannels; ch++) {
          const s = Math.max(-1, Math.min(1, stereoBuf[i * 2 + ch]));
          if (bitDepth === 16) view.setInt16(o, Math.floor(s < 0 ? s * 0x8000 : s * 0x7FFF), true);
          else view.setUint8(o, Math.floor((s + 1) * 127.5), true);
          o += bytesPerSample;
        }
      }
      const blob = new Blob([view], { type: 'audio/wav' });
      const url = URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.href = url;
      a.download = `sonicforge-bounce-${captureRate}hz-${bitDepth}bit.wav`;
      a.click();
      URL.revokeObjectURL(url);
      showToast("Bounce hoàn tất — file gồm MIDI + FX Rack + Mastering Chain!", "success");
    } catch (err) {
      showToast("Lỗi bounce: " + err.message, "error");
    } finally {
      setIsExporting(false);
    }
  };
  const handleSaveLocalProject = (name) => {
    const finalName = name || projectName || 'Dự án mới';
    const localId = currentProjectId && currentProjectId.startsWith('local_') ? currentProjectId : 'local_' + Date.now();
    const projectSchemaObj = serializeProjectToSchema(localId, finalName, bpm, tracks, subTabs, sessionTabs, masteringSettings);
    const dataStr = JSON.stringify(projectSchemaObj);
    localStorage.setItem('sonic_local_project_data', dataStr);
    localStorage.setItem('sonic_project_id', localId);
    localStorage.setItem('sonic_project_name', finalName);
    setCurrentProjectId(localId);
    setProjectName(finalName);
    window.SonicStorage.exportProjectToSFS(projectSchemaObj);
    showToast(`Đã lưu dự án local "${finalName}" thành công!`, "success");
    // Đã đăng nhập → đồng bộ lên Cloud (fire-and-forget): project mở được từ
    // máy khác / browser ẩn danh khi đăng nhập cùng tài khoản.
    if (currentUser && window.SonicAPI && window.SonicAPI.saveCloudProject) {
      window.SonicAPI.saveCloudProject(finalName, dataStr).then(function (res) {
        if (res && res.project_id) {
          console.log('[Cloud] local project synced:', res.project_id);
        }
      }).catch(function (e) { console.warn('Cloud sync local project failed:', e); });
    }
  };

  const handleSaveCloudProject = async (name, existingProjectId) => {
    const finalName = name || projectName || 'Dự án mới';
    var useProjectId = existingProjectId || currentProjectId;
    const projectSchemaObj = serializeProjectToSchema(useProjectId || 'project_' + Date.now(), finalName, bpm, tracks, subTabs, sessionTabs, masteringSettings);
    // Diagnostic: count source vs serialized items
    var srcMidi = 0, srcClip = 0, srcSec = 0;
    tracks.forEach(function(t) { srcMidi += (t.midiItems||[]).length; srcClip += (t.clips||[]).length; srcSec += (t.sections||[]).length; });
    var serItems = 0;
    if (projectSchemaObj.main_session && projectSchemaObj.main_session.tracks) {
      projectSchemaObj.main_session.tracks.forEach(function(st) { if (st.items) serItems += st.items.length; });
    }
    Object.keys(projectSchemaObj.section_store || {}).forEach(function(sk) {
      (projectSchemaObj.section_store[sk].tracks || []).forEach(function(st) { if (st.items) serItems += st.items.length; });
    });
    const dataJson = JSON.stringify(projectSchemaObj);
    try {
      if (existingProjectId) {
        await window.SonicAPI.updateCloudProject(existingProjectId, finalName, dataJson);
        setProjectName(finalName);
        setCurrentProjectId(existingProjectId);
        localStorage.setItem('sonic_project_name', finalName);
        localStorage.setItem('sonic_project_id', existingProjectId);
        showToast('Đã ghi đè Cloud "' + finalName + '" (src midi=' + srcMidi + ' clip=' + srcClip + ' sec=' + srcSec + ' | ser=' + serItems + ').', 'success');
      } else if (currentProjectId && !currentProjectId.startsWith('local_')) {
        await window.SonicAPI.updateCloudProject(currentProjectId, finalName, dataJson);
        setProjectName(finalName);
        localStorage.setItem('sonic_project_name', finalName);
        localStorage.setItem('sonic_project_id', currentProjectId);
        showToast('Đã lưu Cloud "' + finalName + '" (src midi=' + srcMidi + ' clip=' + srcClip + ' sec=' + srcSec + ' | ser=' + serItems + ' id=' + currentProjectId + ').', serItems === 0 ? 'warning' : 'success');
      } else {
        const res = await window.SonicAPI.saveCloudProject(finalName, dataJson);
        const newProjId = res.project_id;
        setProjectName(finalName);
        localStorage.setItem('sonic_project_name', finalName);
        if (newProjId) {
          setCurrentProjectId(newProjId);
          localStorage.setItem('sonic_project_id', newProjId);
        }
        showToast('Đã lưu Cloud mới "' + finalName + '" (src midi=' + srcMidi + ' clip=' + srcClip + ' sec=' + srcSec + ' | ser=' + serItems + ').', serItems === 0 ? 'warning' : 'success');
      }
    } catch (err) {
      showToast(err.message || "Lỗi lưu dự án lên Cloud", "error");
    }
  };

  const handleSaveProject = async () => {
    if (!currentProjectId) {
      setSaveProjectModalOpen(true);
      return;
    }
    if (currentProjectId.startsWith('local_')) {
      handleSaveLocalProject(projectName);
    } else {
      if (!currentUser) { showToast('Cần đăng nhập để lưu Cloud. currentUser=' + (currentUser ? 'OK' : 'NULL') + ' token=' + (localStorage.getItem('sonic_token') ? 'exists' : 'missing'), 'warning'); return; }
      handleSaveCloudProject(projectName);
    }
  };
  const handleSaveProjectRef = useRef(handleSaveProject);
  handleSaveProjectRef.current = handleSaveProject;

  // ── Ctrl-S (Save) toàn cục ──
  // Desktop: lưu project ra THƯ MỤC CỦA HỆ ĐIỀU HÀNH (Documents/SonicForgeDAW/
  // Projects). Docker/headless: lưu lên Cloud (đã login) hoặc local (.sfs).
  const handleGlobalSave = async () => {
    try {
      const finalName = projectName || 'Dự án mới';
      const schemaObj = serializeProjectToSchema(currentProjectId || 'proj_' + Date.now(), finalName, bpm, tracks, subTabs, sessionTabs, masteringSettings);
      const dataJson = JSON.stringify(schemaObj);
      const runtime = (window.SonicRuntime && window.SonicRuntime.capabilities && window.SonicRuntime.capabilities.runtime) || '';
      if (runtime === 'desktop' && window.SonicAPI && window.SonicAPI.saveProjectToDisk) {
        // Desktop → thư mục của hệ điều hành
        const res = await window.SonicAPI.saveProjectToDisk(finalName, dataJson);
        showToast('Đã lưu dự án: ' + ((res && res.path) || finalName), 'success');
      } else if (window.SonicAPI) {
        // Docker / headless → Cloud (nếu đã đăng nhập), ngược lại local
        if (currentProjectId && !currentProjectId.startsWith('local_') && currentUser) {
          await window.SonicAPI.updateCloudProject(currentProjectId, finalName, dataJson);
          showToast('Đã lưu dự án lên Cloud', 'success');
        } else if (currentUser) {
          await handleSaveAsCloud(finalName);
        } else {
          handleSaveLocalProject(finalName);
        }
      }
    } catch (err) {
      showToast('Lỗi lưu dự án: ' + (err.message || err), 'error');
    }
  };
  const handleGlobalSaveRef = useRef(handleGlobalSave);
  handleGlobalSaveRef.current = handleGlobalSave;
  useEffect(() => {
    const handler = (e) => {
      if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 's') {
        const target = e.target;
        if (target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable)) return;
        e.preventDefault();
        handleGlobalSaveRef.current();
      }
    };
    window.addEventListener('keydown', handler);
    return () => window.removeEventListener('keydown', handler);
  }, []);

  const handleSaveAsCloud = async (newName) => {
    const projectSchemaObj = serializeProjectToSchema('project_' + Date.now(), newName, bpm, tracks, subTabs, sessionTabs, masteringSettings);
    const dataJson = JSON.stringify(projectSchemaObj);
    try {
      const res = await window.SonicAPI.saveCloudProject(newName, dataJson);
      const newProjId = res.project_id;
      setProjectName(newName);
      localStorage.setItem('sonic_project_name', newName);
      if (newProjId) {
        setCurrentProjectId(newProjId);
        localStorage.setItem('sonic_project_id', newProjId);
      }
      showToast(`Đã lưu dự án dưới tên mới "${newName}" lên server!`, "success");
    } catch (err) {
      showToast(err.message || "Lỗi Save As lên server", "error");
    }
  };

  const handleSaveCloud = handleSaveProject;
  const handleExportSFS = (customName = null) => {
    const finalName = customName || projectName || 'Dự án SonicForge';
    const projectSchemaObj = serializeProjectToSchema(currentProjectId || 'proj_' + Date.now(), finalName, bpm, tracks, subTabs, sessionTabs, masteringSettings);
    window.SonicStorage.exportProjectToSFS(projectSchemaObj);
    showToast("Đã xuất dự án (.sfs) thành công!", "success");
  };
  const handleImportSFS = () => {
    const input = document.createElement('input');
    input.type = 'file';
    input.accept = '.sfs,application/json';
    input.onchange = async e => {
      if (!e.target.files[0]) return;
      try {
        const proj = await window.SonicStorage.importProjectFromSFSFile(e.target.files[0]);
        let restored = [];
        let restoredBpm = bpm;
        let restoredSessionTabs = [];
        let restoredSubTabs = [];
        if (proj.main_session) {
          const result = deserializeProjectFromSchema(proj);
          restored = result.tracks;
          restoredBpm = result.bpm;
          restoredSessionTabs = result.sessionTabs;
          restoredSubTabs = result.subTabs;
          if (result.masteringSettings) setMasteringSettings(result.masteringSettings);
        } else {
          restored = (proj.tracks || []).map(t => ({
            ...t,
            buffer: null,
            channelInfo: t.channelInfo || null,
            clips: t.clips || [],
            serverFileId: t.serverFileId || null
          }));
        }
        if (restored.length > 0) {
          setTracks(restored);
          loadAudioBuffersForTracks(restored);
          setBpm(restoredBpm.toString());
          setProjectName(proj.name || 'Dự án mới');
          setCurrentProjectId(null);
          if (restoredSessionTabs.length > 0) setSessionTabs(restoredSessionTabs);
          if (restoredSubTabs.length > 0) setSubTabs(restoredSubTabs);
          localStorage.setItem('sonic_project_name', proj.name || 'Dự án mới');
          localStorage.removeItem('sonic_project_id');
          showToast(`Đã nạp dự án "${proj.name || 'Dự án mới'}" từ tệp .sfs thành công!`, "success");
        }
      } catch (err) {
        showToast(err.message || "Lỗi mở tệp .sfs", "error");
      }
    };
    input.click();
  };
  const clientSideExport = async activeTracks => {
    setIsExporting(true);
    showToast("Đang trộn âm thanh đa kênh (Offline Mixdown)...", "info");
    try {
      const targetRate = parseInt(exportSettings.sampleRate);
      const bitDepth = parseInt(exportSettings.bitDepth);
      // MAIN SESSION end time (items trên track main — sections theo bounds) —
      // offline render không cắt sớm; MIDI cache (preview) giữ max với cache.
      let durationLimit = Math.max(0.1, computeMainSessionEndTime(activeTracks));
      // Include preview-captured MIDI cache length (soundfont tracks have no clips)
      Object.keys(midiCacheRef.current).forEach(id => {
        const c = midiCacheRef.current[id];
        if (c && c.duration > durationLimit) durationLimit = c.duration;
      });
      const outChannels = exportSettings.channels === 'mono' ? 1 : 2;
      const offlineCtx = new OfflineAudioContext(outChannels, Math.ceil(targetRate * Math.max(0.1, durationLimit)), targetRate);
      // Build the FULL graph (per-track FX Rack chains + mastering chain — the
      // same DSP as playback), so the exported file matches what you hear.
      const savedMasterBus = masterBus;
      const offlineNodes = {};
      try {
        window.currentMasteringSettings = masteringSettings || window.currentMasteringSettings;
        try { if (typeof stopAllPlayback === 'function') stopAllPlayback(); } catch (e) { }
        masterBus = null;
        initMasterBus(offlineCtx);
        try {
          toggleMasteringOnMaster(!!(masteringSettings && masteringSettings.masterConnected), !!(masteringSettings && masteringSettings.isBypassed));
        } catch (e) { console.warn('offline mastering toggle error:', e); }
        // Main out volume (master fader) áp vào offline bus — file export phải
        // đúng độ lớn nghe được (flow: ... → Mastering FX Chain → Main out).
        // Trước đây offline masterBus.output luôn = 1.0 → export sai volume.
        try {
          if (masterBus && masterBus.output) {
            const mv = (typeof masterVolume === 'number' && isFinite(masterVolume)) ? masterVolume : 0;
            masterBus.output.gain.setValueAtTime(mv <= -50 ? 0 : Math.pow(10, mv / 20), 0);
          }
        } catch (e) { console.warn('offline main-out volume error:', e); }
        activeTracks.forEach(t => {
          const node = buildOfflineTrackNode(t, offlineCtx, offlineNodes);
          if (!node) return;
          const clips = t.clips && t.clips.length > 0 ? t.clips : t.buffer ? [{
            id: 'default',
            buffer: t.buffer,
            startTime: t.startTime || 0,
            name: t.name,
            speed: t.speed || 1.0
          }] : [];
          clips.forEach(clip => {
            if (!clip.buffer) return;
            const source = offlineCtx.createBufferSource();
            source.buffer = clip.buffer;
            source.playbackRate.value = clip.speed || 1.0;
            source.connect(node.gainNode);
            const clipStart = clip.startTime || 0;
            source.start(clipStart);
          });
          // MIDI preview cache → schedule through the SF chain (track FX + mastering)
          if (node.sfEntry) {
            const midiBuf = midiCacheToBuffer(t.id);
            if (midiBuf) {
              const mSrc = offlineCtx.createBufferSource();
              mSrc.buffer = midiBuf;
              mSrc.connect(node.sfEntry);
              mSrc.start(0);
            }
          }
        });
        const renderedBuffer = await offlineCtx.startRendering();
      const numExportCh = renderedBuffer.numberOfChannels;
      const exportLength = renderedBuffer.length;
      const bytesPerSample = bitDepth / 8;
      const headerSize = 44;
      const fileSizeBytes = headerSize + exportLength * bytesPerSample * numExportCh;
      const fileBuffer = new ArrayBuffer(fileSizeBytes);
      const view = new DataView(fileBuffer);
      const writeString = (offset, string) => {
        for (let i = 0; i < string.length; i++) {
          view.setUint8(offset + i, string.charCodeAt(i));
        }
      };
      writeString(0, 'RIFF');
      view.setUint32(4, fileSizeBytes - 8, true);
      writeString(8, 'WAVE');
      writeString(12, 'fmt ');
      view.setUint32(16, 16, true);
      view.setUint16(20, 1, true);
      view.setUint16(22, numExportCh, true);
      view.setUint32(24, targetRate, true);
      view.setUint32(28, targetRate * bytesPerSample * numExportCh, true);
      view.setUint16(32, bytesPerSample, true);
      view.setUint16(34, bitDepth, true);
      writeString(36, 'data');
      view.setUint32(40, exportLength * bytesPerSample * numExportCh, true);
      let offset = 44;
      for (let i = 0; i < exportLength; i++) {
        for (let ch = 0; ch < numExportCh; ch++) {
          const chData = renderedBuffer.getChannelData(ch);
          const sample = Math.max(-1, Math.min(1, chData[i]));
          if (bitDepth === 8) {
            view.setUint8(offset, Math.floor((sample + 1.0) * 127.5));
          } else if (bitDepth === 16) {
            view.setInt16(offset, Math.floor(sample < 0 ? sample * 0x8000 : sample * 0x7FFF), true);
          } else if (bitDepth === 24) {
            const val24 = Math.floor(sample < 0 ? sample * 0x800000 : sample * 0x7FFFFF);
            view.setUint8(offset, val24 & 0xFF);
            view.setUint8(offset + 1, val24 >> 8 & 0xFF);
            view.setUint8(offset + 2, val24 >> 16 & 0xFF);
          }
          offset += bytesPerSample;
        }
      }
      const blob = new Blob([view], {
        type: 'audio/wav'
      });
      const url = URL.createObjectURL(blob);
      if ((exportSettings.format === 'mp3' || exportSettings.format === 'ogg') && serverStatus === 'connected') {
        setIsExporting(true);
        showToast("Đang gửi yêu cầu nén định dạng lên máy chủ...", "info");
        try {
          const file = new File([blob], `session_export.wav`, { type: 'audio/wav' });
          const formData = new FormData();
          formData.append('file', file);
          const uploadResp = await fetch(`${API_AUDIO}/upload`, {
            method: 'POST',
            body: formData
          });
          if (!uploadResp.ok) throw new Error("Upload to transcode server failed");
          const uploadData = await uploadResp.json();
          const fileId = uploadData.file_id;

          const exportResp = await fetch(`${API_AUDIO}/export`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
              file_id: fileId,
              format: exportSettings.format,
              sample_rate: targetRate,
              bit_depth: bitDepth
            })
          });
          if (!exportResp.ok) throw new Error("Transcode request failed");
          const exportData = await exportResp.json();
          const result = await pollTaskResult(exportData.task_id, 20);
          if (result.success) {
            const outId = result.output_file_id;
            const downloadUrl = `${API_AUDIO}/download/${outId}`;
            showToast(`Xuất bản âm thanh định dạng ${exportSettings.format.toUpperCase()} thành công!`, "success", "Tải về", () => {
              const a = document.createElement('a');
              a.href = downloadUrl;
              a.download = `sonicforge-session-${targetRate}hz-${bitDepth}bit.${exportSettings.format}`;
              a.click();
            });
          } else {
            throw new Error(result.error || 'Server encoding failed');
          }
        } catch (transcodeErr) {
          showToast(`Lỗi chuyển đổi: ${transcodeErr.message}.`, "warning", "Tải WAV thay thế", () => {
            const a = document.createElement('a');
            a.href = url;
            a.download = `sonicforge-session-${targetRate}hz-${bitDepth}bit.wav`;
            a.click();
          });
        }
      } else {
        if (exportSettings.format !== 'wav') {
          showToast("Đang ngoại tuyến. Tải về định dạng WAV thay thế.", "warning");
        }
        const a = document.createElement('a');
        a.href = url;
        a.download = `sonicforge-session-${targetRate}hz-${bitDepth}bit.wav`;
        a.click();
        showToast("Xuất bản âm thanh hoàn tất!", "success");
      }
      URL.revokeObjectURL(url);
      } finally {
        // Restore the live master bus + live mastering state after offline render.
        masterBus = savedMasterBus;
        try { if (savedMasterBus) toggleMasteringOnMaster(!!(masteringSettings && masteringSettings.masterConnected), !!(masteringSettings && masteringSettings.isBypassed)); } catch (e) { }
        Object.keys(offlineNodes).forEach(k => { try { if (offlineNodes[k].fxStopFn) offlineNodes[k].fxStopFn(); } catch (e) { } });
      }
    } catch (err) {
      showToast("Lỗi xuất âm thanh: " + err.message, "error");
    } finally {
      setIsExporting(false);
    }
  };

  // ── AI Analysis (server-side with fallback) ──
  const triggerAIAnalysis = async () => {
    setAnalysisState({
      status: 'Connecting to AI Engine...',
      data: null,
      isRunning: true
    });

    // Check if we have a file on the server to analyze
    const activeTrack = tracks.find(t => t.id === selectedTrackId);
    const serverFileId = serverFileIdMap[selectedTrackId];
    if (serverFileId && serverStatus === 'connected') {
      try {
        const resp = await fetch(`${API_AUDIO}/analyze-ai`, {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json'
          },
          body: JSON.stringify({
            file_id: serverFileId,
            api_base_url: aiConfig.baseUrl !== API_BASE_URL ? aiConfig.baseUrl : null,
            model: aiConfig.model
          })
        });
        if (resp.ok) {
          const data = await resp.json();
          showToast("Đang phân tích cấu trúc trên máy chủ...", "info");
          try {
            const result = await pollTaskResult(data.task_id, 15);
            if (result.bpm) {
              if (selLeft !== null && selRight !== null && activeTrack && activeTrack.buffer) {
                const snapStart = findZeroCrossing(activeTrack.buffer, selLeft);
                const snapEnd = findZeroCrossing(activeTrack.buffer, selRight);
                if (selectionMode === 'local') {
                  setLocalSelectionStart(snapStart);
                  setLocalSelectionEnd(snapEnd);
                } else {
                  setSelectionStart(snapStart);
                  setSelectionEnd(snapEnd);
                }
              }
              setAnalysisState({
                status: 'Hoàn thành phân tích (Server)',
                data: result,
                isRunning: false
              });
              showToast(`AI Server: ${result.bpm} BPM | ${result.beats?.length || 0} beats detected (Zero-Crossing Aligned)`, 'success');
              return;
            }
          } catch (e) {
            console.warn('Server AI poll failed, using fallback:', e);
          }
        }
      } catch (err) {
        console.warn('Server AI analysis failed, using fallback:', err);
      }
    }

    // Fallback: client-side analysis with simulated BPM
    setAnalysisState({
      status: 'Processing structural detection...',
      data: null,
      isRunning: true
    });
    setTimeout(() => {
      let bpm = 120;
      // Try to detect BPM from buffer
      if (activeTrack && activeTrack.buffer) {
        const data = activeTrack.buffer.getChannelData(0);
        const sr = activeTrack.buffer.sampleRate;
        // Simple autocorrelation for BPM estimation
        const windowSize = Math.min(sr * 3, data.length);
        if (windowSize > sr) {
          let maxCorr = 0;
          let bestLag = Math.floor(sr * 0.25); // ~240 BPM max
          for (let lag = Math.floor(sr * 0.25); lag < Math.floor(sr * 2); lag++) {
            let corr = 0;
            const n = Math.floor(windowSize / 2);
            for (let i = 0; i < n; i++) {
              corr += data[i] * data[i + lag];
            }
            corr /= n;
            if (corr > maxCorr) {
              maxCorr = corr;
              bestLag = lag;
            }
          }
          if (bestLag > 0) {
            bpm = Math.round(60 * sr / bestLag);
            bpm = Math.max(60, Math.min(200, bpm));
          }
        }
      }
      if (selLeft !== null && selRight !== null && activeTrack && activeTrack.buffer) {
        const snapStart = findZeroCrossing(activeTrack.buffer, selLeft);
        const snapEnd = findZeroCrossing(activeTrack.buffer, selRight);
        if (selectionMode === 'local') {
          setLocalSelectionStart(snapStart);
          setLocalSelectionEnd(snapEnd);
        } else {
          setSelectionStart(snapStart);
          setSelectionEnd(snapEnd);
        }
      }
      setAnalysisState({
        status: 'Hoàn thành phân tích (Client)',
        data: {
          bpm,
          bars: Math.max(4, Math.round(bpm / 30)),
          timeSig: '4/4',
          detectedKey: 'Am'
        },
        isRunning: false
      });
      showToast(`Phân tích: ${bpm} BPM (Zero-Crossing Aligned)`, 'success');
    }, 1500);
  };

  // ── AI Analysic Loop: scan track, detect beats, place markers for loop selection ──
  const handleAIAnalysicLoop = async () => {
    const forcedTrackId = subTabAiTrackIdRef.current;
    subTabAiTrackIdRef.current = null;
    const activeTrackId = forcedTrackId || selectedTrackId;
    const activeTrack = tracks.find(t => t.id === activeTrackId);
    if (!activeTrack || !activeTrack.buffer) {
      showToast("Vui lòng chọn một Track có âm thanh để AI phân tích.", "warning");
      return;
    }
    setAnalysisState({
      status: 'AI Analysic Loop: đang phát hiện nhịp...',
      data: null,
      isRunning: true
    });
    showToast("AI Analysic Loop: đang quét cấu trúc nhịp điệu...", "info");
    try {
      const buffer = activeTrack.buffer;
      const data = buffer.getChannelData(0);
      const sr = buffer.sampleRate;
      const windowSize = Math.min(sr * 3, data.length);

      // Client-side BPM detection via autocorrelation
      let detectedBPM = 120;
      if (windowSize > sr) {
        let maxCorr = 0;
        for (let lag = Math.floor(sr * 0.3); lag <= Math.floor(sr * 2.0); lag++) {
          let corr = 0;
          const step = 4;
          for (let i = 0; i < windowSize && i + lag < data.length; i += step) {
            corr += data[i] * data[i + lag];
          }
          corr /= windowSize / step;
          if (corr > maxCorr) {
            maxCorr = corr;
            detectedBPM = 60 / (lag / sr);
          }
        }
      }
      detectedBPM = Math.round(Math.min(240, Math.max(60, detectedBPM)));
      const beatDuration = 60 / detectedBPM;
      const barDuration = beatDuration * 4;
      const totalDuration = buffer.duration;

      // Place markers at each bar (strong beat) position
      const barMarkers = [];
      for (let t = 0; t < totalDuration; t += barDuration) {
        const zcTime = findZeroCrossing(buffer, t);
        barMarkers.push({
          id: 'ai_bar_' + barMarkers.length + '_' + Date.now(),
          time: zcTime,
          label: `Bar ${barMarkers.length + 1}`,
          color: '#06b6d4'
        });
        // Add beat markers within each bar
        for (let b = 1; b < 4; b++) {
          const bt = t + b * beatDuration;
          if (bt < totalDuration) {
            const zcBt = findZeroCrossing(buffer, bt);
            barMarkers.push({
              id: 'ai_beat_' + barMarkers.length + '_' + Date.now(),
              time: zcBt,
              label: `Beat ${b + 1}`,
              color: '#a855f7'
            });
          }
        }
      }
      setTracks(prev => prev.map(t => {
        if (t.id !== activeTrack.id) return t;
        const existingMarkers = t.markers || [];
        return {
          ...t,
          markers: [...existingMarkers, ...barMarkers]
        };
      }));

      // Find the first strong beat to set as selection start
      const firstBeat = barMarkers.length > 0 ? barMarkers[0].time : 0;
      const secondBar = barMarkers.length > 4 ? barMarkers[Math.min(4, barMarkers.length - 1)].time : Math.min(totalDuration, firstBeat + barDuration);
      setSelectionStart(firstBeat);
      setSelectionEnd(secondBar);
      setAnalysisState({
        status: `AI Analysic Loop: ${detectedBPM} BPM, ${barMarkers.length} markers (Bar/Beat)`,
        data: {
          bpm: detectedBPM,
          bars: Math.floor(totalDuration / barDuration)
        },
        isRunning: false
      });
      showToast(`AI Analysic Loop: ${detectedBPM} BPM - ${Math.floor(totalDuration / barDuration)} bars detected`, "success");
    } catch (err) {
      setAnalysisState({
        status: 'Lỗi AI Analysic Loop',
        data: null,
        isRunning: false
      });
      showToast(err.message || 'Lỗi khi phân tích nhịp', 'error');
    }
  };
  // ── Mark Selection ──
  // ── Helper: set selection range from buffer (used by sub-tab AI) ──
  const setSelectionRangeOnBuffer = (buffer, startTime, endTime) => {
    setSelectionStart(startTime);
    setSelectionEnd(endTime);
  };

  // Ref for forced trackId (used by sub-tab AI buttons, overrides selectedTrackId)
  const subTabAiTrackIdRef = useRef(null);
  const handleAIScan = async () => {
    // Resolve track: prefer sub-tab override, then selectedTrackId
    const forcedTrackId = subTabAiTrackIdRef.current;
    subTabAiTrackIdRef.current = null;
    const activeTrackId = forcedTrackId || selectedTrackId;
    const activeTrack = tracks.find(t => t.id === activeTrackId);
    if (!activeTrack || !activeTrack.buffer) {
      showToast("Vui lòng chọn một Track có âm thanh để AI quét Loop.", "warning");
      return;
    }
    setAnalysisState({
      status: 'AI Loop Scan đang quét nhịp điệu và phách mạnh...',
      data: null,
      isRunning: true
    });
    showToast("AI Scan đang phân tích tempo và phách mạnh...", "info");
    try {
      const buffer = activeTrack.buffer;
      const data = buffer.getChannelData(0);
      const sr = buffer.sampleRate;
      const windowSize = Math.min(sr * 3, data.length);

      // Detect BPM via autocorrelation
      let detectedBPM = 120;
      if (windowSize > sr) {
        let maxCorr = 0;
        for (let lag = Math.floor(sr * 0.3); lag <= Math.floor(sr * 2.0); lag++) {
          let corr = 0;
          const step = 4;
          for (let i = 0; i < windowSize && i + lag < data.length; i += step) {
            corr += data[i] * data[i + lag];
          }
          corr /= windowSize / step;
          if (corr > maxCorr) {
            maxCorr = corr;
            detectedBPM = 60 / (lag / sr);
          }
        }
      }
      detectedBPM = Math.round(Math.min(240, Math.max(60, detectedBPM)));

      // Calculate bar grid
      const beatDuration = 60 / detectedBPM;
      const barDuration = beatDuration * 4;
      const totalDuration = buffer.duration;

      // Place markers at bar starts (strong beats / downbeats)
      const barMarkers = [];
      for (let t = 0; t < totalDuration; t += barDuration) {
        const zcTime = findZeroCrossing(buffer, t);
        barMarkers.push({
          id: 'ai_bar_' + barMarkers.length + '_' + Date.now(),
          time: zcTime,
          label: `Downbeat ${barMarkers.length + 1}`,
          color: '#06b6d4'
        });
      }
      setTracks(prev => prev.map(t => {
        if (t.id !== activeTrack.id) return t;
        const existingMarkers = t.markers || [];
        return {
          ...t,
          markers: [...existingMarkers, ...barMarkers]
        };
      }));

      // Set selection to the first downbeat
      const firstBarStart = barMarkers.length > 0 ? barMarkers[0].time : 0;
      const secondBarStart = barMarkers.length > 1 ? barMarkers[1].time : Math.min(totalDuration, firstBarStart + barDuration);
      setSelectionStart(firstBarStart);
      setSelectionEnd(secondBarStart);
      setAnalysisState({
        status: `AI Scan: ${detectedBPM} BPM, ${barMarkers.length} downbeats (đã snap zero-crossing)`,
        data: {
          bpm: detectedBPM
        },
        isRunning: false
      });
      showToast(`AI Scan: ${detectedBPM} BPM - ${barMarkers.length} downbeats detected`, "success");
    } catch (err) {
      setAnalysisState({
        status: 'Lỗi khi AI Scan',
        data: null,
        isRunning: false
      });
      showToast(err.message || 'Lỗi khi quét AI Loop', 'error');
    }
  };
  const runPythonTool = async toolType => {
    const activeTrack = tracks.find(t => t.id === selectedTrackId);
    if (!activeTrack || !activeTrack.buffer) {
      showToast("Vui lòng chọn một Track để xử lý công cụ Python.", "warning");
      return;
    }
    try {
      if (toolType === 'normalize') {
        const channelData = activeTrack.buffer.getChannelData(0);
        let maxVal = 0;
        for (let i = 0; i < channelData.length; i++) {
          maxVal = Math.max(maxVal, Math.abs(channelData[i]));
        }
        if (maxVal > 0) {
          const gain = 1.0 / maxVal;
          for (let i = 0; i < channelData.length; i++) {
            channelData[i] *= gain;
          }
        }
        showToast("Đã Chuẩn Hóa Peak âm thanh về 0 dB!", "success");
      } else if (toolType === 'invert_phase') {
        const channelData = activeTrack.buffer.getChannelData(0);
        for (let i = 0; i < channelData.length; i++) {
          channelData[i] *= -1;
        }
        showToast("Đã Đảo Pha (180°) âm thanh thành công!", "success");
      } else if (toolType === 'swap_channels') {
        if (activeTrack.buffer.numberOfChannels >= 2) {
          const left = activeTrack.buffer.getChannelData(0);
          const right = activeTrack.buffer.getChannelData(1);
          for (let i = 0; i < left.length; i++) {
            const temp = left[i];
            left[i] = right[i];
            right[i] = temp;
          }
          showToast("Đã Đổi Kênh Left / Right thành công!", "success");
        } else {
          showToast("Track hiện tại là Mono. Chỉ áp dụng Đổi Kênh cho Stereo.", "info");
        }
      } else if (toolType === 'synth_wave') {
        generateSynthToTrack(activeTrack.id, 'synth');
        showToast("Đã tạo Tín Hiệu Sóng Tổng Hợp bằng công cụ Python!", "success");
      }
    } catch (err) {
      showToast(err.message || "Lỗi khi chạy công cụ Python", "error");
    }
  };
  const handleMarkSelection = () => {
    if (selLeft === null || selRight === null || selectionStats.length === 0) {
      showToast("Vui lòng chọn một khoảng thời gian trên sóng âm trước.", "warning");
      return;
    }
    const targetTrack = tracks.find(t => t.id === selectedTrackId);
    if (!targetTrack || !targetTrack.buffer) {
      showToast("Vui lòng nhấp chọn một Track có sóng âm để gán Marker.", "warning");
      return;
    }

    // For local selection, force mark on the local-selected track
    const markTrackId = selectionMode === 'local' && localSelectionTrackId ? localSelectionTrackId : selectedTrackId;
    setTracks(prev => prev.map(t => {
      if (t.id !== markTrackId) return t;
      const snapStart = findZeroCrossing(t.buffer, selLeft);
      const snapEnd = findZeroCrossing(t.buffer, selRight);
      const newMarkers = [...t.markers, {
        id: Date.now() + '_s',
        time: snapStart
      }, {
        id: Date.now() + '_e',
        time: snapEnd
      }].sort((a, b) => a.time - b.time);
      return {
        ...t,
        markers: newMarkers
      };
    }));
    showToast(`Đã tạo 2 Markers tại đầu và cuối dải chọn (Snap Zero-Crossing)`, "success");
  };

  // ── AI Cut to New Track (Music Theory Loop Detection) ──
  const handleAICutToNewTrack = () => {
    const forcedTrackId = subTabAiTrackIdRef.current;
    subTabAiTrackIdRef.current = null;
    const activeTrackId = forcedTrackId || selectedTrackId;
    const activeTrack = tracks.find(t => t.id === activeTrackId);
    if (!activeTrack || !activeTrack.buffer) {
      showToast("Vui lòng chọn một Track có dữ liệu âm thanh trước.", "warning");
      return;
    }
    if (selectionStart === null || selectionEnd === null || selectionStats.length === 0) {
      showToast("Vui lòng kéo chọn một khoảng thời gian trên sóng âm.", "warning");
      return;
    }
    setAnalysisState({
      status: 'AI Cut: đang phân tích nhịp và tìm loop point...',
      data: null,
      isRunning: true
    });
    showToast("AI Cut: đang phân tích nhịp điệu và tìm điểm loop chính xác...", "info");
    setTimeout(() => {
      try {
        const buffer = activeTrack.buffer;
        const sampleRate = buffer.sampleRate;
        const channelData = buffer.getChannelData(0);
        const dataLen = channelData.length;
        const windowSize = Math.min(sampleRate * 3, dataLen);

        // Detect BPM via autocorrelation
        let detectedBPM = 120;
        if (windowSize > sampleRate) {
          let maxCorr = 0;
          for (let lag = Math.floor(sampleRate * 0.3); lag <= Math.floor(sampleRate * 2.0); lag++) {
            let corr = 0;
            const step = 4;
            for (let i = 0; i < windowSize && i + lag < dataLen; i += step) {
              corr += channelData[i] * channelData[i + lag];
            }
            corr /= windowSize / step;
            if (corr > maxCorr) {
              maxCorr = corr;
              detectedBPM = 60 / (lag / sampleRate);
            }
          }
        }
        detectedBPM = Math.round(Math.min(240, Math.max(60, detectedBPM)));
        const beatDuration = 60 / detectedBPM;
        const barDuration = beatDuration * 4;

        // Use the selection range
        const rawStart = selectionStats.start;
        const rawEnd = selectionStats.end;
        const selDuration = rawEnd - rawStart;

        // Find the nearest bar start (downbeat) for loop start
        const barsFromZero = rawStart / barDuration;
        const nearestBarStart = Math.round(barsFromZero) * barDuration;
        const loopStart = Math.max(0, Math.min(rawStart + barDuration, nearestBarStart));

        // Find the nearest beat 4 (bar end) for loop end
        // In 4/4 time: beat 4 = barStart + 3*beatDuration = barEnd
        const barsFromStart = rawEnd / barDuration;
        const nearestBarEnd = Math.round(barsFromStart) * barDuration;
        // Ensure minimum 1 bar loop
        let loopEnd = Math.max(loopStart + barDuration, nearestBarEnd);
        if (loopEnd > rawEnd + beatDuration) loopEnd = loopStart + Math.ceil(selDuration / barDuration) * barDuration;

        // Snap to zero-crossing for click-free loop
        const snapLoopStart = findZeroCrossing(buffer, loopStart);
        const snapLoopEnd = findZeroCrossing(buffer, loopEnd);

        // Place markers for the loop points
        setTracks(prev => prev.map(t => {
          if (t.id !== activeTrack.id) return t;
          const existingMarkers = t.markers || [];
          const filtered = existingMarkers.filter(m => !m.id.startsWith('ai_loop_'));
          return {
            ...t,
            markers: [...filtered, {
              id: 'ai_loop_start_' + Date.now(),
              time: snapLoopStart,
              label: 'Loop Start (Bar ' + (Math.floor(snapLoopStart / barDuration) + 1) + ')',
              color: '#06b6d4'
            }, {
              id: 'ai_loop_end_' + Date.now(),
              time: snapLoopEnd,
              label: 'Loop End (Beat 4)',
              color: '#a855f7'
            }]
          };
        }));
        setSelectionStart(snapLoopStart);
        setSelectionEnd(snapLoopEnd);
        const startSample = Math.max(0, Math.min(dataLen - 1, Math.floor(snapLoopStart * sampleRate)));
        const endSample = Math.max(0, Math.min(dataLen, Math.floor(snapLoopEnd * sampleRate)));
        const sliceLength = endSample - startSample;
        if (sliceLength <= 0) {
          showToast("Dải cắt không hợp lệ hoặc khoảng thời gian quá ngắn.", "error");
          setAnalysisState({
            status: 'Thất bại',
            data: null,
            isRunning: false
          });
          return;
        }
        const context = getAudioContext();
        const numChannels = buffer.numberOfChannels || 1;
        const slicedBuffer = context.createBuffer(numChannels, sliceLength, sampleRate);
        for (let c = 0; c < numChannels; c++) {
          const srcData = buffer.getChannelData(c);
          const dstData = slicedBuffer.getChannelData(c);
          dstData.set(srcData.subarray(startSample, endSample));
        }
        const rearrangeNewId = 'track_ai_cut_' + Date.now();
        const colors = ['#0f766e', '#1d4ed8', '#701a75', '#a21caf', '#b45309'];
        const selectColor = colors[tracks.length % colors.length];
        const barNum = Math.floor(snapLoopStart / barDuration) + 1;
        const barsCount = Math.max(1, Math.round((snapLoopEnd - snapLoopStart) / barDuration));
        const newTrack = {
          id: rearrangeNewId,
          name: `Loop_${barNum}bar_${activeTrack.name.replace('.wav', '').slice(0, 10)}_${snapLoopStart.toFixed(1)}s.wav`,
          buffer: slicedBuffer,
          channelInfo: activeTrack.channelInfo ? {
            ...activeTrack.channelInfo
          } : null,
          startTime: snapLoopStart,
          clips: [{
            id: 'clip_' + rearrangeNewId,
            buffer: slicedBuffer,
            startTime: snapLoopStart,
            name: `Loop_${barNum}bar_${snapLoopStart.toFixed(1)}s`
          }],
          volumeDb: 0,
          pan: 0,
          muted: false,
          solo: false,
          color: selectColor,
          markers: [{
            id: Date.now() + '_s',
            time: 0
          }, {
            id: Date.now() + '_e',
            time: snapLoopEnd - snapLoopStart
          }],
          serverFileId: null
        };
        setTracks(prev => {
          const idx = prev.findIndex(t => t.id === selectedTrackId);
          const updated = [...prev];
          if (idx !== -1) {
            updated.splice(idx + 1, 0, newTrack);
          } else {
            updated.push(newTrack);
          }
          return updated;
        });
        setSelectedTrackId(rearrangeNewId);
        setAnalysisState({
          status: `AI Cut: ${detectedBPM} BPM, ${barsCount} bars loop (Zero-Crossing aligned)`,
          data: {
            bpm: detectedBPM,
            bars: barsCount,
            timeSig: '4/4'
          },
          isRunning: false
        });
        showToast(`AI Cut: ${barsCount} bars loop at ${snapLoopStart.toFixed(3)}s - ${snapLoopEnd.toFixed(3)}s [${detectedBPM} BPM]`, "success");
        setTimeout(() => lucide.createIcons(), 200);
      } catch (err) {
        showToast("Lỗi khi AI Cut: " + err.message, "error");
        setAnalysisState({
          status: 'Lỗi AI Cut',
          data: null,
          isRunning: false
        });
      }
    }, 800);
  };

  // ── AI Prompt Send to Active Provider ──
  const handleAISend = async () => {
    const prompt = aiPrompt.trim();
    if (!prompt) { showToast('Vui lòng nhập nội dung prompt.', 'warning'); return; }

    // Check if piano roll tab is active → route prompt to MIDI generation
    const activePianoRoll = subTabs.find(s => s.id === activeTab && s.type === 'PIANO_ROLL');
    if (activePianoRoll) {
      setAiProcessing(true);
      setAiActionLog(prev => [...prev, { type: 'status', text: ` ⏳ Đang gửi prompt đến AI cho Piano Roll...`, time: Date.now() }]);
      try {
        if (aiProviders.length === 0 || !selectedProviderId) {
          try {
            const data = await window.SonicAPI.getAIConfigs();
            if (data && data.providers && data.providers.length > 0) {
              setAiProviders(data.providers);
              const active = data.providers.find(p => p.is_active) || data.providers[0];
              if (active) setSelectedProviderId(active.id);
            }
          } catch (e) { }
        }
        const prv = aiProviders.find(p => p.id === selectedProviderId) || (aiProviders.length > 0 ? aiProviders[0] : null);
        const provider = prv || aiConfig;
        const baseUrl = provider.api_base_url || provider.baseUrl || `${API_BASE_URL}`;
        const apiKey = provider.api_key || provider.apiKey || '';
        const model = provider.model_name || provider.model || 'deepseek-chat';
        setAiActionLog(prev => [...prev, { type: 'info', text: ` Provider: ${provider.name || 'default'} | Model: ${model}`, time: Date.now() }]);

        let pianoSystemInstruction = 'You are a MIDI composer. Output ONLY valid JSON array of notes. No markdown, no explanation. Use 4/4 time. pitch=C4=60, D4=62, E4=64, F4=65, G4=67, A4=69, B4=71, C5=72.';

        const result = await window.AIGateway.executeAIPrompt({
          prompt: 'Generate MIDI notes for a piano roll. Return ONLY a JSON array: [{pitch(0-127), start_beat, duration_beats, velocity(0.0-1.0)}]. ' + prompt,
          provider: provider.name || 'default',
          model: model,
          apiKey: apiKey,
          baseUrl: baseUrl.replace(/\/chat\/completions$/, '').replace(/\/$/, ''),
          systemInstruction: pianoSystemInstruction
        });
        if (!result) throw new Error('AI không phản hồi');
        let notesData = null;
        const textResp = result.textResponse || result.text || '';
        if (textResp && typeof textResp === 'string') {
          try {
            const cleaned = textResp.replace(/```json?\s*/g, '').replace(/```/g, '').trim();
            notesData = JSON.parse(cleaned);
          } catch (e1) { console.error('Parse notes error:', e1); }
        }
        if (!notesData && result.functionCalls) {
          for (const fc of result.functionCalls) {
            if (fc.arguments && fc.arguments.notes) { notesData = fc.arguments.notes; break; }
          }
        }
        if (Array.isArray(notesData) && notesData.length > 0) {
          const newNotes = notesData.map((n, i) => ({
            id: 'note_ai_' + Date.now() + '_' + i,
            pitch: Math.max(0, Math.min(127, n.pitch || 60)),
            start_beat: Math.max(0, parseFloat(n.start_beat) || 0),
            duration_beats: Math.max(0.125, parseFloat(n.duration_beats) || 0.25),
            velocity: Math.max(0.1, Math.min(1.0, n.velocity ?? 0.8)),
            pan: 0.0
          }));
          setSubTabs(prev => prev.map(s => s.id === activePianoRoll.id ? { ...s, notes: [...(s.notes || []), ...newNotes], isDirty: true } : s));
          setAiActionLog(prev => [...prev, { type: 'status', text: ` ✅ Đã thêm ${newNotes.length} notes vào Piano Roll`, time: Date.now() }]);
          setCanvasRedrawCount(n => n + 1);
        } else {
          setAiActionLog(prev => [...prev, { type: 'error', text: ` ❌ AI không trả về notes hợp lệ.`, time: Date.now() }]);
        }
      } catch (err) {
        setAiActionLog(prev => [...prev, { type: 'error', text: ` ❌ Lỗi: ${err.message}`, time: Date.now() }]);
      }
      setAiProcessing(false);
      return;
    }

    // ── MIDI Rearrange Flow: selected MIDI item → use rearrange tool ──
    if (selectedItemIds && selectedItemIds.size === 1) {
      const selId = selectedItemIds.values().next().value;
      let isMidiItem = false;
      let sourceTrackId = null;
      let sourceItemName = null;
      for (const t of activeTracks || []) {
        const found = (t.midiItems || []).find(m => m.id === selId);
        if (found) { isMidiItem = true; sourceTrackId = t.id; sourceItemName = found.name; break; }
      }
      if (isMidiItem && window.SonicMidiExtractor) {
        try {
          setAiProcessing(true);
          setAiActionLog(prev => [...prev, { type: 'status', text: ` 🎯 Phát hiện MIDI item được chọn — chuyển sang chế độ Rearrange...`, time: Date.now() }]);

          if (aiProviders.length === 0 || !selectedProviderId) {
            try { const d = await window.SonicAPI.getAIConfigs(); if (d && d.providers && d.providers.length > 0) { setAiProviders(d.providers); const a = d.providers.find(p => p.is_active) || d.providers[0]; if (a) setSelectedProviderId(a.id); } } catch (e) {}
          }
          const prv = aiProviders.find(p => p.id === selectedProviderId) || (aiProviders.length > 0 ? aiProviders[0] : null);
          const provider = prv || aiConfig;
          const baseUrl = provider.api_base_url || provider.baseUrl || `${API_BASE_URL}`;
          const apiKey = provider.api_key || provider.apiKey || '';
          const model = provider.model_name || provider.model || 'deepseek-chat';
          setAiActionLog(prev => [...prev, { type: 'info', text: ` Rearrange Provider: ${provider.name || 'default'} | Model: ${model}`, time: Date.now() }]);

          const srcContext = window.SonicMidiExtractor.extractSelectedMIDIContext(activeTracks || tracks, selId, bpm);
          setAiActionLog(prev => [...prev, { type: 'info', text: ` 📋 Trích xuất ${srcContext.total_notes} notes từ "${srcContext.item_name}" (${srcContext.track_name})`, time: Date.now() }]);

          if (window.DAWCommandDispatcher) {
            window.DAWCommandDispatcher.rearrangeSourceTrackId = sourceTrackId;
            window.DAWCommandDispatcher.rearrangeSourceItemName = sourceItemName;
          }

          const messages = window.AIGateway.buildRearrangeMessage(prompt, srcContext);
          const completion = await window.AIGateway.callLLM({
            provider: provider.name || 'default',
            model,
            apiKey,
            baseUrl: baseUrl.replace(/\/chat\/completions$/, '').replace(/\/$/, ''),
            messages,
            tools: [window.AIGateway.REARRANGE_TOOL_SPEC.function],
            toolChoice: 'auto'
          });

          const functionCalls = window.AIGateway.extractFunctionCalls(completion);
          if (functionCalls && functionCalls.length > 0) {
            for (const fc of functionCalls) {
              setAiActionLog(prev => [...prev, { type: 'info', text: ` Gọi lệnh: ${fc.name}(${JSON.stringify(fc.arguments)})`, time: Date.now() }]);
              const cmdName = fc.name.toUpperCase();
              if (window.DAWCommandDispatcher) {
                try {
                  let cmdResult = window.DAWCommandDispatcher.execute(cmdName, fc.arguments);
                  if (cmdResult && typeof cmdResult.then === 'function') cmdResult = await cmdResult;
                  setAiActionLog(prev => [...prev, { type: 'status', text: ` ✅ ${fc.name}: thành công — ${fc.arguments.rearrange_title || ''}`, time: Date.now() }]);
                } catch (cmdErr) {
                  setAiActionLog(prev => [...prev, { type: 'error', text: ` ❌ ${fc.name}: ${cmdErr.message}`, time: Date.now() }]);
                }
              }
            }
          } else {
            setAiActionLog(prev => [...prev, { type: 'error', text: ` ❌ AI không trả về lệnh rearrange hợp lệ.`, time: Date.now() }]);
          }

          const textResp = functionCalls.length === 0 && completion.choices && completion.choices[0] && completion.choices[0].message && completion.choices[0].message.content;
          if (textResp) {
            setAiActionLog(prev => [...prev, { type: 'status', text: ` AI: ${textResp.slice(0, 500)}`, time: Date.now() }]);
          }
          if (prompt) {
            promptHistRef.current = [...promptHistRef.current.slice(-49), prompt];
            setPromptHistory(promptHistRef.current);
          }
          setPromptHistIdx(-1);
          setAiActionLog(prev => [...prev, { type: 'status', text: ` Hoàn tất Rearrange.`, time: Date.now() }]);
          setAiPrompt('');
          setTimeout(() => lucide.createIcons(), 200);
          setAiProcessing(false);
          return;
        } catch (err) {
          setAiActionLog(prev => [...prev, { type: 'error', text: ` Lỗi Rearrange: ${err.message}`, time: Date.now() }]);
          showToast(`AI Rearrange Error: ${err.message}`, 'error');
          setAiProcessing(false);
          return;
        }
      }
    }

    if (window.DAWCommandDispatcher) {
      window.DAWCommandDispatcher.currentSelectedTrackId = selectedTrackId;
      window.DAWCommandDispatcher.currentTracks = activeTracks;
      window.DAWCommandDispatcher.lastCutSourceTrackId = null;
      window.DAWCommandDispatcher.lastCutNewTrackId = null;
    }
    setAiProcessing(true);
    setAiActionLog(prev => [...prev, { type: 'status', text: ` ⏳ Đang gửi prompt đến AI...`, time: Date.now() }]);
    try {
      if (aiProviders.length === 0 || !selectedProviderId) {
        try {
          const data = await window.SonicAPI.getAIConfigs();
          if (data && data.providers && data.providers.length > 0) {
            setAiProviders(data.providers);
            const active = data.providers.find(p => p.is_active) || data.providers[0];
            if (active) setSelectedProviderId(active.id);
          }
        } catch (e) { }
      }
      const prv = aiProviders.find(p => p.id === selectedProviderId) || (aiProviders.length > 0 ? aiProviders[0] : null);
      const provider = prv || aiConfig;
      const baseUrl = provider.api_base_url || provider.baseUrl || `${API_BASE_URL}`;
      const apiKey = provider.api_key || provider.apiKey || '';
      const model = provider.model_name || provider.model || 'deepseek-chat';
      setAiActionLog(prev => [...prev, { type: 'info', text: ` Provider: ${provider.name || 'default'} | Model: ${model} | URL: ${baseUrl.slice(0, 40)}`, time: Date.now() }]);
      const dawContext = window.AIGateway.buildAIPromptContext({
        tracks: activeTracks, bpm, selectedTrackId, currentTime, selLeft, selRight
      });

      let matchedInstruction = '';
      if (aiPromptMgrRef.current) {
        const matchResult = aiPromptMgrRef.current.matchPreset(prompt);
        if (matchResult && matchResult.preset) {
          matchedInstruction = matchResult.preset.system_instruction_template;
          setAiActionLog(prev => [...prev, { type: 'info', text: ` Khớp với preset: "${matchResult.preset.name}"`, time: Date.now() }]);
        }
      }

      const result = await window.AIGateway.executeAIPrompt({
        prompt: prompt,
        provider: provider.name || 'default',
        model: model,
        apiKey: apiKey,
        baseUrl: baseUrl.replace(/\/chat\/completions$/, '').replace(/\/$/, ''),
        dawContext,
        tools: window.AIGateway.DEFAULT_TOOLS,
        systemInstruction: matchedInstruction
      });
      const hasText = !!result.textResponse;
      const hasCalls = result.functionCalls && result.functionCalls.length > 0;
      if (hasText) {
        setAiActionLog(prev => [...prev, { type: 'status', text: ` AI: ${result.textResponse.slice(0, 500)}`, time: Date.now() }]);
      }
      if (hasCalls) {
        setAiActionLog(prev => [...prev, { type: 'info', text: ` Gọi ${result.functionCalls.length} lệnh...`, time: Date.now() }]);
        if (window.DAWCommandDispatcher) {
          window.DAWCommandDispatcher.isExecutingAI = true;
        }
        try {
          for (const fc of result.functionCalls) {
            setAiActionLog(prev => [...prev, { type: 'info', text: ` Gọi lệnh: ${fc.name}(${JSON.stringify(fc.arguments)})`, time: Date.now() }]);
            const cmdName = fc.name.toUpperCase();
            if (window.DAWCommandDispatcher) {
              try {
                let cmdResult = window.DAWCommandDispatcher.execute(cmdName, fc.arguments);
                if (cmdResult && typeof cmdResult.then === 'function') cmdResult = await cmdResult;
                setAiActionLog(prev => [...prev, { type: 'status', text: ` ✅ ${fc.name}: ${cmdResult && cmdResult.success ? 'thành công' : 'thất bại: ' + ((cmdResult && cmdResult.error) || 'unknown')}`, time: Date.now() }]);
              } catch (cmdErr) {
                setAiActionLog(prev => [...prev, { type: 'error', text: ` ❌ ${fc.name}: ${cmdErr.message}`, time: Date.now() }]);
              }
            } else {
              setAiActionLog(prev => [...prev, { type: 'error', text: ` ❌ ${fc.name}: DAWCommandDispatcher not available`, time: Date.now() }]);
            }
          }
        } finally {
          if (window.DAWCommandDispatcher) {
            window.DAWCommandDispatcher.isExecutingAI = false;
          }
        }
      }
      if (!hasText && !hasCalls) {
        const rawKeys = result.raw ? Object.keys(result.raw).join(', ') : 'null';
        const errDetail = result.raw && result.raw.error ? ` (${result.raw.error.message || result.raw.error})` : '';
        setAiActionLog(prev => [...prev, { type: 'error', text: ` AI không trả về lệnh hoặc text. Keys: [${rawKeys}]${errDetail}`, time: Date.now() }]);
      }
      if (prompt) {
        promptHistRef.current = [...promptHistRef.current.slice(-49), prompt];
        setPromptHistory(promptHistRef.current);
      }
      setPromptHistIdx(-1);
      setAiActionLog(prev => [...prev, { type: 'status', text: ` Hoàn tất.`, time: Date.now() }]);
      setAiPrompt('');
      setTimeout(() => lucide.createIcons(), 200);
    } catch (err) {
      setAiActionLog(prev => [...prev, { type: 'error', text: ` Lỗi: ${err.message}`, time: Date.now() }]);
      showToast(`AI Error: ${err.message}`, 'error');
    } finally {
      setAiProcessing(false);
    }
  };

  // ── AI Compose Similar → NEXT TRACK (user request) ──
  // Click MIDI item + gõ prompt → AI phân tích cấu trúc giai điệu trong item
  // (pitch range, intervals, rhythm) → sáng tác giai điệu TƯƠNG TỰ → ghi MIDI
  // notes vào track KẾ TIẾP; nếu track kế tiếp KHÔNG RỖNG → chèn track mới
  // (ngay sau track hiện tại) và ghi MIDI item vào đó.
  // ── AI Compose (ai_midi_rearrange_specification.md) ──
  // Click MIDI item + chọn mode (Variation/Extend) + gõ prompt:
  // 1. SonicMidiExtractor nén note (pitch + note_name, round 2dp) tiết kiệm token
  // 2. Function Tool Schema rearrange_or_extend_midi_melody (2 mode)
  // 3. Prompt engineering: ORIGINAL CONTEXT + USER DIRECTIVE + MODE DIRECTIVE + CONSTRAINTS
  // 4. Ingest: SIMILAR_VARIATION → track mới ngay dưới (A/B); EXTEND_CONTINUATION → append item sau item gốc
  const handleAiComposeFromItem = async (mode) => {
    const selMidi = getSelectedMidiItemInfo();
    if (!selMidi) { showToast('Hãy chọn 1 MIDI item trên timeline trước.', 'warning'); return; }
    const prompt = aiPrompt.trim();
    if (!prompt) { showToast('Vui lòng gõ prompt yêu cầu giai điệu.', 'warning'); return; }
    const composeMode = mode === 'EXTEND_CONTINUATION' ? 'EXTEND_CONTINUATION' : 'SIMILAR_VARIATION';
    setAiProcessing(true);
    setAiActionLog(prev => [...prev, { type: 'status', text: ` 🎵 Đang phân tích "${selMidi.itemName}" (${(selMidi.notes || []).length} notes) + gửi AI [${composeMode === 'EXTEND_CONTINUATION' ? 'Extend' : 'Variation'}]...`, time: Date.now() }]);
    try {
      if (aiProviders.length === 0 || !selectedProviderId) {
        try { const d = await window.SonicAPI.getAIConfigs(); if (d && d.providers && d.providers.length > 0) { setAiProviders(d.providers); const a = d.providers.find(p => p.is_active) || d.providers[0]; if (a) setSelectedProviderId(a.id); } } catch (e) {}
      }
      const prv = aiProviders.find(p => p.id === selectedProviderId) || (aiProviders.length > 0 ? aiProviders[0] : null);
      const provider = prv || aiConfig;
      const baseUrl = provider.api_base_url || provider.baseUrl || `${API_BASE_URL}`;
      const apiKey = provider.api_key || provider.apiKey || '';
      const model = provider.model_name || provider.model || 'deepseek-chat';
      setAiActionLog(prev => [...prev, { type: 'info', text: ` Provider: ${provider.name || 'default'} | Model: ${model}`, time: Date.now() }]);

      // STEP 1: extract + nén MIDI context (note_name, round 2dp) qua SonicMidiExtractor
      if (!window.SonicMidiExtractor) throw new Error('SonicMidiExtractor chưa được nạp.');
      const midiContext = window.SonicMidiExtractor.extractSelectedMIDIContext(activeTracksRef.current || activeTracks, selMidi.itemId, bpm);
      const bpmVal = parseInt(bpm) || 120;
      const secPerBar = (60.0 / bpmVal) * 4;
      const srcItem = (activeTracksRef.current || activeTracks).flatMap(t => (t.midiItems || []).map(m => ({ ...m, trackId: t.id }))).find(m => m.id === selMidi.itemId);
      const startBar = srcItem ? (srcItem.startTime || 0) / secPerBar : 0;
      midiContext.start_bar = startBar;
      midiContext.time_signature = '4/4';

      // STEP 3: prompt engineering (spec §4)
      const notesJSON = JSON.stringify(midiContext.notes, null, 2);
      const nextStartBar = startBar + midiContext.duration_bars;
      const modeDirective = composeMode === 'EXTEND_CONTINUATION'
        ? `MODE: MELODIC EXTENSION (EXTEND / CONTINUATION)
- Source Section: Bars ${startBar.toFixed(2)} -> ${nextStartBar.toFixed(2)} (${midiContext.duration_bars} bars).
- DIRECTIVE: Write a CONTINUATION starting at Bar ${nextStartBar.toFixed(2)} spanning the next ${midiContext.duration_bars} bars.
- Analyze the ending motif/cadence of the original sequence to craft a smooth transition, then develop the melody toward a climax or resolution.
- Set target_start_bar = ${nextStartBar.toFixed(2)}, target_duration_bars = ${midiContext.duration_bars}.`
        : `MODE: REARRANGEMENT / VARIATION (SIMILAR VARIATION / REARRANGE)
- DIRECTIVE: Generate a NEW variation of EQUAL LENGTH (${midiContext.duration_bars} bars, spanning Beats 0.0 -> ${midiContext.total_beats}).
- Retain the core harmonic framework/chord progression while applying variation techniques (Syncopation, Arpeggiator, Passing tones, Swing feel, Harmonization) as requested by the user.
- Set target_start_bar = ${startBar.toFixed(2)}, target_duration_bars = ${midiContext.duration_bars}.`;
      const promptText = `You are a professional AI Music Composer & Arranger.

ORIGINAL MIDI MELODY CONTEXT FROM DAW:
- Track: "${midiContext.track_name}" | Item: "${midiContext.item_name}"
- Tempo: ${midiContext.bpm} BPM | Time Signature: ${midiContext.time_signature}
- Location: Bar ${startBar.toFixed(2)} (Length: ${midiContext.duration_bars} bars / ${midiContext.total_beats} beats)
- Array of ${midiContext.total_notes} original MIDI source notes:
${notesJSON}

USER DIRECTIVE:
"${prompt}"

${modeDirective}

STRICT CONSTRAINTS:
1. Return your answer as a SINGLE valid JSON object (NO markdown, NO code fences, NO explanation) with EXACTLY this shape:
{"mode":"SIMILAR_VARIATION or EXTEND_CONTINUATION","composition_title":"short style title","target_start_bar":number,"target_duration_bars":number,"generated_notes":[{"pitch":60,"start_beat":0.0,"duration_beats":1.0,"velocity":0.8}]}
2. Notes in generated_notes MUST cover the full beat range from 0.0 to ${midiContext.total_beats} without stopping prematurely.
3. Ensure the final note sustains or resolves cleanly at beat ${midiContext.total_beats}.
4. generated_notes start_beat is relative to the start of the generated item (beat 0.0 = first bar of the new section).`;

      // STEP 2 + dispatch: provider hiện tại trả tool-call rác (_unknown) nên
      // KHÔNG gửi tools — yêu cầu JSON thuần trong prompt (text-based, giống
      // flow Piano Roll đã hoạt động).
      const result = await window.AIGateway.executeAIPrompt({
        prompt: promptText,
        provider: provider.name || 'default',
        model: model,
        apiKey: apiKey,
        baseUrl: baseUrl.replace(/\/chat\/completions$/, '').replace(/\/$/, ''),
        systemInstruction: 'You are a professional AI Music Composer & Arranger. Output ONLY a single valid JSON object (no markdown, no code fences) matching the exact shape requested in the prompt.',
        tools: []
      });
      if (!result) throw new Error('AI không phản hồi');

      // STEP 4: decode — ƯU TIÊN textResponse (JSON thuần); functionCalls chỉ
      // dùng khi đúng tên tool rearrange_or_extend_midi_melody (provider cũ
      // trả tool-call rác _unknown chỉ chứa "reason" — bỏ qua).
      let aiResult = null;
      let rawDebug = '';
      const pickNotes = obj => {
        if (!obj) return null;
        if (Array.isArray(obj)) return obj;
        for (const k of ['generated_notes', 'notes', 'rearranged_notes', 'midi_notes']) {
          if (Array.isArray(obj[k]) && obj[k].length > 0) return obj[k];
        }
        return null;
      };
      const textResp = result.textResponse || result.text || '';
      if (textResp && typeof textResp === 'string' && textResp.trim()) {
        try {
          const cleaned = textResp.replace(/```json?\s*/g, '').replace(/```/g, '').trim();
          const parsed = JSON.parse(cleaned);
          const n = pickNotes(parsed);
          if (n) aiResult = { mode: parsed.mode, composition_title: parsed.composition_title, target_start_bar: parsed.target_start_bar, target_duration_bars: parsed.target_duration_bars, generated_notes: n };
        } catch (e1) { rawDebug = 'text: ' + textResp.slice(0, 400); console.error('Parse AI result error:', e1); }
      }
      if (!aiResult || !pickNotes(aiResult)) {
        if (result.functionCalls && result.functionCalls.length > 0) {
          const real = result.functionCalls.find(fc => fc.name === 'rearrange_or_extend_midi_melody');
          if (real) {
            try {
              const a = typeof real.arguments === 'string' ? JSON.parse(real.arguments) : real.arguments;
              const n = pickNotes(a);
              if (n) aiResult = { mode: a.mode, composition_title: a.composition_title, target_start_bar: a.target_start_bar, target_duration_bars: a.target_duration_bars, generated_notes: n };
            } catch (e2) { rawDebug += ' | FC-args-raw: ' + String(real.arguments).slice(0, 300); }
          } else {
            rawDebug += ' | FC names: ' + result.functionCalls.map(fc => fc.name || '_unknown').join(',') + ' (không phải tool đúng — bỏ qua)';
          }
        }
      }
      if (!aiResult || !pickNotes(aiResult)) {
        console.error('[AI Compose] Không parse được notes. Debug:', rawDebug, '| functionCalls:', JSON.stringify(result.functionCalls || []).slice(0, 500), '| textResponse:', (result.textResponse || result.text || '').slice(0, 500));
        setAiActionLog(prev => [...prev, { type: 'error', text: ' ❌ AI không trả về notes hợp lệ. ' + (rawDebug ? 'Raw: ' + rawDebug.slice(0, 200) : ''), time: Date.now() }]);
        return;
      }
      const aiNotes = pickNotes(aiResult);
      const finalMode = aiResult.mode === 'EXTEND_CONTINUATION' ? 'EXTEND_CONTINUATION' : 'SIMILAR_VARIATION';
      const title = aiResult.composition_title || 'AI Composition';
      const targetStartBar = (typeof aiResult.target_start_bar === 'number' ? aiResult.target_start_bar : (finalMode === 'EXTEND_CONTINUATION' ? nextStartBar : startBar));
      const targetDurBars = (typeof aiResult.target_duration_bars === 'number' ? aiResult.target_duration_bars : midiContext.duration_bars);
      const newNotes = aiNotes.map((n, i) => ({
        id: 'note_ai_' + Date.now() + '_' + i,
        pitch: Math.max(0, Math.min(127, parseInt(n.pitch) || 60)),
        start_beat: Math.max(0, parseFloat(n.start_beat) || 0),
        duration_beats: Math.max(0.125, parseFloat(n.duration_beats) || 0.25),
        velocity: Math.max(0.1, Math.min(1.0, n.velocity ?? 0.8)),
        pan: 0.0
      }));
      const newItem = {
        id: 'item_ai_' + Date.now(),
        startTime: Math.max(0, targetStartBar * secPerBar),
        duration: Math.max(1, targetDurBars * secPerBar),
        name: (finalMode === 'EXTEND_CONTINUATION' ? '[Extend] ' : '[Variation] ') + title,
        notes: newNotes
      };

      // STEP 5: ingest — Variation → track mới ngay dưới (A/B); Extend → append item cuối track gốc
      const curTracks = activeTracksRef.current || activeTracks;
      const srcIdx = curTracks.findIndex(t => t.id === selMidi.trackId);
      if (srcIdx === -1) { setAiActionLog(prev => [...prev, { type: 'error', text: ' ❌ Không tìm thấy track nguồn.', time: Date.now() }]); return; }
      const sf = aiResult.soundfont_id || (midiContext.instrument && midiContext.instrument.soundfont_id) || 'generaluser_gs';
      const sfBank = aiResult.soundfont_bank ?? ((midiContext.instrument && midiContext.instrument.soundfont_bank) ?? 0);
      const sfProg = aiResult.soundfont_program ?? ((midiContext.instrument && midiContext.instrument.soundfont_program) ?? 0);
      if (finalMode === 'EXTEND_CONTINUATION') {
        // Append item mới vào CUỐI track gốc
        updateActiveTracks(prev => prev.map(t => t.id === selMidi.trackId ? { ...t, midiItems: [...(t.midiItems || []), newItem] } : t));
        setAiActionLog(prev => [...prev, { type: 'status', text: ` ✅ [Extend] "${newItem.name}" (${newNotes.length} notes) → append sau item gốc (bar ${targetStartBar.toFixed(2)}).`, time: Date.now() }]);
      } else {
        // Tạo track mới ngay dưới track nguồn (A/B testing) — spec §5.
        // TUÂN THỦ RULES như track gốc: clone toàn bộ cấu trúc track nguồn
        // (synth_engine, instrumentProgram/instrumentName, volumeDb, pan,
        // fxActive, bypass flags, color...) rồi reset phần content.
        const newTrackId = 'track_ai_var_' + Date.now();
        const srcTrack = curTracks[srcIdx];
        const newTrack = {
          ...(srcTrack || {}),
          id: newTrackId,
          name: '[AI Var] ' + title,
          // KHÔNG kế thừa midiChannel của track gốc: nếu dùng chung channel,
          // solo/mute track gốc gửi CC7=0 trên channel đó → clone (cùng
          // channel) bị NHỎ/CÂM. ensureTrackMidiChannel/assignTrackMidiChannel
          // sẽ cấp channel RIÊNG cho track mới.
          midiChannel: undefined,
          buffer: null,
          startTime: 0,
          clips: [],
          sections: [],
          midiItems: [newItem],
          muted: false,
          solo: false,
          markers: [],
          serverFileId: null,
          synth_engine: srcTrack && srcTrack.synth_engine ? srcTrack.synth_engine : { type: 'soundfont', soundfont_id: sf, soundfont_bank: sfBank, soundfont_program: sfProg },
          instrumentProgram: srcTrack ? srcTrack.instrumentProgram : sfProg,
          instrumentName: srcTrack ? srcTrack.instrumentName : ('AI ' + title)
        };
        updateActiveTracks(prev => { const copy = [...prev]; copy.splice(srcIdx + 1, 0, newTrack); return copy; });
        setAiActionLog(prev => [...prev, { type: 'status', text: ` ✅ [Variation] "${newItem.name}" (${newNotes.length} notes) → track mới "[AI Var] ${title}" ngay dưới (A/B) — kế thừa synth/instrument của track gốc.`, time: Date.now() }]);
      }
      setCanvasRedrawCount(n => n + 1);
      setTimeout(() => lucide.createIcons(), 200);
    } catch (err) {
      setAiActionLog(prev => [...prev, { type: 'error', text: ` ❌ Lỗi: ${err.message}`, time: Date.now() }]);
      showToast(`AI Error: ${err.message}`, 'error');
    } finally {
      setAiProcessing(false);
    }
  };
  // Alias cũ giữ cho nút UI hiện tại (Compose) hoạt động — mode mặc định Variation
  const handleAiComposeToNextTrack = () => handleAiComposeFromItem('SIMILAR_VARIATION');

  // ── Split Track at Playhead ──
  const handleSplitTrackAtTime = (trackId, clipId, time) => {
    const track = tracks.find(t => t.id === trackId);
    if (!track) return;
    const clips = track.clips && track.clips.length > 0 ? track.clips : track.buffer ? [{
      id: 'default',
      buffer: track.buffer,
      startTime: track.startTime || 0,
      name: track.name
    }] : [];
    const targetClipId = clipId || clips.find(c => time >= c.startTime && time < c.startTime + c.buffer.duration)?.id;
    if (!targetClipId) return;
    const clip = clips.find(c => c.id === targetClipId);
    if (!clip || !clip.buffer) return;
    const relTime = Math.max(0, time - clip.startTime);
    const sr = clip.buffer.sampleRate;
    const cutSample = Math.floor(relTime * sr);
    const originalData = clip.buffer.getChannelData(0);
    if (cutSample <= 0 || cutSample >= originalData.length) {
      showToast("Vị trí cắt nằm ngoài dải âm thanh của clip.", "warning");
      return;
    }
    const beforeSnap = captureTrackSnapshot(trackId);
    const ctx = getAudioContext();
    const b1 = ctx.createBuffer(1, cutSample, sr);
    b1.copyToChannel(originalData.subarray(0, cutSample), 0);
    const b2 = ctx.createBuffer(1, originalData.length - cutSample, sr);
    b2.copyToChannel(originalData.subarray(cutSample), 0);
    const clip1 = {
      id: 'clip_' + Date.now() + '_p1',
      name: `${clip.name.replace(' (Part 1)', '').replace(' (Part 2)', '')} (Part 1)`,
      buffer: b1,
      startTime: clip.startTime
    };
    const clip2 = {
      id: 'clip_' + Date.now() + '_p2',
      name: `${clip.name.replace(' (Part 1)', '').replace(' (Part 2)', '')} (Part 2)`,
      buffer: b2,
      startTime: clip.startTime + cutSample / sr
    };
    setTracks(prev => prev.map(t => {
      if (t.id === trackId) {
        const remainingClips = clips.filter(c => c.id !== targetClipId);
        const updatedClips = [...remainingClips, clip1, clip2];
        return {
          ...t,
          clips: updatedClips,
          buffer: updatedClips[0]?.buffer || null,
          startTime: updatedClips[0]?.startTime || 0,
          name: updatedClips[0]?.name || t.name
        };
      }
      return t;
    }));
    setTimeout(() => {
      const afterSnap = captureTrackSnapshot(trackId);
      pushAction('SPLIT_CLIP', trackId, beforeSnap, afterSnap);
    }, 50);
    showToast(`Đã chia nhỏ clip tại ${formatTime(time)}.`, "info");
  };
  const handleSplitTrack = trackId => {
    handleSplitTrackAtTime(trackId, null, currentTime);
  };
  handleSplitTrackRef.current = handleSplitTrack;

  // ── Glue (Merge) Clips on Selected Track ──
  const handleGlueTracks = () => {
    const track = tracks.find(t => t.id === selectedTrackId);
    if (!track) {
      showToast('Vui lòng chọn một track để thực hiện gộp (glue).', 'warning');
      return;
    }
    const clips = track.clips && track.clips.length > 0 ? track.clips : track.buffer ? [{
      id: 'default',
      buffer: track.buffer,
      startTime: track.startTime || 0,
      name: track.name
    }] : [];
    if (clips.length < 2) {
      showToast('Cần ít nhất 2 clip trên track này để gộp (glue).', 'warning');
      return;
    }
    const beforeSnap = captureTrackSnapshot(track.id);
    const ctx = getAudioContext();
    const sr = clips[0].buffer.sampleRate;
    let minStart = Infinity;
    let maxEnd = -Infinity;
    clips.forEach(c => {
      const start = c.startTime || 0;
      const end = start + c.buffer.duration;
      minStart = Math.min(minStart, start);
      maxEnd = Math.max(maxEnd, end);
    });
    const newDur = maxEnd - minStart;
    const newBuffer = ctx.createBuffer(1, Math.ceil(newDur * sr), sr);
    const newData = newBuffer.getChannelData(0);
    clips.forEach(c => {
      const data = c.buffer.getChannelData(0);
      const offset = Math.floor(((c.startTime || 0) - minStart) * sr);
      for (let i = 0; i < data.length; i++) {
        if (offset + i < newData.length) {
          newData[offset + i] += data[i];
        }
      }
    });
    let maxPeak = 0;
    for (let i = 0; i < newData.length; i++) {
      const abs = Math.abs(newData[i]);
      if (abs > maxPeak) maxPeak = abs;
    }
    if (maxPeak > 1.0) {
      for (let i = 0; i < newData.length; i++) newData[i] /= maxPeak;
    }
    const mergedClip = {
      id: 'clip_merged_' + Date.now(),
      name: `${track.name.replace(' (Part 1)', '').replace(' (Part 2)', '')} (Glued)`,
      buffer: newBuffer,
      startTime: minStart
    };
    setTracks(prev => prev.map(t => {
      if (t.id === track.id) {
        return {
          ...t,
          clips: [mergedClip],
          buffer: newBuffer,
          startTime: minStart,
          name: mergedClip.name
        };
      }
      return t;
    }));
    setTimeout(() => {
      const afterSnap = captureTrackSnapshot(track.id);
      pushAction('GLUE', track.id, beforeSnap, afterSnap);
    }, 50);
    showToast(`Đã gộp ${clips.length} clips thành công.`, 'success');
  };

  // ── DAW Command Registration for AI (28_AI_PANEL.md §1 & §2) ──
  useEffect(() => {
    if (typeof window.DAWCommandDispatcher === 'undefined') return;
    const api = {
      createTrack: (args) => {
        const name = args.name || `AI_Track_${Date.now()}`;
        const type = args.type || 'audio';
        const rearrangeNewId = addNewTrack();
        if (name && name !== `AI_Track_${Date.now()}`) {
          updateTrackName(rearrangeNewId, name);
        }
        return { success: true, trackId: rearrangeNewId, name };
      },
      deleteTrack: (args) => {
        const tid = args.track_id || selectedTrackId;
        if (!tid) return { success: false, error: 'No track_id provided' };
        deleteTrack(tid);
        return { success: true, trackId: tid };
      },
      addClip: (args) => {
        const trackId = args.track_id || selectedTrackId;
        const barDur = 60 / parseInt(bpm || 120) * 4;
        let startTime;
        if (args.start_time !== undefined && args.start_time !== null) startTime = args.start_time;
        else if (args.start_bar !== undefined && args.start_bar !== null) startTime = args.start_bar * barDur;
        else startTime = currentTime;
        const track = tracks.find(t => t.id === trackId);
        if (!track) return { success: false, error: 'Track not found' };
        const ctx = getAudioContext();
        const sr = 44100;
        let duration;
        if (args.duration_seconds !== undefined && args.duration_seconds !== null) duration = args.duration_seconds;
        else if (args.length_bars !== undefined && args.length_bars !== null) duration = args.length_bars * barDur;
        else duration = 2;
        const buffer = ctx.createBuffer(1, Math.floor(sr * duration), sr);
        const data = buffer.getChannelData(0);
        for (let i = 0; i < data.length; i++) data[i] = 0;
        const clipId = nextClipId();
        setTracks(prev => prev.map(t => {
          if (t.id !== trackId) return t;
          const clips = t.clips && t.clips.length > 0 ? t.clips : (t.buffer ? [{ id: 'default_' + t.id, buffer: t.buffer, startTime: t.startTime || 0, name: t.name }] : []);
          return {
            ...t,
            clips: [...clips, { id: clipId, buffer, startTime, name: args.name || 'AI Clip' }],
            buffer: clips.length > 0 ? clips[0].buffer : buffer,
            startTime: clips.length > 0 ? clips[0].startTime : startTime,
            name: clips.length > 0 ? clips[0].name : (args.name || t.name)
          };
        }));
        return { success: true, clipId, trackId };
      },
      removeClip: (args) => {
        const trackId = args.track_id || selectedTrackId;
        const clipId = args.clip_id;
        setTracks(prev => prev.map(t => {
          if (t.id !== trackId) return t;
          const updatedClips = (t.clips || []).filter(c => c.id !== clipId);
          return { ...t, clips: updatedClips, buffer: updatedClips[0]?.buffer || null, startTime: updatedClips[0]?.startTime || 0, name: updatedClips[0]?.name || t.name };
        }));
        return { success: true };
      },
      setTrackVolume: (args) => {
        const trackId = args.track_id || selectedTrackId;
        const vol = args.volume_db ?? args.volume ?? 0;
        updateTrackVolumeDb(trackId, parseFloat(vol));
        return { success: true, trackId, volumeDb: vol };
      },
      setTrackPan: (args) => {
        const trackId = args.track_id || selectedTrackId;
        const pan = args.pan ?? 0;
        updateTrackPan(trackId, parseInt(pan));
        return { success: true, trackId, pan };
      },
      toggleMute: (args) => {
        const trackId = args.track_id || selectedTrackId;
        toggleTrackMute(trackId);
        const track = tracks.find(t => t.id === trackId);
        return { success: true, trackId, muted: track ? track.muted : null };
      },
      toggleSolo: (args) => {
        const trackId = args.track_id || selectedTrackId;
        toggleTrackSoloEvaluate(trackId);
        const track = tracks.find(t => t.id === trackId);
        return { success: true, trackId, solo: track ? track.solo : null };
      },
      processAudioDsp: (args) => {
        const trackId = args.track_id || selectedTrackId;
        const action = args.action;
        const params = args.params || {};
        const track = tracks.find(t => t.id === trackId);
        if (!track || !track.buffer) return { success: false, error: 'Track has no audio buffer' };
        if (action === 'normalize') {
          const channelData = track.buffer.getChannelData(0);
          let maxVal = 0;
          for (let i = 0; i < channelData.length; i++) maxVal = Math.max(maxVal, Math.abs(channelData[i]));
          if (maxVal > 0) {
            const gain = 1.0 / maxVal;
            for (let i = 0; i < channelData.length; i++) channelData[i] *= gain;
          }
          return { success: true, action: 'normalize' };
        } else if (action === 'invert_phase') {
          const channelData = track.buffer.getChannelData(0);
          for (let i = 0; i < channelData.length; i++) channelData[i] *= -1;
          return { success: true, action: 'invert_phase' };
        } else if (action === 'gain') {
          const gainDb = params.gain_db ?? 0;
          const scale = Math.pow(10, gainDb / 20);
          const channelData = track.buffer.getChannelData(0);
          for (let i = 0; i < channelData.length; i++) channelData[i] = Math.max(-1, Math.min(1, channelData[i] * scale));
          return { success: true, action: 'gain', gainDb };
        } else if (action === 'pitch_shift') {
          const semitones = params.semitones ?? 0;
          const ratio = Math.pow(2, semitones / 12);
          const applyResample = (data, r) => {
            const newLen = Math.round(data.length * r);
            const out = new Float32Array(newLen);
            for (let i = 0; i < newLen; i++) {
              const srcIdx = i / r;
              const idx0 = Math.floor(srcIdx);
              const idx1 = Math.min(idx0 + 1, data.length - 1);
              const frac = srcIdx - idx0;
              out[i] = data[idx0] * (1 - frac) + data[idx1] * frac;
            }
            return out;
          };
          const channelData = track.buffer.getChannelData(0);
          const newData = applyResample(channelData, 1 / ratio);
          const ctx = getAudioContext();
          const newBuffer = ctx.createBuffer(1, newData.length, track.buffer.sampleRate);
          newBuffer.copyToChannel(newData, 0);
          setTracks(prev => prev.map(t => t.id === trackId ? { ...t, buffer: newBuffer } : t));
          return { success: true, action: 'pitch_shift', semitones };
        }
        return { success: false, error: `Unknown action: ${action}` };
      },
      renameTrack: (args) => {
        const tid = args.track_id || selectedTrackId;
        const name = args.name;
        if (!tid) return { success: false, error: 'No track_id provided' };
        if (!name) return { success: false, error: 'No name provided' };
        updateTrackName(tid, name);
        return { success: true, trackId: tid, name };
      },
      setSelection: (args) => {
        const barDur = 60 / parseInt(bpm || 120) * 4;
        let start, end;
        if (args.start_time !== undefined && args.start_time !== null) start = args.start_time;
        else if (args.start_bar !== undefined && args.start_bar !== null) start = args.start_bar * barDur;
        else start = currentTime;
        if (args.end_time !== undefined && args.end_time !== null) end = args.end_time;
        else if (args.length_bars !== undefined && args.length_bars !== null) end = start + args.length_bars * barDur;
        else if (args.end_bar !== undefined && args.end_bar !== null) end = args.end_bar * barDur;
        else end = start + barDur;
        clearLocalSelection();
        setSelectionMode('global');
        setSelectionStart(start);
        setSelectionEnd(end);
        selectionRef.current = { start, end };
        return { success: true, start: parseFloat(start.toFixed(3)), end: parseFloat(end.toFixed(3)), length: parseFloat((end - start).toFixed(3)) };
      },
      cutAudio: (args) => {
        const currentTracks = window.DAWCommandDispatcher?.currentTracks || tracks;
        const currentSelTrackId = window.DAWCommandDispatcher?.currentSelectedTrackId || selectedTrackId;
        const tid = args.track_id || currentSelTrackId;
        const track = currentTracks.find(t => t.id === String(tid));
        if (!track) return { success: false, error: 'Track not found' };
        if (!track.buffer) return { success: false, error: 'Track has no audio buffer' };
        const barDur = 60 / parseInt(bpm || 120) * 4;
        const sel = selectionRef.current;
        let rawStart, rawEnd;
        if (args.start_time !== undefined && args.start_time !== null) rawStart = args.start_time;
        else if (args.start_bar !== undefined && args.start_bar !== null) rawStart = args.start_bar * barDur;
        else if (sel.start !== null) rawStart = sel.start;
        else rawStart = currentTime;
        if (args.end_time !== undefined && args.end_time !== null) rawEnd = args.end_time;
        else if (args.end_bar !== undefined && args.end_bar !== null) rawEnd = args.end_bar * barDur;
        else if (args.length_bars !== undefined && args.length_bars !== null) rawEnd = rawStart + args.length_bars * barDur;
        else if (sel.end !== null && sel.end > rawStart) rawEnd = sel.end;
        else return { success: false, error: 'No end position provided. Provide end_time, end_bar, or length_bars.' };
        if (rawEnd <= rawStart) return { success: false, error: 'End position must be after start position.' };
        const buffer = track.buffer;
        const ctx = getAudioContext();
        const snap = args.snap_silence !== false;
        const loopStart = snap ? findZeroCrossing(buffer, rawStart) : rawStart;
        const loopEnd = snap ? findZeroCrossing(buffer, rawEnd) : rawEnd;
        const sampleRate = buffer.sampleRate;
        const startSample = Math.max(0, Math.min(buffer.length - 1, Math.floor(loopStart * sampleRate)));
        const endSample = Math.max(0, Math.min(buffer.length, Math.floor(loopEnd * sampleRate)));
        const sliceLength = endSample - startSample;
        if (sliceLength <= 100) return { success: false, error: 'Selection too short or invalid' };
        const numChannels = buffer.numberOfChannels || 1;
        const slicedBuffer = ctx.createBuffer(numChannels, sliceLength, sampleRate);
        for (let c = 0; c < numChannels; c++) {
          const src = buffer.getChannelData(c);
          const dst = slicedBuffer.getChannelData(c);
          dst.set(src.subarray(startSample, endSample));
        }
        const rearrangeNewId = 'track_cut_' + Date.now();
        const colors = ['#0f766e', '#1d4ed8', '#701a75', '#a21caf', '#b45309'];
        const color = colors[currentTracks.length % colors.length];
        const cutName = args.new_track_name || `Cut_${track.name}`;
        const newTrack = {
          id: rearrangeNewId, name: cutName, buffer: slicedBuffer,
          startTime: 0, volumeDb: 0, pan: 0,
          muted: false, solo: false, color, markers: [],
          clips: [{ id: 'clip_' + rearrangeNewId, buffer: slicedBuffer, startTime: 0, name: cutName }],
          serverFileId: null
        };
        const idx = currentTracks.findIndex(t => t.id === tid);
        const nextTracks = [...currentTracks];
        if (idx !== -1) {
          nextTracks.splice(idx + 1, 0, newTrack);
        } else {
          nextTracks.push(newTrack);
        }
        if (window.DAWCommandDispatcher) {
          window.DAWCommandDispatcher.currentTracks = nextTracks;
          window.DAWCommandDispatcher.currentSelectedTrackId = rearrangeNewId;
          window.DAWCommandDispatcher.lastCutSourceTrackId = tid;
          window.DAWCommandDispatcher.lastCutNewTrackId = rearrangeNewId;
        }
        setTracks(nextTracks);
        setSelectedTrackId(rearrangeNewId);
        selectionRef.current = { start: 0, end: slicedBuffer.duration };
        clearLocalSelection();
        setSelectionMode('global');
        setSelectionStart(0);
        setSelectionEnd(slicedBuffer.duration);
        setTimeout(() => lucide.createIcons(), 200);
        return {
          success: true, trackId: rearrangeNewId, trackName: cutName,
          cutStart: parseFloat(loopStart.toFixed(3)), cutEnd: parseFloat(loopEnd.toFixed(3)),
          duration: parseFloat(slicedBuffer.duration.toFixed(3))
        };
      },
      scanTrack: (args) => {
        const tid = args.track_id || selectedTrackId;
        const track = tracks.find(t => t.id === tid);
        if (!track) return { success: false, error: 'Track not found' };
        if (!track.buffer) return { success: false, error: 'Track has no audio buffer. Load audio first.' };
        const buffer = track.buffer;
        const data = buffer.getChannelData(0);
        const sr = buffer.sampleRate;
        const channels = buffer.numberOfChannels;
        const duration = buffer.duration;
        const totalSamples = buffer.length;
        const windowSize = Math.min(sr * 3, data.length);
        let detectedBPM = 0;
        if (windowSize > sr) {
          let maxCorr = 0;
          for (let lag = Math.floor(sr * 0.3); lag <= Math.floor(sr * 2.0); lag++) {
            let corr = 0;
            const step = 4;
            for (let i = 0; i < windowSize && i + lag < data.length; i += step) corr += data[i] * data[i + lag];
            corr /= windowSize / step;
            if (corr > maxCorr) { maxCorr = corr; detectedBPM = 60 / (lag / sr); }
          }
        }
        detectedBPM = Math.round(Math.min(300, Math.max(30, detectedBPM)));
        if (args.set_tempo !== false && detectedBPM > 0) {
          setBpm(String(detectedBPM));
        }
        const bitDepth = 16;
        const bitrate = Math.round(sr * channels * bitDepth / 1000);
        return {
          success: true,
          trackId: tid,
          trackName: track.name,
          bpm: detectedBPM,
          sampleRate: sr,
          channels,
          duration: parseFloat(duration.toFixed(3)),
          totalSamples,
          bitDepth,
          bitrateKbps: bitrate,
          hasAudio: true
        };
      },
      setBpm: (args) => {
        const bpmVal = args.bpm || args.tempo || 120;
        setBpm(String(bpmVal));
        return { success: true, bpm: bpmVal };
      },
      setPlayhead: (args) => {
        const barDur = 60 / parseInt(bpm || 120) * 4;
        let time;
        if (args.time !== undefined && args.time !== null) time = args.time;
        else if (args.bar !== undefined && args.bar !== null) time = args.bar * barDur;
        else time = 0;
        handlePlayheadSet(time);
        return { success: true, time: parseFloat(time.toFixed(3)) };
      },
      exportAudio: async (args) => {
        const currentTracks = window.DAWCommandDispatcher?.currentTracks || tracks;
        const currentSelTrackId = window.DAWCommandDispatcher?.currentSelectedTrackId || selectedTrackId;
        let tid = args.track_id;
        if (tid && window.DAWCommandDispatcher?.lastCutSourceTrackId &&
          (String(tid) === String(window.DAWCommandDispatcher.lastCutSourceTrackId) ||
            'track_' + tid === String(window.DAWCommandDispatcher.lastCutSourceTrackId))) {
          tid = window.DAWCommandDispatcher.lastCutNewTrackId;
        }
        if (!tid) tid = currentSelTrackId;
        const track = tid && currentTracks.find(t => t.id === String(tid) || t.id === 'track_' + tid);
        if (!track) return { success: false, error: 'No track found' };
        const clips = track.clips && track.clips.length > 0 ? track.clips : (track.buffer ? [{ id: 'default_' + track.id, buffer: track.buffer, startTime: track.startTime || 0, name: track.name }] : []);
        if (clips.length === 0 && (!track.midiItems || track.midiItems.length === 0)) return { success: false, error: 'Track has no audio clips' };
        const beatsPerSec = parseFloat(bpm || 120) / 60;
        const totalDuration = Math.max(
          clips.length > 0 ? Math.max(...clips.map(c => (c.startTime || 0) + (c.buffer ? c.buffer.duration / (c.speed || 1.0) : 0))) : 0,
          ...(track.midiItems || []).map(m => (m.startTime || 0) + (m.duration || 4)),
          ...(track.sections || []).map(s => (s.start || 0) + (s.duration || 4))
        );
        const barDur = 60 / parseInt(bpm || 120) * 4;
        const sel = selectionRef.current;
        let rawStart, rawEnd;
        if (args.start_time !== undefined) rawStart = args.start_time;
        else if (args.start_bar !== undefined) rawStart = args.start_bar * barDur;
        else if (sel.start !== null) rawStart = sel.start;
        else rawStart = 0;
        if (args.end_time !== undefined) rawEnd = args.end_time;
        else if (args.length_bars !== undefined) rawEnd = (rawStart || 0) + args.length_bars * barDur;
        else if (args.end_bar !== undefined) rawEnd = args.end_bar * barDur;
        else if (sel.end !== null && sel.end > rawStart) rawEnd = sel.end;
        else rawEnd = totalDuration;
        if (rawEnd <= rawStart) return { success: false, error: 'Export range is empty or invalid.' };
        const ctx = getAudioContext();
        const sr = parseInt(args.sample_rate || '44100');
        const firstBuffer = clips.find(c => c.buffer)?.buffer;
        const numCh = args.channels === 'mono' ? 1 : (firstBuffer ? firstBuffer.numberOfChannels : 2);
        const bd = parseInt(args.bit_depth || '16');
        const fmt = args.format || 'wav';
        const renderLength = rawEnd - rawStart;
        const offlineCtx = new OfflineAudioContext(numCh, Math.ceil(sr * renderLength), sr);
        clips.forEach(clip => {
          if (!clip.buffer) return;
          const clipStart = clip.startTime || 0;
          const clipDuration = clip.buffer.duration / (clip.speed || 1.0);
          const clipEnd = clipStart + clipDuration;
          if (clipEnd <= rawStart || clipStart >= rawEnd) return;
          const source = offlineCtx.createBufferSource();
          source.buffer = clip.buffer;
          source.playbackRate.value = clip.speed || 1.0;
          source.connect(offlineCtx.destination);
          if (rawStart < clipStart) {
            const delay = clipStart - rawStart;
            const playDur = Math.min(clipDuration, rawEnd - clipStart);
            source.start(delay, 0, playDur * (clip.speed || 1.0));
          } else {
            const offsetInClip = rawStart - clipStart;
            const playDur = Math.min(clipEnd - rawStart, renderLength);
            source.start(0, offsetInClip * (clip.speed || 1.0), playDur * (clip.speed || 1.0));
          }
        });
        const renderedBuffer = await offlineCtx.startRendering();
        const len = renderedBuffer.length;
        const bps = bd / 8;
        const hdrSz = 44;
        const fileBuf = new ArrayBuffer(hdrSz + len * bps * numCh);
        const vw = new DataView(fileBuf);
        const ws = (off, s) => { for (let i = 0; i < s.length; i++) vw.setUint8(off + i, s.charCodeAt(i)); };
        ws(0, 'RIFF'); vw.setUint32(4, fileBuf.byteLength - 8, true); ws(8, 'WAVE');
        ws(12, 'fmt '); vw.setUint32(16, 16, true); vw.setUint16(20, 1, true); vw.setUint16(22, numCh, true);
        vw.setUint32(24, sr, true); vw.setUint32(28, sr * bps * numCh, true); vw.setUint16(32, bps * numCh, true);
        vw.setUint16(34, bd, true); ws(36, 'data'); vw.setUint32(40, len * bps * numCh, true);
        let ofs = 44;
        for (let i = 0; i < len; i++) {
          for (let ch = 0; ch < numCh; ch++) {
            const smp = Math.max(-1, Math.min(1, renderedBuffer.getChannelData(ch)[i]));
            if (bd === 8) vw.setUint8(ofs, Math.floor((smp + 1) * 127.5));
            else if (bd === 16) vw.setInt16(ofs, Math.floor(smp < 0 ? smp * 0x8000 : smp * 0x7FFF), true);
            else { const v24 = Math.floor(smp < 0 ? smp * 0x800000 : smp * 0x7FFFFF); vw.setUint8(ofs, v24 & 0xFF); vw.setUint8(ofs + 1, v24 >> 8 & 0xFF); vw.setUint8(ofs + 2, v24 >> 16 & 0xFF); }
            ofs += bps;
          }
        }
        const blob = new Blob([fileBuf], { type: 'audio/wav' });
        const localUrl = URL.createObjectURL(blob);
        const targetFilename = `export_${Date.now()}.${fmt}`;

        const triggerDownload = (downloadUrl, finalFilename) => {
          if (window.DAWCommandDispatcher?.isExecutingAI) {
            showToast(`Xuất nhạc thành công (${fmt.toUpperCase()})!`, "success", "Tải về", () => {
              const a = document.createElement('a');
              a.href = downloadUrl;
              a.download = finalFilename;
              a.click();
              if (downloadUrl.startsWith('blob:')) {
                URL.revokeObjectURL(downloadUrl);
              }
            });
          } else {
            const a = document.createElement('a');
            a.href = downloadUrl;
            a.download = finalFilename;
            a.click();
            showToast("Xuất bản âm thanh hoàn tất!", "success");
            if (downloadUrl.startsWith('blob:')) {
              URL.revokeObjectURL(downloadUrl);
            }
          }
        };

        if ((fmt === 'mp3' || fmt === 'ogg') && serverStatus === 'connected') {
          try {
            const file = new File([blob], `export_ai.wav`, { type: 'audio/wav' });
            const formData = new FormData();
            formData.append('file', file);
            const uploadResp = await fetch(`${API_AUDIO}/upload`, {
              method: 'POST',
              body: formData
            });
            if (!uploadResp.ok) throw new Error("Upload failed");
            const uploadData = await uploadResp.json();
            const uploadId = uploadData.file_id;

            const exportResp = await fetch(`${API_AUDIO}/export`, {
              method: 'POST',
              headers: { 'Content-Type': 'application/json' },
              body: JSON.stringify({
                file_id: uploadId,
                format: fmt,
                sample_rate: sr,
                bit_depth: bd
              })
            });
            if (!exportResp.ok) throw new Error("Export failed");
            const exportData = await exportResp.json();
            const result = await pollTaskResult(exportData.task_id, 20);
            if (result.success) {
              const downloadUrl = `${API_AUDIO}/download/${result.output_file_id}`;
              triggerDownload(downloadUrl, targetFilename);
            } else {
              throw new Error(result.error || 'Server encoding failed');
            }
          } catch (transcodeErr) {
            showToast(`Lỗi chuyển đổi: ${transcodeErr.message}. Tải về dạng WAV thay thế.`, "warning");
            triggerDownload(localUrl, `export_${Date.now()}.wav`);
          }
        } else {
          const finalFilename = fmt === 'wav' ? `export_${Date.now()}.wav` : `export_${Date.now()}.wav`;
          if (fmt !== 'wav') {
            showToast("Đang ngoại tuyến. Tải về định dạng WAV thay thế.", "warning");
          }
          triggerDownload(localUrl, finalFilename);
        }
        return { success: true, trackId: tid, range: parseFloat((rawEnd - rawStart).toFixed(3)) + 's', format: fmt, channels: numCh === 1 ? 'mono' : 'stereo' };
      },
      selectItem: (args) => {
        if (args.select_all) {
          setSelectedTrackId(null);
          clearLocalSelection();
          setSelectionMode('global');
          setSelectionStart(0);
          const maxDur = tracks.reduce((max, t) => {
            const dur = t.buffer ? t.buffer.duration : 0;
            const clips = t.clips || [];
            const clipMax = clips.reduce((m, c) => Math.max(m, (c.startTime || 0) + (c.buffer ? c.buffer.duration : 0)), 0);
            return Math.max(max, dur, clipMax);
          }, 0);
          setSelectionEnd(Math.max(maxDur, currentTime + 10));
          return { success: true, selection: 'all', duration: parseFloat(Math.max(maxDur, currentTime + 10).toFixed(3)) };
        }
        const tid = args.track_id || selectedTrackId;
        const track = tracks.find(t => t.id === tid);
        if (!track) return { success: false, error: `Track ${tid} not found` };
        const clips = track.clips && track.clips.length > 0 ? track.clips : (track.buffer ? [{ id: 'default_' + track.id, name: track.name, buffer: track.buffer, startTime: track.startTime || 0 }] : []);
        if (args.item_name) {
          const match = clips.find(c => c.name && c.name.toLowerCase().includes(args.item_name.toLowerCase()));
          if (!match) return { success: false, error: `No clip matching "${args.item_name}" on track ${track.name}` };
          setSelectedTrackId(tid);
          clearLocalSelection();
          setSelectionMode('global');
          const start = match.startTime || 0;
          const end = start + (match.buffer ? match.buffer.duration : 2);
          setSelectionStart(start);
          setSelectionEnd(end);
          return { success: true, trackId: tid, trackName: track.name, clipId: match.id, clipName: match.name, start: parseFloat(start.toFixed(3)), end: parseFloat(end.toFixed(3)) };
        }
        setSelectedTrackId(tid);
        clearLocalSelection();
        setSelectionMode('global');
        const trackEnd = track.buffer ? track.buffer.duration : (clips.length > 0 ? Math.max(...clips.map(c => (c.startTime || 0) + (c.buffer ? c.buffer.duration : 0))) : 4);
        setSelectionStart(0);
        setSelectionEnd(trackEnd);
        return { success: true, trackId: tid, trackName: track.name, duration: parseFloat(trackEnd.toFixed(3)) };
      },
      addMarker: (args) => {
        const trackId = args.track_id || selectedTrackId;
        const time = args.time ?? currentTime;
        const track = tracks.find(t => t.id === trackId);
        if (!track) return { success: false, error: 'Track not found' };
        setTracks(prev => prev.map(t => {
          if (t.id !== trackId) return t;
          return { ...t, markers: [...(t.markers || []), { id: 'ai_marker_' + Date.now(), time, label: args.label || 'AI Marker' }] };
        }));
        return { success: true, trackId, time };
      },
      fadeIn: (args) => {
        const currentTracks = window.DAWCommandDispatcher?.currentTracks || tracks;
        const currentSelTrackId = window.DAWCommandDispatcher?.currentSelectedTrackId || selectedTrackId;
        let trackIdRaw = args.track_id;
        if (trackIdRaw && window.DAWCommandDispatcher?.lastCutSourceTrackId &&
          (String(trackIdRaw) === String(window.DAWCommandDispatcher.lastCutSourceTrackId) ||
            'track_' + trackIdRaw === String(window.DAWCommandDispatcher.lastCutSourceTrackId))) {
          trackIdRaw = window.DAWCommandDispatcher.lastCutNewTrackId;
        }
        if (!trackIdRaw) trackIdRaw = currentSelTrackId;
        const track = currentTracks.find(t => t.id === String(trackIdRaw) || t.id === 'track_' + trackIdRaw);
        if (!track) return { success: false, error: `Track ${trackIdRaw} not found` };
        const clips = track.clips && track.clips.length > 0 ? track.clips : (track.buffer ? [{ id: 'default_' + track.id, buffer: track.buffer, startTime: track.startTime || 0, name: track.name }] : []);
        if (clips.length === 0) return { success: false, error: 'No clips on track' };
        let clip = null;
        if (args.clip_id) {
          clip = clips.find(c => c.id === args.clip_id);
        } else if (args.clip_index !== undefined) {
          const idx = parseInt(args.clip_index);
          const realIdx = idx > 0 ? idx - 1 : 0;
          clip = clips[realIdx] || clips[0];
        } else {
          clip = clips[0];
        }
        if (!clip || !clip.buffer) return { success: false, error: 'Clip has no audio buffer' };
        const duration = parseFloat(args.duration_seconds || 3);
        const buffer = clip.buffer;
        const sr = buffer.sampleRate;
        const numChannels = buffer.numberOfChannels;
        const length = buffer.length;
        const ctx = getAudioContext();
        const newBuffer = ctx.createBuffer(numChannels, length, sr);
        for (let c = 0; c < numChannels; c++) {
          newBuffer.getChannelData(c).set(buffer.getChannelData(c));
        }
        const fadeSamples = Math.min(length, Math.floor(duration * sr));
        for (let c = 0; c < numChannels; c++) {
          const data = newBuffer.getChannelData(c);
          for (let i = 0; i < fadeSamples; i++) {
            const factor = (1 - Math.cos(Math.PI * i / fadeSamples)) / 2;
            data[i] *= factor;
          }
        }
        const beforeSnap = captureTrackSnapshot(track.id);
        const nextTracks = currentTracks.map(t => {
          if (t.id !== track.id) return t;
          const existingClips = t.clips && t.clips.length > 0 ? t.clips : (t.buffer ? [{ id: 'default_' + t.id, buffer: t.buffer, startTime: t.startTime || 0, name: t.name }] : []);
          const updatedClips = existingClips.map(c => {
            if (c.id === clip.id || (clip.id.startsWith('default_') && c.id === 'default')) {
              return { ...c, buffer: newBuffer };
            }
            return c;
          });
          const mainBuffer = updatedClips[0]?.buffer || t.buffer;
          return { ...t, clips: updatedClips, buffer: mainBuffer };
        });
        if (window.DAWCommandDispatcher) {
          window.DAWCommandDispatcher.currentTracks = nextTracks;
        }
        setTracks(nextTracks);
        setTimeout(() => {
          const afterSnap = captureTrackSnapshot(track.id);
          pushAction('AI_FADE_IN', track.id, beforeSnap, afterSnap);
        }, 50);
        return { success: true, trackId: track.id, clipId: clip.id, duration_seconds: duration };
      },
      fadeOut: (args) => {
        const currentTracks = window.DAWCommandDispatcher?.currentTracks || tracks;
        const currentSelTrackId = window.DAWCommandDispatcher?.currentSelectedTrackId || selectedTrackId;
        let trackIdRaw = args.track_id;
        if (trackIdRaw && window.DAWCommandDispatcher?.lastCutSourceTrackId &&
          (String(trackIdRaw) === String(window.DAWCommandDispatcher.lastCutSourceTrackId) ||
            'track_' + trackIdRaw === String(window.DAWCommandDispatcher.lastCutSourceTrackId))) {
          trackIdRaw = window.DAWCommandDispatcher.lastCutNewTrackId;
        }
        if (!trackIdRaw) trackIdRaw = currentSelTrackId;
        const track = currentTracks.find(t => t.id === String(trackIdRaw) || t.id === 'track_' + trackIdRaw);
        if (!track) return { success: false, error: `Track ${trackIdRaw} not found` };
        const clips = track.clips && track.clips.length > 0 ? track.clips : (track.buffer ? [{ id: 'default_' + track.id, buffer: track.buffer, startTime: track.startTime || 0, name: track.name }] : []);
        if (clips.length === 0) return { success: false, error: 'No clips on track' };
        let clip = null;
        if (args.clip_id) {
          clip = clips.find(c => c.id === args.clip_id);
        } else if (args.clip_index !== undefined) {
          const idx = parseInt(args.clip_index);
          const realIdx = idx > 0 ? idx - 1 : 0;
          clip = clips[realIdx] || clips[0];
        } else {
          clip = clips[0];
        }
        if (!clip || !clip.buffer) return { success: false, error: 'Clip has no audio buffer' };
        const duration = parseFloat(args.duration_seconds || 3);
        const buffer = clip.buffer;
        const sr = buffer.sampleRate;
        const numChannels = buffer.numberOfChannels;
        const length = buffer.length;
        const ctx = getAudioContext();
        const newBuffer = ctx.createBuffer(numChannels, length, sr);
        for (let c = 0; c < numChannels; c++) {
          newBuffer.getChannelData(c).set(buffer.getChannelData(c));
        }
        const fadeSamples = Math.min(length, Math.floor(duration * sr));
        for (let c = 0; c < numChannels; c++) {
          const data = newBuffer.getChannelData(c);
          for (let i = 0; i < fadeSamples; i++) {
            const idx = length - fadeSamples + i;
            const factor = (1 + Math.cos(Math.PI * i / fadeSamples)) / 2;
            data[idx] *= factor;
          }
        }
        const beforeSnap = captureTrackSnapshot(track.id);
        const nextTracks = currentTracks.map(t => {
          if (t.id !== track.id) return t;
          const existingClips = t.clips && t.clips.length > 0 ? t.clips : (t.buffer ? [{ id: 'default_' + t.id, buffer: t.buffer, startTime: t.startTime || 0, name: t.name }] : []);
          const updatedClips = existingClips.map(c => {
            if (c.id === clip.id || (clip.id.startsWith('default_') && c.id === 'default')) {
              return { ...c, buffer: newBuffer };
            }
            return c;
          });
          const mainBuffer = updatedClips[0]?.buffer || t.buffer;
          return { ...t, clips: updatedClips, buffer: mainBuffer };
        });
        if (window.DAWCommandDispatcher) {
          window.DAWCommandDispatcher.currentTracks = nextTracks;
        }
        setTracks(nextTracks);
        setTimeout(() => {
          const afterSnap = captureTrackSnapshot(track.id);
          pushAction('AI_FADE_OUT', track.id, beforeSnap, afterSnap);
        }, 50);
        return { success: true, trackId: track.id, clipId: clip.id, duration_seconds: duration };
      },
      generateMultitrackMidi: (args) => {
        const { composition_title, bpm: aiBpm, total_bars, tracks: aiTracks } = args;
        if (aiBpm) {
          setBpm(aiBpm.toString());
        }
        const bpmVal = aiBpm || parseInt(bpm) || 120;
        const secondsPerBeat = 60.0 / bpmVal;
        const secondsPerBar = secondsPerBeat * 4;
        const durationSec = total_bars * secondsPerBar;

        updateActiveTracks(prev => {
          let updatedTracks = [...prev];
          aiTracks.forEach(aiTrack => {
            let targetTrack = updatedTracks.find(
              t => t.name.toLowerCase() === aiTrack.track_name.toLowerCase()
            );
            if (!targetTrack) {
              const rearrangeNewId = (updatedTracks.length + 1).toString();
              const colors = ['#0f766e', '#1d4ed8', '#701a75', '#a21caf', '#b45309'];
              const selectColor = colors[updatedTracks.length % colors.length];
              targetTrack = {
                id: rearrangeNewId,
                name: aiTrack.track_name,
                type: 'MIDI',
                volumeDb: 0,
                pan: 0,
                muted: false,
                solo: false,
                color: selectColor,
                markers: [],
                serverFileId: null,
                clips: [],
                sections: [],
                midiItems: [],
                isArmed: false,
                monitoringEnabled: true,
                inputSource: { deviceType: 'NONE', deviceId: '' }
              };
              updatedTracks.push(targetTrack);
            }
            // Place MIDI item at current playhead position
            const itemStartTimeSec = currentTime;
            const newMidiItem = {
              id: 'item_ai_' + Date.now() + '_' + Math.random().toString(36).substr(2, 5),
              name: `${composition_title || 'AI Theme'} - ${aiTrack.track_name}`,
              parent_track_id: targetTrack.id,
              startTime: itemStartTimeSec,
              duration: durationSec,
              length_bars: total_bars,
              notes: aiTrack.notes.map((note, index) => ({
                id: `note_ai_${Date.now()}_${index}`,
                pitch: note.pitch,
                start_beat: note.start_beat,
                duration_beats: note.duration_beats,
                velocity: note.velocity || 0.8,
                pan: 0.0
              }))
            };
            targetTrack.midiItems = [...(targetTrack.midiItems || []), newMidiItem];
            if (aiTrack.soundfont_bank !== undefined && aiTrack.soundfont_program !== undefined) {
              targetTrack.soundfont_bank = aiTrack.soundfont_bank;
              targetTrack.soundfont_program = aiTrack.soundfont_program;
              targetTrack.soundfont_id = aiTrack.soundfont_id || '';
              const sfEngine = {
                type: 'soundfont',
                plugin_id: aiTrack.soundfont_id ? 'sf_' + aiTrack.soundfont_id : null,
                soundfont_bank: aiTrack.soundfont_bank,
                soundfont_program: aiTrack.soundfont_program,
                soundfont_id: aiTrack.soundfont_id || ''
              };
              targetTrack.synth_engine = sfEngine;
              if (window.SonicSF && window.SonicSF.applyAITrackInstrument) {
                window.SonicSF.applyAITrackInstrument(aiTrack.soundfont_bank, aiTrack.soundfont_program, sfEngine);
              }
            }
          });
          return updatedTracks;
        });

        showToast(`Đã nạp ${aiTracks.length} tracks MIDI thế hệ AI!`, 'success');
        return { success: true };
      },
      rearrangeMidiMelody: (args) => {
        const { rearrange_title, rearranged_notes, soundfont_id, soundfont_bank, soundfont_program } = args;
        if (!rearranged_notes || rearranged_notes.length === 0) {
          return { success: false, error: 'No rearranged notes provided' };
        }
        const sourceTrackId = window.DAWCommandDispatcher?.rearrangeSourceTrackId || selectedTrackId;
        const sourceItemName = window.DAWCommandDispatcher?.rearrangeSourceItemName || 'Source';
        const bpmVal = parseInt(bpm) || 120;
        const secondsPerBeat = 60.0 / bpmVal;
        const secondsPerBar = secondsPerBeat * 4;

        const totalBeats = rearranged_notes.reduce((max, n) => Math.max(max, (n.start_beat || 0) + (n.duration_beats || 1)), 0);
        const totalBars = Math.max(1, Math.ceil(totalBeats / 4));
        const durationSec = totalBars * secondsPerBar;

        const rearrangeNewId = (activeTracks.length + 1).toString();
        updateActiveTracks(prev => {
          let updatedTracks = [...prev];
          // Find source track index to insert new track right after it
          const sourceIdx = sourceTrackId ? updatedTracks.findIndex(t => t.id === sourceTrackId || t.id === 'track_' + sourceTrackId) : -1;
          const insertIdx = sourceIdx >= 0 ? sourceIdx + 1 : updatedTracks.length;
          const colors = ['#0f766e', '#1d4ed8', '#701a75', '#a21caf', '#b45309'];
          const newTrack = {
            id: rearrangeNewId,
            name: `[AI Rearrange] ${rearrange_title || 'Variation'}`,
            type: 'MIDI',
            volumeDb: 0,
            pan: 0,
            muted: false,
            solo: false,
            color: colors[insertIdx % colors.length],
            markers: [],
            serverFileId: null,
            clips: [],
            sections: [],
            midiItems: [{
              id: 'item_rearr_' + Date.now() + '_' + Math.random().toString(36).substr(2, 5),
              name: `[AI] ${rearrange_title || 'Rearranged'} - var`,
              parent_track_id: rearrangeNewId,
              startTime: currentTime,
              duration: durationSec,
              length_bars: totalBars,
              notes: rearranged_notes.map((n, i) => ({
                id: `note_rearr_${Date.now()}_${i}`,
                pitch: Math.max(0, Math.min(127, n.pitch || 60)),
                start_beat: Math.max(0, parseFloat(n.start_beat) || 0),
                duration_beats: Math.max(0.125, parseFloat(n.duration_beats) || 0.25),
                velocity: Math.max(0.1, Math.min(1.0, n.velocity ?? 0.8)),
                pan: 0.0
              }))
            }]
          };
          if (soundfont_id || (soundfont_bank !== undefined && soundfont_program !== undefined)) {
            newTrack.soundfont_id = soundfont_id || '';
            newTrack.soundfont_bank = soundfont_bank ?? 0;
            newTrack.soundfont_program = soundfont_program ?? 0;
            newTrack.synth_engine = {
              type: 'soundfont',
              plugin_id: soundfont_id ? 'sf_' + soundfont_id : null,
              soundfont_bank: soundfont_bank ?? 0,
              soundfont_program: soundfont_program ?? 0,
              soundfont_id: soundfont_id || ''
            };
          }
          updatedTracks.splice(insertIdx, 0, newTrack);
          return updatedTracks;
        });

        showToast(`✅ AI Rearrange: "${rearrange_title}" — ${rearranged_notes.length} notes`, 'success');
        return { success: true, trackId: rearrangeNewId, totalBars };
      },
      createMidiItem: (args) => {
        const trackId = String(args.track_id || selectedTrackId);
        if (!trackId) return { success: false, error: 'No track_id provided' };
        const secondsPerBar = (60.0 / (parseInt(bpm) || 120)) * 4;
        const startBar = args.start_bar !== undefined ? parseFloat(args.start_bar) : 0;
        const lengthBars = args.length_bars !== undefined ? parseFloat(args.length_bars) : 4;
        const midiItem = {
          id: `midi_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`,
          name: 'MIDI Item',
          parent_track_id: trackId,
          startTime: startBar * secondsPerBar,
          duration: lengthBars * secondsPerBar,
          length_bars: lengthBars,
          notes: [],
          color: '#a78bfa'
        };
        updateActiveTracks(prev => prev.map(t => {
          if (t.id !== trackId && t.id !== 'track_' + trackId) return t;
          return {
            ...t,
            midiItems: [...(t.midiItems || []), midiItem]
          };
        }));
        return { success: true, itemId: midiItem.id, trackId };
      },
      modifyMidiNotes: (args) => {
        const itemId = args.item_id;
        if (!itemId) return { success: false, error: 'No item_id provided' };
        
        const noteNameToMidi = (name) => {
          if (typeof name === 'number') return name;
          if (!name || typeof name !== 'string') return 60;
          const match = name.match(/^([A-Ga-g]#?|b?)(-?\d+)$/);
          if (!match) {
            const num = parseInt(name);
            return isNaN(num) ? 60 : num;
          }
          const noteNames = { 'c': 0, 'c#': 1, 'db': 1, 'd': 2, 'd#': 3, 'eb': 3, 'e': 4, 'f': 5, 'f#': 6, 'gb': 6, 'g': 7, 'g#': 8, 'ab': 8, 'a': 9, 'a#': 10, 'bb': 10, 'b': 11 };
          const key = match[1].toLowerCase();
          const octave = parseInt(match[2]);
          const base = noteNames[key] !== undefined ? noteNames[key] : 0;
          return (octave + 1) * 12 + base;
        };

        const newNotes = (args.notes || []).map((n, index) => ({
          id: `note_mod_${Date.now()}_${index}`,
          pitch: noteNameToMidi(n.pitch),
          start_beat: parseFloat(n.start_time || 0),
          duration_beats: parseFloat(n.duration || 1),
          velocity: n.velocity !== undefined ? n.velocity / 127.0 : 0.8,
          pan: 0.0
        }));

        updateActiveTracks(prev => prev.map(t => {
          const items = t.midiItems || [];
          const exists = items.some(m => m.id === itemId);
          if (!exists) return t;
          return {
            ...t,
            midiItems: items.map(m => m.id === itemId ? { ...m, notes: newNotes } : m)
          };
        }));
        return { success: true, itemId };
      }
    };
    window.DAWCommandDispatcher.registerDAWCommands(api);
  }, [tracks, selectedTrackId, currentTime, bpm]);

  // ── Save AI config to localStorage ──
  useEffect(() => {
    localStorage.setItem('ai_base_url', aiConfig.baseUrl);
    localStorage.setItem('ai_api_key', aiConfig.apiKey);
    localStorage.setItem('ai_model', aiConfig.model);
  }, [aiConfig]);

  // ── Auto-save user preferences (panel state, provider) ──
  const prefsRef = useRef({});
  prefsRef.current = {
    showAIPanel, showExportPanel, showSelectionPanel, showPythonToolsPanel,
    showMediaExplorer, showFxRack, showMidiEvents,
    panelPositions, rightSidebarWidth, mediaExplorerHeight, selectedProviderId
  };
  useEffect(() => {
    const prefs = prefsRef.current;
    localStorage.setItem('sonic_preferences', JSON.stringify(prefs));
    if (!currentUser || currentUser === 'cached') return;
    const timer = setTimeout(async () => {
      try { await window.SonicAPI.savePreferences(prefs); } catch (e) { }
    }, 2000);
    return () => clearTimeout(timer);
  }, [showAIPanel, showExportPanel, showSelectionPanel, showPythonToolsPanel,
    showMediaExplorer, showFxRack, showMidiEvents, panelPositions,
    rightSidebarWidth, mediaExplorerHeight, selectedProviderId, currentUser]);

  const drawMixerVuMeter = (canvas, peak) => {
    if (!canvas) return;
    const ctx = canvas.getContext('2d');
    if (!ctx) return;
    const w = canvas.width;
    const h = canvas.height;
    ctx.clearRect(0, 0, w, h);
    ctx.fillStyle = '#0d0d0d';
    ctx.fillRect(0, 0, w, h);
    const barH = peak * h;
    const grad = ctx.createLinearGradient(0, h, 0, 0);
    grad.addColorStop(0, '#22c55e');
    grad.addColorStop(0.5, '#eab308');
    grad.addColorStop(0.85, '#ef4444');
    ctx.fillStyle = grad;
    ctx.fillRect(0, h - barH, w, barH);
  };

  // Master & Track VU meter animation loop
  const masterVUAnimRef = useRef(null);
  useEffect(() => {
    function tick() {
      try {
      // Khi STOP (không play, không recording): vẽ VU RỖNG — không đọc analyser/
      // activity → hết animation dính + master không full sau khi dừng play.
      // ⚠️ NGOẠI LỆ: nếu có MIDI activity live (bật ARM track + bấm phím MIDI
      // keyboard — KHÔNG play/record) thì KHÔNG early-return — track VU phải
      // nhảy theo midiVuActivityRef + decay (user bug: ARM + phím MIDI → VU
      // đứng yên vì block này vẽ rỗng và return trước khi đọc activity).
      const isRecActive = activeAudioRecordersRef.current && Object.keys(activeAudioRecordersRef.current).length > 0;
      let liveMidiVu = false;
      try {
        for (const _k in midiVuActivityRef.current) {
          if (midiVuActivityRef.current[_k] > 0.001) { liveMidiVu = true; break; }
        }
      } catch (e) { }
      if (!isPlaying && !isRecActive && !liveMidiVu) {
        setMasterVU(0);
        setMasterMeterPeak(0);
        Object.keys(trackVuRefs.current).forEach(k => {
          const c = trackVuRefs.current[k];
          if (!c) return;
          if (k.endsWith('_mixer')) drawMixerVuMeter(c, 0);
          else drawVuMeter(c, -60);
        });
        masterVUAnimRef.current = requestAnimationFrame(tick);
        return;
      }
      // 1. Master VU Meter
      if (masterBus && masterBus.analyser) {
        const data = new Uint8Array(128);
        masterBus.analyser.getByteTimeDomainData(data);
        let peak = 0;
        for (let i = 0; i < data.length; i++) {
          const v = Math.abs(data[i] - 128) / 128;
          if (v > peak) peak = v;
        }
        // Deadband: bỏ noise nền (log 06:00 — master peak 0.008 liên tục dù
        // không âm → master VU nhảy nhẹ mãi)
        if (peak < 0.01) peak = 0;
        setMasterVU(peak);
        setMasterMeterPeak(prev => Math.max(prev * 0.97, peak));
        // Log master VU khi đang nhảy (chẩn đoán 06:00 — VU nhảy ngoài vùng âm)
        if (peak > 0.001) {
          try {
            console.log('[VU-master] peak=', peak.toFixed(3));
          } catch (e) { }
        }
      }

      // 2. Track VU Meters
      const trackNodes = activeTrackNodesRef.current || {};
      const activeKeys = Object.keys(trackVuRefs.current);
      activeKeys.forEach(key => {
        // Key phân biệt context (user 07:20 — tách VU MAIN vs SECTION-TAB):
        //   - '1' / '1_mixer'      → MAIN track 1
        //   - '_sess_1'            → SECTION-TAB track 1 (clone — id trùng main)
        // trackId = id thuần (node lookup); vuKey = key midiVuActivityRef
        // (context riêng — trigger fire theo context — hết lẫn nhau).
        const trackIdRaw = key.replace('_mixer', '');
        const isSessVu = trackIdRaw.indexOf('_sess_') === 0;
        const trackId = isSessVu ? trackIdRaw.slice(6) : trackIdRaw;
        const vuKey = trackIdRaw;
        const isRecordingThisTrack = activeAudioRecordersRef.current && activeAudioRecordersRef.current[trackId];
        if (isRecordingThisTrack) return;

        const node = trackNodes[trackId];
        const canvas = trackVuRefs.current[key];
        if (!canvas) return;
        // Section tab: track nodes nằm dưới key <parent>_sub_<trackId> —
        // lookup plain key trả undefined → VU không animation. Fallback scan _sub_.
        const vNode = node || (function() {
          for (var k in trackNodes) { if (k.endsWith('_sub_' + trackId)) return trackNodes[k]; }
          return null;
        })();

        // Only animate when real audio actually passes through the track (solo/mute aware)
        var vuTracks = activeTracksRef.current || [];
        var anySoloVU = vuTracks.some(function(t) { return t.solo; });
        var vuTrack = vuTracks.find(function(t) { return t.id === trackId; });
        var isAudible = vuTrack ? (anySoloVU ? !!vuTrack.solo : !vuTrack.muted) : true;

        let audioPeak = 0;
        if (isAudible && vNode && vNode.analyserNode) {
          const analyser = vNode.analyserNode;
          const data = new Uint8Array(128);
          analyser.getByteTimeDomainData(data);
          for (let i = 0; i < data.length; i++) {
            const v = Math.abs(data[i] - 128) / 128;
            if (v > audioPeak) audioPeak = v;
          }
        }
        // Section item trên MAIN track: VU theo sub-nodes của track này (key
        // <trackId>_sub_*) — âm section đi thẳng masterBus.input (không qua
        // node chính) → node chính không có tín hiệu → VU track đứng yên.
        // ⚠️ CHỈ fallback cho canvas SECTION (_sess_) — KHÔNG cho canvas MAIN:
        // section-tab đang play → chuyển sang MAIN SESSION → VU track MAIN
        // không được animate theo âm section (user bug).
        if (audioPeak <= 0.001 && isSessVu) {
          for (var sk in trackNodes) {
            if (sk.indexOf(trackId + '_sub_') === 0) {
              const sn = trackNodes[sk];
              if (sn && sn.analyserNode) {
                const d2 = new Uint8Array(128);
                sn.analyserNode.getByteTimeDomainData(d2);
                for (let i = 0; i < d2.length; i++) {
                  const v = Math.abs(d2[i] - 128) / 128;
                  if (v > audioPeak) audioPeak = v;
                }
              }
            }
          }
        }

        // midiVuActivityRef được set bởi triggerMidiVuActivity — fire ĐÚNG NHỊP
        // note (nhánh delayed: setTimeout khớp thời điểm phát; nhánh instant:
        // note đang kêu). KHÔNG gate theo amplitude SF — gate chặn note đơn /
        // velocity thấp (âm SF nhỏ < 0.03) → VU không nhảy (user bug 07:10).
        let midiPeak = isAudible ? (midiVuActivityRef.current[vuKey] || 0) : 0;
        if (midiPeak > 0) {
          // ARM + MIDI keyboard: còn note đang GIỮ → giữ nguyên peak, KHÔNG
          // decay → VU animate đúng trường độ âm thanh (user bug 08:08: âm còn
          // kêu nhưng VU tắt sau ~0.5s vì decay 0.75/frame). Hết note → decay
          // 0.75 (~0.5s) tắt nhanh như trước.
          const heldCnt = heldMidiNotesRef.current[vuKey] || 0;
          if (heldCnt > 0) {
            // Giữ nguyên peak trong khi âm đang kêu (không decay)
          } else {
            midiVuActivityRef.current[vuKey] = midiPeak * 0.75;
            if (midiVuActivityRef.current[vuKey] < 0.01) {
              midiVuActivityRef.current[vuKey] = 0;
            }
          }
        }
        // ⚠️ KHÔNG gộp sfAudioPeak vào audioPeak: SF là 1 output CHUNG cho mọi
        // track MIDI → gộp làm track VU nhảy CÙNG NHAU (user bug 07:00).
        // audioPeak chỉ theo âm TRACK thật (vNode/sub-node analyser).
        // Deadband: bỏ noise nền analyser (log 07:15 — vNode noise dao động
        // quanh 0.04 — track KHÔNG item (track 2 section) vẫn nhảy qua deadband
        // 0.04) — hết âm → VU tắt NGAY LẬP TỨC (user yêu cầu).
        if (audioPeak < 0.05) audioPeak = 0;

        const peak = Math.max(audioPeak, midiPeak);
        const db = peak > 0 ? 20 * Math.log10(peak) : -60;

        // Log chẩn đoán VU — in MỌI key (kể cả peak 0, rate ~1.3/giây) — xác
        // định track nào + nguồn (audio/midi) khi VU diễn sai (user bug 07:15:
        // track 2 section không item vẫn nhảy).
        try {
          const _vf = (window.__vuFrame = (window.__vuFrame || 0) + 1);
          if (_vf % 45 === 0) {
            console.log('[VU]', trackId, 'audio=', audioPeak.toFixed(3), 'midi=', midiPeak.toFixed(3), 'key=', key, 'peak=', (Math.max(audioPeak, midiPeak)).toFixed(3));
          }
        } catch (e) { }

        if (peak > 0.001) {
          if (key.endsWith('_mixer')) {
            drawMixerVuMeter(canvas, peak);
          } else {
            drawVuMeter(canvas, db);
          }
        } else {
          if (key.endsWith('_mixer')) {
            drawMixerVuMeter(canvas, 0);
          } else {
            drawVuMeter(canvas, -60);
          }
        }
      });

      } catch (e) {
        console.warn('VU tick error:', e);
      }
      masterVUAnimRef.current = requestAnimationFrame(tick);
    }
    masterVUAnimRef.current = requestAnimationFrame(tick);
    return () => { if (masterVUAnimRef.current) cancelAnimationFrame(masterVUAnimRef.current); };
  }, [isPlaying]);

  const handleOpenProject = async (projectId, projectName) => {
    setAppWarningModal({
      title: "Mở dự án",
      message: "Bạn có muốn mở dự án này? Các thay đổi chưa lưu trên workspace hiện tại sẽ bị mất.",
      isAlert: false,
      onConfirm: async () => {
        try {
          const proj = await window.SonicAPI.getCloudProject(projectId);
          const parsed = JSON.parse(proj.data_json);
          let restoredTracks = [];
          let restoredBpm = bpm;
          let restoredSessionTabs = [];
          let restoredSubTabs = [];
          if (parsed.main_session) {
            const result = deserializeProjectFromSchema(parsed);
            restoredTracks = result.tracks;
            restoredBpm = result.bpm;
            restoredSessionTabs = result.sessionTabs;
            restoredSubTabs = result.subTabs;
            // Bỏ qua zoom ≤ 1 (data cũ lưu 1.0 — lỗi __currentZoom của Media
            // Explorer) — áp dụng mới giữ zoom hiện tại/localStorage.
            if (result.zoom > 1 && setZoom) setZoom(result.zoom);
            if (result.masteringSettings) setMasteringSettings(result.masteringSettings);
          } else {
            restoredTracks = (parsed.tracks || []).map(function (t) {
              const rest = Object.assign({}, t); delete rest.height;
              return Object.assign({}, rest, { buffer: null, channelInfo: t.channelInfo || null, clips: t.clips || [], serverFileId: t.serverFileId || null });
            });
          }
          setTracks(restoredTracks);
          loadAudioBuffersForTracks(restoredTracks).catch(function (err) { console.warn('loadAudioBuffersForTracks error:', err); });
          trackMidiChannelsRef.current = {};
          preloadTrackInstruments(restoredTracks).catch(function (err) { console.warn('preloadTrackInstruments error:', err); });
          setBpm(restoredBpm.toString());
          setSelectedTrackId(restoredTracks[0]?.id || '1');
          setProjectName(proj.name);
          setCurrentProjectId(proj.id);
          if (restoredSessionTabs.length > 0) setSessionTabs(restoredSessionTabs);
          if (restoredSubTabs.length > 0) setSubTabs(restoredSubTabs);
          localStorage.setItem('sonic_project_name', proj.name);
          localStorage.setItem('sonic_project_id', proj.id);
          showToast('Đã nạp dự án "' + proj.name + '" thành công!', "success");
        } catch (e) {
          showToast(e.message || "Lỗi khi nạp dự án", "error");
        }
      }
    });
  };

  return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
    className: "h-full w-full flex flex-col bg-[#1e1e1e]"
  }, /*#__PURE__*/React.createElement("header", {
    className: "h-9 bg-[#2e2e2e] border-b border-[#181818] flex items-center px-1 shrink-0 select-none"
  }, [{
    label: 'File',
    items: [{
      label: 'New Project',
      icon: 'file-plus',
      shortcut: 'Ctrl+N',
      action: () => {
        // New project: đóng hết tab + reset MỌI trạng thái về mặc định
        try { stopAllPlayback(); } catch (e) {}
        setSubTabs([]);
        setSessionTabs([]);
        setActiveTab('main');
        // Reset instrument/channel/route context đã load
        trackMidiChannelsRef.current = {};
        Object.keys(trackMidiBypassMap).forEach(function (k) { delete trackMidiBypassMap[k]; });
        Object.keys(trackAudioBypassMap).forEach(function (k) { delete trackAudioBypassMap[k]; });
        Object.keys(trackMasteringBypassMap).forEach(function (k) { delete trackMasteringBypassMap[k]; });
        setSelectedItemIds(new Set());
        setTracks([{
          id: '1',
          name: 'Track 01',
          buffer: null,
          startTime: 0,
          volumeDb: 0,
          pan: 0,
          muted: false,
          solo: false,
          color: '#0f766e',
          markers: [],
          serverFileId: null,
          clips: [],
          sections: [],
          midiItems: []
        }, {
          id: '2',
          name: 'Track 02',
          buffer: null,
          startTime: 0,
          volumeDb: 0,
          pan: 0,
          muted: false,
          solo: false,
          color: '#1d4ed8',
          markers: [],
          serverFileId: null,
          clips: [],
          sections: [],
          midiItems: []
        }]);
        setSelectedTrackId('1');
        setProjectName('');
        setCurrentProjectId(null);
        localStorage.removeItem('sonic_project_name');
        localStorage.removeItem('sonic_project_id');
        showToast('New project created', 'info');
      }
    }, {
      label: 'Open Project...',
      icon: 'folder-open',
      shortcut: 'Ctrl+O',
      action: () => setOpenProjectModalOpen(true)
    }, {
   label: 'Save Project',
   icon: 'upload-cloud',
   shortcut: 'Ctrl+S',
   action: () => {
     // Save project + save TẤT CẢ dirty sub-tabs (user 07:35 — nhấn lưu ở
     // MAIN phải lưu cả piano roll/audio tab đã sửa)
     handleSaveProject();
     subTabsRef.current.filter(s => s.isDirty).forEach(st => {
       if (st.type === 'PIANO_ROLL') {
         handleSaveMidiNotes(st.id, st.trackId, st.target_id, st.notes || []);
       } else if (st.type === 'SECTION') {
         handleSaveSectionTab(st.id);
       } else if (st.buffer) {
         const subTrack = activeTracksRef.current.find(t => t.id === st.trackId);
         if (subTrack) {
           updateActiveTracks(prev => prev.map(t => t.id === st.trackId ? { ...t, buffer: st.buffer } : t));
         }
       }
     });
     showToast('Đã lưu dự án + các tab đã sửa', 'success');
   }
 }, {
      label: 'Save As...',
      icon: 'download',
      shortcut: 'Ctrl+Alt+S',
      action: () => setSaveAsModalOpen(true)
    }, {
      sep: true
    }, {
      label: 'Config AI Providers...',
      icon: 'settings',
      action: () => setAiConfigModalOpen(true)
    }, {
      label: 'Import Audio...',
      icon: 'file-input',
      shortcut: 'Ctrl+Alt+I',
      action: () => {
        const input = document.createElement('input');
        input.type = 'file';
        input.accept = 'audio/*';
        input.onchange = async e => {
          if (e.target.files[0]) {
            addNewTrack();
            const rearrangeNewId = (tracks.length + 1).toString();
            setTimeout(() => loadFileOnTrack(rearrangeNewId, e.target.files[0]), 100);
          }
        };
        input.click();
        showToast('Import audio', 'info');
      }
    }, {
      label: 'Export Mix...',
      icon: 'file-output',
      action: () => triggerWavExport()
    }, {
      label: 'Export MIDI...',
      icon: 'music',
      action: () => triggerMidiExport()
    }, {
      label: 'Mastering Suite',
      icon: 'wand-2',
      shortcut: 'Ctrl+Shift+M',
      action: () => setShowMasteringModal(true)
    }, {
      sep: true
    }, ...(currentUser ? [{
      label: 'Profile',
      icon: 'user',
      action: () => setProfileModalOpen(true)
    }] : []), ...(currentUser && currentUser.role === 'admin' ? [{
      label: 'System Manager',
      icon: 'settings',
      action: () => setSystemManagerModalOpen(true)
    }] : []), {
      label: 'Logout',
      icon: 'log-out',
      action: () => handleLogout()
    }]
  }, {
    label: 'Edit',
    items: [{
      label: 'Insert New Track',
      icon: 'plus',
      shortcut: 'Ctrl+I',
      action: addNewTrack
    }, {
      label: 'Insert Music to Track',
      icon: 'music',
      shortcut: 'Ctrl+Alt+I',
      action: () => showToast('Select music file to insert', 'info')
    }, {
      sep: true
    }, {
      label: 'Edit in New Tab',
      icon: 'file-edit',
      shortcut: 'Ctrl+E',
      action: () => openTempTab()
    }, {
      label: 'Split at Playhead',
      icon: 'scissors',
      shortcut: 'S',
      action: () => handleSplitTrack(selectedTrackId)
    }, {
      label: 'Merge Tracks',
      icon: 'combine',
      shortcut: 'Ctrl+M',
      action: () => {
        handleMergeTracks();
      }
    }, {
      sep: true
    }, {
      label: 'Undo',
      icon: 'undo',
      shortcut: 'Ctrl+Z',
      action: () => {
        handleUndo();
      }
    }, {
      label: 'Redo',
      icon: 'redo',
      shortcut: 'Ctrl+Y',
      action: () => {
        handleRedo();
      }
    }, {
      sep: true
    }, {
      label: 'Copy',
      icon: 'copy',
      shortcut: 'Ctrl+C',
      action: () => {
        handleCopyTrack();
      }
    }, {
      label: 'Cut',
      icon: 'scissors',
      shortcut: 'Ctrl+X',
      action: () => {
        handleCutTrack();
      }
    }, {
      label: 'Paste',
      icon: 'clipboard',
      shortcut: 'Ctrl+V',
      action: handlePasteTrack
    }, {
      sep: true
    }, {
      label: 'Delete Track',
      icon: 'trash-2',
      shortcut: 'Del',
      action: () => {
        handleDeleteTrack();
      }
    }]
  }, {
    label: 'Insert',
    items: [...(!sessionTabs.some(s => s.id === activeTab) ? [{
      label: 'Insert Section',
      icon: 'folder-plus',
      action: insertSectionAtPlayhead
    }] : []), {
      label: 'Insert MIDI item',
      icon: 'music',
      action: insertMidiItemAtPlayhead
    }, {
      label: 'Insert sound clip',
      icon: 'file-input',
      action: insertSoundClipAtCursor
    }, {
      label: 'Insert track',
      icon: 'plus',
      action: insertTrackBelow
    }]
  }, {
    label: 'View',
    items: [{
      label: 'Master Track',
      icon: 'disc',
      action: () => showToast('Master track view', 'info')
    }, {
      label: 'Maker View',
      icon: 'layout',
      action: () => showToast('Maker view', 'info')
    }, {
      label: 'Mixer',
      icon: 'sliders',
      action: () => window.__toggleMixerRef()
    }, {
      label: 'Tempo Track',
      icon: 'timer',
      action: () => showToast('Tempo track', 'info')
    }, {
      label: 'Video',
      icon: 'film',
      action: () => showToast('Video panel', 'info')
    }, {
      label: 'Media Explorer',
      icon: 'folder-search',
      action: () => window.__toggleMediaExplorerRef()
    }]
  }, {
    label: 'Tools',
    items: [{
      label: 'Config AI Providers...',
      icon: 'settings',
      action: () => setAiConfigModalOpen(true)
    }, {
      label: 'AI MIDI Preset Manager...',
      icon: 'sliders',
      action: () => setAiPresetModalOpen(true)
    }, {
      label: 'DSP Tools Panel',
      icon: 'wrench',
      action: () => {
        // DSP Tool đã được move vào SUB-TAB editor (audioclip). Mở sub-tab
        // edit cho track/clip đang chọn — nếu chưa có clip → mở panel cũ.
        const t = activeTracks.find(x => x.id === selectedTrackId);
        if (t && (t.buffer || (t.clips && t.clips.length))) {
          const clipId = (t.clips && t.clips[0]) ? t.clips[0].id : 'default';
          handleEditClipInSubTab(t.id, clipId);
        } else {
          openPanel('python_tools');
        }
      }
    }, {
      sep: true
    }, {
      label: 'Plugin Manager (SoundFont/VSTi)',
      icon: 'zap',
      action: () => {
        setPluginManagerModalOpen(true);
        window.SonicAPI.listPlugins().then(data => setPluginsData(data)).catch(() => {});
      }
    }, {
      sep: true
    }, {
      label: 'Preferences...',
      icon: 'settings-2',
      action: () => setPreferencesModalOpen(true)
    }]
  }, {
    label: 'Help',
    items: [{
      label: 'Hướng dẫn sử dụng...',
      icon: 'book-open',
      action: () => setHelpModalOpen(true)
    }, {
      sep: true
    }, {
      label: 'About SonicForge Studio...',
      icon: 'info',
      action: () => setAboutModalOpen(true)
    }]
  }].map(menu => /*#__PURE__*/React.createElement("div", {
    key: menu.label,
    className: "relative"
  }, /*#__PURE__*/React.createElement("button", {
    onClick: () => setMenuOpen(menuOpen === menu.label ? null : menu.label),
    className: `px-3 py-1 text-xs font-medium transition rounded ${menuOpen === menu.label ? 'bg-zinc-700 text-zinc-100' : 'text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800'}`
  }, menu.label), menuOpen === menu.label && /*#__PURE__*/React.createElement("div", {
    className: `absolute top-full left-0 mt-0.5 bg-[#2a2a2a] border border-zinc-700 rounded-lg shadow-2xl py-1 z-50 ${menu.label === 'Edit' ? 'w-72' : 'w-64'}`,
    onClick: () => setMenuOpen(null)
  }, menu.items.map((item, i) => item.sep ? /*#__PURE__*/React.createElement("div", {
    key: i,
    className: "h-px bg-zinc-700 my-1"
  }) : /*#__PURE__*/React.createElement("button", {
    key: item.label,
    onClick: e => {
      e.stopPropagation();
      item.action();
      setMenuOpen(null);
    },
    className: "w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2"
  }, /*#__PURE__*/React.createElement("span", {
    className: "inline-flex items-center shrink-0"
  }, /*#__PURE__*/React.createElement("i", {
    "data-lucide": item.icon,
    className: "w-3.5 h-3.5 text-zinc-500 shrink-0"
  })), /*#__PURE__*/React.createElement("span", {
    className: "flex-1"
  }, item.label), item.shortcut && /*#__PURE__*/React.createElement("span", {
    className: "text-purple-400 text-xs font-semibold font-mono ml-auto pl-8"
  }, item.shortcut)))))), /*#__PURE__*/React.createElement("div", {
    className: "flex-1"
  }), /*#__PURE__*/React.createElement("div", {
    className: "flex items-center gap-2 px-2"
  }, /*#__PURE__*/React.createElement("span", {
    className: `text-xs font-bold uppercase px-1.5 py-0.5 rounded ${serverStatus === 'connected' ? 'bg-emerald-950 text-emerald-400' : serverStatus === 'checking' ? 'bg-amber-950 text-amber-400' : 'bg-red-950 text-red-400'}`
  }, "Server: ", serverStatus), /*#__PURE__*/React.createElement("button", {
    onClick: () => setShowAIConfig(!showAIConfig),
    className: `px-1.5 py-0.5 rounded text-xs border transition ${showAIConfig ? 'bg-purple-900 text-purple-200 border-purple-700' : 'bg-zinc-800 text-zinc-400 border-zinc-700 hover:text-zinc-200'}`
  }, /*#__PURE__*/React.createElement("span", {
    className: "inline-flex items-center shrink-0"
  }, /*#__PURE__*/React.createElement("i", {
    "data-lucide": "cpu",
    className: "w-3 h-3"
  }))))), menuOpen && /*#__PURE__*/React.createElement("div", {
    className: "fixed inset-0 z-40",
    onClick: () => setMenuOpen(null)
  }), /*#__PURE__*/React.createElement("div", {
    className: "h-7 bg-[#222] border-b border-zinc-800 flex items-stretch px-2 gap-0.5 shrink-0 overflow-x-auto"
  }, /*#__PURE__*/React.createElement("button", {
    onClick: () => setActiveTab('main'),
    className: `px-3 text-xs font-bold uppercase tracking-wider border-b-2 transition flex items-center gap-1 ${activeTab === 'main' ? 'text-cyan-400 border-cyan-500 bg-zinc-800/50' : 'text-zinc-500 border-transparent hover:text-zinc-300 hover:bg-zinc-800/30'}`
  }, /*#__PURE__*/React.createElement("span", {
    className: "inline-flex items-center shrink-0"
  }, /*#__PURE__*/React.createElement("i", {
    "data-lucide": "layout-dashboard",
    className: "w-3 h-3"
  })), " Main Session"), sessionTabs.map(st => {
    const isTabActive = activeTab === st.id;
    const tabColor = st.color || (isTabActive ? '#06b6d4' : null);
    const borderStyle = isTabActive ? { borderBottomColor: tabColor, color: tabColor } : (st.color ? { color: st.color, borderBottomColor: 'transparent' } : {});
    
    return /*#__PURE__*/React.createElement("div", {
      key: st.id,
      className: "flex items-stretch"
    }, /*#__PURE__*/React.createElement("button", {
      onClick: () => setActiveTab(st.id),
      onAuxClick: e => {
        if (e.button === 1) {
          e.preventDefault();
          closeSessionTab(st.id);
        }
      },
      onContextMenu: e => {
        e.preventDefault();
        setTabContextMenu({
          x: e.clientX,
          y: e.clientY,
          tabId: st.id,
          tabType: 'session'
        });
      },
      style: borderStyle,
      className: `px-2 text-xs font-medium border-b-2 transition flex items-center gap-1 ${isTabActive ? 'bg-zinc-800/50' : 'text-zinc-500 border-transparent hover:text-zinc-300 hover:bg-zinc-800/30'}`
    }, /*#__PURE__*/React.createElement("span", {
      className: "inline-flex items-center shrink-0"
    }, /*#__PURE__*/React.createElement("i", {
      "data-lucide": "layers",
      className: "w-3 h-3"
    })), /*#__PURE__*/React.createElement("span", {
      className: "max-w-[120px] truncate"
    }, st.name), st.isDirty && /*#__PURE__*/React.createElement("span", {
      className: "w-1.5 h-1.5 rounded-full bg-amber-500 ml-1",
      title: "Chưa lưu thay đổi"
    })), /*#__PURE__*/React.createElement("button", {
      onClick: () => closeSessionTab(st.id),
      className: "px-1 text-zinc-600 hover:text-red-400 transition text-xs",
      title: "Close tab"
    }, /*#__PURE__*/React.createElement("span", {
      className: "inline-flex items-center shrink-0"
    }, /*#__PURE__*/React.createElement("i", {
      "data-lucide": "x",
      className: "w-3 h-3"
    }))));
  }), subTabs.map(st => {
    const isTabActive = activeTab === st.id;
    const tabColor = st.color || (isTabActive ? '#f59e0b' : null);
    const borderStyle = isTabActive ? { borderBottomColor: tabColor, color: tabColor } : (st.color ? { color: st.color, borderBottomColor: 'transparent' } : {});
    const iconName = st.type === 'PIANO_ROLL' ? 'music' : 'file-edit';
    
    return /*#__PURE__*/React.createElement("div", {
      key: st.id,
      className: "flex items-stretch"
    }, /*#__PURE__*/React.createElement("button", {
      onClick: () => setActiveTab(st.id),
      onAuxClick: e => {
        if (e.button === 1) {
          e.preventDefault();
          closeSubTab(st.id);
        }
      },
      onContextMenu: e => {
        e.preventDefault();
        setTabContextMenu({
          x: e.clientX,
          y: e.clientY,
          tabId: st.id,
          tabType: 'sub'
        });
      },
      style: borderStyle,
      className: `px-2 text-xs font-medium border-b-2 transition flex items-center gap-1 ${isTabActive ? 'bg-zinc-800/50' : 'text-zinc-500 border-transparent hover:text-zinc-300 hover:bg-zinc-800/30'}`
    }, /*#__PURE__*/React.createElement("span", {
      className: "inline-flex items-center shrink-0"
    }, /*#__PURE__*/React.createElement("i", {
      "data-lucide": iconName,
      className: "w-3 h-3"
    })), /*#__PURE__*/React.createElement("span", {
      className: "max-w-[100px] truncate"
    }, st.label), st.isDirty && /*#__PURE__*/React.createElement("span", {
      className: "w-1.5 h-1.5 rounded-full bg-amber-500 ml-1",
      title: "Chưa lưu thay đổi"
    })), /*#__PURE__*/React.createElement("button", {
      onClick: () => closeSubTab(st.id),
      className: "px-1 text-zinc-600 hover:text-red-400 transition text-xs",
      title: "Close tab"
    }, /*#__PURE__*/React.createElement("span", {
      className: "inline-flex items-center shrink-0"
    }, /*#__PURE__*/React.createElement("i", {
      "data-lucide": "x",
      className: "w-3 h-3"
    }))));
  })), showAIConfig && /*#__PURE__*/React.createElement("div", {
    className: "bg-zinc-900 border-b border-purple-900 p-3 flex flex-col gap-2 transition-all"
  }, /*#__PURE__*/React.createElement("div", {
    className: "text-xs font-bold text-purple-400 uppercase tracking-wider flex items-center gap-1"
  }, /*#__PURE__*/React.createElement("span", {
    className: "inline-flex items-center shrink-0"
  }, /*#__PURE__*/React.createElement("i", {
    "data-lucide": "cpu",
    className: "w-4 h-4"
  })), " Cấu hình cổng kết nối API"), /*#__PURE__*/React.createElement("div", {
    className: "grid grid-cols-1 md:grid-cols-3 gap-2 text-xs"
  }, /*#__PURE__*/React.createElement("div", {
    className: "flex flex-col gap-1"
  }, /*#__PURE__*/React.createElement("span", {
    className: "text-xs text-zinc-500 font-bold uppercase"
  }, "Endpoint Base URL"), /*#__PURE__*/React.createElement("input", {
    type: "text",
    value: aiConfig.baseUrl,
    onChange: e => setAiConfig(prev => ({
      ...prev,
      baseUrl: e.target.value
    })),
    className: "bg-zinc-850 text-zinc-200 p-1.5 rounded border border-zinc-800 focus:outline-none focus:border-purple-600 font-mono text-xs",
    placeholder: "https://api.openai.com/v1"
  })), /*#__PURE__*/React.createElement("div", {
    className: "flex flex-col gap-1"
  }, /*#__PURE__*/React.createElement("span", {
    className: "text-xs text-zinc-500 font-bold uppercase"
  }, "API Token Key"), /*#__PURE__*/React.createElement("input", {
    type: "password",
    value: aiConfig.apiKey,
    onChange: e => setAiConfig(prev => ({
      ...prev,
      apiKey: e.target.value
    })),
    className: "bg-zinc-850 text-zinc-200 p-1.5 rounded border border-zinc-800 focus:outline-none focus:border-purple-600 font-mono text-xs",
    placeholder: "sk-..."
  })), /*#__PURE__*/React.createElement("div", {
    className: "flex flex-col gap-1"
  }, /*#__PURE__*/React.createElement("span", {
    className: "text-xs text-zinc-500 font-bold uppercase"
  }, "Model Name"), /*#__PURE__*/React.createElement("input", {
    type: "text",
    value: aiConfig.model,
    onChange: e => setAiConfig(prev => ({
      ...prev,
      model: e.target.value
    })),
    className: "bg-zinc-850 text-zinc-200 p-1.5 rounded border border-zinc-800 focus:outline-none focus:border-purple-600 font-mono text-xs",
    placeholder: "gpt-4o-mini"
  })))), /*#__PURE__*/React.createElement("div", {
    className: "h-9 bg-[#222] border-b border-zinc-800 flex items-center px-3 gap-2 shrink-0 select-none"
  }, /*#__PURE__*/React.createElement("div", {
    className: "flex items-center gap-1 bg-zinc-900 border border-zinc-700 rounded px-1.5 py-0.5 shadow-lg",
    title: "Kéo để di chuyển toolbar",
    style: {
      cursor: 'grab'
    }
  }, /*#__PURE__*/React.createElement("div", {
    className: "flex items-center gap-0.5 mr-1 text-zinc-600"
  }, /*#__PURE__*/React.createElement("span", {
    className: "inline-flex items-center shrink-0"
  }, /*#__PURE__*/React.createElement("i", {
    "data-lucide": "grip-vertical",
    className: "w-3 h-3"
  }))), /*#__PURE__*/React.createElement("button", {
    onClick: () => {
      setActiveTool('select');
      showToast('Select Tool', 'info');
    },
    className: `w-7 h-7 flex items-center justify-center rounded ${activeTool === 'select' ? 'bg-cyan-700 text-white' : 'text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800'}`,
    title: "Select Tool (V)"
  }, /*#__PURE__*/React.createElement("span", {
    className: "inline-flex items-center shrink-0"
  }, /*#__PURE__*/React.createElement("i", {
    "data-lucide": "mouse-pointer",
    className: "w-3.5 h-3.5"
  }))), /*#__PURE__*/React.createElement("button", {
    onClick: () => {
      setActiveTool('grab');
      showToast('Grab Tool', 'info');
    },
    className: `w-7 h-7 flex items-center justify-center rounded ${activeTool === 'grab' ? 'bg-cyan-700 text-white' : 'text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800'}`,
    title: "Grab Tool (H)"
  }, /*#__PURE__*/React.createElement("span", {
    className: "inline-flex items-center shrink-0"
  }, /*#__PURE__*/React.createElement("i", {
    "data-lucide": "hand",
    className: "w-3.5 h-3.5"
  }))), /*#__PURE__*/React.createElement("div", {
    className: "flex items-center gap-0.5 border border-zinc-700 rounded bg-zinc-850 px-0.5"
  }, /*#__PURE__*/React.createElement("button", {
    onClick: () => {
      setActiveTool('razor');
      showToast('Razor Tool', 'info');
    },
    className: `w-7 h-7 flex items-center justify-center rounded ${activeTool === 'razor' ? 'bg-cyan-700 text-white' : 'text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800'}`,
    title: "Razor Tool (C)"
  }, /*#__PURE__*/React.createElement("svg", {
    className: "w-3.5 h-3.5 text-orange-400",
    viewBox: "0 0 24 24",
    fill: "none",
    stroke: "currentColor",
    strokeWidth: "2.5",
    strokeLinecap: "round",
    strokeLinejoin: "round"
  }, /*#__PURE__*/React.createElement("path", {
    d: "M6 3h12a2 2 0 0 1 2 2v2a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z"
  }), /*#__PURE__*/React.createElement("path", {
    d: "M4 9h16l-3 9H7z"
  }), /*#__PURE__*/React.createElement("circle", {
    cx: "12",
    cy: "6",
    r: "1"
  }))), /*#__PURE__*/React.createElement("button", {
    onClick: handleGlueTracks,
    className: "w-7 h-7 flex items-center justify-center rounded text-zinc-400 hover:text-purple-400 hover:bg-zinc-800",
    title: "Glue Clips"
  }, /*#__PURE__*/React.createElement("span", {
    className: "inline-flex items-center shrink-0"
  }, /*#__PURE__*/React.createElement("i", {
    "data-lucide": "link",
    className: "w-3.5 h-3.5"
  })))), /*#__PURE__*/React.createElement("button", {
    onClick: () => {
      setActiveTool('pen');
      showToast('Pen Tool', 'info');
    },
    className: `w-7 h-7 flex items-center justify-center rounded ${activeTool === 'pen' ? 'bg-cyan-700 text-white' : 'text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800'}`,
    title: "Pen Tool (P)"
  }, /*#__PURE__*/React.createElement("span", {
    className: "inline-flex items-center shrink-0"
  }, /*#__PURE__*/React.createElement("i", {
    "data-lucide": "pen-tool",
    className: "w-3.5 h-3.5"
  }))), /*#__PURE__*/React.createElement("div", {
    className: "w-[1px] h-5 bg-zinc-700 mx-0.5"
  }), /*#__PURE__*/React.createElement("button", {
    onClick: handleCutTrack,
    className: "w-7 h-7 flex items-center justify-center rounded text-zinc-400 hover:text-red-400 hover:bg-zinc-800",
    title: "Cut (Ctrl+X)"
  }, /*#__PURE__*/React.createElement("span", {
    className: "inline-flex items-center shrink-0"
  }, /*#__PURE__*/React.createElement("i", {
    "data-lucide": "scissors",
    className: "w-3.5 h-3.5"
  }))), /*#__PURE__*/React.createElement("button", {
    onClick: handleCopyTrack,
    className: "w-7 h-7 flex items-center justify-center rounded text-zinc-400 hover:text-blue-400 hover:bg-zinc-800",
    title: "Copy (Ctrl+C)"
  }, /*#__PURE__*/React.createElement("span", {
    className: "inline-flex items-center shrink-0"
  }, /*#__PURE__*/React.createElement("i", {
    "data-lucide": "copy",
    className: "w-3.5 h-3.5"
  }))), /*#__PURE__*/React.createElement("button", {
    onClick: handlePasteTrack,
    className: "w-7 h-7 flex items-center justify-center rounded text-zinc-400 hover:text-emerald-400 hover:bg-zinc-800",
    title: "Paste (Ctrl+V)"
  }, /*#__PURE__*/React.createElement("span", {
    className: "inline-flex items-center shrink-0"
  }, /*#__PURE__*/React.createElement("i", {
    "data-lucide": "clipboard",
    className: "w-3.5 h-3.5"
  }))), /*#__PURE__*/React.createElement("div", {
    className: "w-[1px] h-5 bg-zinc-700 mx-0.5"
  }), /*#__PURE__*/React.createElement("button", {
    onClick: addNewTrack,
    className: "px-2 py-1 bg-cyan-600 hover:bg-cyan-500 text-white font-semibold rounded text-xs flex items-center gap-1 shadow transition",
    title: "Thêm Track Mới (Ctrl+I)"
  }, /*#__PURE__*/React.createElement("span", {
    className: "inline-flex items-center shrink-0"
  }, /*#__PURE__*/React.createElement("i", {
    "data-lucide": "plus",
    className: "w-3.5 h-3.5"
  })), /*#__PURE__*/React.createElement("span", null, "Track")), sessionTabs.some(s => s.id === activeTab) && /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
    className: "w-[1px] h-5 bg-zinc-700 mx-0.5"
  }), /*#__PURE__*/React.createElement("button", {
    onClick: () => handleSaveSectionTab(activeTab),
    className: "px-2 py-1 bg-emerald-600 hover:bg-emerald-500 text-white font-semibold rounded text-xs flex items-center gap-1 shadow transition",
    title: "Lưu Section vào Main Session"
  }, /*#__PURE__*/React.createElement("span", {
    className: "inline-flex items-center shrink-0"
  }, /*#__PURE__*/React.createElement("i", {
    "data-lucide": "save",
    className: "w-3.5 h-3.5"
  })), /*#__PURE__*/React.createElement("span", null, "Lưu Section"))), /*#__PURE__*/React.createElement("div", {
    className: "w-[1px] h-5 bg-zinc-700 mx-0.5"
  }), /*#__PURE__*/React.createElement("button", {
    onClick: handleUndo,
    disabled: undoStack.length === 0 && (!window.UndoRedoEngine || !window.UndoRedoEngine.canUndo()),
    className: "w-7 h-7 flex items-center justify-center rounded text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800 disabled:opacity-30",
    title: "Undo (Ctrl+Z)"
  }, /*#__PURE__*/React.createElement("span", {
    className: "inline-flex items-center shrink-0"
  }, /*#__PURE__*/React.createElement("i", {
    "data-lucide": "undo",
    className: "w-3.5 h-3.5"
  }))), /*#__PURE__*/React.createElement("button", {
    onClick: handleRedo,
    disabled: redoStack.length === 0 && (!window.UndoRedoEngine || !window.UndoRedoEngine.canRedo()),
    className: "w-7 h-7 flex items-center justify-center rounded text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800 disabled:opacity-30",
    title: "Redo (Ctrl+Y)"
  }, /*#__PURE__*/React.createElement("span", {
    className: "inline-flex items-center shrink-0"
  }, /*#__PURE__*/React.createElement("i", {
    "data-lucide": "redo",
    className: "w-3.5 h-3.5"
  })))), /*#__PURE__*/React.createElement("div", {
    className: "w-[1px] h-5 bg-zinc-800 mx-0.5"
  }), /*#__PURE__*/React.createElement("button", {
    onClick: () => seekPlaybackTo(0),
    className: "w-7 h-7 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700 transition",
    title: "Quay lại đầu"
  }, /*#__PURE__*/React.createElement("span", {
    className: "inline-flex items-center shrink-0"
  }, /*#__PURE__*/React.createElement("i", {
    "data-lucide": "skip-back",
    className: "w-3.5 h-3.5"
  }))), /*#__PURE__*/React.createElement("button", {
    onClick: () => {
      const isSubTab = subTabs.some(sub => sub.id === activeTab);
      if (isSubTab) {
        const st = subTabs.find(s => s.id === activeTab);
        if (!st) return;
        const left = st.selectionStart !== null && st.selectionEnd !== null ? Math.min(st.selectionStart, st.selectionEnd) : null;
        if (left !== null) seekPlaybackTo(left);
      } else {
        if (selLeft !== null) seekPlaybackTo(selLeft);
      }
    },
    className: "w-7 h-7 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700 transition",
    title: "Đầu vùng chọn"
  }, /*#__PURE__*/React.createElement("span", {
    className: "inline-flex items-center shrink-0"
  }, /*#__PURE__*/React.createElement("i", {
    "data-lucide": "step-back",
    className: "w-3.5 h-3.5"
  }))), /*#__PURE__*/React.createElement("button", {
    onClick: handlePlayPause,
    className: `w-7 h-7 flex items-center justify-center rounded border transition ${isPlaying ? 'bg-emerald-600 text-black border-emerald-500 hover:bg-emerald-500' : 'bg-cyan-600 text-white border-cyan-500 hover:bg-cyan-500'}`,
    title: isPlaying ? "Tạm dừng" : "Play"
  }, /*#__PURE__*/React.createElement("span", {
    className: "inline-flex items-center shrink-0"
  }, /*#__PURE__*/React.createElement("i", {
    "data-lucide": isPlaying ? "pause" : "play",
    className: "w-3.5 h-3.5 fill-current"
  }))), /*#__PURE__*/React.createElement("button", {
    onClick: handleStop,
    className: "w-7 h-7 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700 transition",
    title: "Stop"
  }, /*#__PURE__*/React.createElement("span", {
    className: "inline-flex items-center shrink-0"
  }, /*#__PURE__*/React.createElement("i", {
    "data-lucide": "square",
    className: "w-3.5 h-3.5 fill-current"
  }))), /*#__PURE__*/React.createElement("button", {
    onClick: handleRecordClick,
    className: `w-7 h-7 flex items-center justify-center rounded border transition ${recordingState === 'RECORDING' ? 'bg-red-600 text-white border-red-500 hover:bg-red-500 animate-pulse' : recordingState === 'COUNT_IN' ? 'bg-amber-500 text-black border-amber-400 hover:bg-amber-400 animate-pulse' : 'bg-zinc-800 text-red-500 border-zinc-700 hover:bg-zinc-700 hover:text-red-400'}`,
    title: recordingState === 'RECORDING' ? "Đang ghi âm..." : recordingState === 'COUNT_IN' ? "Chuẩn bị ghi âm..." : "Ghi âm (Record)"
  }, /*#__PURE__*/React.createElement("span", {
    className: "inline-flex items-center shrink-0"
  }, /*#__PURE__*/React.createElement("i", {
    "data-lucide": "circle",
    className: "w-3.5 h-3.5 fill-current"
  }))), /*#__PURE__*/React.createElement("button", {
    onClick: () => {
      const isSubTab = subTabs.some(sub => sub.id === activeTab);
      if (isSubTab) {
        setSubTabs(prev => prev.map(s => {
          if (s.id !== activeTab) return s;
          const right = s.selectionStart !== null && s.selectionEnd !== null ? Math.max(s.selectionStart, s.selectionEnd) : null;
          return right !== null ? {
            ...s,
            currentTime: right
          } : s;
        }));
      } else {
        if (selRight !== null) setCurrentTime(selRight);
      }
    },
    className: "w-7 h-7 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700 transition",
    title: "Cuối vùng chọn"
  }, /*#__PURE__*/React.createElement("span", {
    className: "inline-flex items-center shrink-0"
  }, /*#__PURE__*/React.createElement("i", {
    "data-lucide": "step-forward",
    className: "w-3.5 h-3.5"
  }))), /*#__PURE__*/React.createElement("button", {
    onClick: () => {
      const isSubTab = subTabs.some(sub => sub.id === activeTab);
      if (isSubTab) {
        setSubTabs(prev => prev.map(s => {
          if (s.id !== activeTab) return s;
          const duration = s.buffer ? s.buffer.duration / (s.speed || 1.0) : 0;
          return {
            ...s,
            currentTime: duration
          };
        }));
      } else {
        setCurrentTime(maxDuration);
      }
    },
    className: "w-7 h-7 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700 transition",
    title: "Đến cuối"
  }, /*#__PURE__*/React.createElement("span", {
    className: "inline-flex items-center shrink-0"
  }, /*#__PURE__*/React.createElement("i", {
    "data-lucide": "skip-forward",
    className: "w-3.5 h-3.5"
  }))), /*#__PURE__*/React.createElement("div", {
    className: "w-[1px] h-5 bg-zinc-800 mx-0.5"
  }), /*#__PURE__*/React.createElement("button", {
        onClick: () => {
          setIsLoopingSelection(prev => !prev);
          // Sync loop state with active sub-tab (piano roll / audio editor)
          const activeSub = activeTab && subTabs.find(s => s.id === activeTab && ['PIANO_ROLL', 'AUDIO_CLIP_EDITOR', 'SECTION_EDITOR'].includes(s.type));
          if (activeSub) {
            const newLoop = !activeSub.isLooping;
            setSubTabs(prev => prev.map(s => s.id === activeTab ? { ...s, isLooping: newLoop } : s));
            if (newLoop) {
              const bpmVal = parseInt(bpm) || 120;
              const beatSec = 60.0 / bpmVal;
              let maxEnd = 0;
              if (activeSub.type === 'PIANO_ROLL') {
                (activeSub.notes || []).forEach(n => {
                  const end = (n.start_beat || 0) + (n.duration_beats || 1);
                  if (end > maxEnd) maxEnd = end;
                });
              } else if (activeSub.type === 'SECTION_EDITOR') {
                (activeSub.clips || []).forEach(c => {
                  const end = (c.startTime || 0) + (c.duration || 0);
                  if (end > maxEnd) maxEnd = end;
                });
              } else if (activeSub.type === 'AUDIO_CLIP_EDITOR') {
                const dur = activeSub.buffer?.duration || 0;
                if (dur > maxEnd) maxEnd = dur;
              }
              const loopEndTime = activeSub.type === 'PIANO_ROLL' ? Math.max(maxEnd, 16) * beatSec + 1.0 : Math.max(maxEnd, 1);
              setSubTabs(prev => prev.map(s => s.id === activeTab ? { ...s, selectionStart: 0, selectionEnd: loopEndTime } : s));
            }
          } else {
            // Main timeline / section tab: nếu user đã chọn vùng loop riêng →
            // giữ nguyên (loop vùng chọn — nhánh updatePlayhead selLeft/
            // selRight). KHÔNG auto-derive selection: LOOP bật → loop tại
            // projectEnd (content end khi LOOP — 0515/0520) — nhánh
            // updatePlayhead `updatedTime >= projectEndRef` loop lại từ 0 —
            // tránh selection 0→end items ép loop toàn bài (sai ý "loop tại
            // projectEnd").
            const hasSel = (selectionMode === 'local' && selLeft !== null && selRight !== null && selRight > selLeft)
              || (selectionStart !== null && selectionEnd !== null);
            if (hasSel) {
              // giữ selection — loop vùng chọn
            } else {
              // không selection → loop tại projectEnd (không tạo selection)
            }
          }
        },
    className: `w-7 h-7 flex items-center justify-center rounded border transition ${isLoopingSelection ? 'bg-amber-600 text-black border-amber-500 hover:bg-amber-500' : 'bg-zinc-800 text-zinc-200 border-zinc-700 hover:bg-zinc-700'}`,
    title: isLoopingSelection ? (selLeft !== null && selRight !== null ? "Loop vùng chọn" : "Loop timeline") : "Bật loop"
  }, /*#__PURE__*/React.createElement("span", {
    className: "inline-flex items-center shrink-0"
  }, /*#__PURE__*/React.createElement("i", {
    "data-lucide": "repeat",
    className: "w-3.5 h-3.5"
  }))), /*#__PURE__*/React.createElement("span", {
    className: "text-[14px] text-zinc-500 font-bold uppercase ml-2"
  }, "Snap"), /*#__PURE__*/React.createElement("select", {
    value: snapValue,
    onChange: e => onSnapChangeue(e.target.value),
    className: "bg-black text-white text-[14px] px-1.5 py-0.5 rounded border border-zinc-700 focus:outline-none focus:border-cyan-500 font-mono cursor-pointer"
  }, /*#__PURE__*/React.createElement("option", {
    value: "free"
  }, "Free"), /*#__PURE__*/React.createElement("option", {
    value: "1"
  }, "1"), /*#__PURE__*/React.createElement("option", {
    value: "1/2"
  }, "1/2"), /*#__PURE__*/React.createElement("option", {
    value: "1/4"
  }, "1/4"), /*#__PURE__*/React.createElement("option", {
    value: "1/8"
  }, "1/8"), /*#__PURE__*/React.createElement("option", {
    value: "1/16"
  }, "1/16"), /*#__PURE__*/React.createElement("option", {
    value: "1/32"
  }, "1/32"), /*#__PURE__*/React.createElement("option", {
    value: "4"
  }, "4")), /*#__PURE__*/React.createElement("div", {
    className: "w-[1px] h-6 bg-zinc-800 mx-1.5"
  }), /*#__PURE__*/React.createElement("span", {
    className: "text-[20px] text-zinc-500 font-bold ml-1"
  }, "Time:"), /*#__PURE__*/React.createElement("span", {
    className: "font-mono text-[20px] text-cyan-300 font-bold tabular-nums",
    title: "Vị trí playhead"
  }, formatTime(currentTime)), /*#__PURE__*/React.createElement("span", {
    className: "text-[20px] text-zinc-600 font-mono"
  }, "/"), /*#__PURE__*/React.createElement("span", {
    className: "font-mono text-[20px] text-zinc-400 tabular-nums",
    title: "Tổng thời gian dự án"
  }, formatTime(projectEnd)), /*#__PURE__*/React.createElement("div", {
    className: "flex items-center gap-1.5 ml-3",
    title: bridgeUi.connected && !bridgeUi.forceWasm ? "Engine phát qua Native Bridge (C++ FluidSynth/sfizz/VST3)" : "Engine phát qua SonicSF WASM"
  }, /*#__PURE__*/React.createElement("span", {
    className: "text-[9px] font-bold uppercase tracking-wider px-1.5 py-0.5 rounded border " + (bridgeUi.connected && !bridgeUi.forceWasm ? "text-emerald-400 border-emerald-700 bg-emerald-900/30" : "text-zinc-500 border-zinc-700 bg-zinc-800")
  }, bridgeUi.connected && !bridgeUi.forceWasm ? "Bridge" : "WASM"), /*#__PURE__*/React.createElement("button", {
    onClick: () => {
      const v = !bridgeUi.forceWasm;
      setBridgeUi(s => ({ ...s, forceWasm: v }));
      if (window.SonicMidiRouter && window.SonicMidiRouter.setForceWasm) window.SonicMidiRouter.setForceWasm(v);
    },
    className: `px-1.5 py-0.5 rounded text-[9px] font-bold border transition ${bridgeUi.forceWasm ? "bg-amber-600 text-black border-amber-500" : "bg-zinc-800 text-zinc-400 border-zinc-700 hover:text-zinc-200"}`,
    title: "Debug: ép dùng SonicSF WASM bỏ qua bridge"
  }, "Ép WASM")), /*#__PURE__*/React.createElement("div", {
    className: "flex-1"
  })),(() => {
    const dockPanels = {
      top: [],
      right: [],
      bottom: [],
      left: []
    };
    const addPanel = (id, pos, visible) => {
      if (visible) dockPanels[pos].push(id);
    };
    // Export panel giờ là MODAL FLOAT giữa màn hình (không nằm trong dock rows).
    // Selection / FX Rack / MIDI Events panels đã REMOVE khỏi dock (theo yêu cầu
    // user — FX Rack dùng modal float; Selection & MIDI Events không cần).
    addPanel('ai', panelPositions.ai, showAIPanel);
    addPanel('python_tools', panelPositions.python_tools || 'bottom', showPythonToolsPanel);
    const closePanel = id => {
      if (id === 'export') setShowExportPanel(false);
      else if (id === 'ai') setShowAIPanel(false);
      else if (id === 'python_tools') setShowPythonToolsPanel(false);
      else if (id === 'selection') setShowSelectionPanel(false);
      else if (id === 'fx_rack') setShowFxRack(false);
      else if (id === 'midi_events') setShowMidiEvents(false);
    };
    const renderPanelContent = panelId => {
      const h = id => e => {
        startPanelDrag(id, e);
      };
      if (panelId === 'export') return /*#__PURE__*/React.createElement("div", {
        className: "flex flex-col h-full gap-1.5"
      }, /*#__PURE__*/React.createElement("div", {
        className: "flex items-center justify-between shrink-0 cursor-grab active:cursor-grabbing select-none",
        onMouseDown: e => startPanelDrag('export', e)
      }, /*#__PURE__*/React.createElement("h3", {
        className: "font-bold text-xs text-zinc-200 flex items-center gap-1"
      }, /*#__PURE__*/React.createElement("span", {
        className: "inline-flex items-center shrink-0"
      }, /*#__PURE__*/React.createElement("i", {
        "data-lucide": "grip-vertical",
        className: "w-3 h-3 text-zinc-500"
      })), /*#__PURE__*/React.createElement("span", {
        className: "inline-flex items-center shrink-0"
      }, /*#__PURE__*/React.createElement("i", {
        "data-lucide": "save",
        className: "w-3.5 h-3.5 text-cyan-400"
      })), " Export"), /*#__PURE__*/React.createElement("button", {
        onClick: () => closePanel('export'),
        className: "text-zinc-600 hover:text-zinc-300"
      }, /*#__PURE__*/React.createElement("span", {
        className: "inline-flex items-center shrink-0"
      }, /*#__PURE__*/React.createElement("i", {
        "data-lucide": "x",
        className: "w-3 h-3"
      })))), /*#__PURE__*/React.createElement("div", {
        className: "grid grid-cols-2 gap-1"
      }, /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("label", {
        className: "block text-[7px] text-zinc-500 font-bold uppercase mb-0.5"
      }, "Ngu\u1ed3n"), /*#__PURE__*/React.createElement("select", {
        value: exportSettings.source,
        onChange: e => setExportSettings(p => ({ ...p, source: e.target.value })),
        className: "w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none"
      }, /*#__PURE__*/React.createElement("option", { value: "project" }, "Project (Mix)"), /*#__PURE__*/React.createElement("option", { value: "track_mix" }, "Track Selection"), /*#__PURE__*/React.createElement("option", { value: "active_clip" }, "Active Clip"), /*#__PURE__*/React.createElement("option", { value: "clip_selection" }, "Clip Selection"))), /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("label", {
        className: "block text-[7px] text-zinc-500 font-bold uppercase mb-0.5"
      }, "\u0110\u1ecbnh d\u1ea1ng"), /*#__PURE__*/React.createElement("select", {
        value: exportSettings.format,
        onChange: e => setExportSettings(p => ({ ...p, format: e.target.value, sampleRate: e.target.value === 'wav' ? '44100' : e.target.value === 'mp3' ? '44100' : '44100', bitDepth: e.target.value === 'wav' ? '16' : '16', quality: '44khz' })),
        className: "w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none"
      }, /*#__PURE__*/React.createElement("option", { value: "wav" }, "WAV"), /*#__PURE__*/React.createElement("option", { value: "mp3" }, "MP3"), /*#__PURE__*/React.createElement("option", { value: "ogg" }, "OGG")))), exportSettings.format === 'wav' ? /*#__PURE__*/React.createElement("div", {
        className: "grid grid-cols-2 gap-1"
      }, /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("label", {
        className: "block text-[7px] text-zinc-500 font-bold uppercase mb-0.5"
      }, "SR (Hz)"), /*#__PURE__*/React.createElement("select", {
        value: exportSettings.sampleRate,
        onChange: e => setExportSettings(p => ({ ...p, sampleRate: e.target.value })),
        className: "w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none"
      }, /*#__PURE__*/React.createElement("option", { value: "22500" }, "22500"), /*#__PURE__*/React.createElement("option", { value: "44100" }, "44100"))), /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("label", {
        className: "block text-[7px] text-zinc-500 font-bold uppercase mb-0.5"
      }, "Bit"), /*#__PURE__*/React.createElement("select", {
        value: exportSettings.bitDepth,
        onChange: e => setExportSettings(p => ({ ...p, bitDepth: e.target.value })),
        className: "w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none"
      }, /*#__PURE__*/React.createElement("option", { value: "8" }, "8"), /*#__PURE__*/React.createElement("option", { value: "16" }, "16"), /*#__PURE__*/React.createElement("option", { value: "24" }, "24")))) : /*#__PURE__*/React.createElement("div", {
        className: "grid grid-cols-2 gap-1"
      }, /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("label", {
        className: "block text-[7px] text-zinc-500 font-bold uppercase mb-0.5"
      }, "Ch\u1ea5t l\u01b0\u1ee3ng"), /*#__PURE__*/React.createElement("select", {
        value: exportSettings.quality,
        onChange: e => setExportSettings(p => ({ ...p, quality: e.target.value })),
        className: "w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none"
      }, /*#__PURE__*/React.createElement("option", { value: "44khz" }, "44kHz"), /*#__PURE__*/React.createElement("option", { value: "lossless" }, "Lossless"))), /*#__PURE__*/React.createElement("div", null)), /*#__PURE__*/React.createElement("div", {
        className: "grid grid-cols-2 gap-1"
      }, /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("label", {
        className: "block text-[7px] text-zinc-500 font-bold uppercase mb-0.5"
      }, "Kênh"), /*#__PURE__*/React.createElement("select", {
        value: exportSettings.channels,
        onChange: e => setExportSettings(p => ({ ...p, channels: e.target.value })),
        className: "w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none"
      }, /*#__PURE__*/React.createElement("option", { value: "mono" }, "Mono"), /*#__PURE__*/React.createElement("option", { value: "stereo" }, "Stereo"))), /*#__PURE__*/React.createElement("div", null)), /*#__PURE__*/React.createElement("button", {
        onClick: triggerBounceExport,
        disabled: isExporting,
        title: "Bounce realtime - file WAV day du MIDI + FX Rack + Mastering Chain (chay lai project that, thoi gian = do dai bai)",
        className: "w-full py-1 bg-fuchsia-800 hover:bg-fuchsia-700 text-zinc-100 font-bold rounded text-xs flex items-center justify-center gap-1"
      }, /*#__PURE__*/React.createElement("span", {
        className: "inline-flex items-center shrink-0"
      }, /*#__PURE__*/React.createElement("i", {
        "data-lucide": "radio",
        className: "w-3 h-3"
      })), isExporting ? '...' : 'Bounce MIDI'), /*#__PURE__*/React.createElement("button", {
        onClick: triggerWavExport,
        disabled: isExporting,
        className: "w-full py-1 bg-cyan-700 hover:bg-cyan-600 text-zinc-100 font-bold rounded text-xs flex items-center justify-center gap-1"
      }, /*#__PURE__*/React.createElement("span", {
        className: "inline-flex items-center shrink-0"
      }, /*#__PURE__*/React.createElement("i", {
        "data-lucide": "download-cloud",
        className: "w-3 h-3"
      })), isExporting ? '...' : 'Export'));
      if (panelId === 'ai') {
        const selMidiInfo = getSelectedMidiItemInfo();
        const hasSelItem = !!selMidiInfo;
        if (window.PromptTemplateManager && !aiPromptMgrRef.current) {
          aiPromptMgrRef.current = new window.PromptTemplateManager();
        }
        // Re-read from localStorage when presets change (e.g., AIPresetModal saved)
        if (aiPromptMgrRef.current && window.__aiPresetVersion !== aiPresetVersion) {
          window.__aiPresetVersion = aiPresetVersion;
          aiPromptMgrRef.current.loadPresets();
        }
        const promptMgr = aiPromptMgrRef.current;
        const suggestions = promptMgr ? promptMgr.presets : [];
        const handleApplySuggestion = (preset) => {
          if (hasSelItem) {
            setAiPrompt(`Rearrange this melody line in ${preset.name} style`);
          } else {
            setAiPrompt(preset.system_instruction_template);
          }
          setShowAiTypeahead(false);
          setAiSuggestions([]);
        };
        return /*#__PURE__*/React.createElement("div", {
        className: "flex flex-col gap-1.5 flex-1 min-h-0"
      }, /*#__PURE__*/React.createElement("div", {
        className: "flex items-center justify-between shrink-0 cursor-grab active:cursor-grabbing select-none",
        onMouseDown: e => startPanelDrag('ai', e)
      }, /*#__PURE__*/React.createElement("h3", {
        className: "font-bold text-xs text-zinc-200 flex items-center gap-1"
      }, /*#__PURE__*/React.createElement("span", {
        className: "inline-flex items-center shrink-0"
      }, /*#__PURE__*/React.createElement("i", {
        "data-lucide": "grip-vertical",
        className: "w-3 h-3 text-zinc-500"
      })), /*#__PURE__*/React.createElement("span", {
        className: "inline-flex items-center shrink-0"
      }, /*#__PURE__*/React.createElement("i", {
        "data-lucide": "cpu",
        className: "w-3.5 h-3.5 text-purple-400"
      })), " AI Copilot"), /*#__PURE__*/React.createElement("div", {
        className: "flex items-center gap-1"
      }, /*#__PURE__*/React.createElement("button", {
        onClick: () => setAiPresetModalOpen(true),
        className: "text-zinc-600 hover:text-zinc-300 mr-0.5",
        title: "Preset Manager"
      }, /*#__PURE__*/React.createElement("span", {
        className: "inline-flex items-center shrink-0"
      }, /*#__PURE__*/React.createElement("i", {
        "data-lucide": "sliders",
        className: "w-3 h-3"
      }))), /*#__PURE__*/React.createElement("button", {
        onClick: () => {
          setAiActionLog([]);
          showToast('Đã xoá nhật ký AI.', 'info');
        },
        className: "text-zinc-600 hover:text-zinc-300",
        title: "Clear log"
      }, /*#__PURE__*/React.createElement("span", {
        className: "inline-flex items-center shrink-0"
      }, /*#__PURE__*/React.createElement("i", {
        "data-lucide": "trash-2",
        className: "w-3 h-3"
      }))), /*#__PURE__*/React.createElement("button", {
        onClick: () => closePanel('ai'),
        className: "text-zinc-600 hover:text-zinc-300"
      }, /*#__PURE__*/React.createElement("span", {
        className: "inline-flex items-center shrink-0"
      }, /*#__PURE__*/React.createElement("i", {
        "data-lucide": "x",
        className: "w-3 h-3"
      }))))), /*#__PURE__*/React.createElement("div", {
        className: "flex items-center gap-1.5 w-full min-w-0 pb-1"
      }, /*#__PURE__*/React.createElement("span", {
        className: "inline-flex items-center shrink-0"
      }, /*#__PURE__*/React.createElement("i", {
        "data-lucide": "cpu",
        className: "w-3 h-3 text-purple-400"
      })), /*#__PURE__*/React.createElement("select", {
        value: selectedProviderId,
        onChange: e => setSelectedProviderId(e.target.value),
        className: "flex-1 min-w-0 max-w-full bg-[#141414] text-zinc-200 border border-zinc-700 rounded px-1 py-0.5 text-xs font-mono focus:outline-none focus:border-purple-600 truncate"
      }, aiProviders.length === 0 ? /*#__PURE__*/React.createElement("option", {
        value: ""
      }, "Chưa có provider") : aiProviders.map(p => /*#__PURE__*/React.createElement("option", {
        key: p.id,
        value: p.id
      }, p.name, p.is_active ? '' : ' (inactive)')))), /*#__PURE__*/React.createElement("div", {
        className: "border-t border-zinc-800 pt-1 mt-1 shrink-0"
      }, /*#__PURE__*/React.createElement("button", {
        onClick: () => {
          if (window.DAWCommandDispatcher && window.DAWCommandDispatcher.undo) {
            const entry = window.DAWCommandDispatcher.undo();
            if (entry) {
              setAiActionLog(prev => [...prev, { type: 'undo', text: `Undo: ${entry.name}`, time: Date.now() }]);
              showToast(`Undo AI: ${entry.name}`, 'info');
            }
          } else {
            handleUndo();
            setAiActionLog(prev => [...prev, { type: 'undo', text: 'Undo (Ctrl+Z)', time: Date.now() }]);
          }
        },
        className: "w-full text-[10px] text-amber-400 hover:text-amber-300 border border-amber-800 rounded py-0.5"
      }, /*#__PURE__*/React.createElement("span", {
        className: "inline-flex items-center justify-center gap-1"
      }, /*#__PURE__*/React.createElement("i", {
        "data-lucide": "rotate-ccw",
        className: "w-3 h-3"
      }), "Undo"))), /*#__PURE__*/React.createElement("div", {
        className: "flex-1 min-h-0 flex flex-col overflow-hidden mt-1"
      }, /*#__PURE__*/React.createElement("div", {
        className: "text-[10px] font-bold text-zinc-400 uppercase shrink-0 pb-0.5 cursor-pointer hover:text-zinc-200 select-none",
        onClick: () => setShowAIActionLog(!showAIActionLog)
      }, /*#__PURE__*/React.createElement("span", {
        className: "inline-flex items-center gap-1"
      }, /*#__PURE__*/React.createElement("i", {
        "data-lucide": "list",
        className: "w-3 h-3"
      }), " Action Log", showAIActionLog ? " \u2212" : " +")), /*#__PURE__*/React.createElement("div", {
        ref: actionLogContainerRef,
        className: "flex-1 overflow-y-auto no-scrollbar bg-[#0f0f0f] rounded border border-zinc-800 select-text" + (showAIActionLog ? '' : ' hidden')
      }, aiActionLog.length === 0 ? /*#__PURE__*/React.createElement("div", {
        className: "text-xs text-zinc-600 italic select-text p-1"
      }, "Ch\u01B0a c\u00F3 h\u00E0nh \u0111\u1ED9ng n\u00E0o.") : aiActionLog.map(function(entry, i) { return /*#__PURE__*/React.createElement("div", {
        key: i,
        className: 'text-xs font-mono py-0.5 border-b border-zinc-900 last:border-0 ' + (entry.type === 'error' ? 'text-red-400' : entry.type === 'status' ? 'text-zinc-400 italic' : entry.type === 'undo' ? 'text-amber-400' : 'text-zinc-300')
      }, new Date(entry.time).toLocaleTimeString(), entry.text); }))), showAISuggestions ? /*#__PURE__*/React.createElement("div", {
        className: "flex flex-col gap-1 mb-1 shrink-0"
      }, /*#__PURE__*/React.createElement("div", {
        className: "flex items-center gap-1 text-[10px] font-bold text-zinc-400 uppercase"
      }, /*#__PURE__*/React.createElement("span", {
        className: "inline-flex items-center shrink-0"
      }, /*#__PURE__*/React.createElement("i", {
        "data-lucide": "sparkles",
        className: "w-3 h-3 text-purple-400"
      })), " AI Suggestion", hasSelItem ? /*#__PURE__*/React.createElement("span", {
        className: "flex-1 text-right text-[10px] text-amber-400 font-semibold uppercase normal-case truncate ml-2"
      }, "MIDI: ", selMidiInfo.itemName || selMidiInfo.itemId) : null), /*#__PURE__*/React.createElement("div", {
        className: "overflow-y-auto no-scrollbar max-h-36 bg-[#0f0f0f] rounded border border-zinc-800"
      }, suggestions.slice(0, 50).map(p => /*#__PURE__*/React.createElement("button", {
        key: p.id,
        onClick: () => handleApplySuggestion(p),
        className: "w-full text-left px-2 py-1 text-[11px] hover:bg-zinc-800 border-b border-zinc-900 last:border-0 flex items-center justify-between gap-2"
      }, /*#__PURE__*/React.createElement("span", {
        className: "truncate flex-1"
      }, /*#__PURE__*/React.createElement("span", {
        className: "text-zinc-400"
      }, p.is_favorite ? "★ " : "✨ "), /*#__PURE__*/React.createElement("span", {
        className: "text-zinc-200 font-semibold"
      }, p.name)), /*#__PURE__*/React.createElement("span", {
        className: "text-[9px] text-zinc-500 shrink-0"
      }, p.category))))) : null, /*#__PURE__*/React.createElement("div", {
        className: "border-t border-zinc-800 pt-1.5 mt-1 shrink-0"
      }, /*#__PURE__*/React.createElement("div", {
        className: "text-xs font-bold text-zinc-400 uppercase mb-1 flex items-center gap-1"
      }, /*#__PURE__*/React.createElement("span", {
        className: "inline-flex items-center shrink-0"
      }, /*#__PURE__*/React.createElement("i", {
        "data-lucide": "message-square",
        className: "w-3 h-3"
      })), " Copilot Prompt", /*#__PURE__*/React.createElement("button", {
        onClick: () => setShowAISuggestions(!showAISuggestions),
        className: "ml-auto text-[9px] px-1.5 py-0.5 rounded border font-semibold " + (showAISuggestions ? 'bg-zinc-800 text-zinc-400 border-zinc-700 hover:bg-zinc-700' : 'bg-indigo-950/40 text-indigo-400 border-indigo-800/50 hover:bg-indigo-900/50'),
        title: showAISuggestions ? 'Ẩn AI Suggestion' : 'Hiện AI Suggestion'
      }, "Sug")), /*#__PURE__*/React.createElement("textarea", {
        value: aiPrompt,
        onChange: e => {
          const v = e.target.value;
          aiPromptUndoPush(v);
          setAiPrompt(v);
          if (v.trim().length >= 2 && promptMgr) {
            const matches = promptMgr.presets.filter(p =>
              p.keywords.some(kw => kw.toLowerCase().includes(v.toLowerCase())) ||
              p.name.toLowerCase().includes(v.toLowerCase())
            );
            setAiSuggestions(matches);
            setShowAiTypeahead(matches.length > 0);
          } else {
            setShowAiTypeahead(false);
          }
        },
        placeholder: hasSelItem ? "Nhập lệnh rearrange... (VD: Jazz Swing, Arpeggio)" : "Nhập lệnh điều khiển DAW... (VD: Tạo track mới tên Beat, đặt BPM 128)",
        className: "w-full bg-[#1e1e1e] text-zinc-200 p-1.5 rounded border border-zinc-700 focus:outline-none focus:border-purple-600 font-mono text-xs resize-y",
        rows: 8,
        onKeyDown: e => {
          if ((e.ctrlKey || e.metaKey) && e.key === 'z' && !e.shiftKey) {
            e.preventDefault();
            e.stopPropagation();
            const u = aiPromptUndoRef.current;
            if (u.idx > 0) {
              u.idx--;
              setAiPrompt(u.stack[u.idx]);
              showToast('Undo: AI Prompt', 'info');
            }
            return;
          }
          if ((e.ctrlKey || e.metaKey) && (e.key === 'y' || (e.key === 'z' && e.shiftKey))) {
            e.preventDefault();
            e.stopPropagation();
            const u = aiPromptUndoRef.current;
            if (u.idx < u.stack.length - 1) {
              u.idx++;
              setAiPrompt(u.stack[u.idx]);
              showToast('Redo: AI Prompt', 'info');
            }
            return;
          }
          if (e.key === 'Enter' && !e.shiftKey) {
            e.preventDefault();
            handleAISend();
          } else if (e.key === 'Tab' && showAiTypeahead && aiSuggestions.length > 0) {
            e.preventDefault();
            handleApplySuggestion(aiSuggestions[0]);
          } else if (e.key === 'ArrowUp' && promptHistRef.current.length > 0 && e.target.selectionStart === 0) {
            e.preventDefault();
            const idx = promptHistIdx === -1 ? promptHistRef.current.length - 1 : Math.max(0, promptHistIdx - 1);
            setPromptHistIdx(idx);
            setAiPrompt(promptHistRef.current[idx]);
          } else if (e.key === 'ArrowDown' && e.target.selectionStart === aiPrompt.length) {
            e.preventDefault();
            if (promptHistIdx === -1) return;
            const idx = promptHistIdx + 1;
            if (idx >= promptHistRef.current.length) {
              setPromptHistIdx(-1);
              setAiPrompt('');
            } else {
              setPromptHistIdx(idx);
              setAiPrompt(promptHistRef.current[idx]);
            }
          }
        }
      }), showAiTypeahead && aiSuggestions.length > 0 && /*#__PURE__*/React.createElement("div", {
        ref: aiTypeaheadRef,
        className: "absolute bottom-full left-0 right-0 bg-[#1e1e1e] border border-indigo-600/50 rounded-lg shadow-2xl z-50 max-h-36 overflow-y-auto mb-1"
      }, /*#__PURE__*/React.createElement("div", {
        className: "px-2 py-1 text-[10px] uppercase tracking-wider font-semibold text-indigo-400 bg-[#141414] border-b border-zinc-800"
      }, "Gợi ý (", aiSuggestions.length, ")"), aiSuggestions.slice(0, 8).map(p => /*#__PURE__*/React.createElement("div", {
        key: p.id,
        onClick: () => handleApplySuggestion(p),
        className: "px-2 py-1 hover:bg-indigo-700/30 cursor-pointer border-b border-zinc-800/30 flex items-center justify-between text-[11px]"
      }, /*#__PURE__*/React.createElement("span", null, /*#__PURE__*/React.createElement("span", {
        className: "font-semibold text-zinc-200"
      }, p.name), /*#__PURE__*/React.createElement("span", {
        className: "ml-1.5 text-zinc-500"
      }, "(", p.category, ")")), /*#__PURE__*/React.createElement("span", {
        className: "text-[10px] bg-zinc-800 text-zinc-400 px-1 py-0.5 rounded"
      }, "Tab"))))), /*#__PURE__*/React.createElement("div", {
        className: "flex items-center gap-1 mt-1 shrink-0"
      }, /*#__PURE__*/React.createElement("button", {
        onClick: handleAISend,
        disabled: aiProcessing,
        className: "flex-1 py-1 bg-purple-700 hover:bg-purple-600 text-white font-bold rounded text-xs border border-purple-500 flex items-center justify-center gap-1"
      }, aiProcessing ? 'Đang suy luận...' : /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("span", {
        className: "inline-flex items-center shrink-0"
      }, /*#__PURE__*/React.createElement("i", {
        "data-lucide": "send",
        className: "w-3 h-3"
      })), " Gửi")), /*#__PURE__*/React.createElement("button", {
        onClick: () => setAiComposeMode(aiComposeMode === 'EXTEND_CONTINUATION' ? 'SIMILAR_VARIATION' : 'EXTEND_CONTINUATION'),
        title: "Chọn mode AI: Variation (biến tấu cùng độ dài) / Extend (viết tiếp các bar sau)",
        className: "px-2 py-1 rounded text-[10px] border font-semibold flex items-center justify-center gap-1 " + (aiComposeMode === 'EXTEND_CONTINUATION' ? 'bg-sky-800 text-sky-100 border-sky-600' : 'bg-amber-800 text-amber-100 border-amber-600')
      }, /*#__PURE__*/React.createElement("span", {
        className: "inline-flex items-center shrink-0"
      }, /*#__PURE__*/React.createElement("i", {
        "data-lucide": aiComposeMode === 'EXTEND_CONTINUATION' ? "arrow-right" : "shuffle",
        className: "w-3 h-3"
      })), aiComposeMode === 'EXTEND_CONTINUATION' ? 'Extend' : 'Variation'), /*#__PURE__*/React.createElement("button", {
        onClick: () => handleAiComposeFromItem(aiComposeMode),
        disabled: aiProcessing,
        title: "AI phân tích giai điệu MIDI item đang chọn → sáng tác theo mode đã chọn → ghi kết quả (Variation: track mới ngay dưới A/B; Extend: append sau item gốc)",
        className: "px-2 py-1 bg-teal-800 hover:bg-teal-700 text-white rounded text-[10px] border border-teal-600 flex items-center justify-center gap-1" + (hasSelItem ? '' : ' opacity-40 pointer-events-none')
      }, /*#__PURE__*/React.createElement("span", {
        className: "inline-flex items-center shrink-0"
      }, /*#__PURE__*/React.createElement("i", {
        "data-lucide": "music-2",
        className: "w-3 h-3"
      })), " Compose"), /*#__PURE__*/React.createElement("button", {
        onClick: () => {
          setAiPrompt('');
          setAiActionLog([]);
        },
        className: "px-2 py-1 bg-zinc-800 hover:bg-zinc-700 text-zinc-400 rounded text-xs border border-zinc-700"
      }, "Clear")), /*#__PURE__*/React.createElement("div", {
        className: "text-xs text-zinc-600 shrink-0"
      }, hasSelItem ? "Enter gửi rearrange | Tab chọn gợi ý" : "Enter để gửi nhanh"));
      }
      if (panelId === 'python_tools') return /*#__PURE__*/React.createElement("div", {
        className: "flex flex-col h-full gap-1.5"
      }, /*#__PURE__*/React.createElement("div", {
        className: "flex items-center justify-between shrink-0 cursor-grab active:cursor-grabbing select-none",
        onMouseDown: e => startPanelDrag('python_tools', e)
      }, /*#__PURE__*/React.createElement("h3", {
        className: "font-bold text-xs text-amber-300 flex items-center gap-1"
      }, /*#__PURE__*/React.createElement("span", {
        className: "inline-flex items-center shrink-0"
      }, /*#__PURE__*/React.createElement("i", {
        "data-lucide": "grip-vertical",
        className: "w-3 h-3 text-zinc-500"
      })), /*#__PURE__*/React.createElement("span", {
        className: "inline-flex items-center shrink-0"
      }, /*#__PURE__*/React.createElement("i", {
        "data-lucide": "wrench",
        className: "w-3.5 h-3.5 text-amber-400"
      })), " DSP Tools"), /*#__PURE__*/React.createElement("button", {
        onClick: () => closePanel('python_tools'),
        className: "text-zinc-600 hover:text-zinc-300"
      }, /*#__PURE__*/React.createElement("span", {
        className: "inline-flex items-center shrink-0"
      }, /*#__PURE__*/React.createElement("i", {
        "data-lucide": "x",
        className: "w-3 h-3"
      })))), /*#__PURE__*/React.createElement("div", {
        className: "p-1.5 bg-[#141414] rounded border border-zinc-800 text-[10px] font-mono text-zinc-400 space-y-0.5 leading-relaxed"
      }, dspSelectionStats ? [
        /*#__PURE__*/React.createElement("div", { key: "track" }, `Track: ${dspSelectionStats.trackName}`),
        /*#__PURE__*/React.createElement("div", { key: "range" }, `Range: ${dspSelectionStats.timeRange}`),
        /*#__PURE__*/React.createElement("div", { key: "ch" }, `Channels: ${dspSelectionStats.channels}`),
        /*#__PURE__*/React.createElement("div", { key: "peak" }, `Peak Vol: ${dspSelectionStats.peakVolume}`)
      ] : "Chưa chọn track"), /*#__PURE__*/React.createElement("div", {
        className: "grid grid-cols-2 gap-1 text-xs"
      }, /*#__PURE__*/React.createElement("button", {
        onClick: () => runPythonTool('normalize'),
        className: "py-1 bg-amber-950/80 hover:bg-amber-900 text-amber-300 border border-amber-800/80 rounded font-bold flex items-center justify-center gap-1"
      }, "⚡ Peak Norm (0dB)"), /*#__PURE__*/React.createElement("button", {
        onClick: () => runPythonTool('invert_phase'),
        className: "py-1 bg-zinc-800 hover:bg-zinc-700 text-zinc-200 border border-zinc-700 rounded font-bold flex items-center justify-center gap-1"
      }, "🔄 Phase Invert"), /*#__PURE__*/React.createElement("button", {
        onClick: () => runPythonTool('swap_channels'),
        className: "py-1 bg-zinc-800 hover:bg-zinc-700 text-zinc-200 border border-zinc-700 rounded font-bold flex items-center justify-center gap-1"
      }, "🔀 Swap L/R"), /*#__PURE__*/React.createElement("button", {
        onClick: () => runPythonTool('synth_wave'),
        className: "py-1 bg-teal-950/80 hover:bg-teal-900 text-teal-300 border border-teal-800/80 rounded font-bold flex items-center justify-center gap-1"
      }, "🎹 Gen Synth Tone")));
      if (panelId === 'selection') return /*#__PURE__*/React.createElement("div", {
        className: "flex flex-col h-full gap-1.5"
      }, /*#__PURE__*/React.createElement("div", {
        className: "flex items-center justify-between shrink-0 cursor-grab active:cursor-grabbing select-none",
        onMouseDown: e => startPanelDrag('selection', e)
      }, /*#__PURE__*/React.createElement("span", {
        className: "text-xs text-zinc-500 font-bold uppercase flex items-center gap-1"
      }, /*#__PURE__*/React.createElement("span", {
        className: "inline-flex items-center shrink-0"
      }, /*#__PURE__*/React.createElement("i", {
        "data-lucide": "grip-vertical",
        className: "w-3 h-3 text-zinc-500"
      })), " Selection"), /*#__PURE__*/React.createElement("button", {
        onClick: () => closePanel('selection'),
        className: "text-zinc-600 hover:text-zinc-300"
      }, /*#__PURE__*/React.createElement("span", {
        className: "inline-flex items-center shrink-0"
      }, /*#__PURE__*/React.createElement("i", {
        "data-lucide": "x",
        className: "w-3 h-3"
      })))), /*#__PURE__*/React.createElement("div", {
        className: "grid grid-cols-3 gap-1 bg-[#141414] p-1.5 rounded border border-zinc-800"
      }, /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("span", {
        className: "block text-[7px] text-zinc-500 font-bold uppercase mb-0.5"
      }, "Start"), /*#__PURE__*/React.createElement("input", {
        type: "number",
        step: "0.01",
        value: selectionStats.start,
        onChange: e => handleSelectionInputChange('start', e.target.value),
        className: "w-full bg-[#242424] text-amber-400 text-center font-mono text-xs rounded py-0.5 border border-zinc-700 focus:outline-none"
      })), /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("span", {
        className: "block text-[7px] text-zinc-500 font-bold uppercase mb-0.5"
      }, "End"), /*#__PURE__*/React.createElement("input", {
        type: "number",
        step: "0.01",
        value: selectionStats.end,
        onChange: e => handleSelectionInputChange('end', e.target.value),
        className: "w-full bg-[#242424] text-amber-400 text-center font-mono text-xs rounded py-0.5 border border-zinc-700 focus:outline-none"
      })), /*#__PURE__*/React.createElement("div", {
        className: "flex flex-col justify-center"
      }, /*#__PURE__*/React.createElement("span", {
        className: "text-[7px] text-zinc-500 font-bold uppercase"
      }, "Len"), /*#__PURE__*/React.createElement("span", {
        className: "text-zinc-200 font-mono text-xs font-semibold mt-0.5"
      }, selectionStats.length, "s"))), /*#__PURE__*/React.createElement("div", {
        className: "grid grid-cols-3 gap-1 bg-[#141414] p-1.5 rounded border border-zinc-800 mt-1"
      }, /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("span", {
        className: "block text-[7px] text-zinc-500 font-bold uppercase mb-0.5"
      }, "Begin Bar"), /*#__PURE__*/React.createElement("input", {
        type: "number",
        min: "0",
        value: beginBar,
        onChange: e => {
          const b = parseInt(e.target.value) || 0;
          setBeginBar(b);
          const beatDuration = 60 / parseInt(bpm || 120);
          const t = b * beatDuration * 4;
          clearLocalSelection();
          setSelectionMode('global');
          setSelectionStart(t);
          setSelectionEnd(t + beatDuration * 4);
        },
        className: "w-full bg-[#242424] text-white text-center font-mono text-xs rounded py-0.5 border border-zinc-700 focus:outline-none"
      })), /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("span", {
        className: "block text-[7px] text-zinc-500 font-bold uppercase mb-0.5"
      }, "End Bar"), /*#__PURE__*/React.createElement("input", {
        type: "number",
        min: "0",
        value: endBar,
        onChange: e => {
          const b = parseInt(e.target.value) || 0;
          setEndBar(b);
          const beatDuration = 60 / parseInt(bpm || 120);
          const t = b * beatDuration * 4;
          // selectionEnd = vạch bar b (không +1 bar) — khớp hiển thị endBar
          // (quét 8 bars hiển thị "0 đến 8"): nhập 8 → selection tới vạch 8.
          setSelectionEnd(t);
          setNumberBar(Math.max(0, b - beginBar));
        },
        className: "w-full bg-[#242424] text-white text-center font-mono text-xs rounded py-0.5 border border-zinc-700 focus:outline-none"
      })), /*#__PURE__*/React.createElement("div", {
        className: "flex flex-col justify-center"
      }, /*#__PURE__*/React.createElement("span", {
        className: "text-[7px] text-zinc-500 font-bold uppercase"
      }, "# Bars"), /*#__PURE__*/React.createElement("span", {
        className: "text-zinc-200 font-mono text-xs font-semibold mt-0.5"
      }, numberBar))));
      if (panelId === 'fx_rack') return /*#__PURE__*/React.createElement("div", {
        className: "flex flex-col h-full gap-1.5"
      }, /*#__PURE__*/React.createElement("div", {
        className: "flex items-center justify-between shrink-0 cursor-grab active:cursor-grabbing select-none",
        onMouseDown: e => startPanelDrag('fx_rack', e)
      }, /*#__PURE__*/React.createElement("h3", {
        className: "font-bold text-xs text-rose-300 flex items-center gap-1"
      }, /*#__PURE__*/React.createElement("span", {
        className: "inline-flex items-center shrink-0"
      }, /*#__PURE__*/React.createElement("i", {
        "data-lucide": "grip-vertical",
        className: "w-3 h-3 text-zinc-500"
      })), /*#__PURE__*/React.createElement("span", {
        className: "inline-flex items-center shrink-0"
      }, /*#__PURE__*/React.createElement("i", {
        "data-lucide": "sliders",
        className: "w-3.5 h-3.5 text-rose-400"
      })), " Plugin FX Rack"), /*#__PURE__*/React.createElement("button", {
        onClick: () => closePanel('fx_rack'),
        className: "text-zinc-600 hover:text-zinc-300"
      }, /*#__PURE__*/React.createElement("span", {
        className: "inline-flex items-center shrink-0"
      }, /*#__PURE__*/React.createElement("i", {
        "data-lucide": "x",
        className: "w-3 h-3"
      })))), /*#__PURE__*/React.createElement("div", {
        className: "flex-1 text-xs text-zinc-500 italic flex items-center justify-center"
      }, "No FX plugins loaded"));
      if (panelId === 'midi_events') return /*#__PURE__*/React.createElement("div", {
        className: "flex flex-col h-full gap-1.5"
      }, /*#__PURE__*/React.createElement("div", {
        className: "flex items-center justify-between shrink-0 cursor-grab active:cursor-grabbing select-none",
        onMouseDown: e => startPanelDrag('midi_events', e)
      }, /*#__PURE__*/React.createElement("h3", {
        className: "font-bold text-xs text-sky-300 flex items-center gap-1"
      }, /*#__PURE__*/React.createElement("span", {
        className: "inline-flex items-center shrink-0"
      }, /*#__PURE__*/React.createElement("i", {
        "data-lucide": "grip-vertical",
        className: "w-3 h-3 text-zinc-500"
      })), /*#__PURE__*/React.createElement("span", {
        className: "inline-flex items-center shrink-0"
      }, /*#__PURE__*/React.createElement("i", {
        "data-lucide": "music",
        className: "w-3.5 h-3.5 text-sky-400"
      })), " MIDI Event List"), /*#__PURE__*/React.createElement("button", {
        onClick: () => closePanel('midi_events'),
        className: "text-zinc-600 hover:text-zinc-300"
      }, /*#__PURE__*/React.createElement("span", {
        className: "inline-flex items-center shrink-0"
      }, /*#__PURE__*/React.createElement("i", {
        "data-lucide": "x",
        className: "w-3 h-3"
      })))), /*#__PURE__*/React.createElement("div", {
        className: "flex-1 text-xs text-zinc-500 italic flex items-center justify-center"
      }, "No MIDI events selected"));
      return null;
    };
    const renderDock = (pos, title) => {
      const panels = dockPanels[pos];
      if (panels.length === 0) return null;
      const isSide = pos === 'left' || pos === 'right';
      const borderClass = pos === 'left' ? 'border-r' : pos === 'right' ? 'border-l' : pos === 'top' ? 'border-b' : 'border-t';
      const bgClass = 'bg-[#1e1e1e]';
      const highlight = panelDragRef.current && panelDropZone === pos;
      if (pos === 'right') return /*#__PURE__*/React.createElement("div", {
        id: "right-sidebar",
        className: `${borderClass} ${bgClass} flex flex-col overflow-hidden select-none h-full`,
        style: {
          width: `${rightSidebarWidth}px`,
          minWidth: '200px',
          maxWidth: '600px',
          flexShrink: 0
        }
      }, /*#__PURE__*/React.createElement("div", {
        className: "flex-1 flex flex-col gap-2 p-2 overflow-hidden h-full"
      }, panels.map((p, idx) => /*#__PURE__*/React.createElement(React.Fragment, {
        key: p
      }, /*#__PURE__*/React.createElement("div", {
        className: 'flex flex-col flex-1 min-h-0 border border-zinc-700 rounded-lg bg-[#262626] shadow-sm p-3'
      }, renderPanelContent(p)), idx < panels.length - 1 && /*#__PURE__*/React.createElement("div", {
        className: "h-1.5 cursor-ns-resize hover:bg-cyan-500/50 transition-colors rounded shrink-0",
        onMouseDown: startRowResize
      })))));
      const sideClass = isSide ? 'shrink-0 overflow-y-auto' : 'shrink-0';
      return /*#__PURE__*/React.createElement("div", {
        id: "daw-bottom",
        className: `${sideClass} ${borderClass} ${bgClass} flex ${isSide ? 'flex-col p-2 gap-2' : 'flex-row p-1.5 gap-3'} select-none ${highlight ? 'ring-2 ring-cyan-500 ring-inset' : ''}`
      }, panels.map(p => /*#__PURE__*/React.createElement("div", {
        key: p,
        className: `${isSide ? 'w-full' : 'flex-none w-[320px]'} border border-zinc-700 rounded-lg bg-[#262626] shadow-sm p-2`
      }, renderPanelContent(p))));
    };
    return /*#__PURE__*/React.createElement("div", {
      ref: workspaceRef,
      className: "flex-1 flex flex-col overflow-hidden select-none daw-bg relative"
    }, panelDragRef.current && panelDropZone && /*#__PURE__*/React.createElement("div", {
      className: "absolute inset-0 z-50 pointer-events-none"
    }, panelDropZone === 'top' && /*#__PURE__*/React.createElement("div", {
      className: "absolute top-0 left-0 right-0 h-1 bg-cyan-400 shadow-[0_0_8px_#06b6d4]"
    }), panelDropZone === 'bottom' && /*#__PURE__*/React.createElement("div", {
      className: "absolute bottom-0 left-0 right-0 h-1 bg-cyan-400 shadow-[0_0_8px_#06b6d4]"
    }), panelDropZone === 'left' && /*#__PURE__*/React.createElement("div", {
      className: "absolute top-0 bottom-0 left-0 w-1 bg-cyan-400 shadow-[0_0_8px_#06b6d4]"
    }), panelDropZone === 'right' && /*#__PURE__*/React.createElement("div", {
      className: "absolute top-0 bottom-0 right-0 w-1 bg-cyan-400 shadow-[0_0_8px_#06b6d4]"
    })), dragGhostPanel && dragGhostPos && /*#__PURE__*/React.createElement("div", {
      className: "fixed z-[100] pointer-events-none opacity-80 border border-cyan-500 rounded-lg bg-[#262626] shadow-2xl p-3 w-56",
      style: {
        left: dragGhostPos.x,
        top: dragGhostPos.y
      }
    }, /*#__PURE__*/React.createElement("div", {
      className: "flex items-center gap-2 text-xs text-zinc-200 font-bold"
    }, /*#__PURE__*/React.createElement("span", {
      className: "inline-flex items-center shrink-0"
    }, /*#__PURE__*/React.createElement("i", {
      "data-lucide": "move",
      className: "w-3.5 h-3.5 text-cyan-400"
    })), dragGhostPanel === 'export' ? 'Export Panel' : dragGhostPanel === 'ai' ? 'AI Panel' : dragGhostPanel === 'python_tools' ? 'Audio Processing Panel' : 'Selection Panel'), /*#__PURE__*/React.createElement("div", {
      className: "text-xs text-zinc-500 mt-1"
    }, "Drop at edge to dock")), renderDock('top', 'Top'), /*#__PURE__*/React.createElement("div", {
      className: "flex-1 flex overflow-hidden"
    }, renderDock('left', 'Left'), /*#__PURE__*/React.createElement("div", {
      className: "flex-1 flex flex-col overflow-hidden min-w-0"
    }, /*#__PURE__*/React.createElement("div", {
      className: "flex-1 flex overflow-hidden"
    }, activeTab === 'main' || sessionTabs.some(s => s.id === activeTab) ? /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
      ref: tcpContainerRef,
      onScroll: handleTCPScroll,
      className: "shrink-0 z-20 bg-[#262626] overflow-y-auto flex flex-col border-r border-zinc-900 no-scrollbar",
      style: {
        width: tcpWidth + 'px',
        scrollbarWidth: 'none',
        msOverflowStyle: 'none'
      }
    }, /*#__PURE__*/React.createElement("div", {
      className: "sticky top-0 z-50 flex items-center justify-between h-10 border-b border-zinc-900 bg-[#242424] px-2 shrink-0"
    }, /*#__PURE__*/React.createElement("span", {
      className: "text-xs font-bold text-zinc-300 flex items-center gap-1.5"
    }, /*#__PURE__*/React.createElement("span", {
      className: "inline-flex items-center shrink-0"
    }, /*#__PURE__*/React.createElement("i", {
      "data-lucide": "sliders",
      className: "w-3.5 h-3.5 text-cyan-400"
    })), "TRACKS (", activeTracks.length, ")"), /*#__PURE__*/React.createElement("button", {
      onClick: addNewTrack,
      className: "px-2.5 py-1 bg-cyan-600 hover:bg-cyan-500 text-white rounded text-xs font-semibold flex items-center gap-1 shadow transition"
    }, /*#__PURE__*/React.createElement("span", {
      className: "inline-flex items-center shrink-0"
    }, /*#__PURE__*/React.createElement("i", {
      "data-lucide": "plus",
      className: "w-3 h-3"
    })), " Add Track")), /*#__PURE__*/React.createElement("div", {
      className: "sticky top-10 z-40 flex h-[40px] border-b border-purple-500/30 bg-[#1a1a2e] p-2 border-l-4 border-purple-500 shrink-0"
    }, /*#__PURE__*/React.createElement("div", {
      className: "flex items-center justify-between w-full"
    }, /*#__PURE__*/React.createElement("div", {
      className: "flex items-center gap-2"
    }, /*#__PURE__*/React.createElement("span", {
      className: "text-xs font-bold text-purple-400 font-mono"
    }, "TM"), /*#__PURE__*/React.createElement("span", {
      className: "text-xs font-semibold text-zinc-300"
    }, "Tempo")), /*#__PURE__*/React.createElement("div", {
      className: "flex items-center gap-1"
      }, /*#__PURE__*/React.createElement("input", {
        type: "number",
        value: bpm,
        onChange: e => { setBpm(e.target.value); },
        onBlur: e => { const v = e.target.value; if (v && String(bpm) !== v) setBpmWithUndo(v); localStorage.setItem('studio_bpm', bpm); },
        onKeyDown: e => { if (e.key === 'Enter') { e.target.blur(); } },
        className: "w-12 bg-zinc-800 border border-zinc-700 rounded text-xs text-zinc-300 text-center font-mono focus:outline-none focus:border-purple-500",
        min: "40",
        max: "300"
      }), /*#__PURE__*/React.createElement("span", {
        className: "text-xs text-zinc-500"
      }, "BPM")))), /*#__PURE__*/React.createElement("div", {
      className: "flex-1 flex flex-col divide-y divide-[#141414] bg-[#262626]"
    }, activeTracks.length === 0 ? /*#__PURE__*/React.createElement("div", {
      className: "p-6 text-center text-zinc-400 flex flex-col items-center justify-center space-y-3"
    }, /*#__PURE__*/React.createElement("span", {
      className: "inline-flex items-center shrink-0"
    }, /*#__PURE__*/React.createElement("i", {
      "data-lucide": "plus-circle",
      className: "w-8 h-8 text-cyan-400 opacity-80"
    })), /*#__PURE__*/React.createElement("p", {
      className: "text-xs font-medium"
    }, "Chưa có Track nào trong dự án."), /*#__PURE__*/React.createElement("button", {
      onClick: addNewTrack,
      className: "px-3 py-1.5 bg-cyan-600 hover:bg-cyan-500 text-white rounded text-xs font-semibold flex items-center gap-1.5 shadow"
    }, /*#__PURE__*/React.createElement("span", {
      className: "inline-flex items-center shrink-0"
    }, /*#__PURE__*/React.createElement("i", {
      "data-lucide": "plus-circle",
      className: "w-3.5 h-3.5"
    })), " Thêm Track Mới")) : activeTracks.map((track, idx) => {
      const isSelected = selectedTrackId === track.id;
      const autoHeight = track.height || (track.isArmed ? 164 : 140);
      return /*#__PURE__*/React.createElement("div", {
        key: track.id,
        style: {
          height: `${autoHeight}px`
        },
        className: `shrink-0 relative flex flex-col justify-between p-2.5 bg-[#1e1e1e] border-r border-zinc-900 cursor-pointer border-l-4 border-b border-[#141414] overflow-hidden ${isSelected ? 'border-cyan-500 bg-[#252525]' : 'border-transparent hover:bg-zinc-800/20'}`,
        onClick: () => {
          // Chỉ CHỌN track — KHÔNG tự dừng main play + chuyển sang piano roll
          // tab (trước đây gây "mất âm thanh khi click track number").
          setSelectedTrackId(track.id);
        }
      }, /*#__PURE__*/React.createElement("div", {
        className: "flex items-start justify-between"
      }, /*#__PURE__*/React.createElement("div", {
        className: "flex items-center gap-2"
      }, /*#__PURE__*/React.createElement("span", {
        className: "text-xs font-bold text-zinc-500 font-mono"
      }, (idx + 1).toString().padStart(2, '0')), /*#__PURE__*/React.createElement("label", {
        className: "cursor-pointer relative block"
      }, /*#__PURE__*/React.createElement("input", {
        type: "color",
        value: track.color || '#0f766e',
        onClick: e => e.stopPropagation(),
        onMouseDown: e => e.stopPropagation(),
        onChange: e => {
          e.stopPropagation();
          updateTrackColor(track.id, e.target.value);
        },
        className: "absolute inset-0 w-full h-full opacity-0 cursor-pointer"
      }), /*#__PURE__*/React.createElement("div", {
        className: "w-3 h-3 rounded-full border border-zinc-600 hover:scale-110 transition-transform",
        style: {
          backgroundColor: track.color
        }
      })), editingTrackName === track.id ? /*#__PURE__*/React.createElement("input", {
        type: "text",
        value: editNameInput,
        autoFocus: true,
        onChange: e => setEditNameInput(e.target.value),
        onBlur: () => {
          updateTrackName(track.id, editNameInput || track.name);
          setEditingTrackName(null);
        },
        onKeyDown: e => {
          if (e.key === 'Enter') {
            updateTrackName(track.id, editNameInput || track.name);
            setEditingTrackName(null);
          }
          if (e.key === 'Escape') setEditingTrackName(null);
        },
        onClick: e => e.stopPropagation(),
        className: "text-xs font-semibold bg-black text-zinc-200 border border-cyan-500 rounded px-1 py-0 w-24 outline-none"
      }) : /*#__PURE__*/React.createElement("span", {
        className: "text-xs font-semibold text-zinc-300 truncate max-w-[80px] cursor-text hover:text-cyan-300 transition-colors",
        title: "Click to rename",
        onClick: e => {
          e.stopPropagation();
          setEditingTrackName(track.id);
          setEditNameInput(track.name);
        }
      }, track.name)), /*#__PURE__*/React.createElement("div", {
        className: "flex flex-wrap gap-0.5 max-w-[100px] mb-0.5"
      }, (track.clips && track.clips.length > 0 ? track.clips : track.buffer ? [{ id: 'default', name: track.name, startTime: track.startTime }] : []).slice(0, 3).map(c => /*#__PURE__*/React.createElement("span", {
        key: c.id,
        className: "text-[9px] font-mono text-zinc-500 bg-zinc-800/60 rounded px-0.5 truncate max-w-[90px] cursor-pointer hover:text-cyan-400 hover:bg-zinc-700",
        title: c.name || track.name,
        onClick: e => {
          e.stopPropagation();
          setSelectedTrackId(track.id);
          clearLocalSelection();
          setSelectionMode('global');
          const start = c.startTime || 0;
          const end = start + (c.buffer ? c.buffer.duration : 2);
          setSelectionStart(start);
          setSelectionEnd(end);
          showToast(`Selected: ${c.name || track.name}`, 'info');
        }
      }, c.name || track.name), editingClipName && editingClipName.trackId === track.id && editingClipName.clipId === c.id ? /*#__PURE__*/React.createElement("input", {
        type: "text",
        value: editNameInput,
        autoFocus: true,
        onChange: e => setEditNameInput(e.target.value),
        onBlur: () => {
          if (editNameInput.trim()) updateClipName(track.id, c.id, editNameInput.trim());
          setEditingClipName(null);
        },
        onKeyDown: e => {
          if (e.key === 'Enter') { if (editNameInput.trim()) updateClipName(track.id, c.id, editNameInput.trim()); setEditingClipName(null); }
          if (e.key === 'Escape') setEditingClipName(null);
        },
        onClick: e => e.stopPropagation(),
        className: "w-20 text-[9px] font-mono bg-black text-cyan-300 border border-cyan-500 rounded px-0.5 py-0 outline-none"
      }) : /*#__PURE__*/React.createElement("button", {
        className: "text-[9px] text-zinc-600 hover:text-cyan-400 ml-0.5 shrink-0",
        title: "Sửa tên clip",
        onClick: e => {
          e.stopPropagation();
          setEditingClipName({ trackId: track.id, clipId: c.id });
          setEditNameInput(c.name || track.name);
        }
      }, /*#__PURE__*/React.createElement("i", {
        "data-lucide": "pencil",
        className: "w-2.5 h-2.5"
      })))), /*#__PURE__*/React.createElement("div", {
        className: "flex items-center gap-1"
      }, /*#__PURE__*/React.createElement("button", {
        onClick: e => {
          e.stopPropagation();
          toggleTrackMute(track.id);
        },
        title: "Mute",
        className: `px-1.5 py-0.5 text-[10px] rounded font-mono font-bold border transition flex items-center gap-0.5 ${track.muted ? 'bg-red-950 text-red-400 border-red-700' : 'bg-zinc-800 text-zinc-400 border-transparent hover:text-zinc-200'}`
      }, /*#__PURE__*/React.createElement("i", { "data-lucide": track.muted ? "volume-x" : "volume-2", className: "w-3 h-3" })), /*#__PURE__*/React.createElement("button", {
        onClick: e => {
          e.stopPropagation();
          toggleTrackSoloEvaluate(track.id);
        },
        title: "Solo",
        className: `px-1.5 py-0.5 text-[10px] rounded font-mono font-bold border transition flex items-center gap-0.5 ${track.solo ? 'bg-amber-950 text-amber-400 border-amber-600' : 'bg-zinc-800 text-zinc-400 border-transparent hover:text-zinc-200'}`
      }, /*#__PURE__*/React.createElement("i", { "data-lucide": track.solo ? "headphones" : "headphone-off", className: "w-3 h-3" })), /*#__PURE__*/React.createElement("button", {
        onClick: e => {
          e.stopPropagation();
          toggleTrackDrum(track.id);
        },
        title: track.is_percussion ? "Drum Channel (CH 10) - Click to disable" : "Toggle Drum Channel (CH 10)",
        className: `px-1.5 py-0.5 text-[9px] rounded font-mono font-bold border transition flex items-center gap-0.5 ${track.is_percussion ? 'bg-rose-900 text-rose-300 border-rose-700' : 'bg-zinc-800 text-zinc-500 border-transparent hover:text-zinc-300'}`
      }, /*#__PURE__*/React.createElement("span", { className: "text-[11px]" }, "🥁"), track.is_percussion ? /*#__PURE__*/React.createElement("span", { className: "text-[9px]" }, "D") : null), /*#__PURE__*/React.createElement("button", {
        onClick: e => {
          e.stopPropagation();
          toggleTrackArm(track.id);
        },
        title: "ARM (Record)",
        className: `px-1.5 py-0.5 text-[10px] rounded font-mono font-bold border transition flex items-center gap-0.5 ${track.isArmed ? 'bg-red-600 text-white border-red-500 hover:bg-red-500' : 'bg-zinc-800 text-zinc-400 border-transparent hover:text-zinc-200'}`
      }, /*#__PURE__*/React.createElement("i", { "data-lucide": "circle", className: `w-2.5 h-2.5 ${track.isArmed ? 'fill-white' : ''}` })), /*#__PURE__*/React.createElement("button", {
        onClick: e => {
          e.stopPropagation();
          toggleTrackMonitor(track.id);
        },
        title: "Input Monitor",
        className: `px-1.5 py-0.5 text-[10px] rounded font-mono font-bold border transition flex items-center gap-0.5 ${track.monitoringEnabled ? 'bg-amber-600 text-white border-amber-500 hover:bg-amber-500' : 'bg-zinc-800 text-zinc-500 border-transparent hover:text-zinc-305'}`
      }, /*#__PURE__*/React.createElement("i", { "data-lucide": track.monitoringEnabled ? "mic" : "mic-off", className: "w-3 h-3" })), /*#__PURE__*/React.createElement("button", {
        onClick: e => {
          e.stopPropagation();
          deleteTrack(track.id);
        },
        className: "p-0.5 rounded text-red-500 hover:bg-red-950/40 hover:text-red-400 transition"
      }, /*#__PURE__*/React.createElement("span", {
        className: "inline-flex items-center shrink-0"
      }, /*#__PURE__*/React.createElement("i", {
        "data-lucide": "trash-2",
        className: "w-3 h-3"
      }))))), /*#__PURE__*/React.createElement("div", {
        className: "flex flex-col gap-0.5 text-xs",
        onClick: e => e.stopPropagation()
      }, /*#__PURE__*/React.createElement("div", {
        className: "flex items-center gap-1"
      }, /*#__PURE__*/React.createElement("span", {
        className: "w-8 text-right text-zinc-500 text-xs"
      }, "Vol:"), /*#__PURE__*/React.createElement("input", {
        type: "range",
        min: "-50",
        max: "7",
        step: "0.5",
        value: track.volumeDb ?? 0,
        onChange: e => updateTrackVolumeDb(track.id, parseFloat(e.target.value)),
        className: "flex-1 h-1 bg-zinc-700 rounded-lg appearance-none cursor-pointer accent-cyan-500",
        style: {
          height: '4px'
        }
      }), /*#__PURE__*/React.createElement("span", {
        className: "w-12 text-right font-mono text-zinc-300 text-xs"
      }, track.volumeDb ?? 0, "dB")), /*#__PURE__*/React.createElement("div", {
        className: "flex items-center gap-1"
      }, /*#__PURE__*/React.createElement("span", {
        className: "w-8 text-right text-zinc-500 text-xs"
      }, "Pan:"), /*#__PURE__*/React.createElement("input", {
        type: "range",
        min: "-100",
        max: "100",
        step: "1",
        value: track.pan ?? 0,
        onChange: e => updateTrackPan(track.id, parseInt(e.target.value)),
        className: "flex-1 h-1 bg-zinc-700 rounded-lg appearance-none cursor-pointer accent-purple-500",
        style: {
          height: '4px'
        }
      }), /*#__PURE__*/React.createElement("span", {
        className: "w-12 text-right font-mono text-zinc-300 text-xs"
      }, track.pan > 0 ? 'R' + track.pan : track.pan < 0 ? 'L' + Math.abs(track.pan) : 'C')), /*#__PURE__*/React.createElement("div", {
        className: "flex items-center gap-1 mt-0.5"
      }, /*#__PURE__*/React.createElement("span", {
        className: "w-8 text-right text-zinc-500 text-[10px]"
      }, "In:"), /*#__PURE__*/React.createElement("select", {
        value: `${track.inputSource?.deviceType || 'NONE'}:${track.inputSource?.deviceId || ''}`,
        onChange: e => {
          const val = e.target.value;
          const parts = val.split(':');
          const type = parts[0];
          const id = parts.slice(1).join(':');
          updateTrackInputSource(track.id, type, id);
        },
        className: "flex-1 bg-[#18181b] text-zinc-300 text-[10px] rounded border border-zinc-700 focus:outline-none py-0.5 px-1 truncate max-w-[120px]"
      }, /*#__PURE__*/React.createElement("option", {
        value: "NONE:"
      }, "No Input"), /*#__PURE__*/React.createElement("optgroup", {
        label: "Microphones"
      }, audioDevices.map(d => /*#__PURE__*/React.createElement("option", {
        key: d.deviceId,
        value: `MICROPHONE:${d.deviceId}`
      }, d.label || `Microphone ${d.deviceId.slice(0, 5)}`))), /*#__PURE__*/React.createElement("optgroup", {
        label: "MIDI Keyboards"
      }, /*#__PURE__*/React.createElement("option", {
        value: "MIDI_KEYBOARD:ALL"
      }, "Any MIDI Keyboard"), midiDevices.map(d => /*#__PURE__*/React.createElement("option", {
        key: d.id,
        value: `MIDI_KEYBOARD:${d.id}`
      }, d.name || `MIDI Input ${d.id.slice(0, 5)}`)))), /*#__PURE__*/React.createElement("div", {
        className: "flex items-center gap-0.5 w-16 shrink-0 ml-1"
      }, /*#__PURE__*/React.createElement("span", {
        className: "text-zinc-500 text-[9px]"
      }, "VU"), /*#__PURE__*/React.createElement("canvas", {
        ref: el => {
          // TÁCH VU MAIN vs SECTION-TAB (user 07:20): section clone id TRÙNG
          // main id → key canvas trùng → VU main/section lẫn nhau. Section
          // strip dùng key '_sess_<id>' — VU tick parse theo key.
          const _vuK = track._isSectionClone ? '_sess_' + track.id : track.id;
          if (el) trackVuRefs.current[_vuK] = el; else delete trackVuRefs.current[_vuK];
        },
        width: 100,
        height: 4,
        className: "flex-1 bg-[#18181b] rounded h-1"
      })))), /*#__PURE__*/React.createElement("div", {
        className: "flex items-center gap-1.5 mt-1",
        onClick: e => e.stopPropagation()
      }, /*#__PURE__*/React.createElement("input", {
        type: "file",
        id: `upload-${track.id}`,
        accept: "audio/*",
        className: "hidden",
        onChange: e => loadFileOnTrack(track.id, e.target.files[0])
      }), /*#__PURE__*/React.createElement("label", {
        htmlFor: `upload-${track.id}`,
        className: "px-2.5 py-1 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 rounded text-xs flex items-center gap-1 cursor-pointer border border-zinc-700 font-medium"
      }, /*#__PURE__*/React.createElement("span", {
        className: "inline-flex items-center shrink-0"
      }, /*#__PURE__*/React.createElement("i", {
        "data-lucide": "upload",
        className: "w-3 h-3"
      })), " File"), /*#__PURE__*/React.createElement("button", {
        onClick: e => { e.stopPropagation(); setFxSelectorTrackId(track.id); },
        className: "px-2.5 py-1 bg-zinc-800 hover:bg-zinc-700 text-purple-400 border border-purple-900 rounded text-xs font-bold flex items-center gap-1"
      }, /*#__PURE__*/React.createElement("span", {
        className: "inline-flex items-center shrink-0"
      }, /*#__PURE__*/React.createElement("i", {
        "data-lucide": "wand-2",
        className: "w-3 h-3"
      })), " FX: ", /*#__PURE__*/React.createElement("span", {
        className: "text-zinc-500 font-normal"
      }, track.fxType || "None")), /*#__PURE__*/React.createElement("button", {
        onClick: (e) => { e.stopPropagation(); openInstrumentSelector(track.id); },
        className: "px-1.5 py-1 bg-zinc-800 hover:bg-zinc-700 text-amber-400 border border-amber-800 rounded text-xs font-bold flex items-center gap-1 max-w-[90px] shrink-0"
      }, /*#__PURE__*/React.createElement("span", {
        className: "inline-flex items-center shrink-0"
      }, /*#__PURE__*/React.createElement("i", {
        "data-lucide": "music",
        className: "w-2.5 h-2.5 shrink-0"
      })), /*#__PURE__*/React.createElement("span", { className: "truncate text-xs min-w-0" }, track.instrumentName || track.instrumentId || "Synth"), /*#__PURE__*/React.createElement("i", { "data-lucide": "chevron-down", className: "w-3 h-3 shrink-0" })), (track.instrumentId && typeof track.instrumentId === 'string' && !track.instrumentId.startsWith('sf_') ? /*#__PURE__*/React.createElement("button", {
        onClick: (e) => {
          e.stopPropagation();
          ensureAndOpenVstGui(track.id, track.instrumentId);
        },
        title: "Mở lại GUI VSTi",
        className: "px-1.5 py-1 bg-zinc-800 hover:bg-zinc-700 text-violet-300 border border-violet-800 rounded text-xs font-bold flex items-center gap-1 shrink-0"
      }, /*#__PURE__*/React.createElement("i", { "data-lucide": "sliders-horizontal", className: "w-3 h-3 shrink-0" }), /*#__PURE__*/React.createElement("span", { className: "truncate text-xs min-w-0" }, "GUI")) : null)), /*#__PURE__*/React.createElement("div", {
        onMouseDown: e => handleTrackResizeMouseDown(e, track.id),
        className: "absolute bottom-0 left-0 right-0 h-1 cursor-ns-resize z-30 hover:bg-cyan-500/50 transition-colors",
        onClick: e => e.stopPropagation()
      }));
    })), /*#__PURE__*/React.createElement("div", {
      className: "h-[48px] border-r border-zinc-900 bg-[#1e1e1e]/50 shrink-0"
    })), /*#__PURE__*/React.createElement("div", {
      className: "w-1 cursor-ew-resize hover:bg-cyan-500/50 active:bg-cyan-400 transition-colors shrink-0 z-10",
      onMouseDown: startTcpResize
    }), /*#__PURE__*/React.createElement("div", {
      ref: handleTimelineWrapperRef,
      onScroll: handleTimelineScroll,
      className: "flex-1 relative z-10 overflow-x-auto overflow-y-auto min-w-0"
    }, /*#__PURE__*/React.createElement("div", {
      style: {
        width: `${timelineWidth}px`
      },
      className: "relative flex flex-col min-h-full"
    }, /*#__PURE__*/React.createElement("div", {
      className: "sticky top-0 z-30 bg-[#1a1a1a]"
    }, /*#__PURE__*/React.createElement(TimelineRuler, {
      bpm: parseInt(bpm) || 120,
      zoom: zoom,
      timelineWidth: timelineWidth,
      viewportWidth: viewportWidth,
      onPlayheadSet: handlePlayheadSet,
      snapValue: snapValue,
      onRulerMouseDown: e => { setSelectionFollowsTempo(false); handleRulerMouseDown(e); },
      scrollLeft: scrollLeft,
      canvasRedrawCount: canvasRedrawCount
    }), /*#__PURE__*/React.createElement(TempoTrackLane, {
      bpm: parseInt(bpm) || 120,
      zoom: zoom,
      timelineWidth: timelineWidth,
      viewportWidth: viewportWidth,
      onPlayheadSet: handlePlayheadSet,
      snapValue: snapValue,
      onRulerMouseDown: e => { setSelectionFollowsTempo(true); handleRulerMouseDown(e); },
      scrollLeft: scrollLeft,
      canvasRedrawCount: canvasRedrawCount
    })), selectionMode === 'global' && selLeft !== null && selRight !== null && selRight > selLeft && /*#__PURE__*/React.createElement("div", {
      className: "absolute inset-0 pointer-events-none z-20",
      style: {
        left: `${selLeft * zoom}px`,
        width: `${(selRight - selLeft) * zoom}px`,
        top: '80px'
      }
    }, /*#__PURE__*/React.createElement("div", {
      className: "w-full h-full bg-amber-500/10",
      style: {
        borderLeft: '1px solid #f59e0b',
        borderRight: '1px solid #f59e0b'
      }
    })), /*#__PURE__*/React.createElement("div", {
      className: "flex-1 flex flex-col divide-y divide-[#141414] relative bg-[#111111] min-h-full",
      onDragOver: e => e.preventDefault(),
      onDrop: async e => {
        e.preventDefault();
        // Media Explorer drag (client FS handle / server file_id / local path)
        if (window.__mediaExplorerDragFile) {
          const f = await resolveMediaExplorerDropFile();
          window.__mediaExplorerDragFile = null;
          if (!f) return;
          if (/\.mid$|\.midi$/i.test(f.name || '')) {
            handleDropMidiToNewTracks(f);
          } else if (selectedTrackId) {
            loadFileOnTrack(selectedTrackId, f);
          }
          return;
        }
        const f = e.dataTransfer.files && e.dataTransfer.files[0];
        if (!f) return;
        if (/\.mid$|\.midi$/i.test(f.name || '')) {
          handleDropMidiToNewTracks(f);
        } else if (selectedTrackId) {
          loadFileOnTrack(selectedTrackId, f);
        }
      },
      onMouseDown: e => {
        if (e.ctrlKey && !e.shiftKey && !e.altKey && e.button === 0) {
          const wrapper = timelineWrapperRef.current;
          if (wrapper) {
            const rect = wrapper.getBoundingClientRect();
            const sl = wrapper.scrollLeft;
            const raw = Math.max(0, (e.clientX - rect.left + sl) / zoom - leadInMargin);
            const time = snapValue !== 'free' ? snapTime(raw, snapValue, bpm) : raw;
            handleSweepSelectStart(null, time);
          }
        }
      }
    }, activeTracks.map((track, idx) => {
      const isSelected = selectedTrackId === track.id;
      const autoHeight = track.height || (track.isArmed ? 164 : 140);
      return /*#__PURE__*/React.createElement("div", {
        key: track.id,
        style: {
          height: `${autoHeight}px`
        },
        className: `shrink-0 relative border-b border-[#141414] hover:bg-zinc-850/5 transition-colors ${isSelected ? 'bg-zinc-800/10' : ''}`,
        onDragOver: e => e.preventDefault(),
        onDrop: async e => {
          e.preventDefault();
          e.stopPropagation();
          // Media Explorer drag (client FS handle / server file_id / local path)
          if (window.__mediaExplorerDragFile) {
            const f = await resolveMediaExplorerDropFile();
            window.__mediaExplorerDragFile = null;
            if (f) loadFileOnTrack(track.id, f);
            return;
          }
          const f = e.dataTransfer.files && e.dataTransfer.files[0];
          if (!f) return;
          loadFileOnTrack(track.id, f);
        },
        onMouseEnter: () => { setHoveredTrackId(track.id); hoveredTrackIdRef.current = track.id; }
      }, /*#__PURE__*/React.createElement(WaveformLane, {
        track: track,
        zoom: zoom,
        timelineWidth: timelineWidth,
        viewportWidth: viewportWidth,
        scrollLeft: scrollLeft,
        onSelectRange: handleSelectRange,
        onPlayheadSet: handlePlayheadSet,
        isSelected: isSelected,
         selectedItemIds: selectedItemIds,
         onSelectTrack: setSelectedTrackId,
         markers: track.markers,
         onTrackLaneMouseDown: handleTrackLaneMouseDown,
         onClearSelection: () => { captureSelectionUndo(); setSelectedItemIds(new Set()); },
         onSweepSelectStart: handleSweepSelectStart,
         onDeselectItem: handleDeselectItem,
         onAddToSelection: handleAddToSelection,
         onSetPendingDrag: handleSetPendingDrag,
         onSetPendingDragMove: handleSetPendingDragMove,
         onContextMenu: handleContextMenu,
         onClipDragStart: handleClipDragStart,
         onClipStretchStart: handleClipStretchStart,
         onSectionItemDragStart: handleSectionItemDragStart,
         onSectionItemResizeStart: handleSectionItemResizeStart,
         onSelectionEdgeDragStart: handleSelectionEdgeDragStart,
         setSelectedClipId: setSelectedClipId,
         selectedClipId: selectedClipId,
         activeTool: activeTool,
         onSplitTrackAtTime: handleSplitTrackAtTime,
         onEditClipInSubTab: handleEditClipInSubTab,
         onEditSectionInTab: handleEditSectionInTab,
         onEditMidiInTab: handleEditMidiInTab,
         snapValue: snapValue,
         bpm: bpm,
         selectionMode: selectionMode,
         localSelectionTrackId: localSelectionTrackId,
         localSelectionStart: localSelectionStart,
         currentTime: currentTime,
         getLocalAnchor: () => localSelectionAnchorRef.current,
         onClearLocalSelection: () => { captureSelectionUndo(); clearLocalSelection(); },
         onSetSelectionMode: mode => { captureSelectionUndo(); setSelectionMode(mode); },
         onSetSelectionStart: val => { captureSelectionUndo(); setSelectionStart(val); },
         onSetSelectionEnd: val => { captureSelectionUndo(); setSelectionEnd(val); },
         onSetCurrentTime: setCurrentTime,
        onSetLocalSelectionTrackId: setLocalSelectionTrackId,
        onSetLocalSelectionStart: setLocalSelectionStart,
        onSetLocalSelectionEnd: setLocalSelectionEnd,
        localSelLeft: localSelectionStart !== null && localSelectionEnd !== null ? Math.min(localSelectionStart, localSelectionEnd) : null,
        localSelRight: localSelectionStart !== null && localSelectionEnd !== null ? Math.max(localSelectionStart, localSelectionEnd) : null,
        scrollLeft: scrollLeft,
        recordingState: recordingState,
        recTempMidiNotes: recTempMidiNotes,
        recTempAudioBuffer: recTempAudioBuffer,
        recStartTimelineTime: recStartTimelineTime,
        canvasRedrawCount: canvasRedrawCount
      }), selectionMode === 'local' && localSelectionTrackId === track.id && localSelectionStart !== null && localSelectionEnd !== null && Math.abs(localSelectionEnd - localSelectionStart) > 0 && /*#__PURE__*/React.createElement("div", {
        className: "absolute top-0 bottom-0 border-l border-r border-amber-400 bg-amber-500/20 z-20 pointer-events-none",
        style: {
          left: `${Math.min(localSelectionStart, localSelectionEnd) * zoom}px`,
          width: `${Math.abs(localSelectionEnd - localSelectionStart) * zoom}px`
        }
      }, /*#__PURE__*/React.createElement("div", {
        className: "absolute -left-[1px] top-0 bottom-0 w-[2px] bg-amber-400 z-30 pointer-events-auto cursor-ew-resize",
        onMouseDown: e => handleHandleDragStart(e, 'left')
      }), /*#__PURE__*/React.createElement("div", {
        className: "absolute -right-[1px] top-0 bottom-0 w-[2px] bg-amber-400 z-30 pointer-events-auto cursor-ew-resize",
        onMouseDown: e => handleHandleDragStart(e, 'right')
      })), sweepSelect && /*#__PURE__*/React.createElement("div", {
        className: "absolute top-0 bottom-0 border-l border-r border-amber-400 bg-amber-500/10 z-20 pointer-events-none",
        style: {
          left: `${Math.min(sweepSelect.startTime, sweepSelect.endTime) * zoom}px`,
          width: `${Math.abs(sweepSelect.endTime - sweepSelect.startTime) * zoom}px`
        }
      }), track.buffer && /*#__PURE__*/React.createElement("div", {
        className: "absolute right-2 top-2 flex items-center gap-1 z-10 opacity-70 hover:opacity-100"
      }, /*#__PURE__*/React.createElement("button", {
        onClick: () => handleSplitTrack(track.id),
        className: "px-1.5 py-0.5 bg-zinc-900/95 text-zinc-300 rounded text-xs flex items-center gap-1 border border-zinc-700/50"
      }, /*#__PURE__*/React.createElement("span", {
        className: "inline-flex items-center shrink-0"
      }, /*#__PURE__*/React.createElement("i", {
        "data-lucide": "scissors",
        className: "w-2.5 h-2.5 text-cyan-400"
      })), " Cắt")), /*#__PURE__*/React.createElement("div", {
        onMouseDown: e => handleTrackResizeMouseDown(e, track.id),
        className: "absolute bottom-0 left-0 right-0 h-1 cursor-ns-resize z-30 hover:bg-cyan-500/50 transition-colors",
        onClick: e => e.stopPropagation()
      }));
    }), /*#__PURE__*/React.createElement("div", {
      className: "h-[48px] flex items-center justify-center text-xs text-zinc-500 hover:bg-zinc-900/20 cursor-pointer select-none border-t border-dashed border-zinc-800",
      onMouseEnter: () => {
        if (draggedClipRef.current || draggedSectionItemRef.current) {
          setHoveredTrackId(addNewTrack());
        }
      },
      onClick: addNewTrack
    }, /*#__PURE__*/React.createElement("span", {
      className: "flex items-center gap-1 text-zinc-400"
    }, /*#__PURE__*/React.createElement("span", {
      className: "inline-flex items-center shrink-0"
    }, /*#__PURE__*/React.createElement("i", {
      "data-lucide": "plus",
      className: "w-3.5 h-3.5"
    })), " Kéo clip xuống hoặc Click tạo Track")), selectionMode === 'global' && selLeft !== null && selRight !== null && selRight > selLeft && /*#__PURE__*/React.createElement("div", {
      className: "absolute top-0 bottom-0 border-l border-r border-amber-500 bg-amber-500/10 z-20 selection-interactive-box cursor-grab active:cursor-grabbing",
      style: {
        left: `${selLeft * zoom}px`,
        width: `${(selRight - selLeft) * zoom}px`
      },
      onMouseDown: handleSelectionBodyDragStart
    }, /*#__PURE__*/React.createElement("div", {
      className: "absolute -left-[1px] top-0 bottom-0 w-[1px] bg-amber-500 hover:bg-amber-400 cursor-ew-resize z-30",
      onMouseDown: e => handleHandleDragStart(e, 'left')
    }), /*#__PURE__*/React.createElement("div", {
      className: "absolute -right-[1px] top-0 bottom-0 w-[1px] bg-amber-500 hover:bg-amber-400 cursor-ew-resize z-30",
      onMouseDown: e => handleHandleDragStart(e, 'right')
    })), /*#__PURE__*/React.createElement("div", {
      className: "absolute top-0 bottom-0 w-[2px] bg-[#ef4444] z-20 pointer-events-none",
      style: {
        left: `${playheadLeftPos}px`
      }
    }, /*#__PURE__*/React.createElement("div", {
      className: "w-3 h-3 bg-[#ef4444] rotate-45 transform -translate-x-1/2 -translate-y-1/2 absolute top-0"
    })))))) : (() => {
      const st = subTabs.find(s => s.id === activeTab);
      if (!st) return null;
      if (st.type === 'PIANO_ROLL') {
         return /*#__PURE__*/React.createElement(PianoRollTabEditor, {
            st: st,
            zoom: zoom,
            bpm: bpm,
            viewportWidth: viewportWidth,
            activeTracks: activeTracks,
            onClose: () => closeSubTab(st.id),
            onUpdateNotes: handleUpdateMidiNotes,
            onSaveNotes: handleSaveMidiNotes,
            setSubTabs: setSubTabs,
            onPlayPause: handlePlayPause,
            onStop: stopAllPlayback,
            isPlaying: isPlaying,
            playPreviewNote: playMidiPreviewNote,
            showToast: showToast,
            midiDevices: midiDevices,
             recordingState: recordingState,
             recTempMidiNotes: recTempMidiNotes,
             onRecord: handleRecordClick,
             selectedMidiInputId: selectedMidiInputId,
             onMidiInputSelect: handleMidiInputSelect,
             activeMidiPitches: activeMidiPitches,
              onInstrumentSelect: (trackId) => { openInstrumentSelector(trackId); },
              onRealtimePlay: handlePianoRollRealtimePlay,
              onCopyNotes: (n) => setPianoRollClipboard(JSON.parse(JSON.stringify(n || []))),
              clipboardNotes: pianoRollClipboard,
             onExportMidiAudio: handleExportMidiAudio,
             onRescheduleMidi: (updatedNotes) => {
                 const playingSub = subTabs.find(s => s.id === activeTab && s.type === 'PIANO_ROLL' && s.isPlaying);
                 if (playingSub) {
                   const offset = playingSub.currentTime || 0;
                   const ctx = getAudioContext();
                   const tNode = activeTrackNodesRef.current[playingSub.trackId];
                   if (tNode && tNode.gainNode) {
                     tNode.gainNode.gain.setValueAtTime(tNode.gainNode.gain.value || 1, ctx.currentTime);
                     tNode.gainNode.gain.linearRampToValueAtTime(0.001, ctx.currentTime + 0.04);
                   }
                   setTimeout(() => {
                     window.SonicSF.stopAll();
                     stopAllNativeSfNotes();
                     if (tNode && tNode.gainNode) {
                       const trackData = activeTracksRef.current ? activeTracksRef.current.find(t => t.id === playingSub.trackId) : null;
                       const volDb = trackData ? (trackData.volumeDb ?? 0) : 0;
                       const volLinear = volDb <= -50 ? 0 : Math.pow(10, volDb / 20);
                       tNode.gainNode.gain.setValueAtTime(0.001, ctx.currentTime);
                       tNode.gainNode.gain.linearRampToValueAtTime(volLinear || 0.8, ctx.currentTime + 0.015);
                     }
                     startOffsetTimeRef.current = offset;
                     startAudioTimeRef.current = ctx.currentTime;
                     startBufferOffsetRef.current = offset * (playingSub.speed || 1.0);
                     schedulePianoRollMidi(playingSub, offset, updatedNotes);
                   }, 50);
                 }
               },
               onSeekPlayhead: (clickTime) => {
                 const seekSt = subTabs.find(s => s.id === activeTab && s.type === 'PIANO_ROLL');
                 if (!seekSt) return;
                 if (seekSt.isPlaying) {
                   stopAllPlayback();
                   window.SonicSF.stopAll();
                   stopAllNativeSfNotes();
                   setSubTabs(prev => prev.map(s => s.id === seekSt.id ? { ...s, currentTime: clickTime, isPlaying: true } : s));
                   const ctx = getAudioContext();
                   startOffsetTimeRef.current = clickTime;
                   startAudioTimeRef.current = ctx.currentTime;
                   startBufferOffsetRef.current = clickTime * (seekSt.speed || 1.0);
                   schedulePianoRollMidi(seekSt, clickTime);
                   startSubTabPlayback(seekSt, clickTime);
                 } else {
                   setSubTabs(prev => prev.map(s => s.id === seekSt.id ? { ...s, currentTime: clickTime } : s));
                 }
               }
           });
      }
      const subTrack = tracks.find(t => t.id === st.trackId);
      const vTrack = subTrack ? {
        ...subTrack,
        buffer: st.buffer,
        isSubTab: true
      } : null;
      const subTabDuration = st.type === 'PIANO_ROLL' ? (() => {
        const notes = st.notes || [];
        const beatSec = 60.0 / (parseFloat(bpm) || 120);
        let maxEnd = 0;
        notes.forEach(n => {
          const end = (n.start_beat || 0) + (n.duration_beats || 1);
          if (end > maxEnd) maxEnd = end;
        });
        return maxEnd * beatSec + 1.0;
      })() : (st.buffer && 'duration' in st.buffer ? st.buffer.duration : 4.0);
      const subTabTimelineWidth = Math.max(zoom * subTabDuration, viewportWidth);
      return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
      className: "shrink-0 z-30 bg-[#262626] overflow-y-scroll no-scrollbar flex flex-col border-r border-zinc-900",
      style: {
        width: tcpWidth + 'px',
        scrollbarWidth: 'none',
        msOverflowStyle: 'none'
      }
    }, /*#__PURE__*/React.createElement("div", {
      className: "sticky top-0 z-50 flex items-center h-10 border-b border-zinc-900 bg-[#242424] px-2 justify-between shrink-0"
      }, /*#__PURE__*/React.createElement("span", {
        className: "text-xs font-bold text-zinc-500 uppercase"
      }, "Sub-Tab"), /*#__PURE__*/React.createElement("button", {
        onClick: () => closeSubTab(st.id),
        className: "px-1.5 py-0.5 text-xs bg-zinc-800 hover:bg-zinc-700 text-zinc-400 rounded border border-zinc-700 flex items-center gap-1"
      }, /*#__PURE__*/React.createElement("span", {
        className: "inline-flex items-center shrink-0"
      }, /*#__PURE__*/React.createElement("i", {
        "data-lucide": "x",
        className: "w-3 h-3"
      })), " Close")), vTrack ? /*#__PURE__*/React.createElement("div", {
        key: vTrack.id,
        className: "flex-1 flex flex-col p-2.5 bg-[#1e1e1e] border-r border-zinc-900 border-l-4 border-l-cyan-500"
      }, /*#__PURE__*/React.createElement("div", {
        className: "flex items-center justify-between mb-2"
      }, /*#__PURE__*/React.createElement("div", {
        className: "flex items-center gap-2"
      }, /*#__PURE__*/React.createElement("label", {
        className: "cursor-pointer relative block"
      }, /*#__PURE__*/React.createElement("input", {
        type: "color",
        value: vTrack.color || '#0f766e',
        onClick: e => e.stopPropagation(),
        onMouseDown: e => e.stopPropagation(),
        onChange: e => {
          e.stopPropagation();
          updateTrackColor(vTrack.id, e.target.value);
        },
        className: "absolute inset-0 w-full h-full opacity-0 cursor-pointer"
      }), /*#__PURE__*/React.createElement("div", {
        className: "w-3 h-3 rounded-full border border-zinc-600 hover:scale-110 transition-transform",
        style: {
          backgroundColor: vTrack.color
        }
      })), editingTrackName === vTrack.id ? /*#__PURE__*/React.createElement("input", {
        type: "text",
        value: editNameInput,
        autoFocus: true,
        onChange: e => setEditNameInput(e.target.value),
        onBlur: () => {
          updateTrackName(vTrack.id, editNameInput || vTrack.name);
          setEditingTrackName(null);
        },
        onKeyDown: e => {
          if (e.key === 'Enter') {
            updateTrackName(vTrack.id, editNameInput || vTrack.name);
            setEditingTrackName(null);
          }
          if (e.key === 'Escape') setEditingTrackName(null);
        },
        onClick: e => e.stopPropagation(),
        className: "text-xs font-semibold bg-black text-zinc-200 border border-cyan-500 rounded px-1 py-0 w-24 outline-none"
      }) : /*#__PURE__*/React.createElement("span", {
        className: "text-xs font-semibold text-zinc-300 truncate max-w-[80px] cursor-text hover:text-cyan-300 transition-colors",
        title: "Click to rename",
        onClick: e => {
          e.stopPropagation();
          setEditingTrackName(vTrack.id);
          setEditNameInput(vTrack.name);
        }
      }, vTrack.name)), /*#__PURE__*/React.createElement("div", {
        className: "flex items-center gap-1"
      }, /*#__PURE__*/React.createElement("button", {
        onClick: e => {
          e.stopPropagation();
          toggleTrackMute(vTrack.id);
        },
        className: `px-1.5 py-0.5 text-xs rounded font-mono font-bold border ${vTrack.muted ? 'bg-red-950 text-red-400 border-red-700' : 'bg-zinc-800 text-zinc-400 border-transparent hover:text-zinc-200'}`
      }, "M"), /*#__PURE__*/React.createElement("button", {
        onClick: e => {
          e.stopPropagation();
          toggleTrackSoloEvaluate(vTrack.id);
        },
        className: `px-1.5 py-0.5 text-xs rounded font-mono font-bold border ${vTrack.solo ? 'bg-amber-950 text-amber-400 border-amber-600' : 'bg-zinc-800 text-zinc-400 border-transparent hover:text-zinc-200'}`
      }, "S"))), /*#__PURE__*/React.createElement("div", {
        className: "flex flex-col gap-2.5 text-[14px] mb-3"
      }, /*#__PURE__*/React.createElement("div", {
        className: "flex items-center gap-1"
      }, /*#__PURE__*/React.createElement("span", {
        className: "w-10 text-right text-zinc-500 text-[14px]"
      }, "Vol:"), /*#__PURE__*/React.createElement("input", {
        type: "range",
        min: "-50",
        max: "7",
        step: "0.5",
        id: `tcp-vol-${st.id}`,
        value: vTrack.volumeDb ?? 0,
        onChange: e => updateTrackVolumeDb(vTrack.id, parseFloat(e.target.value)),
        className: "flex-1 h-1 bg-zinc-700 rounded-lg appearance-none cursor-pointer accent-cyan-500",
        style: {
          height: '4px'
        }
      }), /*#__PURE__*/React.createElement("span", {
        className: "w-16 text-right font-mono text-zinc-300 text-[14px]",
        id: `tcp-vol-label-${st.id}`
      }, vTrack.volumeDb ?? 0, "dB")), /*#__PURE__*/React.createElement("div", {
        className: "flex items-center gap-1"
      }, /*#__PURE__*/React.createElement("span", {
        className: "w-10 text-right text-zinc-500 text-[14px]"
      }, "Pan:"), /*#__PURE__*/React.createElement("input", {
        type: "range",
        min: "-100",
        max: "100",
        step: "1",
        id: `tcp-pan-${st.id}`,
        value: vTrack.pan ?? 0,
        onChange: e => updateTrackPan(vTrack.id, parseInt(e.target.value)),
        className: "flex-1 h-1 bg-zinc-700 rounded-lg appearance-none cursor-pointer accent-purple-500",
        style: {
          height: '4px'
        }
      }), /*#__PURE__*/React.createElement("span", {
        className: "w-16 text-right font-mono text-zinc-300 text-[14px]",
        id: `tcp-pan-label-detailed-${st.id}`
      }, vTrack.pan > 0 ? 'R' + vTrack.pan : vTrack.pan < 0 ? 'L' + Math.abs(vTrack.pan) : 'C')), /*#__PURE__*/React.createElement("div", {
        className: "flex items-center gap-1.5 text-[14px]"
      }, /*#__PURE__*/React.createElement("button", {
        onClick: () => setSubTabs(prev => prev.map(s => s.id === st.id ? {
          ...s,
          isLooping: !s.isLooping
        } : s)),
        className: `px-2 py-0.5 text-[14px] rounded border font-bold ${st.isLooping ? 'bg-amber-700 text-white border-amber-500' : 'bg-zinc-800 text-zinc-400 border-zinc-700'}`
      }, /*#__PURE__*/React.createElement("span", {
        className: "inline-flex items-center shrink-0"
      }, /*#__PURE__*/React.createElement("i", {
        "data-lucide": "repeat",
        className: "w-3.5 h-3.5"
      }))), /*#__PURE__*/React.createElement("button", {
        onClick: () => updateSubTabEffects(st.id, {
          reverse: !(st.effects || {}).reverse
        }),
        className: `px-2 py-0.5 rounded border text-[14px] font-bold ${(st.effects || {}).reverse ? 'bg-zinc-600 text-white border-zinc-500' : 'bg-zinc-800 text-zinc-400 border-zinc-700'} flex items-center justify-center`,
        title: "Reverse"
      }, /*#__PURE__*/React.createElement("span", {
        className: "inline-flex items-center shrink-0"
      }, /*#__PURE__*/React.createElement("i", {
        "data-lucide": "arrow-left-right",
        className: "w-3.5 h-3.5"
      }))), /*#__PURE__*/React.createElement("span", {
        className: "w-10 text-right text-zinc-500 text-[14px]"
      }, "Loop:"), /*#__PURE__*/React.createElement("input", {
        type: "number",
        min: "0",
        max: "999",
        value: st.loopCount || 0,
        onChange: e => setSubTabs(prev => prev.map(s => s.id === st.id ? {
          ...s,
          loopCount: Math.max(0, parseInt(e.target.value) || 0)
        } : s)),
        className: "w-12 bg-black text-amber-300 text-[14px] px-1 py-0.5 rounded border border-zinc-700 text-center font-mono",
        title: "Loop count"
      }))), /*#__PURE__*/React.createElement("div", {
        className: "flex items-center justify-between text-[14px] text-zinc-500 mb-1 font-semibold uppercase mt-2"
      }, /*#__PURE__*/React.createElement("span", null, "DSP"), /*#__PURE__*/React.createElement("span", {
        className: "font-mono text-zinc-600 text-[11px] normal-case"
      }, "áp dụng vùng chọn / cả clip")), /*#__PURE__*/React.createElement("div", {
        className: "grid grid-cols-3 gap-1 mb-2"
      }, /*#__PURE__*/React.createElement("button", {
        onClick: () => applySubTabEffect(st.id, 'invert_phase', 0),
        className: "py-1 bg-sky-900 hover:bg-sky-800 text-sky-200 font-bold rounded text-[14px] border border-sky-700 flex items-center justify-center gap-1"
      }, /*#__PURE__*/React.createElement("span", {
        className: "inline-flex items-center shrink-0"
      }, /*#__PURE__*/React.createElement("i", {
        "data-lucide": "arrow-down-up",
        className: "w-3 h-3"
      })), "Phase Inv"), /*#__PURE__*/React.createElement("button", {
        onClick: () => applySubTabEffect(st.id, 'swap_channels', 0),
        className: "py-1 bg-sky-900 hover:bg-sky-800 text-sky-200 font-bold rounded text-[14px] border border-sky-700 flex items-center justify-center gap-1"
      }, /*#__PURE__*/React.createElement("span", {
        className: "inline-flex items-center shrink-0"
      }, /*#__PURE__*/React.createElement("i", {
        "data-lucide": "shuffle",
        className: "w-3 h-3"
      })), "Swap L/R"), /*#__PURE__*/React.createElement("button", {
        onClick: () => applySubTabEffect(st.id, 'reverse', 0),
        className: "py-1 bg-sky-900 hover:bg-sky-800 text-sky-200 font-bold rounded text-[14px] border border-sky-700 flex items-center justify-center gap-1"
      }, /*#__PURE__*/React.createElement("span", {
        className: "inline-flex items-center shrink-0"
      }, /*#__PURE__*/React.createElement("i", {
        "data-lucide": "arrow-left-right",
        className: "w-3 h-3"
      })), "Reverse")), /*#__PURE__*/React.createElement("div", {
        className: "flex gap-1 justify-between my-2.5"
      }, /*#__PURE__*/React.createElement("div", {
        className: "flex flex-col items-center bg-zinc-900/60 p-1.5 rounded border border-zinc-800 flex-1"
      }, /*#__PURE__*/React.createElement("span", {
        className: "text-[14px] font-bold text-zinc-400 uppercase tracking-tighter",
        title: "Normalize"
      }, "Norm"), /*#__PURE__*/React.createElement("input", {
        type: "range",
        min: "-12",
        max: "0",
        step: "0.1",
        value: subTabNormVal,
        onChange: e => setSubTabNormVal(parseFloat(e.target.value)),
        style: {
          writingMode: 'vertical-lr',
          direction: 'rtl',
          height: '120px',
          width: '20px',
          accentColor: '#a1a1aa'
        },
        className: "my-2 cursor-pointer"
      }), /*#__PURE__*/React.createElement("span", {
        className: "text-[14px] font-mono text-zinc-300 font-bold"
      }, subTabNormVal, "dB"), /*#__PURE__*/React.createElement("button", {
        onClick: () => applySubTabEffect(st.id, 'normalize', subTabNormVal),
        className: "mt-1.5 px-1 py-1 bg-zinc-700 hover:bg-zinc-600 text-zinc-100 rounded text-[14px] font-bold w-full uppercase border border-zinc-650"
      }, "Apply")), /*#__PURE__*/React.createElement("div", {
        className: "flex flex-col items-center bg-zinc-900/60 p-1.5 rounded border border-zinc-800 flex-1"
      }, /*#__PURE__*/React.createElement("span", {
        className: "text-[14px] font-bold text-zinc-400 uppercase tracking-tighter",
        title: "Pitch Shift"
      }, "Pitch"), /*#__PURE__*/React.createElement("input", {
        type: "range",
        min: "-12",
        max: "12",
        step: "0.5",
        value: subTabPitchVal,
        onChange: e => setSubTabPitchVal(parseFloat(e.target.value)),
        style: {
          writingMode: 'vertical-lr',
          direction: 'rtl',
          height: '120px',
          width: '20px',
          accentColor: '#a1a1aa'
        },
        className: "my-2 cursor-pointer"
      }), /*#__PURE__*/React.createElement("span", {
        className: "text-[14px] font-mono text-zinc-300 font-bold"
      }, subTabPitchVal > 0 ? '+' : '', subTabPitchVal, "st"), /*#__PURE__*/React.createElement("button", {
        onClick: () => applySubTabEffect(st.id, 'pitch', subTabPitchVal),
        className: "mt-1.5 px-1 py-1 bg-zinc-700 hover:bg-zinc-600 text-zinc-100 rounded text-[14px] font-bold w-full uppercase border border-zinc-650"
      }, "Apply")), /*#__PURE__*/React.createElement("div", {
        className: "flex flex-col items-center bg-zinc-900/60 p-1.5 rounded border border-zinc-800 flex-1"
      }, /*#__PURE__*/React.createElement("span", {
        className: "text-[14px] font-bold text-zinc-400 uppercase tracking-tighter",
        title: "Gain Multiplier"
      }, "Gain"), /*#__PURE__*/React.createElement("input", {
        type: "range",
        min: "0",
        max: "150",
        step: "1",
        value: subTabGainVal,
        onChange: e => setSubTabGainVal(parseInt(e.target.value)),
        style: {
          writingMode: 'vertical-lr',
          direction: 'rtl',
          height: '120px',
          width: '20px',
          accentColor: '#a1a1aa'
        },
        className: "my-2 cursor-pointer"
      }), /*#__PURE__*/React.createElement("span", {
        className: "text-[14px] font-mono text-zinc-300 font-bold"
      }, subTabGainVal, "%"), /*#__PURE__*/React.createElement("button", {
        onClick: () => applySubTabEffect(st.id, 'gain', subTabGainVal),
        className: "mt-1.5 px-1 py-1 bg-zinc-700 hover:bg-zinc-600 text-zinc-100 rounded text-[14px] font-bold w-full uppercase border border-zinc-650"
      }, "Apply"))), /*#__PURE__*/React.createElement("div", {
        className: "flex-1"
      }), /*#__PURE__*/React.createElement("div", {
        className: "mt-auto pt-2.5 border-t border-zinc-800"
      }, /*#__PURE__*/React.createElement("div", {
        className: "flex items-center justify-between text-[14px] text-zinc-500 mb-1 font-semibold uppercase"
      }, /*#__PURE__*/React.createElement("span", null, "Duration:"), /*#__PURE__*/React.createElement("span", {
        className: "font-mono text-zinc-300"
      }, st.buffer ? formatTime(subTabDuration) : '0s')), /*#__PURE__*/React.createElement("div", {
        className: "flex items-center justify-between text-[14px] text-zinc-500 mb-2.5 font-semibold uppercase"
      }, /*#__PURE__*/React.createElement("span", null, "SR:"), /*#__PURE__*/React.createElement("span", {
        className: "font-mono text-zinc-300"
      }, st.buffer ? st.buffer.sampleRate : 0, " Hz")), /*#__PURE__*/React.createElement("div", {
        className: "grid grid-cols-2 gap-1 mb-2"
      }, /*#__PURE__*/React.createElement("button", {
        onClick: async e => {
          subTabAiTrackIdRef.current = st.trackId;
          handleAIScan();
        },
        disabled: analysisState.isRunning,
        className: "py-1 bg-purple-900 hover:bg-purple-800 text-purple-100 font-bold rounded text-xs border border-purple-700 flex items-center justify-center gap-1"
      }, /*#__PURE__*/React.createElement("span", {
        className: "inline-flex items-center shrink-0"
      }, /*#__PURE__*/React.createElement("i", {
        "data-lucide": "map-pin",
        className: "w-3 h-3"
      })), " Scan"), /*#__PURE__*/React.createElement("button", {
        onClick: async e => {
          subTabAiTrackIdRef.current = st.trackId;
          handleAICutToNewTrack();
        },
        disabled: analysisState.isRunning,
        className: "py-1 bg-fuchsia-800 hover:bg-fuchsia-700 text-white font-bold rounded text-xs flex items-center justify-center gap-1"
      }, /*#__PURE__*/React.createElement("span", {
        className: "inline-flex items-center shrink-0"
      }, /*#__PURE__*/React.createElement("i", {
        "data-lucide": "scissors",
        className: "w-3 h-3"
      })), " Cut"), /*#__PURE__*/React.createElement("button", {
        onClick: async e => {
          subTabAiTrackIdRef.current = st.trackId;
          if (st.buffer) {
            setSelectionRangeOnBuffer(st.buffer, st.selectionStart || 0, st.selectionEnd || st.buffer.duration);
          }
          handleAIAnalysicLoop();
        },
        disabled: analysisState.isRunning,
        className: "py-1 bg-violet-800 hover:bg-violet-700 text-violet-100 font-bold rounded text-xs border border-violet-600 flex items-center justify-center gap-1 col-span-2"
      }, /*#__PURE__*/React.createElement("span", {
        className: "inline-flex items-center shrink-0"
      }, /*#__PURE__*/React.createElement("i", {
        "data-lucide": "sparkles",
        className: "w-3 h-3"
      })), " AI Analysic Loop")), /*#__PURE__*/React.createElement("button", {
        onClick: () => exportSubTabBuffer(st.id),
        className: "w-full py-1.5 mb-1.5 bg-blue-700 hover:bg-blue-600 text-white font-bold rounded text-[14px] flex items-center justify-center gap-1 transition"
      }, /*#__PURE__*/React.createElement("span", {
        className: "inline-flex items-center shrink-0"
      }, /*#__PURE__*/React.createElement("i", {
        "data-lucide": "download",
        className: "w-4 h-4"
      })), " Export"), /*#__PURE__*/React.createElement("button", {
        onClick: () => applySubTab(st.id),
        className: "w-full py-1.5 bg-emerald-700 hover:bg-emerald-600 text-white font-bold rounded text-[14px] flex items-center justify-center gap-1 transition"
      }, /*#__PURE__*/React.createElement("span", {
        className: "inline-flex items-center shrink-0"
      }, /*#__PURE__*/React.createElement("i", {
        "data-lucide": "save",
        className: "w-4 h-4"
      })), " Save"))) : /*#__PURE__*/React.createElement("div", {
        className: "flex-1 flex items-center justify-center text-xs text-zinc-500"
      }, "Track not found")), /*#__PURE__*/React.createElement("div", {
        className: "w-1 cursor-ew-resize hover:bg-cyan-500/50 active:bg-cyan-400 transition-colors shrink-0 z-10",
        onMouseDown: startTcpResize
      }), /*#__PURE__*/React.createElement("div", {
        ref: handleTimelineWrapperRef,
        className: "flex-1 relative z-10 overflow-x-auto overflow-y-auto min-w-0"
      }, /*#__PURE__*/React.createElement("div", {
        style: {
          width: `${subTabTimelineWidth}px`
        },
        className: "relative flex flex-col min-h-full"
      }, /*#__PURE__*/React.createElement("div", {
        className: "sticky top-0 z-30 bg-[#1a1a1a]"
      }, /*#__PURE__*/React.createElement(TimelineRuler, {
        bpm: parseInt(bpm) || 120,
        zoom: zoom,
        timelineWidth: subTabTimelineWidth,
        viewportWidth: viewportWidth,
        onPlayheadSet: setCurrentTime,
        snapValue: snapValue,
        onRulerMouseDown: e => {
          setSelectionFollowsTempo(false);
          const wrapper = timelineWrapperRef.current;
          if (!wrapper) return;
          const rect = wrapper.getBoundingClientRect();
          const sl = wrapper.scrollLeft;
          const raw = Math.max(0, (e.clientX - rect.left + sl) / zoom);
          const t = snapValue !== 'free' ? snapTime(raw, snapValue, bpm) : raw;
          if (e.ctrlKey) {
            e.preventDefault();
            e.stopPropagation();
            setSubTabs(prev => prev.map(s => s.id === activeTab ? { ...s, selectionStart: null, selectionEnd: null } : s));
            return;
          }
          if (e.shiftKey) { e.preventDefault(); e.stopPropagation(); }
          handlePlayheadSet(t);
          subTabDragStartRef.current = t;
          isDraggingSubTabRef.current = true;
        },
        scrollLeft: scrollLeft,
        canvasRedrawCount: canvasRedrawCount
      })), /*#__PURE__*/React.createElement("div", {
        className: "flex-1 flex flex-col relative bg-[#111111] min-h-full"
      }, vTrack && /*#__PURE__*/React.createElement("div", {
        style: {
          height: `${subTabHeight}px`
        },
        className: "relative overflow-hidden border-2 border-purple-500/40 rounded bg-[#151515] mx-1 mt-1 shrink-0"
      }, /*#__PURE__*/React.createElement(SubTabWaveform, {
        buffer: st.buffer,
        subTabId: st.id,
        activeTab: activeTab,
        activeTool: activeTool,
        currentTime: st.currentTime,
        selectionStart: st.selectionStart,
        selectionEnd: st.selectionEnd,
        onSelectRange: (start, end) => setSubTabs(prev => prev.map(s => s.id === st.id ? {
          ...s,
          selectionStart: start,
          selectionEnd: end
        } : s)),
        onPlayheadSet: time => setSubTabs(prev => prev.map(s => s.id === st.id ? {
          ...s,
          currentTime: time
        } : s)),
        onContextMenu: (e, clickTime) => setContextMenu({
          x: e.clientX,
          y: e.clientY,
          isSubTab: true,
          subTabId: st.id,
          time: clickTime
        }),
        zoom: zoom,
        timelineWidth: subTabTimelineWidth,
        color: vTrack.color,
        name: vTrack.name,
        speed: st.speed || 1.0,
        volumeNodes: st.volumeNodes || [],
        panningNodes: st.panningNodes || [],
        fadeInLen: st.fadeInLen || 0,
        fadeOutLen: st.fadeOutLen || 0,
        graphMode: st.graphMode,
        channelInfo: st.channelInfo,
        selectedNodeTime: subTabSelectedNodeTime,
        setSelectedNodeTime: setSubTabSelectedNodeTime,
        onUpdateNodes: nodes => setSubTabs(prev => prev.map(s => s.id === st.id ? {
          ...s,
          [s.graphMode === 'pan' ? 'panningNodes' : 'volumeNodes']: nodes
        } : s)),
        onUpdateFade: fade => setSubTabs(prev => prev.map(s => s.id === st.id ? {
          ...s,
          fadeInLen: fade.fadeInLen ?? s.fadeInLen,
          fadeOutLen: fade.fadeOutLen ?? s.fadeOutLen
        } : s)),
        onModeToggle: () => setSubTabs(prev => prev.map(s => s.id === st.id ? {
          ...s,
          graphMode: s.graphMode === 'pan' ? null : 'pan'
        } : s)),
        onSpeedChange: newSpeed => {
          setSubTabs(prev => prev.map(s => {
            if (s.id !== st.id) return s;
            const oldSpeed = s.speed || 1.0;
            const ratio = oldSpeed / newSpeed;
            const newVolumeNodes = (s.volumeNodes || []).map(n => ({
              ...n,
              time: n.time * ratio
            }));
            const newPanningNodes = (s.panningNodes || []).map(n => ({
              ...n,
              time: n.time * ratio
            }));
            return {
              ...s,
              speed: newSpeed,
              volumeNodes: newVolumeNodes,
              panningNodes: newPanningNodes,
              fadeInLen: (s.fadeInLen || 0) * ratio,
              fadeOutLen: (s.fadeOutLen || 0) * ratio,
              currentTime: (s.currentTime || 0) * ratio,
              label: s.label.replace(/\s\(\d+%\)$/, '') + ` (${Math.round(newSpeed * 100)}%)`
            };
          }));
          const n = activeTrackNodesRef.current[st.trackId];
          if (n && n.source) n.source.playbackRate.value = newSpeed;
          // Reset time refs to prevent playhead jump when speed changes mid-playback
          const ctx = getAudioContext();
          const elapsed = ctx.currentTime - startAudioTimeRef.current;
          const oldSpeed = activePlaybackSpeedRef.current;
          const ratio = oldSpeed / newSpeed;
          startBufferOffsetRef.current = startBufferOffsetRef.current + elapsed * oldSpeed;
          startOffsetTimeRef.current = (startOffsetTimeRef.current + elapsed) * ratio;
          startAudioTimeRef.current = ctx.currentTime;
          activePlaybackSpeedRef.current = newSpeed;
        }
      }), /*#__PURE__*/React.createElement("div", {
        onMouseDown: handleSubTabResizeMouseDown,
        className: "absolute bottom-0 left-0 right-0 h-1.5 cursor-ns-resize z-30 hover:bg-purple-500/50 transition-colors"
      })), st.selectionStart !== null && st.selectionEnd !== null && st.selectionEnd > st.selectionStart && /*#__PURE__*/React.createElement("div", {
        className: "absolute top-0 bottom-0 border-l border-r border-amber-500 bg-amber-500/10 z-20 pointer-events-none",
        style: {
          left: `${Math.min(st.selectionStart, st.selectionEnd) * zoom}px`,
          width: `${Math.abs(st.selectionEnd - st.selectionStart) * zoom}px`
        }
      })))));
    })())), /*#__PURE__*/React.createElement("div", {
      className: "w-1 cursor-ew-resize hover:bg-cyan-500/50 active:bg-cyan-400 transition-colors shrink-0 z-10",
      onMouseDown: startColResize
    }), renderDock('right', 'Right')), renderDock('bottom', 'Bottom'), showMixer && /*#__PURE__*/React.createElement("div", {
      className: "flex flex-col border-t border-zinc-700 bg-slate-900 shrink-0 select-none",
      style: { height: mixerHeight + 'px' }
    }, /*#__PURE__*/React.createElement("div", {
      className: "flex items-center justify-between px-2 py-0.5 bg-zinc-800/80 shrink-0 cursor-ns-resize",
      onMouseDown: e => {
        e.preventDefault();
        var startY = e.clientY;
        var startH = mixerHeight;
        var onMove = function(ev) {
          var newH = Math.max(80, Math.min(400, startH - (ev.clientY - startY)));
          setMixerHeight(newH);
          localStorage.setItem('studio_mixer_height', newH.toString());
        };
        var onUp = function() {
          document.removeEventListener('mousemove', onMove);
          document.removeEventListener('mouseup', onUp);
        };
        document.addEventListener('mousemove', onMove);
        document.addEventListener('mouseup', onUp);
      }
    }, /*#__PURE__*/React.createElement("div", {
      className: "flex items-center gap-2"
    }, /*#__PURE__*/React.createElement("span", {
      className: "inline-flex items-center shrink-0"
    }, /*#__PURE__*/React.createElement("i", {
      "data-lucide": "grip-horizontal",
      className: "w-3 h-3 text-zinc-600"
    })), /*#__PURE__*/React.createElement("span", {
      className: "text-[10px] font-bold text-zinc-400 uppercase tracking-wider"
    }, "MIXER")), /*#__PURE__*/React.createElement("button", {
      onClick: () => setShowMixer(false),
      className: "p-0.5 rounded text-zinc-500 hover:text-zinc-200 hover:bg-zinc-700 transition"
    }, /*#__PURE__*/React.createElement("span", {
      className: "inline-flex items-center shrink-0"
    }, /*#__PURE__*/React.createElement("i", {
      "data-lucide": "x",
      className: "w-3 h-3"
    })))), /*#__PURE__*/React.createElement("div", {
      className: "flex-1 flex overflow-x-auto p-1.5 gap-1.5 items-stretch"
    }, /*#__PURE__*/React.createElement(MasterStripConsole, {
      masterVolume: masterVolume,
      setMasterVolume: setMasterVolume,
      showMasteringModal: showMasteringModal,
      setShowMasteringModal: setShowMasteringModal,
      masteringSettings: masteringSettings,
      setMasteringSettings: setMasteringSettings,
      isPlaying: isPlaying
    }), activeTracks.length > 0 && /*#__PURE__*/React.createElement("div", {
      className: "w-px bg-zinc-700 shrink-0 self-stretch mx-0.5"
    }), activeTracks.map(function(track, idx) {
      return /*#__PURE__*/React.createElement(TrackStripConsole, {
        key: track.id,
        track: track,
        index: idx,
        onUpdateTrack: updateTrackProp,
        trackVuRefs: trackVuRefs
      });
    }))), /*#__PURE__*/React.createElement("div", {
      className: "flex flex-col border-t border-zinc-700 bg-slate-900 shrink-0 select-none",
      style: { height: mediaExplorerPanelHeight + 'px', display: showMediaExplorer ? 'flex' : 'none' }
    }, /*#__PURE__*/React.createElement("div", {
      className: "flex items-center justify-between px-2 py-0.5 bg-zinc-800/80 shrink-0 cursor-ns-resize",
      onMouseDown: e => {
        e.preventDefault();
        var startY = e.clientY;
        var startH = mediaExplorerPanelHeight;
        var onMove = function(ev) {
          var newH = Math.max(120, Math.min(520, startH - (ev.clientY - startY)));
          setMediaExplorerPanelHeight(newH);
          localStorage.setItem('studio_media_explorer_height', newH.toString());
        };
        var onUp = function() {
          document.removeEventListener('mousemove', onMove);
          document.removeEventListener('mouseup', onUp);
        };
        document.addEventListener('mousemove', onMove);
        document.addEventListener('mouseup', onUp);
      }
    }, /*#__PURE__*/React.createElement("div", {
      className: "flex items-center gap-2"
    }, /*#__PURE__*/React.createElement("span", {
      className: "inline-flex items-center shrink-0"
    }, /*#__PURE__*/React.createElement("i", {
      "data-lucide": "grip-horizontal",
      className: "w-3 h-3 text-zinc-600"
    })), /*#__PURE__*/React.createElement("span", {
      className: "text-[10px] font-bold text-emerald-400 uppercase tracking-wider"
    }, "Media Explorer (F6)")), /*#__PURE__*/React.createElement("button", {
      onClick: () => setShowMediaExplorer(false),
      className: "p-0.5 rounded text-zinc-500 hover:text-zinc-200 hover:bg-zinc-700 transition"
    }, /*#__PURE__*/React.createElement("span", {
      className: "inline-flex items-center shrink-0"
    }, /*#__PURE__*/React.createElement("i", {
      "data-lucide": "x",
      className: "w-3 h-3"
    })))), /*#__PURE__*/React.createElement("div", {
      className: "flex-1 min-h-0 overflow-hidden bg-[#262626]"
    }, /*#__PURE__*/React.createElement(MediaExplorerPanel, {
      height: mediaExplorerPanelHeight,
      clipboardRef: clipboardRef,
      active: showMediaExplorer
    }))));
  })(), /*#__PURE__*/React.createElement("div", {
    className: "h-9 bg-[#141414] border-t border-zinc-900 px-4 flex items-center justify-between text-[18px] text-zinc-500 select-none shrink-0"
  }, /*#__PURE__*/React.createElement("div", {
    className: "flex items-center gap-4"
  }, /*#__PURE__*/React.createElement("span", null, "Status: ", isPlaying ? 'Playing' : 'Stopped'), activeTab !== 'main' && /*#__PURE__*/React.createElement("span", {
    className: "text-amber-400 font-semibold uppercase"
  }, "Sub-Tab"), activeTab === 'main' && /*#__PURE__*/React.createElement("span", {
    className: "text-cyan-400 font-semibold uppercase"
  }, "Track: ID ", selectedTrackId), selectionMode === 'local' && /*#__PURE__*/React.createElement("span", {
    className: "text-amber-400 font-semibold uppercase text-[18px]"
  }, "Local Sel"), selectionMode === 'global' && /*#__PURE__*/React.createElement("span", {
    className: "text-purple-400 font-semibold uppercase text-[18px]"
  }, "Global Sel"), hasAnySolo && /*#__PURE__*/React.createElement("span", {
    className: "text-amber-500 font-semibold"
  }, "Solo: ", tracks.filter(t => t.solo).length, " track(s)"), isLoopingSelection && selectionMode === 'local' && /*#__PURE__*/React.createElement("span", {
    className: "text-emerald-400 font-semibold uppercase text-[18px]"
  }, "Solo Loop"), isLoopingSelection && selectionMode !== 'local' && /*#__PURE__*/React.createElement("span", {
    className: "text-cyan-400 font-semibold uppercase text-[18px]"
  }, "Master Loop")), /*#__PURE__*/React.createElement("div", {
    className: "flex items-center gap-1.5 ml-3"
  }, /*#__PURE__*/React.createElement("span", {
    className: "text-[18px] font-bold text-zinc-400 uppercase"
  }, "Bars:"), /*#__PURE__*/React.createElement("input", {
    type: "number",
    min: "0",
    value: beginBar,
    onChange: e => {
      const b = parseInt(e.target.value) || 0;
      setBeginBar(b);
      const beatDuration = 60 / parseInt(bpm || 120);
      const t = b * beatDuration * 4;
      clearLocalSelection();
      setSelectionMode('global');
      setSelectionStart(t);
      setSelectionEnd(t + beatDuration * 4);
    },
    className: "w-[60px] bg-black text-white text-[18px] px-1 py-0 rounded border border-zinc-700 text-center font-mono"
  }), /*#__PURE__*/React.createElement("span", {
    className: "text-[18px] text-zinc-500"
  }, "-"), /*#__PURE__*/React.createElement("input", {
    type: "number",
    min: "0",
    value: endBar,
    onChange: e => {
      const b = parseInt(e.target.value) || 0;
      setEndBar(b);
      const beatDuration = 60 / parseInt(bpm || 120);
      const t = b * beatDuration * 4;
      // selectionEnd = vạch bar b (không +1 bar) — khớp hiển thị endBar
      // (quét 8 bars hiển thị "0 đến 8"): nhập 8 → selection tới vạch 8.
      setSelectionEnd(t);
      setNumberBar(Math.max(0, b - beginBar));
    },
    className: "w-[60px] bg-black text-white text-[18px] px-1 py-0 rounded border border-zinc-700 text-center font-mono"
  }), /*#__PURE__*/React.createElement("span", {
    className: "text-[18px] text-zinc-500"
  }, "#"), /*#__PURE__*/React.createElement("input", {
    type: "number",
    min: "1",
    value: numberBar,
    readOnly: true,
    className: "w-[60px] bg-black text-zinc-400 text-[18px] px-1 py-0 rounded border border-zinc-800 text-center font-mono"
  }), /*#__PURE__*/React.createElement("button", {
    onClick: () => setSelectionFollowsTempo(prev => !prev),
    className: `px-1.5 py-0 text-[18px] rounded border font-bold ${selectionFollowsTempo ? 'bg-amber-700 text-white border-amber-500' : 'bg-zinc-800 text-zinc-400 border-zinc-700'}`,
    title: selectionFollowsTempo ? "Selection theo tempo (đổi BPM → selection thay đổi)" : "Selection theo thời gian (cố định)"
  }, selectionFollowsTempo ? "♪T" : "⏱T"), /*#__PURE__*/React.createElement("div", {
    className: "w-[1px] h-6 bg-zinc-800 mx-1"
  }), /*#__PURE__*/React.createElement("span", {
    className: "text-[18px] text-zinc-500"
  }, "Start:"), /*#__PURE__*/React.createElement("input", {
    type: "text",
    value: selLeft !== null ? formatTime(selLeft) : '',
    onChange: e => {
      const parts = e.target.value.split(/[:.]/);
      if (parts.length === 3) {
        const secs = parseInt(parts[0]) * 60 + parseInt(parts[1]) + parseFloat('0.' + (parts[2] || '0'));
        clearLocalSelection();
        setSelectionMode('global');
        setSelectionStart(secs);
      }
    },
    className: "w-24 bg-black text-zinc-200 text-[18px] px-1 py-0 rounded border border-zinc-700 text-center font-mono"
  }), /*#__PURE__*/React.createElement("span", {
    className: "text-[18px] text-zinc-500"
  }, "End:"), /*#__PURE__*/React.createElement("input", {
    type: "text",
    value: selRight !== null ? formatTime(selRight) : '',
    onChange: e => {
      const parts = e.target.value.split(/[:.]/);
      if (parts.length === 3) {
        const secs = parseInt(parts[0]) * 60 + parseInt(parts[1]) + parseFloat('0.' + (parts[2] || '0'));
        setSelectionEnd(secs);
      }
    },
    className: "w-24 bg-black text-zinc-200 text-[18px] px-1 py-0 rounded border border-zinc-700 text-center font-mono"
  }), /*#__PURE__*/React.createElement("span", {
    className: "text-[18px] text-zinc-500"
  }, "Len:"), /*#__PURE__*/React.createElement("input", {
    type: "text",
    value: selLeft !== null && selRight !== null ? formatTime(Math.abs(selRight - selLeft)) : '',
    onChange: e => {
      const parts = e.target.value.split(/[:.]/);
      if (parts.length === 3 && selLeft !== null) {
        const secs = parseInt(parts[0]) * 60 + parseInt(parts[1]) + parseFloat('0.' + (parts[2] || '0'));
        setSelectionEnd(selLeft + secs);
      }
    },
    className: "w-24 bg-black text-amber-300 text-[18px] px-1 py-0 rounded border border-zinc-700 text-center font-mono"
  })), /*#__PURE__*/React.createElement("div", {
    className: "flex items-center gap-2"
  }, /*#__PURE__*/React.createElement("button", {
    onClick: () => setShowExportPanel(p => !p),
    className: `px-1.5 py-0.5 rounded text-xs font-semibold transition flex items-center gap-1 ${showExportPanel ? 'bg-cyan-900 text-cyan-300' : 'text-zinc-500 hover:text-zinc-300'}`,
    title: `Export (floating modal)`
  }, /*#__PURE__*/React.createElement("span", {
    className: "inline-flex items-center shrink-0"
  }, /*#__PURE__*/React.createElement("i", {
    "data-lucide": "save",
    className: "w-3 h-3"
  })), /*#__PURE__*/React.createElement("span", {
    className: "text-[7px] opacity-60"
  }, showExportPanel ? panelPositions.export[0].toUpperCase() : '')), /*#__PURE__*/React.createElement("button", {
    onClick: () => setShowAIPanel(p => !p),
    className: `px-1.5 py-0.5 rounded text-xs font-semibold transition flex items-center gap-1 ${showAIPanel ? 'bg-purple-900 text-purple-300' : 'text-zinc-500 hover:text-zinc-300'}`,
    title: `AI Panel (${panelPositions.ai})`
  }, /*#__PURE__*/React.createElement("span", {
    className: "inline-flex items-center shrink-0"
  }, /*#__PURE__*/React.createElement("i", {
    "data-lucide": "cpu",
    className: "w-3 h-3"
  })), /*#__PURE__*/React.createElement("span", {
    className: "text-[7px] opacity-60"
  }, showAIPanel ? panelPositions.ai[0].toUpperCase() : '')), /*#__PURE__*/React.createElement("button", {
    onClick: () => window.__toggleMixerRef(),
    className: `px-1.5 py-0.5 rounded text-xs font-semibold transition flex items-center gap-1 ${showMixer ? 'bg-indigo-900 text-indigo-300' : 'text-zinc-500 hover:text-zinc-300'}`,
    title: "Mixer Panel (F7)"
  }, /*#__PURE__*/React.createElement("span", {
    className: "inline-flex items-center shrink-0"
  }, /*#__PURE__*/React.createElement("i", {
    "data-lucide": "sliders-horizontal",
    className: "w-3 h-3"
  }))), /*#__PURE__*/React.createElement("button", {
    onClick: () => window.__toggleMediaExplorerRef(),
    className: `px-1.5 py-0.5 rounded text-xs font-semibold transition flex items-center gap-1 ${showMediaExplorer ? 'bg-emerald-900 text-emerald-300' : 'text-zinc-500 hover:text-zinc-300'}`,
    title: "Media Explorer Panel (F6)"
  }, /*#__PURE__*/React.createElement("span", {
    className: "inline-flex items-center shrink-0"
  }, /*#__PURE__*/React.createElement("i", {
    "data-lucide": "folder-open",
    className: "w-3 h-3"
  }))), /*#__PURE__*/React.createElement("span", {
    className: "w-[1px] h-3 bg-zinc-800 mx-1"
  }), /*#__PURE__*/React.createElement("span", {
    className: "flex items-center gap-1"
  }, /*#__PURE__*/React.createElement("span", {
    className: "inline-flex items-center shrink-0"
  }, /*#__PURE__*/React.createElement("i", {
    "data-lucide": "info",
    className: "w-3 h-3 text-zinc-600"
  })), prHint ? /*#__PURE__*/React.createElement("span", {
    className: "text-cyan-400"
  }, prHint) : /*#__PURE__*/React.createElement("span", {
    className: "text-zinc-600 italic"
  }, "Adaptive tips: hover vào vùng làm việc để xem hướng dẫn")))), contextMenu && (contextMenu.isSubTab ? /*#__PURE__*/React.createElement("div", {
    className: "fixed z-[60] bg-[#2a2a2a] border border-zinc-700 rounded-lg shadow-2xl py-1 w-64 overflow-y-auto",
    style: {
      left: Math.min(contextMenu.x, window.innerWidth - 260),
      top: contextMenu.y + 200 > window.innerHeight ? undefined : contextMenu.y,
      bottom: contextMenu.y + 200 > window.innerHeight ? (window.innerHeight - contextMenu.y) : undefined,
      maxHeight: '60vh'
    },
    onClick: e => e.stopPropagation()
  }, /*#__PURE__*/React.createElement("div", {
    className: "px-3 py-1 text-xs text-zinc-500 font-bold uppercase border-b border-zinc-800 pb-1 mb-1"
  }, "Selection: ", formatTime(subTabs.find(s => s.id === contextMenu.subTabId)?.selectionStart || 0), " - ", formatTime(subTabs.find(s => s.id === contextMenu.subTabId)?.selectionEnd || 0)), /*#__PURE__*/React.createElement("div", {
    className: "h-px bg-zinc-700 my-1"
  }), /*#__PURE__*/React.createElement("button", {
    onClick: () => {
      handleSubTabCut(contextMenu.subTabId);
      closeContextMenu();
    },
    className: "w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2"
  }, /*#__PURE__*/React.createElement("span", {
    className: "inline-flex items-center shrink-0"
  }, /*#__PURE__*/React.createElement("i", {
    "data-lucide": "scissors",
    className: "w-3.5 h-3.5 text-rose-400 shrink-0"
  })), /*#__PURE__*/React.createElement("span", {
    className: "flex-1"
  }, "Cut"), /*#__PURE__*/React.createElement("span", {
    className: "text-purple-400 text-xs font-semibold font-mono ml-auto"
  }, "Ctrl+X")), /*#__PURE__*/React.createElement("button", {
    onClick: () => {
      handleSubTabCopy(contextMenu.subTabId);
      closeContextMenu();
    },
    className: "w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2"
  }, /*#__PURE__*/React.createElement("span", {
    className: "inline-flex items-center shrink-0"
  }, /*#__PURE__*/React.createElement("i", {
    "data-lucide": "copy",
    className: "w-3.5 h-3.5 text-zinc-400 shrink-0"
  })), /*#__PURE__*/React.createElement("span", {
    className: "flex-1"
  }, "Copy"), /*#__PURE__*/React.createElement("span", {
    className: "text-purple-400 text-xs font-semibold font-mono ml-auto"
  }, "Ctrl+C")), /*#__PURE__*/React.createElement("button", {
    onClick: () => {
      handleSubTabPaste(contextMenu.subTabId);
      closeContextMenu();
    },
    className: "w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2"
  }, /*#__PURE__*/React.createElement("span", {
    className: "inline-flex items-center shrink-0"
  }, /*#__PURE__*/React.createElement("i", {
    "data-lucide": "clipboard",
    className: "w-3.5 h-3.5 text-emerald-400 shrink-0"
  })), /*#__PURE__*/React.createElement("span", {
    className: "flex-1"
  }, "Paste"), /*#__PURE__*/React.createElement("span", {
    className: "text-purple-400 text-xs font-semibold font-mono ml-auto"
  }, "Ctrl+V")), /*#__PURE__*/React.createElement("button", {
    onClick: () => {
      handleSubTabDelete(contextMenu.subTabId);
      closeContextMenu();
    },
    className: "w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2"
  }, /*#__PURE__*/React.createElement("span", {
    className: "inline-flex items-center shrink-0"
  }, /*#__PURE__*/React.createElement("i", {
    "data-lucide": "trash-2",
    className: "w-3.5 h-3.5 text-red-400 shrink-0"
  })), /*#__PURE__*/React.createElement("span", {
    className: "flex-1"
  }, "Delete Selected Segment"), /*#__PURE__*/React.createElement("span", {
    className: "text-amber-400 text-xs font-semibold font-mono ml-auto"
  }, "Del")), /*#__PURE__*/React.createElement("div", {
    className: "h-px bg-zinc-700 my-1"
  }), /*#__PURE__*/React.createElement("button", {
    onClick: () => {
      handleSubTabLoop(contextMenu.subTabId, 4);
      closeContextMenu();
    },
    className: "w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2"
  }, /*#__PURE__*/React.createElement("span", {
    className: "inline-flex items-center shrink-0"
  }, /*#__PURE__*/React.createElement("i", {
    "data-lucide": "repeat",
    className: "w-3.5 h-3.5 text-cyan-400 shrink-0"
  })), /*#__PURE__*/React.createElement("span", {
    className: "flex-1"
  }, "Loop Selection 4 times"), /*#__PURE__*/React.createElement("span", {
    className: "text-purple-400 text-xs font-semibold font-mono ml-auto"
  }, "Ctrl+L"))) : /*#__PURE__*/React.createElement("div", {
    className: "fixed z-[60] bg-[#2a2a2a] border border-zinc-700 rounded-lg shadow-2xl py-1 w-64 overflow-y-auto",
    style: {
      left: Math.min(contextMenu.x, window.innerWidth - 260),
      top: contextMenu.y + 260 > window.innerHeight ? undefined : contextMenu.y,
      bottom: contextMenu.y + 260 > window.innerHeight ? (window.innerHeight - contextMenu.y) : undefined,
      maxHeight: '60vh'
    },
    onClick: e => e.stopPropagation()
  }, contextMenu.sectionId ? /*#__PURE__*/React.createElement("button", {
    onClick: contextMenuEditSection,
    className: "w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2"
  }, /*#__PURE__*/React.createElement("span", {
    className: "inline-flex items-center shrink-0"
  }, /*#__PURE__*/React.createElement("i", {
    "data-lucide": "file-edit",
    className: "w-3.5 h-3.5 text-amber-400 shrink-0"
  })), /*#__PURE__*/React.createElement("span", {
    className: "flex-1"
  }, "Edit Section"), /*#__PURE__*/React.createElement("span", {
    className: "text-purple-400 text-xs font-semibold font-mono ml-auto pl-8"
  }, "Ctrl+E")) : /*#__PURE__*/React.createElement("button", {
    onClick: contextMenuEdit,
    className: "w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2"
  }, /*#__PURE__*/React.createElement("span", {
    className: "inline-flex items-center shrink-0"
  }, /*#__PURE__*/React.createElement("i", {
    "data-lucide": "file-edit",
    className: "w-3.5 h-3.5 text-amber-400 shrink-0"
  })), /*#__PURE__*/React.createElement("span", {
    className: "flex-1"
  }, "Edit"), /*#__PURE__*/React.createElement("span", {
    className: "text-purple-400 text-xs font-semibold font-mono ml-auto pl-8"
  }, "Ctrl+E")), /*#__PURE__*/React.createElement("button", {
    onClick: contextMenuSplit,
    className: "w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2"
  }, /*#__PURE__*/React.createElement("span", {
    className: "inline-flex items-center shrink-0"
  }, /*#__PURE__*/React.createElement("i", {
    "data-lucide": "scissors",
    className: "w-3.5 h-3.5 text-cyan-400 shrink-0"
  })), /*#__PURE__*/React.createElement("span", {
    className: "flex-1"
  }, "Split"), /*#__PURE__*/React.createElement("span", {
    className: "text-purple-400 text-xs font-semibold font-mono ml-auto pl-8"
  }, "S")), /*#__PURE__*/React.createElement("button", {
    onClick: contextMenuMerge,
    className: "w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2"
  }, /*#__PURE__*/React.createElement("span", {
    className: "inline-flex items-center shrink-0"
  }, /*#__PURE__*/React.createElement("i", {
    "data-lucide": "combine",
    className: "w-3.5 h-3.5 text-purple-400 shrink-0"
  })), /*#__PURE__*/React.createElement("span", {
    className: "flex-1"
  }, "Merge"), /*#__PURE__*/React.createElement("span", {
    className: "text-purple-400 text-xs font-semibold font-mono ml-auto pl-8"
  }, "Ctrl+M")), /*#__PURE__*/React.createElement("div", {
    className: "h-px bg-zinc-700 my-1"
  }), /*#__PURE__*/React.createElement("div", {
    className: "px-3 py-0.5 text-[10px] text-zinc-500 font-bold uppercase tracking-wider"
  }, "Insert"), !sessionTabs.some(s => s.id === activeTab) && /*#__PURE__*/React.createElement("button", {
    onClick: () => { insertSectionAtPlayhead(); closeContextMenu(); },
    className: "w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2"
  }, /*#__PURE__*/React.createElement("span", {
    className: "inline-flex items-center shrink-0"
  }, /*#__PURE__*/React.createElement("i", {
    "data-lucide": "folder-plus",
    className: "w-3.5 h-3.5 text-amber-400 shrink-0"
  })), /*#__PURE__*/React.createElement("span", {
    className: "flex-1"
  }, "Insert Section")), /*#__PURE__*/React.createElement("button", {
    onClick: () => { insertMidiItemAtPlayhead(); closeContextMenu(); },
    className: "w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2"
  }, /*#__PURE__*/React.createElement("span", {
    className: "inline-flex items-center shrink-0"
  }, /*#__PURE__*/React.createElement("i", {
    "data-lucide": "music",
    className: "w-3.5 h-3.5 text-purple-400 shrink-0"
  })), /*#__PURE__*/React.createElement("span", {
    className: "flex-1"
  }, "Insert MIDI Item")), /*#__PURE__*/React.createElement("button", {
    onClick: () => { insertSoundClipAtCursor(); closeContextMenu(); },
    className: "w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2"
  }, /*#__PURE__*/React.createElement("span", {
    className: "inline-flex items-center shrink-0"
  }, /*#__PURE__*/React.createElement("i", {
    "data-lucide": "file-audio",
    className: "w-3.5 h-3.5 text-emerald-400 shrink-0"
  })), /*#__PURE__*/React.createElement("span", {
    className: "flex-1"
  }, "Insert Sound Clip")), /*#__PURE__*/React.createElement("button", {
    onClick: () => { insertTrackBelow(); closeContextMenu(); },
    className: "w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2"
  }, /*#__PURE__*/React.createElement("span", {
    className: "inline-flex items-center shrink-0"
  }, /*#__PURE__*/React.createElement("i", {
    "data-lucide": "plus-square",
    className: "w-3.5 h-3.5 text-cyan-400 shrink-0"
  })), /*#__PURE__*/React.createElement("span", {
    className: "flex-1"
  }, "Insert Track")), /*#__PURE__*/React.createElement("div", {
    className: "h-px bg-zinc-700 my-1"
  }), /*#__PURE__*/React.createElement("button", {
    onClick: contextMenuCopy,
    className: "w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2"
  }, /*#__PURE__*/React.createElement("span", {
    className: "inline-flex items-center shrink-0"
  }, /*#__PURE__*/React.createElement("i", {
    "data-lucide": "copy",
    className: "w-3.5 h-3.5 text-zinc-400 shrink-0"
  })), /*#__PURE__*/React.createElement("span", {
    className: "flex-1"
  }, "Copy"), /*#__PURE__*/React.createElement("span", {
    className: "text-purple-400 text-xs font-semibold font-mono ml-auto pl-8"
  }, "Ctrl+C")), /*#__PURE__*/React.createElement("button", {
    onClick: contextMenuCut,
    className: "w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2"
  }, /*#__PURE__*/React.createElement("span", {
    className: "inline-flex items-center shrink-0"
  }, /*#__PURE__*/React.createElement("i", {
    "data-lucide": "scissors",
    className: "w-3.5 h-3.5 text-rose-400 shrink-0"
  })), /*#__PURE__*/React.createElement("span", {
    className: "flex-1"
  }, "Cut"), /*#__PURE__*/React.createElement("span", {
    className: "text-purple-400 text-xs font-semibold font-mono ml-auto pl-8"
  }, "Ctrl+X")), /*#__PURE__*/React.createElement("button", {
    onClick: contextMenuPaste,
    className: "w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2"
  }, /*#__PURE__*/React.createElement("span", {
    className: "inline-flex items-center shrink-0"
  }, /*#__PURE__*/React.createElement("i", {
    "data-lucide": "clipboard",
    className: "w-3.5 h-3.5 text-emerald-400 shrink-0"
  })), /*#__PURE__*/React.createElement("span", {
    className: "flex-1"
  }, "Paste"), /*#__PURE__*/React.createElement("span", {
    className: "text-purple-400 text-xs font-semibold font-mono ml-auto pl-8"
  }, "Ctrl+V")), /*#__PURE__*/React.createElement("div", {
    className: "h-px bg-zinc-700 my-1"
  }), /*#__PURE__*/React.createElement("button", {
    onClick: contextMenuDelete,
    className: "w-full px-3 py-1.5 text-xs text-red-300 hover:bg-red-950 text-left flex items-center gap-2"
  }, /*#__PURE__*/React.createElement("span", {
    className: "inline-flex items-center shrink-0"
  }, /*#__PURE__*/React.createElement("i", {
    "data-lucide": "trash-2",
    className: "w-3.5 h-3.5 text-red-400 shrink-0"
  })), /*#__PURE__*/React.createElement("span", {
    className: "flex-1"
  }, "Delete"), /*#__PURE__*/React.createElement("span", {
    className: "text-amber-400 text-xs font-semibold font-mono ml-auto pl-8"
  }, "Del")))), toastMessage && /*#__PURE__*/React.createElement("div", {
    className: "absolute top-14 right-4 z-50 px-4 py-2.5 rounded shadow-lg text-xs font-semibold flex items-center gap-2 border bg-zinc-900 text-zinc-100 border-zinc-800"
  }, /*#__PURE__*/React.createElement("span", {
    className: "inline-flex items-center shrink-0"
  }, /*#__PURE__*/React.createElement("i", {
    "data-lucide": "bell",
    className: `w-4 h-4 ${toastMessage.type === 'success' ? 'text-emerald-400' : toastMessage.type === 'error' ? 'text-rose-400' : toastMessage.type === 'warning' ? 'text-amber-400' : 'text-cyan-400'}`
  })), toastMessage.text, toastMessage.onActionClick && /*#__PURE__*/React.createElement("button", {
    onClick: () => {
      toastMessage.onActionClick();
      setToastMessage(null);
    },
    className: "ml-2 px-2 py-0.5 bg-purple-700 hover:bg-purple-600 text-white rounded text-[10px] uppercase font-bold"
  }, toastMessage.actionText || 'Tải về'))), appWarningModal && /*#__PURE__*/React.createElement("div", {
    className: "fixed inset-0 z-[100] flex items-center justify-center bg-black/60 backdrop-blur-sm"
  }, /*#__PURE__*/React.createElement("div", {
    className: "bg-[#222222] border border-zinc-800 rounded-lg shadow-2xl p-6 max-w-md w-full text-slate-200"
  }, /*#__PURE__*/React.createElement("div", {
    className: "flex items-center gap-2 mb-3 text-cyan-400 font-bold"
  }, /*#__PURE__*/React.createElement("h3", {
    className: "text-base font-bold"
  }, appWarningModal.title)), /*#__PURE__*/React.createElement("div", {
    className: "text-xs text-slate-300 mb-5 leading-relaxed whitespace-pre-line"
  }, appWarningModal.message), /*#__PURE__*/React.createElement("div", {
    className: "flex justify-end gap-2"
  }, appWarningModal.isAlert ? /*#__PURE__*/React.createElement("button", {
    onClick: () => setAppWarningModal(null),
    className: "px-4 py-2 bg-cyan-600 hover:bg-cyan-500 active:bg-cyan-700 text-white rounded text-xs font-semibold transition"
  }, "Đóng") : /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("button", {
    onClick: () => setAppWarningModal(null),
    className: "px-4 py-2 bg-zinc-700 hover:bg-zinc-600 active:bg-zinc-800 text-slate-300 rounded text-xs font-semibold transition"
  }, "Hủy"), /*#__PURE__*/React.createElement("button", {
    onClick: () => {
      const fn = appWarningModal.onConfirm;
      setAppWarningModal(null);
      if (fn) fn();
    },
    className: "px-4 py-2 bg-cyan-600 hover:bg-cyan-500 active:bg-cyan-700 text-white rounded text-xs font-semibold transition"
  }, "Xác nhận"))))), tabContextMenu && /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
    className: "fixed inset-0 z-[190]",
    onClick: () => setTabContextMenu(null),
    onContextMenu: e => {
      e.preventDefault();
      setTabContextMenu(null);
    }
  }), /*#__PURE__*/React.createElement("div", {
    className: "fixed bg-zinc-900 border border-zinc-800 rounded-lg shadow-2xl p-2 z-[200] flex gap-1.5",
    style: {
      top: tabContextMenu.y,
      left: tabContextMenu.x
    },
    onClick: e => e.stopPropagation()
  }, ['#f43f5e', '#f59e0b', '#10b981', '#06b6d4', '#8b5cf6', '#64748b'].map(color => /*#__PURE__*/React.createElement("button", {
    key: color,
    className: "w-5 h-5 rounded-full border border-zinc-700 hover:scale-110 active:scale-95 transition",
    style: {
      backgroundColor: color
    },
    onClick: () => {
      handleSetTabColor(tabContextMenu.tabId, tabContextMenu.tabType, color);
      setTabContextMenu(null);
    }
  })))), /*#__PURE__*/React.createElement(AuthModal, {
    isOpen: authModalOpen,
    mode: authMode,
    forceMandatory: isMandatoryLogin,
    onClose: () => setAuthModalOpen(false),
    onSuccess: handleAuthSuccess
  }), /*#__PURE__*/React.createElement(ProfileModal, {
    isOpen: profileModalOpen,
    onClose: () => setProfileModalOpen(false),
    tracks: tracks,
    setTracks: setTracks,
    setSelectedTrackId: setSelectedTrackId,
    projectName: projectName,
    setProjectName: setProjectName,
    currentProjectId: currentProjectId,
    setCurrentProjectId: setCurrentProjectId,
    showToast: showToast,
    loadAudioBuffersForTracks: loadAudioBuffersForTracks,
    setAppWarningModal: setAppWarningModal,
    bpm: bpm,
    setBpm: setBpm,
    setMasteringSettings: setMasteringSettings,
    setSessionTabs: setSessionTabs,
    setSubTabs: setSubTabs,
    trackMidiChannelsRef: trackMidiChannelsRef
  }), /*#__PURE__*/React.createElement(SaveProjectModal, {
    isOpen: saveProjectModalOpen,
    onClose: () => setSaveProjectModalOpen(false),
    projectName: projectName,
    onSaveCloud: (newName, existingId) => {
      handleSaveCloudProject(newName, existingId);
    },
    onSaveLocal: (newName) => {
      handleSaveLocalProject(newName);
    }
  }), /*#__PURE__*/React.createElement(SaveAsModal, {
    isOpen: saveAsModalOpen,
    onClose: () => setSaveAsModalOpen(false),
    projectName: projectName,
    onSaveCloud: (newName) => {
      handleSaveAsCloud(newName);
    },
    onSaveLocal: (newName) => {
      handleExportSFS(newName);
      setProjectName(newName);
      localStorage.setItem('sonic_project_name', newName);
    }
  }), /*#__PURE__*/React.createElement(OpenProjectModal, {
    isOpen: openProjectModalOpen,
    onClose: () => setOpenProjectModalOpen(false),
    onOpenCloud: (projId, projName) => {
      setOpenProjectModalOpen(false);
      handleOpenProject(projId, projName);
    },
    onOpenLocal: () => {
      setOpenProjectModalOpen(false);
      handleImportSFS();
    }
  }), /*#__PURE__*/React.createElement(AIConfigModal, {
    isOpen: aiConfigModalOpen,
    onClose: () => setAiConfigModalOpen(false),
    onConfigSaved: (providers) => {
      setAiProviders(providers);
      const active = providers.find(p => p.is_active) || providers[0];
      if (active) setSelectedProviderId(active.id);
    }
  }), /*#__PURE__*/React.createElement(AboutModal, {
    isOpen: aboutModalOpen,
    onClose: () => setAboutModalOpen(false)
  }), /*#__PURE__*/React.createElement(HelpModal, {
    isOpen: helpModalOpen,
    onClose: () => setHelpModalOpen(false),
    lang: prefs.language
  }), /*#__PURE__*/React.createElement(PreferencesModal, {
    isOpen: preferencesModalOpen,
    onClose: () => setPreferencesModalOpen(false),
    prefs: prefs,
    onPrefsChange: handlePrefsChange
  }), /*#__PURE__*/React.createElement(SystemManagerModal, {
    isOpen: systemManagerModalOpen,
    onClose: () => setSystemManagerModalOpen(false)
  }), /*#__PURE__*/React.createElement(PluginManagerModal, {
    isOpen: pluginManagerModalOpen,
    onClose: () => setPluginManagerModalOpen(false),
    pluginsData: pluginsData,
    onInsertInstrument: (inst) => {
      // Chèn instrument (soundfont bank/program) vào track đang chọn — nút Synth
      const tid = selectedTrackId || (activeTracks && activeTracks[0] && activeTracks[0].id);
      if (tid && inst && inst.instrumentId) {
        setTrackInstrumentWithProgram(tid, inst.instrumentId, inst.program, inst.displayName || inst.name, inst.bank);
        showToast('Đã chèn nhạc cụ: ' + (inst.displayName || inst.name), 'success');
      }
      setPluginManagerModalOpen(false);
    }
  }), /*#__PURE__*/React.createElement(AIPresetModal, {
    isOpen: aiPresetModalOpen,
    onClose: () => { setAiPresetModalOpen(false); setAiPresetVersion(v => v + 1); }
  }), /*#__PURE__*/React.createElement(MasteringModal, {
    isOpen: showMasteringModal,
    onClose: () => setShowMasteringModal(false),
    masteringSettings: masteringSettings,
    setMasteringSettings: setMasteringSettings
  }), fxRackTarget && /*#__PURE__*/React.createElement(FXRackModal, {
    track: tracks.find(t => t.id === fxRackTarget.trackId) || null,
    onUpdateTrack: updateTrackProp,
    onClose: () => setFxRackTarget(null)
  }), /*#__PURE__*/React.createElement(ExportModal, {
    open: showExportPanel,
    onClose: () => setShowExportPanel(false),
    exportSettings: exportSettings,
    setExportSettings: setExportSettings,
    isExporting: isExporting,
    onExport: triggerWavExport,
    onBounce: triggerBounceExport
  }), instrumentSelectorTrackId && /*#__PURE__*/React.createElement("div", {
    className: "fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm",
    onClick: closeInstrumentSelector
  }, /*#__PURE__*/React.createElement("div", {
    className: "bg-[#262626] border border-[#383838] rounded-xl shadow-2xl w-full max-w-4xl p-5 text-slate-200",
    onClick: e => e.stopPropagation()
  },
      /*#__PURE__*/React.createElement("div", { className: "flex justify-between items-center pb-3 border-b border-[#383838]" },
      /*#__PURE__*/React.createElement("h3", { className: "text-sm font-bold text-amber-400" }, "Select Instrument"),
      /*#__PURE__*/React.createElement("div", { className: "flex items-center gap-2" },
        /*#__PURE__*/React.createElement("select", {
          value: instrumentSelectorTrackId || '',
          onChange: e => { var tid = e.target.value; if (tid) { openInstrumentSelector(tid); } },
          className: "bg-[#1e1e1e] text-zinc-300 border border-zinc-700 rounded px-2 py-1 text-xs font-mono cursor-pointer"
        }, activeTracks.map(function(at) { return /*#__PURE__*/React.createElement("option", { key: at.id, value: at.id }, at.name + ' (' + (at.instrumentName || 'Synth') + ')'); })),
        /*#__PURE__*/React.createElement("button", { onClick: closeInstrumentSelector, className: "text-slate-400 hover:text-slate-200" }, "\u2715")
      )
      ),
      /*#__PURE__*/React.createElement("input", {
        type: "text",
        placeholder: "T\u00ecm nh\u1ea1c c\u1ee5...",
        value: sfPresetSearchQuery,
        onChange: e => setSfPresetSearchQuery(e.target.value),
        autoFocus: true,
        className: "w-full bg-black border border-zinc-700 rounded px-2 py-1.5 outline-none my-3 text-sm"
      }),
      /*#__PURE__*/React.createElement("div", { className: "flex gap-4", style: { height: "420px" } },
        /*#__PURE__*/React.createElement("div", { className: "w-1/3 border-r border-[#383838] overflow-y-auto space-y-0.5 pr-2" },
          /*#__PURE__*/React.createElement("button", {
            onClick: () => { setSelectedSoundFontId(null); setSynthCategory(null); },
            className: "w-full text-left px-3 py-2 text-sm rounded " + (!selectedSoundFontId ? "bg-amber-700 text-white" : "bg-zinc-800 hover:bg-zinc-700 text-zinc-300")
          }, "All Instruments"),
          (instrumentSelectorData?.soundfonts || []).map(sf => {
            const sfId = sf.id.startsWith('sf_') ? sf.id : 'sf_' + sf.id;
            const sfName = sf.display || sf.name || sf.id;
            return /*#__PURE__*/React.createElement("button", {
              key: sfId,
              onClick: () => {
                setSelectedSoundFontId(sfId);
                setSfPresets(null);
                const baseId = sfId.replace('sf_', '');
                window.SonicAPI.listSoundfontInstruments(baseId).then(data => {
                  if (data && data.presets) {
                    const mapping = data.presets.map(p => ({ ...p, _sfId: sfId, _sfName: sfName, _sfDisplay: sfName.substring(0, 30) }));
                    setSfPresets(mapping);
                    setInstrumentSelectorData(prev => prev ? { ...prev, soundfonts: (prev.soundfonts || []).map(s => s.id === sf.id ? { ...s, presets: data.presets } : s) } : prev);
                  } else {
                    setSfPresets([]);
                  }
                }).catch(() => { setSfPresets([]); });
              },
              className: "w-full text-left px-3 py-2 text-sm rounded " + (selectedSoundFontId === sfId ? "bg-amber-700 text-white" : "bg-zinc-800 hover:bg-zinc-700 text-zinc-300")
            }, sfName);
          }),
          // ── VST Instruments (scanned bởi Plugin Manager) ──
          (instrumentSelectorData?.vst_instruments?.length > 0) && /*#__PURE__*/React.createElement("div", { className: "mt-3 text-[10px] text-zinc-500 uppercase font-bold px-1 flex items-center gap-1" }, /*#__PURE__*/React.createElement("i", { "data-lucide": "cpu", className: "w-3 h-3 text-violet-400" }), "VST Instruments"),
          (instrumentSelectorData?.vst_instruments || []).map(v => /*#__PURE__*/React.createElement("button", {
            key: 'vst_modal_' + (v.id || v.name),
            onClick: () => {
              setTrackInstrumentWithUndo(instrumentSelectorTrackId, v.id, v.name || v.id);
              closeInstrumentSelector();
            },
            className: "w-full text-left px-3 py-2 text-sm rounded bg-zinc-800 hover:bg-violet-800 text-zinc-300 flex items-center justify-between gap-2"
          }, /*#__PURE__*/React.createElement("span", { className: "truncate" }, v.name || v.id), /*#__PURE__*/React.createElement("span", { className: "text-[9px] text-cyan-400 shrink-0" }, v.type || "VST")))
        ),
        /*#__PURE__*/React.createElement("div", { className: "flex-1 overflow-y-auto space-y-0.5" },
          /*#__PURE__*/React.createElement("button", {
            onClick: () => setTrackInstrumentWithUndo(instrumentSelectorTrackId, null),
            className: "w-full text-left px-3 py-2 text-sm rounded bg-zinc-800 hover:bg-zinc-700 text-zinc-400"
          }, "None (Default Synth)"),
          sfPresets === null ? (
            /*#__PURE__*/React.createElement("p", { className: "text-sm text-zinc-500 py-2" }, "Loading instruments...")
          ) : (
            sfPresets.length > 0 ? (
              sfPresets
                .filter(p => !selectedSoundFontId || p._sfId === selectedSoundFontId)
                .filter(p => !sfPresetSearchQuery || (p.name || '').toLowerCase().includes(sfPresetSearchQuery.toLowerCase()) || (p._sfName || '').toLowerCase().includes(sfPresetSearchQuery.toLowerCase()))
                .map((p, i) => /*#__PURE__*/React.createElement("button", {
                  key: i,
                  onClick: () => setTrackInstrumentWithUndo(instrumentSelectorTrackId, p._sfId, p.name || 'Preset ' + p.program, p.bank, p.program),
                  onDoubleClick: () => { setTrackInstrumentWithUndo(instrumentSelectorTrackId, p._sfId, p.name || 'Preset ' + p.program, p.bank, p.program); closeInstrumentSelector(); },
                  className: "w-full text-left px-3 py-2 text-sm rounded bg-zinc-800/60 hover:bg-amber-800 text-zinc-300 truncate flex items-center gap-2"
                },
                  /*#__PURE__*/React.createElement("span", { className: "text-xs text-zinc-500 shrink-0" }, p._sfDisplay),
                  p.bank === 128 ? /*#__PURE__*/React.createElement("span", { className: "mr-1" }, "🥁") : null,
                  p.name || 'Preset ' + p.program
                ))
            ) : (
              /*#__PURE__*/React.createElement("p", { className: "text-sm text-zinc-500 py-2" }, sfPresetSearchQuery ? "Kh\u00f4ng t\u00ecm th\u1ea5y nh\u1ea1c c\u1ee5 ph\u00f9 h\u1ee3p." : "No presets found.")
            )
          )
        )
      )
    )
  ), fxSelectorTrackId && /*#__PURE__*/React.createElement("div", {
    className: "fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm",
    onClick: () => setFxSelectorTrackId(null)
  }, /*#__PURE__*/React.createElement("div", {
    className: "bg-[#262626] border border-[#383838] rounded-xl shadow-2xl w-64 p-4 text-slate-200",
    onClick: e => e.stopPropagation()
  }, /*#__PURE__*/React.createElement("h3", { className: "text-sm font-bold text-purple-400 mb-3" }, "Track FX"), /*#__PURE__*/React.createElement("div", { className: "space-y-1" },
    /*#__PURE__*/React.createElement("button", { onClick: () => handleSetTrackFx(fxSelectorTrackId, null), className: "w-full text-left px-3 py-2 text-xs rounded bg-zinc-800 hover:bg-zinc-700 text-zinc-400" }, "None"),
    /*#__PURE__*/React.createElement("button", { onClick: () => handleSetTrackFx(fxSelectorTrackId, 'chorus'), className: "w-full text-left px-3 py-2 text-xs rounded bg-zinc-800 hover:bg-cyan-800 text-cyan-300" }, "Chorus"),
    /*#__PURE__*/React.createElement("button", { onClick: () => handleSetTrackFx(fxSelectorTrackId, 'reverb'), className: "w-full text-left px-3 py-2 text-xs rounded bg-zinc-800 hover:bg-purple-800 text-purple-300" }, "Reverb")
  ))), instrumentDropdownTrackId && instrumentDropdownBtnRect && /*#__PURE__*/React.createElement("div", {
    "data-instr-dropdown": "",
    className: "fixed z-[100] bg-[#1e1e1e] border border-zinc-700 rounded shadow-xl w-[220px] max-h-[300px] flex flex-col",
    style: {
      top: instrumentDropdownBtnRect.bottom + 4,
      left: Math.max(4, Math.min(instrumentDropdownBtnRect.left, window.innerWidth - 224))
    }
  }, /*#__PURE__*/React.createElement("input", {
    type: "text",
    placeholder: "T\u00ecm nh\u1ea1c c\u1ee5...",
    value: instrumentSearchQuery,
    onChange: e => setInstrumentSearchQuery(e.target.value),
    autoFocus: true,
    className: "w-full bg-black border-b border-zinc-700 px-2 py-1.5 text-xs outline-none"
  }), /*#__PURE__*/React.createElement("div", { className: "flex-1 overflow-y-auto" },
    /*#__PURE__*/React.createElement("button", {
      onClick: () => setTrackInstrumentWithUndo(instrumentDropdownTrackId, null),
      className: "w-full text-left px-3 py-1.5 text-xs bg-zinc-800 hover:bg-zinc-700 text-zinc-400"
    }, "None (Default Synth)"),
    filteredInstruments.soundfonts.length > 0 && /*#__PURE__*/React.createElement("div", { className: "text-[10px] text-zinc-500 px-3 py-1 mt-1 uppercase font-bold" }, "SoundFonts"),
    filteredInstruments.soundfonts.map((sf, i) => /*#__PURE__*/React.createElement("button", {
      key: "sfd_" + i,
      onClick: () => { setInstrumentDropdownTrackId(null); setInstrumentDropdownBtnRect(null); setTrackInstrumentWithUndo(instrumentDropdownTrackId, "sf_" + sf.id, sf.display || sf.name || sf.id, undefined, undefined); },
      className: "w-full text-left px-3 py-1.5 text-xs bg-zinc-800 hover:bg-amber-800 text-zinc-200 flex items-center justify-between"
    }, /*#__PURE__*/React.createElement("span", { className: "truncate" }, sf.display || sf.name || sf.id), /*#__PURE__*/React.createElement("span", { className: "text-[10px] text-amber-400 shrink-0 ml-1" }, "SF"))),
    filteredInstruments.vst.length > 0 && /*#__PURE__*/React.createElement("div", { className: "text-[10px] text-zinc-500 px-3 py-1 mt-1 uppercase font-bold" }, "VST Instruments"),
    filteredInstruments.vst.map((v, i) => /*#__PURE__*/React.createElement("div", {
      key: "vstd_" + i,
      className: "flex items-stretch"
    },
      /*#__PURE__*/React.createElement("button", {
        onClick: () => {
          setInstrumentDropdownTrackId(null);
          setInstrumentDropdownBtnRect(null);
          setTrackInstrumentWithUndo(instrumentDropdownTrackId, v.id, v.name || v.id);
        },
        className: "flex-1 min-w-0 text-left px-3 py-1.5 text-xs bg-zinc-800 hover:bg-violet-800 text-zinc-200 flex items-center justify-between"
      }, /*#__PURE__*/React.createElement("span", { className: "truncate" }, v.name || v.id), /*#__PURE__*/React.createElement("span", { className: "text-[10px] text-cyan-400 shrink-0 ml-1" }, v.type || "VST")),
    )),
    filteredInstruments.vst.length > 0 && /*#__PURE__*/React.createElement("div", { className: "text-[10px] text-zinc-500 px-3 py-1 mt-1 uppercase font-bold" }, "VST Presets (.vstpreset)"),
    (window.SonicRuntime && window.SonicRuntime.presets || []).map((p, i) => /*#__PURE__*/React.createElement("button", {
      key: "psd_" + i,
      onClick: () => setTrackPreset(instrumentDropdownTrackId, p.id),
      className: "w-full text-left px-3 py-1 text-xs bg-zinc-800 hover:bg-emerald-800 text-zinc-300 flex items-center justify-between"
    }, /*#__PURE__*/React.createElement("span", { className: "truncate" }, p.name || p.id), /*#__PURE__*/React.createElement("span", { className: "text-[10px] text-emerald-400 shrink-0 ml-1" }, "PRESET"))),
    /*#__PURE__*/React.createElement("button", {
      onClick: () => {
        const inp = document.createElement('input');
        inp.type = 'file';
        inp.accept = '.vstpreset,.fxp,.fxb,.dspreset';
        inp.onchange = () => {
          const f = inp.files && inp.files[0];
          if (!f) return;
          window.SonicAPI.uploadPreset(f).then(function (r) {
            if (r && r.success) {
              if (window.SonicRuntime) window.SonicRuntime.refreshPresets();
              showToast('Đã upload preset: ' + (r.original_name || r.name || ''), 'success');
            } else { showToast('Upload preset thất bại', 'error'); }
          }).catch(function (err) { showToast('Upload lỗi: ' + (err.message || err), 'error'); });
        };
        inp.click();
      },
      className: "w-full text-left px-3 py-1 text-xs bg-zinc-800 hover:bg-teal-800 text-teal-300 border-t border-zinc-700",
      title: "Upload preset .vstpreset xuất từ Carla (native GUI)"
    }, "\u2B06 Upload preset (t\u1EEB Carla...)"),
    (!filteredInstruments.soundfonts.length && !filteredInstruments.vst.length) && /*#__PURE__*/React.createElement("p", { className: "text-xs text-zinc-500 py-4 text-center" }, "Kh\u00f4ng t\u00ecm th\u1ea5y nh\u1ea1c c\u1ee5 n\u00e0o")
  )));
};
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(/*#__PURE__*/React.createElement(App, null));
setTimeout(() => lucide.createIcons(), 300);
