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

// ── FastAPI Backend Configuration ──
const API_BASE_URL = window.location.origin;
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 };
};

// Đả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);
  }
};


// 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) => {
  if (masteringChainOn()) return false;
  return trackMidiBypassMap[track.id] !== undefined ? !!trackMidiBypassMap[track.id] : !!(track.midiBypass ?? track.masteringBypass);
};
const effAudioBypass = (track) => {
  if (masteringChainOn()) return false;
  return trackAudioBypassMap[track.id] !== undefined ? !!trackAudioBypassMap[track.id] : !!(track.audioBypass ?? track.masteringBypass);
};

// 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;
  if (track && track.id && trackAudioBypassMap[track.id] !== undefined) {
    bypass = !!trackAudioBypassMap[track.id];
  } else if (track) {
    bypass = !!(track.audioBypass ?? track.masteringBypass);
  }
  // Mastering chain ON → MỌI track qua chain (♪ bị override — user requirement)
  if (masteringChainOn()) bypass = false;
  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;
}

// 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.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 {
    // '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 === '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);
  }
  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
      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;
            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();
        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; }
      }
      if (onContextMenu) onContextMenu(e, track.id, time, hitSectionId);
    }
  }));
};
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);
  useEffect(() => {
    if (mode) setActiveTab(mode);
    if (mode === 'force_change' && !oldPassword) {
      setOldPassword('admin123');
    }
  }, [mode]);
  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 ", /*#__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' && /*#__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 && /*#__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 }) => {
  if (!isOpen) return null;
  const [localData, setLocalData] = React.useState(pluginsData);
  const [sfUploadStatus, setSfUploadStatus] = React.useState('');
  const [sfToDelete, setSfToDelete] = React.useState(null);
  React.useEffect(() => {
    if (isOpen) {
      window.SonicAPI.listPlugins()
        .then(data => setLocalData(data))
        .catch(() => setLocalData({ vst_instruments: [], soundfonts: [] }));
      setTimeout(() => { try { window.lucide.createIcons(); } catch(e) {} }, 50);
    }
  }, [isOpen]);
  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);
    }
  };
  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',
    style: { maxHeight: '80vh' },
    onClick: e => e.stopPropagation()
  },
    // 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('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('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) =>
                React.createElement('div', {
                  key: i,
                  className: 'flex items-center justify-between bg-[#252525] px-4 py-3 rounded-lg border border-[#333] hover:border-amber-800/50 transition group'
                },
                  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)
                    )
                  ),
                  React.createElement('div', { className: 'flex items-center gap-2' },
                    React.createElement('button', {
                      onClick: () => setSfToDelete(sf),
                      className: 'text-[10px] text-zinc-500 hover:text-red-400 opacity-0 group-hover:opacity-100 transition px-2 py-1'
                    }, 'Delete')
                  )
              )
            ))
          ),
          // 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')
        )
      )
    )
  );
};

// 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) => {
  if (!window.SonicSF || !window.SonicSF.selectInstrument) return;
  const list = tracks || [];
  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) {}
  }
};

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")))))))));
};

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 }) => {
  const [activeRollTool, setActiveRollTool] = React.useState('select');
  const [renderTick, setRenderTick] = React.useState(0);
  const [ccMode, setCcMode] = React.useState('velocity');
  const [rollZoom, setRollZoom] = React.useState(60); // local horizontal zoom factor
  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);
  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() {
    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]);

  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));
      }
    };
    window.addEventListener('keydown', handler);
    return () => window.removeEventListener('keydown', handler);
  }, [notes, setSelectedNoteIds]);

  // 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');
  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 => {
            window.SonicSF.playNote(n.pitch, (n.velocity || 0.8) * 127, 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);

    // 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);
      ctx.fillStyle = isBlack ? '#1a1a1e' : '#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]);

  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]);

  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);
        window.SonicSF.playNote(notes[clickedNoteIdx].pitch, 100, 300, ctx.currentTime, clCtx.program, null, clCtx.ch, clCtx.synthEngine);
      }
    }

    // Ctrl+click: toggle selection (multi-select)
    if (e.ctrlKey && !e.altKey && !e.shiftKey) {
      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;
      } 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;
        }
      }
      // 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;
      }
      if (window.SonicSF && window.SonicSF.playNote) {
        const ctx = getAudioContext();
        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));
        // 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).
        window.SonicSF.playNote(dwPitch, Math.round(brushVelocityRef.current * 127), 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();
        if (window.SonicSF && window.SonicSF.playNote) {
          var pvCtx = getAudioContext();
          var pvTrk = activeTracks.find(function(t) { return t.id === st.trackId; });
          var pvCtxInst = resolveTrackInstrumentCtx(pvTrk, activeTracks);
          var pvVel = Math.round(brushVelocityRef.current * 127);
          // playNote (FluidSynth — nhạc cụ THẬT). _playNoteFallback = oscillator
          // beep sai âm (percussion/soundfont).
          window.SonicSF.playNote(p, pvVel, durMs, pvCtx.currentTime, pvCtxInst.program, null, pvCtxInst.ch, pvCtxInst.synthEngine);
          previewPitchRef.current = p;
        }
      }

      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);
    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);

  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;
  };

  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 {
        ccDragRef.current = { active: true, lastBeat: beat, lastPainted: noteIdx !== -1 ? [noteIdx] : [] };
      }
      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;
    }

    const candidateIdx = findCCNoteIndex(beat, y, h);
    if (candidateIdx !== -1 && !painted.includes(candidateIdx)) {
      const currentVal = ccMode === 'pan' ? ((notes[candidateIdx].pan || 0) / 2.0 + 0.5) : (notes[candidateIdx].velocity !== undefined ? notes[candidateIdx].velocity : 0.8);
      if (Math.abs(currentVal - val) > 0.001) {
        const updatedNotes = notes.map((n, i) => {
          if (i !== candidateIdx) 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, candidateIdx];
    }
  };

  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 {
              if (window.triggerMidiVuActivity) {
                window.triggerMidiVuActivity(st.trackId, 100);
              }
              if (window.SonicSF) {
                window.SonicSF.playNote(pitch, 100, 500, undefined, kbCtx.program, null, kbCtx.ch, kbCtx.synthEngine);
              }
            } catch (err) {
              console.error('playNote error:', err);
            }
          },
          onMouseEnter: (e) => {
            if (keybedMouseDownRef.current) {
              try {
                if (window.triggerMidiVuActivity) {
                  window.triggerMidiVuActivity(st.trackId, 100);
                }
                if (window.SonicSF) {
                  window.SonicSF.playNote(pitch, 100, 200, undefined, kbCtx.program, null, kbCtx.ch, kbCtx.synthEngine);
                }
              } catch (err) { console.error('playNote error:', err); }
            }
          },
          onMouseUp: () => { keybedMouseDownRef.current = false; }
        }, 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;
  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 [showCC, setShowCC] = React.useState(true);
  const [ccHeight, setCcHeight] = React.useState(80);

  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;
  };

  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 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-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("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'))), 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"
    }, "\\uD83D\\uDC7B MIDI ghost notes"), React.createElement("div", {
      style: { flexBasis: "100%", height: 0 }
    }), React.createElement("button", {
      onClick: applyHumanize,
      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: randomize velocity + timing"
    }, "\uD83C\uDF9A Humanize"), React.createElement("select", {
      key: "humstr", value: humanizeStrength,
      onChange: function(e) { setHumanizeStrength(parseFloat(e.target.value)); },
      className: "px-1 py-1 rounded text-xs bg-zinc-900 border border-zinc-700 text-zinc-300 cursor-pointer",
      title: "Cường độ humanize"
    }, 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)"
    }, "\uD83C\uDFB5 Chuy\u1EC3n gi\u1ECDng")), React.createElement("div", {
      className: "flex items-center gap-1"
    }, 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"), React.createElement("button", {
      onClick: onClose,
      className: "px-2.5 py-1 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 border border-zinc-700 rounded text-xs flex items-center gap-1 transition"
    }, React.createElement("i", {
      "data-lucide": "x",
      className: "w-3 h-3"
    }), "Đóng"))), 

    /* 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"
    }, /* 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("button", {
          key: m._trackId,
          onClick: function() {
            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);
          },
          className: "flex items-center justify-center h-[20px] border border-zinc-600 rounded-md cursor-pointer outline-none mx-1 my-[2px] " + (isActive ? 'bg-yellow-600 text-black font-bold' : (isPlayOn ? 'bg-red-700 text-white font-semibold' : 'bg-zinc-700 text-zinc-300 hover:bg-zinc-600'))
        }, React.createElement("span", {
          className: "text-[14px] font-sans truncate px-1",
          title: track ? track.name : m._trackName
        }, track ? track.name : m._trackName)));
      });
      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 */
    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: {
            referenced_section_id: s.sectionId || s.id
          }
        });
      });
    }

    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) => {
  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;
        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 ? deserializeTracksList(secContainer.tracks, secondsPerBar, sectionStore) : 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
    },
    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)
  };
};

// ──────────────────────────────────────────────
// 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' }
};
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 }
};

// 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 });

  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>
          )}
        </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);

  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' }
  };
  const chainFlag = (type) => type === 'eq' ? 'eqActive' : type === 'eqpro' ? 'eqproActive' : type === 'imager' ? 'imagerActive' : type === 'maximizer' ? 'maximizerActive' : type === 'compressor' ? 'compActive' : type === 'limiter' ? 'limActive' : type === 'exciter' ? 'excActive' : '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)) };
    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>

            {/* 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 }) => {
  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);
  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)}`;
    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)) {
      selectTokenRef.current++;
      const token = selectTokenRef.current;
      try { if (window.SonicSF && typeof window.SonicSF.stopAll === 'function') window.SonicSF.stopAll(); } catch (e) {}
      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();
      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;
      if (curInst && window.SonicSF.selectInstrument) {
        try { await window.SonicSF.selectInstrument(0, 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 }));
        });
      });
      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, 0, 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; }
    if (window.SonicSF && typeof window.SonicSF.stopAll === 'function') {
      try { window.SonicSF.stopAll(); } catch (e) {}
    }
    setIsPlaying(false);
    setIsPaused(false);
  }, []);

  const playSelected = async (f, token) => {
    if (!f) return;
    stopMediaPlayback();
    if (isMidiFile(f)) {
      if (f.handle || f.path || f.file_id || f.fileId) {
        // 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)) {
      selectTokenRef.current++;
      const token = selectTokenRef.current;
      try { if (window.SonicSF && typeof window.SonicSF.stopAll === 'function') window.SonicSF.stopAll(); } catch (e) {}
      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)) {
          selectTokenRef.current++;
          const token = selectTokenRef.current;
          try { if (window.SonicSF && typeof window.SonicSF.stopAll === 'function') window.SonicSF.stopAll(); } catch (e) {}
          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]);
  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"
  ];
  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; } }
    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 SpessaSynth 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);
    }
    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);
  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]);
  const [isLoopingSelection, setIsLoopingSelection] = useState(false);
  const [beginBar, setBeginBar] = useState(0);
  const [endBar, setEndBar] = useState(0);
  const [numberBar, setNumberBar] = useState(1);
  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));
                   // Route MIDI input to ALL armed tracks on their dedicated channels
                   // Do NOT use raw MIDI hardware channel (msg.data[0] & 0x0F)
                   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);
                          }
                          window.SonicSF.playNote(pitch, scaledVel, 60000, undefined, asProg, null, asCh, asSe);
                       });
                     }
                     // 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);
                        }
                        window.SonicSF.playNote(pitch, scaledVel, 60000, undefined, atProg, atDest, atCh, atSe);
                     });
                   }
               } 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.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);
                     window.SonicSF.stopNote(stCh, pitch);
                   });
                 }
              }

            // ── 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 "<trackId>_sub_<subId>") so section items bypass instantly.
      Object.keys(activeTrackNodesRef.current).forEach(function(k) {
        if (k === trackId || k.indexOf(trackId + '_sub_') === 0) {
          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);
      }
      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);
      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.
      if ((t.midiItems && t.midiItems.length > 0) || t.type === 'MIDI') {
        try {
          if (window.SonicSF && typeof window.SonicSF.controllerChange === 'function') {
            const ch = ensureTrackMidiChannel(t, effective);
            window.SonicSF.controllerChange(ch, 7, audible ? 100 : 0);
          }
        } 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 }
  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);
  };

  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);
  };

  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 [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;

  // 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(() => {
    const all = [...(tracks || []), ...(sessionTabs || []).reduce((acc, s) => acc.concat(s.tracks || []), [])];
    all.forEach(t => {
      trackMasteringBypassMap[t.id] = !!t.audioBypass;
      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.
      if ((t.midiItems && t.midiItems.length > 0) || t.type === 'MIDI') {
        try {
          if (window.SonicSF && typeof window.SonicSF.controllerChange === 'function') {
            const ch = ensureTrackMidiChannel(t, list);
            window.SonicSF.controllerChange(ch, 7, audible ? 100 : 0);
          }
        } catch (e) {}
      }
      trackAudibleRef.current[t.id] = audible;
    });
    updateSfRouting();
  }, [tracks, sessionTabs]);

  const prevActiveTabRef = useRef(activeTab);
  useEffect(() => {
    const prevTab = prevActiveTabRef.current;
    prevActiveTabRef.current = activeTab;
    updateSfRouting();
    // 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) {
      const prevSub = subTabsRef.current.find(s => s.id === prevTab);
      if (prevSub && prevSub.isPlaying) {
        try { stopAllPlayback(); } catch (e) {}
        setSubTabs(prev => prev.map(s => s.id === prevTab ? { ...s, isPlaying: false } : s));
      }
    }
  }, [activeTab]);

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

  const midiVuActivityRef = 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;
  };
  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);
  // 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: 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,
  });

  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 = effAudioBypass(_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);

  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 };
      }));
      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 }] };
      }
      return { ...t, buffer: trackBuffer, channelInfo: trackChannelInfo, clips: updatedClips };
    }));
    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 };
        });
      });
      setSessionTabs(prev => prev.map(st => ({
        ...st,
        tracks: (st.tracks || []).map(t => {
          const found = updatedTracks.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;
        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);
      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,
          });
        }
      } 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) { }
      })();
    }
  };
  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]);

  // ── 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);
  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;
    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, 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, 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, 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 = endSample - startSample;
    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, 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 = endSample - startSample;
    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, 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');
  };
  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)) {
        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();
        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 (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();
        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');
        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();
        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();
        handleCopyTrack();
        return;
      }
      if (ctrl && !alt && e.key === 'x') {
        e.preventDefault();
        handleCutTrack();
        return;
      }
      if (ctrl && !alt && e.key === 'v') {
        e.preventDefault();
        handlePasteTrack();
        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') {
          // handled by main handler
        } else if (curTab.startsWith('session_')) {
          // Main session: save project + save all dirty sub-tabs
          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));
              }
            }
            showToast('Đã lưu', 'success');
          });
        } else if (curTab.startsWith('session_')) {
          // Section tab: save section
          handleSaveSectionTabRef.current(curTab);
          showToast('Đã lưu Section', '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) 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);
    // 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');
  };

  const handleSaveSectionTab = (tabId) => {
    const tab = sessionTabs.find(s => s.id === tabId);
    if (!tab) return;
    const bpmVal = parseInt(bpm) || 120;
    const secondsPerBeat = 60.0 / bpmVal;
    const secondsPerBar = secondsPerBeat * 4;

    const contentTracks = tab.tracks ? tab.tracks.filter(tr => tr.clips?.length > 0 || tr.midiItems?.length > 0) : [];
    let maxEndTime = 0;
    (contentTracks || []).forEach(tr => {
      (tr.clips || []).forEach(c => {
        const end = (c.startTime || 0) + (c.buffer ? c.buffer.duration / (c.speed || 1.0) : 4);
        if (end > maxEndTime) maxEndTime = end;
      });
      (tr.midiItems || []).forEach(m => {
        const end = (m.startTime || 0) + (m.duration || 4);
        if (end > maxEndTime) maxEndTime = end;
      });
    });
    const durationSec = Math.max(maxEndTime, 4 * secondsPerBar);

    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,
            duration: durationSec,
            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 ? (JSON.parse(JSON.stringify(section.tracks))).map(t => ({
      ...t,
      clips: [],
      sections: [],
      markers: [],
      isArmed: false,
      monitoringEnabled: true,
      instrumentId: t.instrumentId || null,
      instrumentProgram: t.instrumentProgram,
      instrumentName: t.instrumentName || null,
      _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,
      _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;
      }
    }

    // Update subTab buffer state
    setSubTabs(prev => prev.map(s => {
      if (s.id !== tabId) return s;
      return {
        ...s,
        buffer: resultBuffer,
        selectionStart: null,
        selectionEnd: null
      };
    }));
    showToast(`Đã áp dụng ${effectType === 'normalize' ? 'Normalize' : effectType === 'gain' ? 'Gain' : 'Pitch'} 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) => {
    e.preventDefault();
    e.stopPropagation();
    setContextMenu({
      x: e.clientX,
      y: e.clientY,
      trackId,
      time: clickTime || currentTime,
      sectionId: sectionId || 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();
      showToast('Đã xoá section item.', 'info');
      return;
    }

    if (clickedMidi) {
      const beforeSnap = captureTrackSnapshot(tid);
      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();
      showToast('Đã xoá audio clip.', 'info');
      return;
    }

    // No item clicked under cursor -> Attempt to delete the track itself
    const hasClips = (track.clips && track.clips.length > 0) || !!track.buffer;
    const hasMidi = track.midiItems && track.midiItems.length > 0;
    const hasSections = track.sections && track.sections.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 = () => {
    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 = () => {
    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 = () => doPaste(selectedTrackId, currentTime);
  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);
    } 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]);
  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(() => {
    if (zoom < minZoom) {
      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 ──
  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 = () => {
    // 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';
    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 ? 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 || !st.isPlaying || !st.buffer) 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) {
      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 = [];
      if ((track.midiItems && track.midiItems.length > 0)) {
        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;
        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;
      if (_activeSub && _activeSub.type === 'PIANO_ROLL') {
        const _prNode = activeTrackNodesRef.current[_activeSub.trackId];
        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);
            return;
          }
        }
      }
      const midiAudible = list.filter(t => (t.midiItems && t.midiItems.length > 0) && computeTrackAudibleGain(list, t) > 0);
      if (midiAudible.length === 1) {
        const t = midiAudible[0];
        const node = activeTrackNodesRef.current[t.id];
        // 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.
        if (node && node.sfEntry && window.SonicSF && window.SonicSF.setOutputDestination) {
          window.SonicSF.setOutputDestination(node.sfEntry);
          // Ép route qua mastering chain NGAY tại thời điểm routing.
          if (node.sfRouteGain && node.sfDryGain) {
            const _b = effMidiBypass(t.id);
            node.sfRouteGain.gain.value = _b ? 0 : 1;
            node.sfDryGain.gain.value = _b ? 1 : 0;
          }
          return;
        }
        if (node && node.gainNode && window.SonicSF && window.SonicSF.setOutputDestination) {
          window.SonicSF.setOutputDestination(node.gainNode);
          return;
        }
      }
      if (window.SonicSF && window.SonicSF.setOutputDestination) {
        window.SonicSF.setOutputDestination(null);
      }
    } catch (e) {
      console.warn('updateSfRouting error:', e);
    }
  };
  const updateSfRoutingRef = useRef(null);
  // ── 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);
      gainNode.connect(parentNode);
      node = { gainNode, pannerNode: null };
      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;
    window.SonicSF.playNote(
      pitch,
      velocity,
      durationMs,
      context.currentTime,
      program,
      destNode,
      prevCh,
      track ? track.synth_engine : undefined
    );
  };

  const startTrackPlayback = offsetTime => {
    const context = getAudioContext();
    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'));
    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;
      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 || [];
      if ((track.type === 'MIDI' || midiItems.length > 0) && window.SonicSF) {
        // 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 (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);
              if (offsetTime < noteStartSec) {
                const delay = noteStartSec - offsetTime;
                const startTime = context.currentTime + delay;
                window.SonicSF.playNote(
                  note.pitch || 60,
                  note.velocity || 0.8,
                  durationMs,
                  startTime,
                  program,
                  gainNode,
                  trkCh,
                  track.synth_engine
                );
                // Trigger VU meter flash when the note starts playing
                setTimeout(() => {
                  if (window.triggerMidiVuActivity) {
                    window.triggerMidiVuActivity(track.id, note.velocity || 0.8);
                  }
                }, delay * 1000);
              } else {
                const playOffset = offsetTime - noteStartSec;
                const remainingDurMs = (noteEndSec - offsetTime) * 1000;
                window.SonicSF.playNote(
                  note.pitch || 60,
                  note.velocity || 0.8,
                  remainingDurMs,
                  context.currentTime,
                  program,
                  gainNode,
                  trkCh,
                  track.synth_engine
                );
                // Trigger VU meter flash instantly
                if (window.triggerMidiVuActivity) {
                  window.triggerMidiVuActivity(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;

            // 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 || [];
            if (window.SonicSF && subMidiItems.length > 0) {
              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);

                    if (offsetTime < noteStartMain) {
                      const delay = noteStartMain - offsetTime;
                      const startTime = context.currentTime + delay;
                      const playDurMs = (notePlayEndMain - noteStartMain) * 1000;
                      window.SonicSF.playNote(
                        note.pitch || 60,
                        note.velocity || 0.8,
                        playDurMs,
                        startTime,
                        program,
                        subNode,
                        subCh,
                        subTrack.synth_engine
                      );
                    } else {
                      const remainingDurMs = (notePlayEndMain - offsetTime) * 1000;
                      window.SonicSF.playNote(
                        note.pitch || 60,
                        note.velocity || 0.8,
                        remainingDurMs,
                        context.currentTime,
                        program,
                        subNode,
                        subCh,
                        subTrack.synth_engine
                      );
                    }
                  }
                });
              });
            }
          });
        });
      }
    });
    updateSfRouting();
  };

  // Solo playback for Local Selection Loop (LOOP_MAKER.md §2.2)
  const startLocalTrackPlayback = (trackId, offsetTime) => {
    const context = getAudioContext();
    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 || [];
    if ((track.type === 'MIDI' || midiItems.length > 0) && window.SonicSF) {
      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;
            if (offsetTime < noteStartSec) {
              const delay = noteStartSec - offsetTime;
              const startTime = context.currentTime + delay;
              window.SonicSF.playNote(
                note.pitch || 60,
                note.velocity || 0.8,
                durationMs,
                startTime,
                program,
                gainNode,
                lcCh,
                track.synth_engine
              );
            } else {
              const remainingDurMs = (noteEndSec - offsetTime) * 1000;
              window.SonicSF.playNote(
                note.pitch || 60,
                note.velocity || 0.8,
                remainingDurMs,
                context.currentTime,
                program,
                gainNode,
                lcCh,
                track.synth_engine
              );
            }
          }
        });
      });
    }
  };
  const schedulePianoRollMidi = (st, offsetSeconds, notesOverride) => {
    if (st.type !== 'PIANO_ROLL') return;
    const context = getAudioContext();
    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.
    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;
        if (window.SonicSF) {
          window.SonicSF.playNote(note.pitch || 60, note.velocity || 0.8, durMs, scheduledTime, instrumentProgram, destNode, mainCh, synthEngine);
        }
      }
    });
    // 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;
          if (window.SonicSF) {
            window.SonicSF.playNote(note.pitch || 60, note.velocity || 0.8, durMs, schedTime, ghostProg, ghostDest, ghostCh, ghostSynth);
          }
        }
      });
    });
  };
  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;
      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();
      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 {
    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 = {};
    stopMidiCapture();
    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();
        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 ──
  const handlePlayheadSet = (time, shiftKey) => {
    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.length > 0) || !!track.buffer;
    const hasMidi = track.midiItems && track.midiItems.length > 0;
    const hasSections = track.sections && track.sections.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);
      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);
    };
    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();
    setTracks(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);
    setTracks(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) => {
    setTracks(prev => prev.map(t => t.id === trackId ? { ...t, ...props } : t));
    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);
    setTracks(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 nodes = activeTrackNodesRef.current[trackId];
    if (nodes) {
      const volLinear = val <= -50 ? 0 : Math.pow(10, val / 20);
      nodes.gainNode.gain.setValueAtTime(volLinear, getAudioContext().currentTime);
    }
  };
  const updateTrackPan = (trackId, val) => {
    const beforeSnap = captureTrackSnapshot(trackId);
    setTracks(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' });
      }
    } 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];
    updateActiveTracks(prev => [...prev, {
      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: [],
      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;

  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); }
        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 = 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,
          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.
      const isRecActive = activeAudioRecordersRef.current && Object.keys(activeAudioRecordersRef.current).length > 0;
      if (!isPlaying && !isRecActive) {
        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;
        }
        setMasterVU(peak);
        setMasterMeterPeak(prev => Math.max(prev * 0.97, peak));
      }

      // 2. Track VU Meters
      const trackNodes = activeTrackNodesRef.current || {};
      const activeKeys = Object.keys(trackVuRefs.current);
      activeKeys.forEach(key => {
        const trackId = key.replace('_mixer', '');
        const isRecordingThisTrack = activeAudioRecordersRef.current && activeAudioRecordersRef.current[trackId];
        if (isRecordingThisTrack) return;

        const node = trackNodes[trackId];
        const canvas = trackVuRefs.current[key];
        if (!canvas) return;

        // 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 && node && node.analyserNode) {
          const analyser = node.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;
          }
        }

        // midiVuActivityRef được set bởi triggerMidiVuActivity — cả khi play
        // MIDI item LẪN khi ARM + nhấn phím MIDI keyboard (preview) — nên VU
        // nhảy trong cả 2 trường hợp (không chặn bởi isPlaying).
        let midiPeak = isAudible ? (midiVuActivityRef.current[trackId] || 0) : 0;
        if (midiPeak > 0) {
          midiVuActivityRef.current[trackId] = midiPeak * 0.90;
          if (midiVuActivityRef.current[trackId] < 0.01) {
            midiVuActivityRef.current[trackId] = 0;
          }
        }

        const peak = Math.max(audioPeak, midiPeak);
        const db = peak > 0 ? 20 * Math.log10(peak) : -60;

        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]);

  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: () => {
        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: () => handleSaveProject()
    }, {
      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: () => openPanel('python_tools')
    }, {
      sep: true
    }, {
      label: 'Plugin Manager (SoundFont/VSTi)',
      icon: 'zap',
      action: () => {
        setPluginManagerModalOpen(true);
        window.SonicAPI.listPlugins().then(data => setPluginsData(data)).catch(() => {});
      }
    }]
  }, {
    label: 'Help',
    items: [{
      label: 'About SonicForge',
      icon: 'info',
      action: () => showToast('SonicForge Studio v1.0 - Professional DAW', 'info')
    }]
  }].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: if the user already selected a loop
            // region, KEEP it — only auto-derive (0 → content end) when no
            // selection exists yet. Previously this always overwrote the
            // selection with 0 → (contentEnd + 2 bars).
            const hasSel = (selectionMode === 'local' && selLeft !== null && selRight !== null && selRight > selLeft)
              || (selectionStart !== null && selectionEnd !== null);
            if (!hasSel) {
              // Auto-derive loop region = ĐÚNG endtime của session (items trên
              // track main: clips theo buffer.duration/speed, midiItems, section
              // bounds) — KHÔNG cộng thêm bars buffer (loop không được dài hơn
              // duration hiện có).
              const maxEnd = computeMainSessionEndTime(activeTracks);
              if (maxEnd > 0) {
                setSelectionStart(0);
                setSelectionEnd(maxEnd);
              }
            }
          }
        },
    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-[14px] text-zinc-500 font-bold"
  }, "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-14 bg-black text-white text-[14px] px-1 py-0.5 rounded border border-zinc-700 text-center font-mono"
  }), /*#__PURE__*/React.createElement("span", {
    className: "text-[14px] 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;
      setSelectionEnd(t + beatDuration * 4);
      setNumberBar(b - beginBar + 1);
    },
    className: "w-14 bg-black text-white text-[14px] px-1 py-0.5 rounded border border-zinc-700 text-center font-mono"
  }), /*#__PURE__*/React.createElement("span", {
    className: "text-[14px] text-zinc-500"
  }, "#"), /*#__PURE__*/React.createElement("input", {
    type: "number",
    min: "1",
    value: numberBar,
    readOnly: true,
    className: "w-14 bg-black text-zinc-400 text-[14px] px-1 py-0.5 rounded border border-zinc-800 text-center font-mono"
  }), /*#__PURE__*/React.createElement("button", {
    onClick: () => setSelectionFollowsTempo(prev => !prev),
    className: `px-1.5 py-0.5 text-[14px] 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.5"
  }), /*#__PURE__*/React.createElement("span", {
    className: "text-[14px] 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-20 bg-black text-zinc-200 text-[14px] px-1 py-0.5 rounded border border-zinc-700 text-center font-mono"
  }), /*#__PURE__*/React.createElement("span", {
    className: "text-[14px] 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-20 bg-black text-zinc-200 text-[14px] px-1 py-0.5 rounded border border-zinc-700 text-center font-mono"
  }), /*#__PURE__*/React.createElement("span", {
    className: "text-[14px] 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-20 bg-black text-amber-300 text-[14px] px-1 py-0.5 rounded border border-zinc-700 text-center font-mono"
  }), /*#__PURE__*/React.createElement("div", {
    className: "flex-1"
  }), /*#__PURE__*/React.createElement("div", {
    className: "text-xs font-bold font-mono text-zinc-100"
  }, formatTime(currentTime))), (() => {
    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;
          setSelectionEnd(t + beatDuration * 4);
          setNumberBar(b - beginBar + 1);
        },
        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 => {
          if (el) trackVuRefs.current[track.id] = el; else delete trackVuRefs.current[track.id];
        },
        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-2.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-[120px]"
      }, /*#__PURE__*/React.createElement("span", {
        className: "inline-flex items-center shrink-0"
      }, /*#__PURE__*/React.createElement("i", {
        "data-lucide": "music",
        className: "w-3 h-3"
      })), /*#__PURE__*/React.createElement("span", { className: "truncate text-[10px]" }, track.instrumentName || track.instrumentId || "Synth"), /*#__PURE__*/React.createElement("i", { "data-lucide": "chevron-down", className: "w-3 h-3 shrink-0" }))), /*#__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,
             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();
                     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();
                   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 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
    }))));
  })(), /*#__PURE__*/React.createElement("div", {
    className: "h-6 bg-[#141414] border-t border-zinc-900 px-4 flex items-center justify-between text-xs 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-xs"
  }, "Local Sel"), selectionMode === 'global' && /*#__PURE__*/React.createElement("span", {
    className: "text-purple-400 font-semibold uppercase text-xs"
  }, "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-xs"
  }, "Solo Loop"), isLoopingSelection && selectionMode !== 'local' && /*#__PURE__*/React.createElement("span", {
    className: "text-cyan-400 font-semibold uppercase text-xs"
  }, "Master Loop")), /*#__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"
  })), " Scroll: Zoom"), /*#__PURE__*/React.createElement("span", null, "|"), /*#__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": "keyboard",
    className: "w-3 h-3 text-zinc-600"
  })), " Ctrl+Scroll: Playhead"))), 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(SystemManagerModal, {
    isOpen: systemManagerModalOpen,
    onClose: () => setSystemManagerModalOpen(false)
  }), /*#__PURE__*/React.createElement(PluginManagerModal, {
    isOpen: pluginManagerModalOpen,
    onClose: () => setPluginManagerModalOpen(false),
    pluginsData: pluginsData
  }), /*#__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);
          })
        ),
        /*#__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("button", {
      key: "vstd_" + i,
      onClick: () => { setInstrumentDropdownTrackId(null); setInstrumentDropdownBtnRect(null); setTrackInstrumentWithUndo(instrumentDropdownTrackId, v.id, v.name || v.id); },
      className: "w-full 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.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);
