PageSourceSearch

https://www.hamienet.com/assets/ScoreEditorCanvas-hnha0PBU.js

js hamienet.com collected 2026-09-24 19:00:13 UTC 156,590 bytes, 4,101 lines download raw bytes

1import { r as reactExports, j as jsxRuntimeExports, R as React } from "./vendor-react-Bay9Y7Lb.js";
2import { a as Midi } from "./vendor-audio-DH3w0BzW.js";
3import { u as useMidiKeyboard } from "./useMidiKeyboard-DlyEIN8T.js";
4import { MidiEngine } from "./MidiEngine-BqW6hqRc.js";
5const EditorContext = reactExports.createContext(null);
6const useEditor = () => {
7  const ctx = reactExports.useContext(EditorContext);
8  if (!ctx) throw new Error("useEditor must be used within EditorProvider");
9  return ctx;
10};
11const EditorProvider = ({ children, projectId }) => {
12  const engineRef = reactExports.useRef(null);
13  const [isEngineReady, setIsEngineReady] = reactExports.useState(false);
14  const [engineError, setEngineError] = reactExports.useState(null);
15  const [loadingStatus, setLoadingStatus] = reactExports.useState("Initializing DAW Engine...");
16  const [midiVersion, setMidiVersion] = reactExports.useState(0);
17  const [currentTime, setCurrentTime_] = reactExports.useState(0);
18  const currentTimeRef = reactExports.useRef(0);
19  const setCurrentTime = (t) => {
20    currentTimeRef.current = t;
21    setCurrentTime_(t);
22  };
23  const [isPlaying, setIsPlaying_] = reactExports.useState(false);
24  const isPlayingRef = reactExports.useRef(false);
25  const setIsPlaying = (p) => {
26    isPlayingRef.current = p;
27    setIsPlaying_(p);
28  };
29  const [duration, setDuration] = reactExports.useState(0);
30  const playbackStartTimeRef = reactExports.useRef(0);
31  const playbackStartOffsetRef = reactExports.useRef(0);
32  const playbackModeRef = reactExports.useRef("song");
33  const activePatternIdRef = reactExports.useRef(null);
34  const activeTrackIndexRef = reactExports.useRef(null);
35  const [activeTrackIndex, setActiveTrackIndex_] = reactExports.useState(null);
36  const setActiveTrackIndex = (idx) => {
37    activeTrackIndexRef.current = idx;
38    setActiveTrackIndex_(idx);
39  };
40  const [selectedNoteIds, setSelectedNoteIds] = reactExports.useState([]);
41  const [selectedTempoIndex, setSelectedTempoIndex] = reactExports.useState(null);
42  const [selectedTimeSigIndex, setSelectedTimeSigIndex] = reactExports.useState(null);
43  const [hint, setHint] = reactExports.useState(null);
44  const [patterns, setPatterns_] = reactExports.useState([]);
45  const patternsRef = reactExports.useRef([]);
46  const setPatterns = reactExports.useCallback((pats) => {
47    setPatterns_((prev) => {
48      const next = typeof pats === "function" ? pats(prev) : pats;
49      patternsRef.current = next;
50      return next;
51    });
52  }, []);
53  const [activePatternId, setActivePatternId_] = reactExports.useState(null);
54  const setActivePatternId = (id) => {
55    activePatternIdRef.current = id;
56    setActivePatternId_(id);
57  };
58  const [playlistTracks, setPlaylistTracks_] = reactExports.useState([]);
59  const playlistTracksRef = reactExports.useRef([]);
60  const setPlaylistTracks = reactExports.useCallback((tracks) => {
61    setPlaylistTracks_((prev) => {
62      const next = typeof tracks === "function" ? tracks(prev) : tracks;
63      playlistTracksRef.current = next;
64      return next;
65    });
66  }, []);
67  const [playlistClips, setPlaylistClips_] = reactExports.useState([]);
68  const playlistClipsRef = reactExports.useRef([]);
69  const setPlaylistClips = reactExports.useCallback((clips) => {
70    setPlaylistClips_((prev) => {
71      const next = typeof clips === "function" ? clips(prev) : clips;
72      playlistClipsRef.current = next;
73      return next;
74    });
75  }, []);
76  const [selectedClipIds, setSelectedClipIds] = reactExports.useState([]);
77  const [automationClips, setAutomationClips_] = reactExports.useState([]);
78  const automationClipsRef = reactExports.useRef([]);
79  const setAutomationClips = reactExports.useCallback((clips) => {
80    setAutomationClips_((prev) => {
81      const next = typeof clips === "function" ? clips(prev) : clips;
82      automationClipsRef.current = next;
83      return next;
84    });
85  }, []);
86  const [timeMarkers, setTimeMarkers] = reactExports.useState([]);
87  const [loopRegion, setLoopRegion] = reactExports.useState(null);
88  const [loopMarkerTick, setLoopMarkerTick_] = reactExports.useState(null);
89  const loopMarkerTickRef = reactExports.useRef(null);
90  const setLoopMarkerTick = (tick) => {
91    loopMarkerTickRef.current = tick;
92    setLoopMarkerTick_(tick);
93  };
94  const [gridSnap, setGridSnap] = reactExports.useState("beat");
95  const [currentPlaylistTool, setCurrentPlaylistTool] = reactExports.useState("draw");
96  const [easyMode, setEasyMode_] = reactExports.useState(false);
97  const setEasyMode = setEasyMode_;
98  const [easyToProMergeState, setEasyToProMergeState] = reactExports.useState(null);
99  const proModeBackupRef = reactExports.useRef(null);
100  const absoluteNotesSnapshotRef = reactExports.useRef([]);
101  const [playbackMode, setPlaybackMode_] = reactExports.useState("song");
102  const setPlaybackMode = (mode) => {
103    playbackModeRef.current = mode;
104    setPlaybackMode_(mode);
105  };
106  const [isRecording, setIsRecording] = reactExports.useState(false);
107  const [isTypingKeyboardEnabled, setIsTypingKeyboardEnabled] = reactExports.useState(false);
108  const [activeMidiKeys, setActiveMidiKeys] = reactExports.useState(/* @__PURE__ */ new Set());
109  const [isCountdownEnabled, setIsCountdownEnabled] = reactExports.useState(false);
110  const [isMetronomeEnabled, setIsMetronomeEnabled] = reactExports.useState(false);
111  const [isLoopRecordingEnabled, setIsLoopRecordingEnabled] = reactExports.useState(false);
112  const [masterVolume, setMasterVolumeState] = reactExports.useState(0.8);
113  const [masterPitch, setMasterPitchState] = reactExports.useState(0);
114  const [tempo, setTempoState] = reactExports.useState(130);
115  const tempoRef = reactExports.useRef(130);
116  const metronomeEnabledRef = reactExports.useRef(false);
117  reactExports.useEffect(() => {
118    tempoRef.current = tempo;
119  }, [tempo]);
120  reactExports.useEffect(() => {
121    metronomeEnabledRef.current = isMetronomeEnabled;
122  }, [isMetronomeEnabled]);
123  const activePattern = patterns.find((p) => p.id === activePatternId) || null;
124  const handleSetSelectedNoteIds = reactExports.useCallback((ids) => {
125    setSelectedNoteIds(ids);
126    if (ids.length > 0) {
127      setSelectedTempoIndex(null);
128      setSelectedTimeSigIndex(null);
129    }
130  }, []);
131  const handleSetSelectedTempoIndex = reactExports.useCallback((idx) => {
132    setSelectedTempoIndex(idx);
133    if (idx !== null) {
134      setSelectedNoteIds([]);
135      setSelectedTimeSigIndex(null);
136    }
137  }, []);
138  const handleSetSelectedTimeSigIndex = reactExports.useCallback((idx) => {
139    setSelectedTimeSigIndex(idx);
140    if (idx !== null) {
141      setSelectedNoteIds([]);
142      setSelectedTempoIndex(null);
143    }
144  }, []);
145  const [undoStack, setUndoStack] = reactExports.useState([]);
146  const [redoStack, setRedoStack] = reactExports.useState([]);
147  const [clipboard, setClipboard] = reactExports.useState(null);
148  const playMetronomeTick = reactExports.useCallback((isDownbeat) => {
149    if (!engineRef.current) return;
150    const ctx = engineRef.current.audioCtx;
151    if (!ctx) return;
152    const now = ctx.currentTime;
153    const osc = ctx.createOscillator();
154    const gain = ctx.createGain();
155    const filter = ctx.createBiquadFilter();
156    osc.type = isDownbeat ? "sine" : "triangle";
157    osc.frequency.setValueAtTime(isDownbeat ? 2e3 : 800, now);
158    filter.type = "lowpass";
159    filter.frequency.setValueAtTime(isDownbeat ? 4e3 : 2e3, now);
160    gain.gain.setValueAtTime(0, now);
161    gain.gain.linearRampToValueAtTime(isDownbeat ? 0.8 : 0.3, now + 2e-3);
162    gain.gain.exponentialRampToValueAtTime(1e-3, now + (isDownbeat ? 0.15 : 0.06));
163    osc.connect(filter);
164    filter.connect(gain);
165    gain.connect(engineRef.current.destNode);
166    osc.start(now);
167    osc.stop(now + (isDownbeat ? 0.12 : 0.08));
168    setTimeout(() => {
169      osc.disconnect();
170      gain.disconnect();
171      filter.disconnect();
172    }, 200);
173  }, []);
174  const rafRef = reactExports.useRef(null);
175  const lastBeatRef = reactExports.useRef(-1);
176  const loop = reactExports.useCallback(() => {
177    var _a, _b, _c;
178    if (engineRef.current) {
179      const hasNotes = engineRef.current.getNoteCount() > 0;
180      let time = 0;
181      let playing = false;
182      if (hasNotes) {
183        time = engineRef.current.getCurrentTime();
184        playing = engineRef.current.isPlaying() && isPlayingRef.current;
185        const currentTicks = engineRef.current.secondsToTicks(time);
186        if (loopRegion) {
187          if (currentTicks >= loopRegion.endTick) {
188            const startSec = engineRef.current.ticksToSeconds(loopRegion.startTick);
189            engineRef.current.seek(startSec);
190            time = startSec;
191            setCurrentTime(startSec);
192            playbackStartOffsetRef.current = startSec;
193            playbackStartTimeRef.current = performance.now();
194          }
195        } else {
196          const songEndSecs = engineRef.current.getDuration();
197          if (time >= songEndSecs) {
198            if (playbackModeRef.current === "pat") {
199              if (loopMarkerTickRef.current !== null) {
200                const markerSec = engineRef.current.ticksToSeconds(loopMarkerTickRef.current);
201                engineRef.current.seek(markerSec);
202                time = markerSec;
203                setCurrentTime(markerSec);
204                playbackStartOffsetRef.current = markerSec;
205                playbackStartTimeRef.current = performance.now();
206              } else {
207                engineRef.current.seek(0);
208                time = 0;
209                setCurrentTime(0);
210                playbackStartOffsetRef.current = 0;
211                playbackStartTimeRef.current = performance.now();
212              }
213            } else {
214              engineRef.current.pause();
215              engineRef.current.seek(0);
216              time = 0;
217              setCurrentTime(0);
218              setIsPlaying(false);
219              playing = false;
220            }
221          }
222        }
223        setIsPlaying(playing);
224        setCurrentTime(time);
225      } else {
226        playing = isPlayingRef.current;
227        if (playing) {
228          time = (performance.now() - playbackStartTimeRef.current) / 1e3 + playbackStartOffsetRef.current;
229          if (loopRegion) {
230            const currentTicks = engineRef.current.secondsToTicks(time);
231            if (currentTicks >= loopRegion.endTick) {
232              const startSec = engineRef.current.ticksToSeconds(loopRegion.startTick);
233              time = startSec;
234              playbackStartOffsetRef.current = startSec;
235              playbackStartTimeRef.current = performance.now();
236            }
237          }
238          setCurrentTime(time);
239        } else {
240          time = currentTimeRef.current;
241        }
242      }
243      if (playing && metronomeEnabledRef.current) {
244        const beatDuration = 60 / tempoRef.current;
245        const currentBeat = Math.floor(time / beatDuration);
246        if (currentBeat !== lastBeatRef.current) {
247          if (lastBeatRef.current !== -1 && Math.abs(currentBeat - lastBeatRef.current) === 1) {
248            const numerator = ((_c = (_b = (_a = engineRef.current.midi) == null ? void 0 : _a.header) == null ? void 0 : _b.timeSignatures[0]) == null ? void 0 : _c.timeSignature[0]) || 4;
249            const isDownbeat = currentBeat % numerator === 0;
250            playMetronomeTick(isDownbeat);
251          }
252          lastBeatRef.current = currentBeat;
253        }
254      } else if (!playing) {
255        lastBeatRef.current = -1;
256      }
257    }
258    rafRef.current = requestAnimationFrame(loop);
259  }, [playMetronomeTick]);
260  reactExports.useEffect(() => {
261    rafRef.current = requestAnimationFrame(loop);
262    return () => {
263      if (rafRef.current) cancelAnimationFrame(rafRef.current);
264    };
265  }, [loop]);
266  const reconstructMidi = reactExports.useCallback((currentPatterns, currentClips, currentTracks, currentAutomation) => {
267    if (!engineRef.current) return;
268    const midi = engineRef.current.midi;
269    if (!midi) return;
270    while (midi.tracks.length < currentTracks.length) {
271      midi.addTrack();
272    }
273    while (midi.tracks.length > currentTracks.length) {
274      midi.tracks.pop();
275    }
276    midi.tracks.forEach((track, idx) => {
277      track.notes = [];
278      track.controlChanges = {};
279      const pt = currentTracks[idx];
280      if (pt) {
281        track.name = pt.name;
282        const instNum = pt.instrumentNumber !== void 0 ? pt.instrumentNumber : 0;
283        const chan = pt.channel !== void 0 ? pt.channel : idx === 9 ? 9 : idx % 16;
284        track.instrument.number = instNum;
285        track.channel = chan;
286        if (track.addControlChange) {
287          track.addControlChange({
288            number: 7,
289            ticks: 0,
290            time: 0,
291            value: pt.volume
292          });
293          const mappedPan = (pt.panning + 1) / 2;
294          track.addControlChange({
295            number: 10,
296            ticks: 0,
297            time: 0,
298            value: mappedPan
299          });
300        }
301      }
302    });
303    const currentPlaybackMode = playbackModeRef.current;
304    const currentActivePatternId = activePatternIdRef.current;
305    const currentActiveTrackIndex = activeTrackIndexRef.current;
306    let compileClips = currentClips;
307    if (currentPlaybackMode === "pat" || currentPlaybackMode === "song" && currentClips.length === 0) {
308      const activePat = currentPatterns.find((p) => p.id === currentActivePatternId);
309      if (activePat && activePat.notes.length > 0) {
310        const trackIdx = currentActiveTrackIndex !== null ? currentActiveTrackIndex : 0;
311        const maxNoteTick = activePat.notes.reduce((max, n) => Math.max(max, n.ticks + n.durationTicks), 0);
312        const ppq = midi.header.ppq || 96;
313        const ticksPerBar = ppq * 4;
314        const patternLength = activePat.durationTicks || Math.max(ticksPerBar * 4, Math.ceil(maxNoteTick / ticksPerBar) * ticksPerBar);
315        const activeClip = {
316          id: "active-pat-clip",
317          patternId: activePat.id,
318          trackIndex: trackIdx,
319          startTick: 0,
320          durationTicks: patternLength,
321          offsetTicks: 0,
322          isMuted: false
323        };
324        compileClips = [activeClip];
325      } else {
326        compileClips = [];
327      }
328    }
329    compileClips.forEach((clip) => {
330      if (clip.isMuted) return;
331      const pat = currentPatterns.find((p) => p.id === clip.patternId);
332      if (!pat || pat.notes.length === 0) {
333        return;
334      }
335      const track = midi.tracks[clip.trackIndex];
336      if (!track) {
337        console.error(`[DEBUG] Track index ${clip.trackIndex} does not exist in midi.tracks.`);
338        return;
339      }
340      const maxNoteTick = pat.notes.reduce((max, n) => Math.max(max, n.ticks + n.durationTicks), 0);
341      const ppq = midi.header.ppq || 96;
342      const ticksPerBar = ppq * 4;
343      const patternLength = pat.durationTicks || Math.max(ticksPerBar * 4, Math.ceil(maxNoteTick / ticksPerBar) * ticksPerBar);
344      if (patternLength <= 0) return;
345      const clipEndTick = clip.startTick + clip.durationTicks;
346      let loopIdx = 0;
347      while (true) {
348        const loopStartTick = clip.startTick + loopIdx * patternLength - clip.offsetTicks;
349        if (loopStartTick >= clipEndTick) break;
350        pat.notes.forEach((note) => {
351          if (note.isMuted) return;
352          const noteAbsTick = loopStartTick + note.ticks;
353          if (noteAbsTick >= clip.startTick && noteAbsTick < clipEndTick) {
354            const noteEndAbsTick = noteAbsTick + note.durationTicks;
355            const clippedEndTick = Math.min(noteEndAbsTick, clipEndTick);
356            const durTicks = clippedEndTick - noteAbsTick;
357            if (durTicks > 0) {
358              const time = engineRef.current.ticksToSeconds(noteAbsTick);
359              const duration2 = engineRef.current.ticksToSeconds(noteAbsTick + durTicks) - time;
360              track.addNote({
361                midi: note.midi,
362                ticks: noteAbsTick,
363                durationTicks: durTicks,
364                time,
365                duration: duration2,
366                velocity: note.velocity
367              });
368              const added = track.notes[track.notes.length - 1];
369              if (added && note.voice !== void 0) {
370                added.voice = note.voice;
371              }
372            }
373          }
374        });
375        loopIdx++;
376      }
377    });
378    const compileAutomation = currentPlaybackMode === "pat" ? [] : currentAutomation;
379    compileAutomation.forEach((clip) => {
380      const track = midi.tracks[clip.trackIndex];
381      if (!track || clip.nodes.length === 0) return;
382      const sortedNodes = [...clip.nodes].sort((a, b) => a.tick - b.tick);
383      const clipStart = clip.startTick;
384      const clipEnd = clip.startTick + clip.durationTicks;
385      const ppq = midi.header.ppq || 96;
386      const samplingInterval = Math.max(12, Math.round(ppq / 8));
387      const evaluateCurve = (tick) => {
388        if (sortedNodes.length === 0) return 0.5;
389        if (tick <= sortedNodes[0].tick) return sortedNodes[0].value;
390        if (tick >= sortedNodes[sortedNodes.length - 1].tick) return sortedNodes[sortedNodes.length - 1].value;
391        let i = 0;
392        while (i < sortedNodes.length - 1 && tick > sortedNodes[i + 1].tick) {
393          i++;
394        }
395        const n1 = sortedNodes[i];
396        const n2 = sortedNodes[i + 1];
397        const t = (tick - n1.tick) / (n2.tick - n1.tick);
398        if (Math.abs(n1.tension || 0) < 0.01) {
399          return n1.value + t * (n2.value - n1.value);
400        } else {
401          const k = (n1.tension || 0) * 5;
402          const curveT = (Math.exp(t * k) - 1) / (Math.exp(k) - 1);
403          return n1.value + curveT * (n2.value - n1.value);
404        }
405      };
406      if (track.addControlChange) {
407        for (let tick = clipStart; tick <= clipEnd; tick += samplingInterval) {
408          const val = evaluateCurve(tick);
409          const time = engineRef.current.ticksToSeconds(tick);
410          track.addControlChange({
411            number: clip.ccNumber,
412            ticks: tick,
413            time,
414            value: val
415          });
416        }
417      }
418    });
419  }, []);
420  const forceSync = reactExports.useCallback(async () => {
421    if (engineRef.current) {
422      const midi = engineRef.current.midi;
423      if (midi) {
424        let nextTracks = [...playlistTracksRef.current];
425        if (midi.tracks.length > nextTracks.length) {
426          for (let i = nextTracks.length; i < midi.tracks.length; i++) {
427            const mt = midi.tracks[i];
428            nextTracks.push({
429              id: `track-${i}-${Date.now()}`,
430              name: mt.name || `Track ${i + 1}`,
431              color: mt.channel === 9 ? "#e11d48" : `#${Math.floor((0.2 + 0.6 * Math.random()) * 16777215).toString(16)}`,
432              icon: mt.channel === 9 ? "drum" : "synth",
433              isMuted: false,
434              isSoloed: false,
435              volume: 0.8,
436              panning: 0,
437              height: 64,
438              parentId: null
439            });
440          }
441        } else if (midi.tracks.length < nextTracks.length) {
442          nextTracks = nextTracks.slice(0, midi.tracks.length);
443        }
444        nextTracks = nextTracks.map((t, idx) => {
445          const mt = midi.tracks[idx];
446          if (mt) {
447            return {
448              ...t,
449              name: mt.name || t.name,
450              channel: mt.channel,
451              instrumentNumber: mt.instrument.number
452            };
453          }
454          return t;
455        });
456        reconstructMidi(patternsRef.current, playlistClipsRef.current, nextTracks, automationClipsRef.current);
457        setPlaylistTracks(nextTracks);
458      }
459      engineRef.current.getNoteCount();
460      await engineRef.current.syncSequencer();
461      setDuration(engineRef.current.getDuration());
462      setMidiVersion((v) => v + 1);
463    }
464  }, [reconstructMidi]);
465  const [meiDocs, setMeiDocs] = reactExports.useState({});
466  const setMeiDoc = reactExports.useCallback((trackIdx, mei) => {
467    setMeiDocs((prev) => ({ ...prev, [trackIdx]: mei }));
468  }, []);
469  reactExports.useCallback((trackIdx) => {
470    setMeiDocs((prev) => {
471      if (!prev[trackIdx]) return prev;
472      const next = { ...prev };
473      delete next[trackIdx];
474      return next;
475    });
476  }, []);
477  const createDefaultProject = reactExports.useCallback((engine) => {
478    var _a, _b;
479    const defaultPatId = `pat-1-${Date.now()}`;
480    const defaultTracks = [];
481    engine.midi.tracks.forEach((track, idx) => {
482      defaultTracks.push({
483        id: `track-${idx}-${Date.now()}`,
484        name: track.name || `Track ${idx + 1}`,
485        color: track.channel === 9 ? "#e11d48" : "#6366f1",
486        icon: track.channel === 9 ? "drum" : "synth",
487        isMuted: false,
488        isSoloed: false,
489        volume: 0.8,
490        panning: 0,
491        height: 64,
492        parentId: null
493      });
494    });
495    if (defaultTracks.length === 0) {
496      defaultTracks.push({
497        id: `track-0-${Date.now()}`,
498        name: "Track 1",
499        color: "#6366f1",
500        icon: "synth",
501        isMuted: false,
502        isSoloed: false,
503        volume: 0.8,
504        panning: 0,
505        height: 64,
506        parentId: null
507      });
508    }
509    const defaultPatterns = [{
510      id: defaultPatId,
511      name: "Pattern 1",
512      color: "#6366f1",
513      notes: []
514    }];
515    const ppq = ((_b = (_a = engine.midi) == null ? void 0 : _a.header) == null ? void 0 : _b.ppq) || 96;
516    const defaultClip = {
517      id: `clip-1-${Date.now()}`,
518      patternId: defaultPatId,
519      trackIndex: 0,
520      startTick: 0,
521      durationTicks: ppq * 16,
522      // Default 4 bars
523      offsetTicks: 0,
524      isMuted: false
525    };
526    setPatterns(defaultPatterns);
527    setPlaylistTracks(defaultTracks);
528    setPlaylistClips([defaultClip]);
529    setAutomationClips([]);
530    setTimeMarkers([]);
531    setLoopRegion(null);
532    setActivePatternId(defaultPatId);
533    setTimeout(() => {
534      reconstructMidi(defaultPatterns, [defaultClip], defaultTracks, []);
535      engine.syncSequencer();
536    }, 0);
537  }, [reconstructMidi]);
538  const migrateLegacyMidi = reactExports.useCallback((engine) => {
539    var _a;
540    const midi = engine.midi;
541    const newPats = [];
542    const newClips = [];
543    const newTracks = [];
544    midi.tracks.forEach((track, idx) => {
545      const trackId = `track-${idx}-${Date.now()}`;
546      const patId = `pat-${idx}-${Date.now()}`;
547      newTracks.push({
548        id: trackId,
549        name: track.name || `Track ${idx + 1}`,
550        color: track.channel === 9 ? "#e11d48" : `#${Math.floor((0.2 + 0.6 * Math.random()) * 16777215).toString(16)}`,
551        icon: track.channel === 9 ? "drum" : "synth",
552        isMuted: false,
553        isSoloed: false,
554        volume: 0.8,
555        panning: 0,
556        height: 64,
557        parentId: null
558      });
559      if (track.notes.length > 0) {
560        const patNotes = track.notes.map((n) => ({
561          midi: n.midi,
562          ticks: n.ticks,
563          durationTicks: n.durationTicks,
564          velocity: n.velocity,
565          voice: n.voice || 1,
566          isMuted: n.isMuted || false
567        }));
568        newPats.push({
569          id: patId,
570          name: `Pattern ${idx + 1}`,
571          color: newTracks[idx].color,
572          notes: patNotes
573        });
574        const maxTick = patNotes.reduce((max, n) => Math.max(max, n.ticks + n.durationTicks), 0);
575        const ppq = midi.header.ppq || 96;
576        const durationTicks = Math.max(maxTick, ppq * 16);
577        newClips.push({
578          id: `clip-${idx}-${Date.now()}`,
579          patternId: patId,
580          trackIndex: idx,
581          startTick: 0,
582          durationTicks,
583          offsetTicks: 0,
584          isMuted: false
585        });
586      }
587    });
588    if (newPats.length === 0) {
589      const defaultPatId = `pat-1-${Date.now()}`;
590      newPats.push({
591        id: defaultPatId,
592        name: "Pattern 1",
593        color: "#6366f1",
594        notes: []
595      });
596    }
597    setPatterns(newPats);
598    setPlaylistTracks(newTracks);
599    setPlaylistClips(newClips);
600    setAutomationClips([]);
601    setTimeMarkers([]);
602    setLoopRegion(null);
603    setActivePatternId(((_a = newPats[0]) == null ? void 0 : _a.id) || null);
604    setTimeout(() => {
605      reconstructMidi(newPats, newClips, newTracks, []);
606      engine.syncSequencer();
607    }, 0);
608  }, [reconstructMidi]);
609  const convertToEasyModeStructure = reactExports.useCallback((currentPats, currentClips, currentTracks) => {
610    const pats = currentPats || patternsRef.current;
611    const clips = currentClips || playlistClipsRef.current;
612    const tracks = currentTracks || playlistTracksRef.current;
613    const newPatterns = [];
614    const newClips = [];
615    tracks.forEach((track, idx) => {
616      const patId = `pat-easy-${idx}-${Date.now()}`;
617      const trackNotes = [];
618      clips.forEach((clip) => {
619        if (clip.trackIndex !== idx) return;
620        const pattern = pats.find((p) => p.id === clip.patternId);
621        if (!pattern) return;
622        pattern.notes.forEach((note) => {
623          const absTick = clip.startTick + note.ticks - clip.offsetTicks;
624          if (note.ticks >= clip.offsetTicks && note.ticks < clip.offsetTicks + clip.durationTicks) {
625            trackNotes.push({
626              ...note,
627              ticks: absTick
628            });
629          }
630        });
631      });
632      newPatterns.push({
633        id: patId,
634        name: `${track.name || `Track ${idx + 1}`}`,
635        color: track.color,
636        notes: trackNotes
637      });
638      const maxNoteTick = trackNotes.reduce((max, n) => Math.max(max, n.ticks + n.durationTicks), 0);
639      const ppq = 96;
640      const minDuration = ppq * 4 * 128;
641      const durationTicks = Math.max(maxNoteTick + ppq * 16, minDuration);
642      newClips.push({
643        id: `clip-easy-${idx}-${Date.now()}`,
644        patternId: patId,
645        trackIndex: idx,
646        startTick: 0,
647        durationTicks,
648        offsetTicks: 0,
649        isMuted: false
650      });
651    });
652    setPatterns(newPatterns);
653    setPlaylistClips(newClips);
654    const currentActiveTrackIdx = activeTrackIndexRef.current;
655    if (currentActiveTrackIdx !== null && currentActiveTrackIdx >= 0 && currentActiveTrackIdx < tracks.length) {
656      setActivePatternId(newPatterns[currentActiveTrackIdx].id);
657    } else if (newPatterns.length > 0) {
658      setActivePatternId(newPatterns[0].id);
659      setActiveTrackIndex(0);
660    }
661    setTimeout(() => {
662      reconstructMidi(newPatterns, newClips, tracks, []);
663      if (engineRef.current) {
664        engineRef.current.syncSequencer();
665      }
666    }, 0);
667  }, [reconstructMidi, setActiveTrackIndex]);
668  reactExports.useEffect(() => {
669    let isMounted = true;
670    let localEngine = null;
671    let localCtx = null;
672    const initEngine = async () => {
673      var _a;
674      const ctx = new (window.AudioContext || window.webkitAudioContext)();
675      localCtx = ctx;
676      const dest = ctx.createGain();
677      dest.connect(ctx.destination);
678      const engine = new MidiEngine(ctx, dest);
679      localEngine = engine;
680      try {
681        setLoadingStatus("Initializing DAW Engine...");
682        await engine.initialize();
683        if (!isMounted) {
684          engine.destroy();
685          ctx.close().catch(() => {
686          });
687          return;
688        }
689        engineRef.current = engine;
690        if (projectId) {
691          const isSavedProject = isNaN(Number(projectId));
692          if (isSavedProject) {
693            setLoadingStatus("Loading saved MIDI project in progress...");
694            const res = await fetch(`/api/projects/${projectId}`);
695            if (res.ok) {
696              const data = await res.json();
697              if (data.projectData) {
698                if (data.projectData.patterns) {
699                  setPatterns(data.projectData.patterns);
700                  setPlaylistTracks(data.projectData.playlistTracks || []);
701                  setPlaylistClips(data.projectData.playlistClips || []);
702                  setAutomationClips(data.projectData.automationClips || []);
703                  setTimeMarkers(data.projectData.timeMarkers || []);
704                  setLoopRegion(data.projectData.loopRegion || null);
705                  setEasyMode(!!data.projectData.easyMode);
706                  if (data.projectData.gridSnap) setGridSnap(data.projectData.gridSnap);
707                  setActivePatternId(data.projectData.activePatternId || (((_a = data.projectData.patterns[0]) == null ? void 0 : _a.id) || null));
708                  if (data.projectData.midi) {
709                    const binaryString = atob(data.projectData.midi);
710                    const len = binaryString.length;
711                    const bytes = new Uint8Array(len);
712                    for (let i = 0; i < len; i++) {
713                      bytes[i] = binaryString.charCodeAt(i);
714                    }
715                    engine.loadMidi(bytes.buffer);
716                  }
717                } else if (data.projectData.midi) {
718                  const binaryString = atob(data.projectData.midi);
719                  const len = binaryString.length;
720                  const bytes = new Uint8Array(len);
721                  for (let i = 0; i < len; i++) {
722                    bytes[i] = binaryString.charCodeAt(i);
723                  }
724                  engine.loadMidi(bytes.buffer);
725                  migrateLegacyMidi(engine);
726                } else {
727                  engine.midi.fromJSON(data.projectData);
728                  migrateLegacyMidi(engine);
729                }
730              } else {
731                engine.loadEmptyMidi();
732                createDefaultProject(engine);
733              }
734            } else {
735              engine.loadEmptyMidi();
736              createDefaultProject(engine);
737            }
738          } else {
739            setLoadingStatus("Loading MIDI file in progress...");
740            const res = await fetch(`/api/tracks/${projectId}/midi`);
741            if (res.ok) {
742              const buffer = await res.arrayBuffer();
743              engine.loadMidi(buffer);
744              migrateLegacyMidi(engine);
745            } else {
746              engine.loadEmptyMidi();
747              createDefaultProject(engine);
748            }
749          }
750        } else {
751          setLoadingStatus("Creating empty project...");
752          engine.loadEmptyMidi();
753          createDefaultProject(engine);
754        }
755        if (engine.midi.tracks.length > 0) {
756          setActiveTrackIndex(0);
757        } else {
758          setActiveTrackIndex(null);
759        }
760        if (engine.midi.header.tempos && engine.midi.header.tempos.length > 0) {
761          setTempoState(engine.midi.header.tempos[0].bpm);
762        }
763        setDuration(engine.getDuration());
764        setIsEngineReady(true);
765        setLoadingStatus("");
766        setEngineError(null);
767      } catch (err) {
768        console.error("Failed to initialize MidiEngine", err);
769        setLoadingStatus("");
770        setEngineError(err.message || "An unknown error occurred during engine initialization.");
771      }
772    };
773    initEngine();
774    return () => {
775      isMounted = false;
776      if (localEngine) {
777        localEngine.destroy();
778      }
779      if (localCtx) {
780        localCtx.close().catch(() => {
781        });
782      }
783      if (engineRef.current) {
784        engineRef.current = null;
785      }
786    };
787  }, [projectId]);
788  const countdownTimeoutRef = reactExports.useRef(null);
789  const play = reactExports.useCallback(async () => {
790    var _a;
791    if (engineRef.current) {
792      if (((_a = engineRef.current.audioCtx) == null ? void 0 : _a.state) === "suspended") {
793        try {
794          await engineRef.current.audioCtx.resume();
795        } catch (e) {
796          console.error("Failed to resume AudioContext:", e);
797        }
798      }
799      if (countdownTimeoutRef.current) {
800        clearTimeout(countdownTimeoutRef.current);
801        countdownTimeoutRef.current = null;
802      }
803      await forceSync();
804      const hasNotes = engineRef.current.getNoteCount() > 0;
805      if (!hasNotes) {
806        playbackStartTimeRef.current = performance.now();
807        playbackStartOffsetRef.current = currentTimeRef.current;
808      }
809      if (isRecording && isCountdownEnabled) {
810        let count = 4;
811        const beatDurationMs = 60 / tempoRef.current * 1e3;
812        const playCountdownClick = async () => {
813          var _a2;
814          if (count > 0) {
815            const isDownbeat = count === 4;
816            playMetronomeTick(isDownbeat);
817            count--;
818            countdownTimeoutRef.current = setTimeout(playCountdownClick, beatDurationMs);
819          } else {
820            if (!hasNotes) {
821              playbackStartTimeRef.current = performance.now();
822            }
823            await ((_a2 = engineRef.current) == null ? void 0 : _a2.play());
824            setIsPlaying(true);
825          }
826        };
827        playCountdownClick();
828      } else {
829        await engineRef.current.play();
830        setIsPlaying(true);
831      }
832    }
833  }, [isRecording, isCountdownEnabled, playMetronomeTick, forceSync]);
834  const pause = reactExports.useCallback(() => {
835    if (countdownTimeoutRef.current) {
836      clearTimeout(countdownTimeoutRef.current);
837      countdownTimeoutRef.current = null;
838    }
839    if (engineRef.current) {
840      engineRef.current.pause();
841      setIsPlaying(false);
842    }
843  }, []);
844  const stop = reactExports.useCallback(() => {
845    if (countdownTimeoutRef.current) {
846      clearTimeout(countdownTimeoutRef.current);
847      countdownTimeoutRef.current = null;
848    }
849    if (engineRef.current) {
850      engineRef.current.stop();
851      engineRef.current.seek(0);
852      setIsPlaying(false);
853      setCurrentTime(0);
854      playbackStartOffsetRef.current = 0;
855    }
856  }, []);
857  const seek = reactExports.useCallback((time) => {
858    if (engineRef.current) {
859      engineRef.current.seek(time);
860      setCurrentTime(time);
861      playbackStartOffsetRef.current = time;
862      playbackStartTimeRef.current = performance.now();
863    }
864  }, []);
865  const handleSetActiveTrackIndex = reactExports.useCallback((idx) => {
866    setActiveTrackIndex(idx);
867    if (playbackMode === "pat") {
868      setTimeout(() => forceSync(), 0);
869    }
870  }, [playbackMode, forceSync]);
871  const handleSetActivePatternId = reactExports.useCallback((id) => {
872    setActivePatternId(id);
873    if (playbackMode === "pat") {
874      setTimeout(() => forceSync(), 0);
875    }
876  }, [playbackMode, forceSync]);
877  const handleSetEasyMode = reactExports.useCallback((val) => {
878    if (val === easyMode) return;
879    if (val) {
880      const pats = patternsRef.current;
881      const clips = playlistClipsRef.current;
882      proModeBackupRef.current = {
883        patterns: pats,
884        playlistClips: clips
885      };
886      const snapshot = [];
887      clips.forEach((clip) => {
888        const pattern = pats.find((p) => p.id === clip.patternId);
889        if (!pattern) return;
890        pattern.notes.forEach((note, noteIdx) => {
891          const absTick = clip.startTick + note.ticks - clip.offsetTicks;
892          if (note.ticks >= clip.offsetTicks && note.ticks < clip.offsetTicks + clip.durationTicks) {
893            snapshot.push({
894              midi: note.midi,
895              absoluteTick: absTick,
896              durationTicks: note.durationTicks,
897              velocity: note.velocity,
898              voice: note.voice,
899              originalPatternId: pattern.id,
900              originalNoteIndex: noteIdx,
901              trackIndex: clip.trackIndex
902            });
903          }
904        });
905      });
906      absoluteNotesSnapshotRef.current = snapshot;
907      setEasyMode_(true);
908      setPlaybackMode("song");
909      setTimeout(() => {
910        convertToEasyModeStructure(pats, clips, playlistTracksRef.current);
911      }, 0);
912    } else {
913      if (!proModeBackupRef.current) {
914        setEasyMode_(false);
915        return;
916      }
917      const backupPats = JSON.parse(JSON.stringify(proModeBackupRef.current.patterns));
918      const backupClips = JSON.parse(JSON.stringify(proModeBackupRef.current.playlistClips));
919      const snapshot = absoluteNotesSnapshotRef.current;
920      const addedNotesPerTrack = {};
921      let totalAdded = 0;
922      playlistTracksRef.current.forEach((track, idx) => {
923        const trackClip = playlistClipsRef.current.find((c) => c.trackIndex === idx);
924        if (!trackClip) return;
925        const trackEasyPattern = patternsRef.current.find((p) => p.id === trackClip.patternId);
926        if (!trackEasyPattern) return;
927        trackEasyPattern.notes.forEach((note) => {
928          const wasInSnapshot = snapshot.some(
929            (sn) => sn.trackIndex === idx && sn.midi === note.midi && sn.absoluteTick === note.ticks
930          );
931          if (!wasInSnapshot) {
932            if (!addedNotesPerTrack[idx]) {
933              addedNotesPerTrack[idx] = [];
934            }
935            addedNotesPerTrack[idx].push({
936              midi: note.midi,
937              absoluteTick: note.ticks,
938              durationTicks: note.durationTicks,
939              velocity: note.velocity,
940              voice: note.voice
941            });
942            totalAdded++;
943          }
944        });
945      });
946      const applyTransition = (resolutions) => {
947        const nextPatterns = [...backupPats];
948        const nextClips = [...backupClips];
949        const nextTracks = [...playlistTracksRef.current];
950        const deletedNoteInfos = snapshot.filter((backupNote) => {
951          const trackClip = playlistClipsRef.current.find((c) => c.trackIndex === backupNote.trackIndex);
952          if (!trackClip) return true;
953          const trackEasyPattern = patternsRef.current.find((p) => p.id === trackClip.patternId);
954          if (!trackEasyPattern) return true;
955          const exists = trackEasyPattern.notes.some((n) => n.midi === backupNote.midi && n.ticks === backupNote.absoluteTick);
956          return !exists;
957        });
958        deletedNoteInfos.forEach((delInfo) => {
959          const pattern = nextPatterns.find((p) => p.id === delInfo.originalPatternId);
960          if (pattern) {
961            const clip = backupClips.find((c) => c.trackIndex === delInfo.trackIndex && c.patternId === delInfo.originalPatternId);
962            const clipStart = clip ? clip.startTick : 0;
963            const clipOffset = clip ? clip.offsetTicks : 0;
964            const relTick = delInfo.absoluteTick - clipStart + clipOffset;
965            const noteIdx = pattern.notes.findIndex((n) => n.midi === delInfo.midi && n.ticks === relTick);
966            if (noteIdx !== -1) {
967              pattern.notes.splice(noteIdx, 1);
968            }
969          }
970        });
971        snapshot.forEach((backupNote) => {
972          const trackClip = playlistClipsRef.current.find((c) => c.trackIndex === backupNote.trackIndex);
973          if (!trackClip) return;
974          const trackEasyPattern = patternsRef.current.find((p) => p.id === trackClip.patternId);
975          if (!trackEasyPattern) return;
976          const matchedEasyNote = trackEasyPattern.notes.find((n) => n.midi === backupNote.midi && n.ticks === backupNote.absoluteTick);
977          if (matchedEasyNote) {
978            if (matchedEasyNote.durationTicks !== backupNote.durationTicks || matchedEasyNote.velocity !== backupNote.velocity) {
979              const pattern = nextPatterns.find((p) => p.id === backupNote.originalPatternId);
980              if (pattern) {
981                const clip = backupClips.find((c) => c.trackIndex === backupNote.trackIndex && c.patternId === backupNote.originalPatternId);
982                const clipStart = clip ? clip.startTick : 0;
983                const clipOffset = clip ? clip.offsetTicks : 0;
984                const relTick = backupNote.absoluteTick - clipStart + clipOffset;
985                const noteIdx = pattern.notes.findIndex((n) => n.midi === backupNote.midi && n.ticks === relTick);
986                if (noteIdx !== -1) {
987                  pattern.notes[noteIdx].durationTicks = matchedEasyNote.durationTicks;
988                  pattern.notes[noteIdx].velocity = matchedEasyNote.velocity;
989                }
990              }
991            }
992          }
993        });
994        Object.entries(resolutions).forEach(([trackIdxStr, res]) => {
995          var _a;
996          const trackIdx = parseInt(trackIdxStr, 10);
997          const added = addedNotesPerTrack[trackIdx];
998          if (!added || added.length === 0) return;
999          if (res.action === "merge") {
1000            const targetPatId = res.targetPatternId;
1001            const pattern = nextPatterns.find((p) => p.id === targetPatId);
1002            if (pattern) {
1003              const clip = nextClips.find((c) => c.trackIndex === trackIdx && c.patternId === targetPatId);
1004              const clipStart = clip ? clip.startTick : 0;
1005              const clipOffset = clip ? clip.offsetTicks : 0;
1006              added.forEach((n) => {
1007                const relTick = n.absoluteTick - clipStart + clipOffset;
1008                pattern.notes.push({
1009                  midi: n.midi,
1010                  ticks: relTick,
1011                  durationTicks: n.durationTicks,
1012                  velocity: n.velocity,
1013                  voice: n.voice || 1
1014                });
1015              });
1016            }
1017          } else if (res.action === "new-track") {
1018            const nextTrackId = `track-easy-new-${trackIdx}-${Date.now()}`;
1019            const nextTrackName = `New Track (${((_a = nextTracks[trackIdx]) == null ? void 0 : _a.name) || `Track ${trackIdx + 1}`})`;
1020            const nextTrackColor = `#${Math.floor((0.3 + 0.6 * Math.random()) * 16777215).toString(16)}`;
1021            const newTrack = {
1022              id: nextTrackId,
1023              name: nextTrackName,
1024              color: nextTrackColor,
1025              icon: "synth",
1026              isMuted: false,
1027              isSoloed: false,
1028              volume: 0.8,
1029              panning: 0,
1030              height: 64,
1031              parentId: null
1032            };
1033            nextTracks.push(newTrack);
1034            const newPatId = `pat-easy-new-${trackIdx}-${Date.now()}`;
1035            const newPat = {
1036              id: newPatId,
1037              name: nextTrackName,
1038              color: nextTrackColor,
1039              notes: added.map((n) => ({
1040                midi: n.midi,
1041                ticks: n.absoluteTick,
1042                durationTicks: n.durationTicks,
1043                velocity: n.velocity,
1044                voice: n.voice || 1
1045              }))
1046            };
1047            nextPatterns.push(newPat);
1048            const maxNoteTick = newPat.notes.reduce((max, n) => Math.max(max, n.ticks + n.durationTicks), 0);
1049            const durationTicks = Math.max(maxNoteTick + 96 * 16, 96 * 4 * 128);
1050            const newClip = {
1051              id: `clip-easy-new-${trackIdx}-${Date.now()}`,
1052              patternId: newPatId,
1053              trackIndex: nextTracks.length - 1,
1054              startTick: 0,
1055              durationTicks,
1056              offsetTicks: 0,
1057              isMuted: false
1058            };
1059            nextClips.push(newClip);
1060            if (engineRef.current) {
1061              engineRef.current.addTrack();
1062            }
1063          }
1064        });
1065        setPatterns(nextPatterns);
1066        setPlaylistClips(nextClips);
1067        setPlaylistTracks(nextTracks);
1068        if (nextPatterns.length > 0) {
1069          setActivePatternId(nextPatterns[0].id);
1070          setActiveTrackIndex(0);
1071        }
1072        setEasyMode_(false);
1073        setEasyToProMergeState(null);
1074        proModeBackupRef.current = null;
1075        absoluteNotesSnapshotRef.current = [];
1076        setTimeout(() => {
1077          var _a;
1078          reconstructMidi(nextPatterns, nextClips, nextTracks, []);
1079          (_a = engineRef.current) == null ? void 0 : _a.syncSequencer();
1080        }, 0);
1081      };
1082      if (totalAdded > 0) {
1083        const mergeTracks = [];
1084        Object.entries(addedNotesPerTrack).forEach(([trackIdxStr, notes]) => {
1085          const trackIdx = parseInt(trackIdxStr, 10);
1086          const track = playlistTracksRef.current[trackIdx];
1087          const availablePats = backupClips.filter((c) => c.trackIndex === trackIdx).map((c) => {
1088            const p = backupPats.find((pat) => pat.id === c.patternId);
1089            return { id: c.patternId, name: (p == null ? void 0 : p.name) || "Unnamed Pattern" };
1090          });
1091          const seen = /* @__PURE__ */ new Set();
1092          const uniquePats = availablePats.filter((p) => {
1093            if (seen.has(p.id)) return false;
1094            seen.add(p.id);
1095            return true;
1096          });
1097          mergeTracks.push({
1098            trackIndex: trackIdx,
1099            trackName: (track == null ? void 0 : track.name) || `Track ${trackIdx + 1}`,
1100            addedNotes: notes,
1101            availablePatterns: uniquePats
1102          });
1103        });
1104        setEasyToProMergeState({
1105          tracks: mergeTracks,
1106          resolve: (resolutions) => {
1107            applyTransition(resolutions);
1108          }
1109        });
1110      } else {
1111        applyTransition({});
1112      }
1113    }
1114  }, [easyMode, convertToEasyModeStructure, setPlaybackMode, setPatterns, setPlaylistClips, setPlaylistTracks, setActivePatternId, setActiveTrackIndex, reconstructMidi]);
1115  const setMasterVolume = reactExports.useCallback((vol) => {
1116    setMasterVolumeState(vol);
1117    if (engineRef.current) {
1118      engineRef.current.setMasterVolume(vol);
1119    }
1120  }, []);
1121  const setMasterPitch = reactExports.useCallback((pitch) => {
1122    setMasterPitchState(pitch);
1123    if (engineRef.current) {
1124      engineRef.current.setMasterPitch(pitch);
1125    }
1126  }, []);
1127  const setTempo = reactExports.useCallback((bpm) => {
1128    setTempoState(bpm);
1129    if (engineRef.current) {
1130      engineRef.current.addTempo(0, bpm);
1131      setMidiVersion((v) => v + 1);
1132    }
1133  }, []);
1134  const setTrackVolume = reactExports.useCallback((trackIdx, volume) => {
1135    setPlaylistTracks((prev) => prev.map((track, idx) => {
1136      if (idx === trackIdx) {
1137        return { ...track, volume };
1138      }
1139      return track;
1140    }));
1141    if (engineRef.current) {
1142      engineRef.current.setTrackVolume(trackIdx, volume);
1143    }
1144  }, [setPlaylistTracks]);
1145  const setTrackPanning = reactExports.useCallback((trackIdx, panning) => {
1146    setPlaylistTracks((prev) => prev.map((track, idx) => {
1147      if (idx === trackIdx) {
1148        return { ...track, panning };
1149      }
1150      return track;
1151    }));
1152    if (engineRef.current) {
1153      engineRef.current.setTrackPan(trackIdx, panning);
1154    }
1155  }, [setPlaylistTracks]);
1156  const toggleTrackMute = reactExports.useCallback((trackIdx) => {
1157    setPlaylistTracks((prev) => {
1158      const next = prev.map((track, idx) => {
1159        if (idx === trackIdx) {
1160          const isMuted = !track.isMuted;
1161          if (engineRef.current) {
1162            engineRef.current.setTrackMute(idx, isMuted);
1163          }
1164          return { ...track, isMuted };
1165        }
1166        return track;
1167      });
1168      return next;
1169    });
1170  }, [setPlaylistTracks]);
1171  const toggleTrackSolo = reactExports.useCallback((trackIdx) => {
1172    setPlaylistTracks((prev) => {
1173      const targetTrack = prev[trackIdx];
1174      if (!targetTrack) return prev;
1175      const isCurrentlySoloed = targetTrack.isSoloed;
1176      const willSolo = !isCurrentlySoloed;
1177      const next = prev.map((track, idx) => {
1178        if (willSolo) {
1179          const isMuted = idx !== trackIdx;
1180          const isSoloed = idx === trackIdx;
1181          if (engineRef.current) {
1182            engineRef.current.setTrackMute(idx, isMuted);
1183          }
1184          return { ...track, isMuted, isSoloed };
1185        } else {
1186          if (engineRef.current) {
1187            engineRef.current.setTrackMute(idx, false);
1188          }
1189          return { ...track, isMuted: false, isSoloed: false };
1190        }
1191      });
1192      return next;
1193    });
1194  }, [setPlaylistTracks]);
1195  const pushHistoryState = reactExports.useCallback(() => {
1196    if (!engineRef.current) return;
1197    const state = {
1198      patterns: JSON.parse(JSON.stringify(patterns)),
1199      playlistTracks: JSON.parse(JSON.stringify(playlistTracks)),
1200      playlistClips: JSON.parse(JSON.stringify(playlistClips)),
1201      automationClips: JSON.parse(JSON.stringify(automationClips)),
1202      timeMarkers: JSON.parse(JSON.stringify(timeMarkers)),
1203      loopRegion: loopRegion ? { ...loopRegion } : null,
1204      easyMode,
1205      activePatternId,
1206      meiDocs: { ...meiDocs }
1207    };
1208    setUndoStack((prev) => {
1209      const next = [...prev, state];
1210      if (next.length > 30) next.shift();
1211      return next;
1212    });
1213    setRedoStack([]);
1214  }, [patterns, playlistTracks, playlistClips, automationClips, timeMarkers, loopRegion, easyMode, activePatternId, meiDocs]);
1215  const undo = reactExports.useCallback(() => {
1216    if (undoStack.length === 0 || !engineRef.current) return;
1217    const currentState = {
1218      patterns: JSON.parse(JSON.stringify(patterns)),
1219      playlistTracks: JSON.parse(JSON.stringify(playlistTracks)),
1220      playlistClips: JSON.parse(JSON.stringify(playlistClips)),
1221      automationClips: JSON.parse(JSON.stringify(automationClips)),
1222      timeMarkers: JSON.parse(JSON.stringify(timeMarkers)),
1223      loopRegion: loopRegion ? { ...loopRegion } : null,
1224      easyMode,
1225      activePatternId,
1226      meiDocs: { ...meiDocs }
1227    };
1228    setRedoStack((prev) => [...prev, currentState]);
1229    const prevState = undoStack[undoStack.length - 1];
1230    setUndoStack((prev) => prev.slice(0, -1));
1231    setPatterns(prevState.patterns);
1232    setPlaylistTracks(prevState.playlistTracks);
1233    setPlaylistClips(prevState.playlistClips);
1234    setAutomationClips(prevState.automationClips);
1235    setTimeMarkers(prevState.timeMarkers);
1236    setLoopRegion(prevState.loopRegion);
1237    setEasyMode(prevState.easyMode);
1238    setActivePatternId(prevState.activePatternId);
1239    setMeiDocs(prevState.meiDocs);
1240    setTimeout(() => {
1241      var _a;
1242      reconstructMidi(prevState.patterns, prevState.playlistClips, prevState.playlistTracks, prevState.automationClips);
1243      (_a = engineRef.current) == null ? void 0 : _a.syncSequencer();
1244      setMidiVersion((v) => v + 1);
1245    }, 0);
1246    setSelectedNoteIds([]);
1247  }, [undoStack, patterns, playlistTracks, playlistClips, automationClips, timeMarkers, loopRegion, easyMode, activePatternId, meiDocs, reconstructMidi]);
1248  const redo = reactExports.useCallback(() => {
1249    if (redoStack.length === 0 || !engineRef.current) return;
1250    const currentState = {
1251      patterns: JSON.parse(JSON.stringify(patterns)),
1252      playlistTracks: JSON.parse(JSON.stringify(playlistTracks)),
1253      playlistClips: JSON.parse(JSON.stringify(playlistClips)),
1254      automationClips: JSON.parse(JSON.stringify(automationClips)),
1255      timeMarkers: JSON.parse(JSON.stringify(timeMarkers)),
1256      loopRegion: loopRegion ? { ...loopRegion } : null,
1257      easyMode,
1258      activePatternId,
1259      meiDocs: { ...meiDocs }
1260    };
1261    setUndoStack((prev) => [...prev, currentState]);
1262    const nextState = redoStack[redoStack.length - 1];
1263    setRedoStack((prev) => prev.slice(0, -1));
1264    setPatterns(nextState.patterns);
1265    setPlaylistTracks(nextState.playlistTracks);
1266    setPlaylistClips(nextState.playlistClips);
1267    setAutomationClips(nextState.automationClips);
1268    setTimeMarkers(nextState.timeMarkers);
1269    setLoopRegion(nextState.loopRegion);
1270    setEasyMode(nextState.easyMode);
1271    setActivePatternId(nextState.activePatternId);
1272    setMeiDocs(nextState.meiDocs);
1273    setTimeout(() => {
1274      var _a;
1275      reconstructMidi(nextState.patterns, nextState.playlistClips, nextState.playlistTracks, nextState.automationClips);
1276      (_a = engineRef.current) == null ? void 0 : _a.syncSequencer();
1277      setMidiVersion((v) => v + 1);
1278    }, 0);
1279    setSelectedNoteIds([]);
1280  }, [redoStack, patterns, playlistTracks, playlistClips, automationClips, timeMarkers, loopRegion, easyMode, activePatternId, meiDocs, reconstructMidi]);
1281  const copy = reactExports.useCallback(() => {
1282    if (!activePatternId || selectedNoteIds.length === 0) return;
1283    const pat = patterns.find((p) => p.id === activePatternId);
1284    if (!pat) return;
1285    const copiedNotes = selectedNoteIds.map((idx) => {
1286      const n = pat.notes[idx];
1287      if (!n) return null;
1288      return {
1289        midi: n.midi,
1290        durationTicks: n.durationTicks,
1291        velocity: n.velocity,
1292        originalTicks: n.ticks,
1293        voice: n.voice
1294      };
1295    }
1295).filter(Boolean);
1296    if (copiedNotes.length > 0) {
1297      const minTick = Math.min(...copiedNotes.map((n) => n.originalTicks));
1298      const normalizedNotes = copiedNotes.map((n) => ({
1299        ...n,
1300        offsetTicks: n.originalTicks - minTick
1301      }));
1302      setClipboard({ trackIdx: activeTrackIndex || 0, notes: normalizedNotes });
1303    }
1304  }, [activePatternId, selectedNoteIds, patterns, activeTrackIndex]);
1305  const paste = reactExports.useCallback(() => {
1306    if (!activePatternId || !clipboard || clipboard.notes.length === 0 || !engineRef.current) return;
1307    pushHistoryState();
1308    const playheadTicks = engineRef.current.secondsToTicks(currentTime);
1309    setPatterns((prev) => {
1310      const next = prev.map((p) => {
1311        if (p.id === activePatternId) {
1312          const notes = [...p.notes];
1313          clipboard.notes.forEach((noteData) => {
1314            const pasteTicks = playheadTicks + noteData.offsetTicks;
1315            notes.push({
1316              midi: noteData.midi,
1317              ticks: pasteTicks,
1318              durationTicks: noteData.durationTicks,
1319              velocity: noteData.velocity,
1320              voice: noteData.voice || 1
1321            });
1322          });
1323          return { ...p, notes };
1324        }
1325        return p;
1326      });
1327      setSelectedNoteIds([]);
1328      setTimeout(() => forceSync(), 0);
1329      return next;
1330    });
1331  }, [activePatternId, clipboard, currentTime, forceSync, pushHistoryState]);
1332  const addNote = reactExports.useCallback((trackIdx, midiNote, ticks, durationTicks, velocity, source, voice = 1) => {
1333    pushHistoryState();
1334    if (!activePatternId) return;
1335    if (easyMode) {
1336      setPlaylistClips((prevClips) => {
1337        return prevClips.map((clip) => {
1338          if (clip.patternId === activePatternId) {
1339            const noteEnd = ticks + durationTicks;
1340            if (noteEnd > clip.durationTicks) {
1341              return {
1342                ...clip,
1343                durationTicks: noteEnd + 96 * 16
1344              };
1345            }
1346          }
1347          return clip;
1348        });
1349      });
1350    }
1351    setPatterns((prev) => {
1352      const next = prev.map((p) => {
1353        if (p.id === activePatternId) {
1354          return {
1355            ...p,
1356            notes: [
1357              ...p.notes,
1358              { midi: midiNote, ticks, durationTicks, velocity, voice }
1359            ]
1360          };
1361        }
1362        return p;
1363      });
1364      setTimeout(() => forceSync(), 0);
1365      return next;
1366    });
1367  }, [activePatternId, forceSync, pushHistoryState, easyMode]);
1368  const addNotes = reactExports.useCallback((trackIdx, notesToAdd, source) => {
1369    pushHistoryState();
1370    if (!activePatternId) return;
1371    if (easyMode && notesToAdd.length > 0) {
1372      const maxNoteEnd = notesToAdd.reduce((max, n) => Math.max(max, n.ticks + n.durationTicks), 0);
1373      setPlaylistClips((prevClips) => {
1374        return prevClips.map((clip) => {
1375          if (clip.patternId === activePatternId) {
1376            if (maxNoteEnd > clip.durationTicks) {
1377              return {
1378                ...clip,
1379                durationTicks: maxNoteEnd + 96 * 16
1380              };
1381            }
1382          }
1383          return clip;
1384        });
1385      });
1386    }
1387    setPatterns((prev) => {
1388      const next = prev.map((p) => {
1389        if (p.id === activePatternId) {
1390          const newNotes = notesToAdd.map((n) => ({
1391            midi: n.midi,
1392            ticks: n.ticks,
1393            durationTicks: n.durationTicks,
1394            velocity: n.velocity,
1395            voice: n.voice || 1
1396          }));
1397          return {
1398            ...p,
1399            notes: [...p.notes, ...newNotes]
1400          };
1401        }
1402        return p;
1403      });
1404      setTimeout(() => forceSync(), 0);
1405      return next;
1406    });
1407  }, [activePatternId, forceSync, pushHistoryState, easyMode]);
1408  const updateNote = reactExports.useCallback((trackIdx, noteIndex, newProps, source) => {
1409    pushHistoryState();
1410    if (!activePatternId) return;
1411    if (easyMode) {
1412      const currentPattern = patternsRef.current.find((p) => p.id === activePatternId);
1413      const note = currentPattern == null ? void 0 : currentPattern.notes[noteIndex];
1414      if (note) {
1415        const finalTicks = newProps.ticks !== void 0 ? newProps.ticks : note.ticks;
1416        const finalDuration = newProps.durationTicks !== void 0 ? newProps.durationTicks : note.durationTicks;
1417        const noteEnd = finalTicks + finalDuration;
1418        setPlaylistClips((prevClips) => {
1419          return prevClips.map((clip) => {
1420            if (clip.patternId === activePatternId) {
1421              if (noteEnd > clip.durationTicks) {
1422                return {
1423                  ...clip,
1424                  durationTicks: noteEnd + 96 * 16
1425                };
1426              }
1427            }
1428            return clip;
1429          });
1430        });
1431      }
1432    }
1433    setPatterns((prev) => {
1434      const next = prev.map((p) => {
1435        if (p.id === activePatternId) {
1436          const notes = [...p.notes];
1437          if (notes[noteIndex]) {
1438            notes[noteIndex] = {
1439              ...notes[noteIndex],
1440              ...newProps
1441            };
1442          }
1443          return { ...p, notes };
1444        }
1445        return p;
1446      });
1447      setTimeout(() => forceSync(), 0);
1448      return next;
1449    });
1450  }, [activePatternId, forceSync, pushHistoryState, easyMode]);
1451  const updateNotes = reactExports.useCallback((trackIdx, updates, source) => {
1452    pushHistoryState();
1453    if (!activePatternId || updates.length === 0) return;
1454    if (easyMode) {
1455      const currentPattern = patternsRef.current.find((p) => p.id === activePatternId);
1456      if (currentPattern) {
1457        let maxNoteEnd = 0;
1458        updates.forEach(({ noteIndex, newProps }) => {
1459          const note = currentPattern.notes[noteIndex];
1460          if (note) {
1461            const finalTicks = newProps.ticks !== void 0 ? newProps.ticks : note.ticks;
1462            const finalDuration = newProps.durationTicks !== void 0 ? newProps.durationTicks : note.durationTicks;
1463            const noteEnd = finalTicks + finalDuration;
1464            if (noteEnd > maxNoteEnd) {
1465              maxNoteEnd = noteEnd;
1466            }
1467          }
1468        });
1469        if (maxNoteEnd > 0) {
1470          setPlaylistClips((prevClips) => {
1471            return prevClips.map((clip) => {
1472              if (clip.patternId === activePatternId) {
1473                if (maxNoteEnd > clip.durationTicks) {
1474                  return {
1475                    ...clip,
1476                    durationTicks: maxNoteEnd + 96 * 16
1477                  };
1478                }
1479              }
1480              return clip;
1481            });
1482          });
1483        }
1484      }
1485    }
1486    setPatterns((prev) => {
1487      const next = prev.map((p) => {
1488        if (p.id === activePatternId) {
1489          const notes = [...p.notes];
1490          updates.forEach(({ noteIndex, newProps }) => {
1491            if (notes[noteIndex]) {
1492              notes[noteIndex] = {
1493                ...notes[noteIndex],
1494                ...newProps
1495              };
1496            }
1497          });
1498          return { ...p, notes };
1499        }
1500        return p;
1501      });
1502      setTimeout(() => forceSync(), 0);
1503      return next;
1504    });
1505  }, [activePatternId, forceSync, pushHistoryState, easyMode]);
1506  const deleteNote = reactExports.useCallback((trackIdx, noteIndex, source) => {
1507    pushHistoryState();
1508    if (!activePatternId) return;
1509    setPatterns((prev) => {
1510      const next = prev.map((p) => {
1511        if (p.id === activePatternId) {
1512          const notes = [...p.notes];
1513          notes.splice(noteIndex, 1);
1514          return { ...p, notes };
1515        }
1516        return p;
1517      });
1518      setTimeout(() => forceSync(), 0);
1519      return next;
1520    });
1521  }, [activePatternId, forceSync, pushHistoryState]);
1522  const splitNote = reactExports.useCallback((trackIdx, noteIndex, splitTick, source) => {
1523    pushHistoryState();
1524    if (!activePatternId) return;
1525    setPatterns((prev) => {
1526      const next = prev.map((p) => {
1527        if (p.id === activePatternId) {
1528          const notes = [...p.notes];
1529          const note = notes[noteIndex];
1530          if (note && splitTick > note.ticks && splitTick < note.ticks + note.durationTicks) {
1531            const firstDuration = splitTick - note.ticks;
1532            const secondDuration = note.durationTicks - firstDuration;
1533            const secondTick = splitTick;
1534            notes[noteIndex] = {
1535              ...note,
1536              durationTicks: firstDuration
1537            };
1538            notes.push({
1539              midi: note.midi,
1540              ticks: secondTick,
1541              durationTicks: secondDuration,
1542              velocity: note.velocity,
1543              voice: note.voice,
1544              isMuted: note.isMuted
1545            });
1546          }
1547          return { ...p, notes };
1548        }
1549        return p;
1550      });
1551      setTimeout(() => forceSync(), 0);
1552      return next;
1553    });
1554  }, [activePatternId, forceSync, pushHistoryState]);
1555  const deleteSelection = reactExports.useCallback(() => {
1556    if (!activePatternId || selectedNoteIds.length === 0) return;
1557    pushHistoryState();
1558    setPatterns((prev) => {
1559      const next = prev.map((p) => {
1560        if (p.id === activePatternId) {
1561          const sortedIds = [...selectedNoteIds].sort((a, b) => b - a);
1562          const notes = [...p.notes];
1563          for (const idx of sortedIds) {
1564            if (idx >= 0 && idx < notes.length) {
1565              notes.splice(idx, 1);
1566            }
1567          }
1568          return { ...p, notes };
1569        }
1570        return p;
1571      });
1572      setSelectedNoteIds([]);
1573      setTimeout(() => forceSync(), 0);
1574      return next;
1575    });
1576  }, [activePatternId, selectedNoteIds, forceSync, pushHistoryState]);
1577  const activeRecordingNotesRef = reactExports.useRef({});
1578  const commitRecordedNote = reactExports.useCallback((note, startTick, endTick, velocity) => {
1579    var _a;
1580    if (!activePatternId || !isRecording || !isPlaying || !engineRef.current) return;
1581    const durationTicks = Math.max(1, endTick - startTick);
1582    const channel = activeTrackIndex !== null ? ((_a = playlistTracks[activeTrackIndex]) == null ? void 0 : _a.channel) || 0 : 0;
1583    addNote(activeTrackIndex || 0, note, startTick, durationTicks, velocity, "pianoroll", channel);
1584  }, [activePatternId, isRecording, isPlaying, activeTrackIndex, playlistTracks, addNote]);
1585  const handleNoteOn = reactExports.useCallback((note, velocity = 100) => {
1586    var _a;
1587    if (!engineRef.current || !isEngineReady) return;
1588    const channel = activeTrackIndex !== null ? ((_a = playlistTracks[activeTrackIndex]) == null ? void 0 : _a.channel) || 0 : 0;
1589    engineRef.current.startNotePreview(note, channel);
1590    setActiveMidiKeys((prev) => {
1591      const next = new Set(prev);
1592      next.add(note);
1593      return next;
1594    });
1595    if (isRecording && isPlaying) {
1596      const currentTick = Math.floor(engineRef.current.secondsToTicks(engineRef.current.getCurrentTime()));
1597      activeRecordingNotesRef.current[note] = currentTick;
1598    }
1599  }, [isRecording, isPlaying, activeTrackIndex, playlistTracks, isEngineReady]);
1600  const handleNoteOff = reactExports.useCallback((note) => {
1601    var _a;
1602    if (!engineRef.current || !isEngineReady) return;
1603    const channel = activeTrackIndex !== null ? ((_a = playlistTracks[activeTrackIndex]) == null ? void 0 : _a.channel) || 0 : 0;
1604    engineRef.current.stopNotePreview(note, channel);
1605    setActiveMidiKeys((prev) => {
1606      const next = new Set(prev);
1607      next.delete(note);
1608      return next;
1609    });
1610    if (isRecording && isPlaying && activeRecordingNotesRef.current[note] !== void 0) {
1611      const endTick = Math.floor(engineRef.current.secondsToTicks(engineRef.current.getCurrentTime()));
1612      const startTick = activeRecordingNotesRef.current[note];
1613      delete activeRecordingNotesRef.current[note];
1614      commitRecordedNote(note, startTick, endTick, 100);
1615    }
1616  }, [isRecording, isPlaying, activeTrackIndex, playlistTracks, commitRecordedNote, isEngineReady]);
1617  const {
1618    isMidiSupported,
1619    isMidiConnected,
1620    connectMidi,
1621    error: midiError
1622  } = useMidiKeyboard({
1623    playNote: handleNoteOn,
1624    stopNote: handleNoteOff,
1625    enabled: isEngineReady
1626  });
1627  reactExports.useEffect(() => {
1628    if (!isTypingKeyboardEnabled || !isEngineReady || !engineRef.current) return;
1629    const keyMap = {
1630      "a": 60,
1631      // C4
1632      "w": 61,
1633      // C#4
1634      "s": 62,
1635      // D4
1636      "e": 63,
1637      // D#4
1638      "d": 64,
1639      // E4
1640      "f": 65,
1641      // F4
1642      "t": 66,
1643      // F#4
1644      "g": 67,
1645      // G4
1646      "y": 68,
1647      // G#4
1648      "h": 69,
1649      // A4
1650      "u": 70,
1651      // A#4
1652      "j": 71,
1653      // B4
1654      "k": 72,
1655      // C5
1656      "o": 73,
1657      // C#5
1658      "l": 74,
1659      // D5
1660      "p": 75,
1661      // D#5
1662      ";": 76,
1663      // E5
1664      "'": 77
1665      // F5
1666    };
1667    const activeNotes = /* @__PURE__ */ new Set();
1668    const handleKeyDown = (e) => {
1669      const activeEl = document.activeElement;
1670      if (activeEl) {
1671        const tag = activeEl.tagName.toLowerCase();
1672        if (tag === "input" || tag === "textarea" || activeEl.hasAttribute("contenteditable")) {
1673          return;
1674        }
1675      }
1676      const key = e.key.toLowerCase();
1677      if (keyMap[key] !== void 0) {
1678        e.preventDefault();
1679        const note = keyMap[key];
1680        if (!activeNotes.has(note)) {
1681          activeNotes.add(note);
1682          handleNoteOn(note);
1683        }
1684      }
1685    };
1686    const handleKeyUp = (e) => {
1687      const key = e.key.toLowerCase();
1688      if (keyMap[key] !== void 0) {
1689        const note = keyMap[key];
1690        if (activeNotes.has(note)) {
1691          activeNotes.delete(note);
1692          handleNoteOff(note);
1693        }
1694      }
1695    };
1696    window.addEventListener("keydown", handleKeyDown);
1697    window.addEventListener("keyup", handleKeyUp);
1698    return () => {
1699      window.removeEventListener("keydown", handleKeyDown);
1700      window.removeEventListener("keyup", handleKeyUp);
1701      activeNotes.forEach((note) => {
1702        var _a, _b;
1703        const channel = activeTrackIndex !== null ? ((_a = playlistTracks[activeTrackIndex]) == null ? void 0 : _a.channel) || 0 : 0;
1704        (_b = engineRef.current) == null ? void 0 : _b.stopNotePreview(note, channel);
1705      });
1706    };
1707  }, [isTypingKeyboardEnabled, isEngineReady, activeTrackIndex, playlistTracks, handleNoteOn, handleNoteOff]);
1708  const loadMidiData = reactExports.useCallback((buffer) => {
1709    if (engineRef.current) {
1710      engineRef.current.loadMidi(buffer);
1711      migrateLegacyMidi(engineRef.current);
1712      if (engineRef.current.midi.header.tempos && engineRef.current.midi.header.tempos.length > 0) {
1713        setTempoState(engineRef.current.midi.header.tempos[0].bpm);
1714      }
1715    }
1716  }, [migrateLegacyMidi]);
1717  const loadProjectData = reactExports.useCallback((projectData) => {
1718    var _a, _b, _c;
1719    if (!projectData) return;
1720    pushHistoryState();
1721    if (projectData.patterns) {
1722      setPatterns(projectData.patterns);
1723      setPlaylistTracks(projectData.playlistTracks || []);
1724      setPlaylistClips(projectData.playlistClips || []);
1725      setAutomationClips(projectData.automationClips || []);
1726      setTimeMarkers(projectData.timeMarkers || []);
1727      setLoopRegion(projectData.loopRegion || null);
1728      setEasyMode_(!!projectData.easyMode);
1729      if (projectData.gridSnap) setGridSnap(projectData.gridSnap);
1730      setActivePatternId(projectData.activePatternId || (((_a = projectData.patterns[0]) == null ? void 0 : _a.id) || null));
1731      if (projectData.meiDocs) {
1732        setMeiDocs(projectData.meiDocs);
1733      } else {
1734        setMeiDocs({});
1735      }
1736      if (projectData.midi) {
1737        const binaryString = atob(projectData.midi);
1738        const len = binaryString.length;
1739        const bytes = new Uint8Array(len);
1740        for (let i = 0; i < len; i++) {
1741          bytes[i] = binaryString.charCodeAt(i);
1742        }
1743        if (engineRef.current) {
1744          engineRef.current.loadMidi(bytes.buffer);
1745          if (projectData.title) {
1746            engineRef.current.midi.name = projectData.title;
1747          }
1748          if (((_c = (_b = engineRef.current.midi) == null ? void 0 : _b.header) == null ? void 0 : _c.tempos) && engineRef.current.midi.header.tempos.length > 0) {
1749            setTempoState(engineRef.current.midi.header.tempos[0].bpm);
1750          }
1751        }
1752      }
1753      setMidiVersion((v) => v + 1);
1754    }
1755  }, [setPatterns, setPlaylistTracks, setPlaylistClips, setAutomationClips, setTimeMarkers, setLoopRegion, setGridSnap, setA
1755ctivePatternId, pushHistoryState]);
1756  const makePatternUnique = reactExports.useCallback((clipId) => {
1757    pushHistoryState();
1758    setPlaylistClips((prevClips) => {
1759      const targetClipIndex = prevClips.findIndex((c) => c.id === clipId);
1760      if (targetClipIndex === -1) return prevClips;
1761      const targetClip = prevClips[targetClipIndex];
1762      const originalPat = patterns.find((p) => p.id === targetClip.patternId);
1763      if (!originalPat) return prevClips;
1764      const newPatId = `pat-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
1765      const newPat = {
1766        ...originalPat,
1767        id: newPatId,
1768        name: `${originalPat.name} (Unique)`,
1769        notes: JSON.parse(JSON.stringify(originalPat.notes))
1770      };
1771      setPatterns((prevPats) => [...prevPats, newPat]);
1772      const updatedClips = [...prevClips];
1773      updatedClips[targetClipIndex] = {
1774        ...targetClip,
1775        patternId: newPatId
1776      };
1777      setActivePatternId(newPatId);
1778      setTimeout(() => forceSync(), 0);
1779      return updatedClips;
1780    });
1781  }, [patterns, forceSync, pushHistoryState]);
1782  const value = {
1783    engine: engineRef.current,
1784    isEngineReady,
1785    midiVersion,
1786    currentTime,
1787    isPlaying,
1788    duration,
1789    activeTrackIndex,
1790    selectedNoteIds,
1791    selectedTempoIndex,
1792    selectedTimeSigIndex,
1793    play,
1794    pause,
1795    stop,
1796    seek,
1797    setActiveTrackIndex: handleSetActiveTrackIndex,
1798    setSelectedNoteIds: handleSetSelectedNoteIds,
1799    setSelectedTempoIndex: handleSetSelectedTempoIndex,
1800    setSelectedTimeSigIndex: handleSetSelectedTimeSigIndex,
1801    forceSync,
1802    addNote,
1803    addNotes,
1804    updateNote,
1805    updateNotes,
1806    deleteNote,
1807    splitNote,
1808    deleteSelection,
1809    engineError,
1810    loadingStatus,
1811    loadMidiData,
1812    loadProjectData,
1813    meiDocs,
1814    setMeiDoc,
1815    canUndo: undoStack.length > 0,
1816    canRedo: redoStack.length > 0,
1817    undo,
1818    redo,
1819    pushHistoryState,
1820    clipboard,
1821    copy,
1822    paste,
1823    hint,
1824    setHint,
1825    // Pattern DAW integrations
1826    patterns,
1827    setPatterns,
1828    activePatternId,
1829    setActivePatternId: handleSetActivePatternId,
1830    playlistTracks,
1831    setPlaylistTracks,
1832    playlistClips,
1833    setPlaylistClips,
1834    selectedClipIds,
1835    setSelectedClipIds,
1836    automationClips,
1837    setAutomationClips,
1838    timeMarkers,
1839    setTimeMarkers,
1840    loopRegion,
1841    setLoopRegion,
1842    loopMarkerTick,
1843    setLoopMarkerTick,
1844    gridSnap,
1845    setGridSnap,
1846    currentPlaylistTool,
1847    setCurrentPlaylistTool,
1848    easyMode,
1849    setEasyMode: handleSetEasyMode,
1850    easyToProMergeState,
1851    setEasyToProMergeState,
1852    makePatternUnique,
1853    activePattern,
1854    // FL Studio Transport additionals
1855    playbackMode,
1856    setPlaybackMode,
1857    isRecording,
1858    setIsRecording,
1859    isTypingKeyboardEnabled,
1860    setIsTypingKeyboardEnabled,
1861    activeMidiKeys,
1862    isMidiSupported,
1863    isMidiConnected,
1864    connectMidi,
1865    midiError,
1866    isCountdownEnabled,
1867    setIsCountdownEnabled,
1868    isMetronomeEnabled,
1869    setIsMetronomeEnabled,
1870    isLoopRecordingEnabled,
1871    setIsLoopRecordingEnabled,
1872    masterVolume,
1873    setMasterVolume,
1874    masterPitch,
1875    setMasterPitch,
1876    tempo,
1877    setTempo,
1878    setTrackVolume,
1879    setTrackPanning,
1880    toggleTrackMute,
1881    toggleTrackSolo
1882  };
1883  return /* @__PURE__ */ jsxRuntimeExports.jsx(EditorContext.Provider, { value, children });
1884};
1885const DRUM_MAP = {
1886  // Kicks
1887  35: { pname: "f", oct: 4 },
1888  // Acoustic Bass Drum
1889  36: { pname: "f", oct: 4 },
1890  // Bass Drum 1
1891  // Snares
1892  38: { pname: "c", oct: 5 },
1893  // Acoustic Snare
1894  40: { pname: "c", oct: 5 },
1895  // Electric Snare
1896  37: { pname: "c", oct: 5, shape: "x" },
1897  // Side Stick
1898  // Toms
1899  41: { pname: "e", oct: 4 },
1900  // Low Floor Tom
1901  43: { pname: "g", oct: 4 },
1902  // High Floor Tom
1903  45: { pname: "a", oct: 4 },
1904  // Low Tom
1905  47: { pname: "c", oct: 5 },
1906  // Low-Mid Tom (same line as snare usually, or 'b' 4)
1907  48: { pname: "d", oct: 5 },
1908  // Hi-Mid Tom
1909  50: { pname: "e", oct: 5 },
1910  // High Tom
1911  // Hi-Hats
1912  42: { pname: "g", oct: 5, shape: "x" },
1913  // Closed Hi Hat
1914  44: { pname: "g", oct: 5, shape: "x" },
1915  // Pedal Hi-Hat
1916  46: { pname: "g", oct: 5, shape: "diamond" },
1917  // Open Hi-Hat
1918  // Cymbals
1919  49: { pname: "a", oct: 5, shape: "x" },
1920  // Crash Cymbal 1
1921  51: { pname: "f", oct: 5, shape: "x" },
1922  // Ride Cymbal 1
1923  52: { pname: "a", oct: 5, shape: "x" },
1924  // Chinese Cymbal
1925  53: { pname: "f", oct: 5, shape: "diamond" },
1926  // Ride Bell
1927  55: { pname: "a", oct: 5, shape: "x" },
1928  // Splash Cymbal
1929  57: { pname: "a", oct: 5, shape: "x" },
1930  // Crash Cymbal 2
1931  59: { pname: "f", oct: 5, shape: "x" },
1932  // Ride Cymbal 2
1933  // Extras
1934  56: { pname: "g", oct: 4, shape: "triangle" },
1935  // Cowbell
1936  81: { pname: "g", oct: 4, shape: "triangle" },
1937  // Open Triangle
1938  80: { pname: "g", oct: 4, shape: "triangle" }
1939  // Mute Triangle
1940};
1941const REVERSE_DRUM_MAP = Object.entries(DRUM_MAP).reduce((acc, [midi, props]) => {
1942  const key = `${props.pname}${props.oct}`;
1943  if (!acc[key]) acc[key] = parseInt(midi);
1944  return acc;
1945}, {});
1946const createEmptyMEI = (title = "Title", isPercussion = false, isGrandStaff = false) => {
1947  let measures = "";
1948  for (let i = 1; i <= 20; i++) {
1949    const systemBreak = i > 1 && (i - 1) % 4 === 0 ? "<sb />" : "";
1950    let staves = "";
1951    if (isGrandStaff) {
1952      staves = `
1953                                <staff n="1" xml:id="s-${i}-1">
1954                                    <layer n="1" xml:id="l-${i}-1-1">
1955                                        <mRest xml:id="mr-${i}-1" />
1956                                    </layer>
1957                                </staff>
1958                                <staff n="2" xml:id="s-${i}-2">
1959                                    <layer n="1" xml:id="l-${i}-2-1">
1960                                        <mRest xml:id="mr-${i}-2" />
1961                                    </layer>
1962                                </staff>`;
1963    } else {
1964      staves = `
1965                                <staff n="1" xml:id="s-${i}">
1966                                    <layer n="1" xml:id="l-${i}">
1967                                        <mRest xml:id="mr-${i}" />
1968                                    </layer>
1969                                </staff>`;
1970    }
1971    measures += `
1972                        ${systemBreak}
1973                        <measure n="${i}" xml:id="m-${i}">${staves}
1974                        </measure>`;
1975  }
1976  let staffGrp = "";
1977  if (isGrandStaff) {
1978    staffGrp = `
1979                            <staffGrp symbol="brace" bar.thru="true">
1980                                <staffDef n="1" lines="5" clef.shape="G" clef.line="2" meter.count="4" meter.unit="4" key.sig="0" />
1981                                <staffDef n="2" lines="5" clef.shape="F" clef.line="4" meter.count="4" meter.unit="4" key.sig="0" />
1982                            </staffGrp>`;
1983  } else {
1984    staffGrp = `<staffDef n="1" lines="5" clef.shape="${isPercussion ? "perc" : "G"}" clef.line="${isPercussion ? "3" : "2"}" meter.count="4" meter.unit="4" key.sig="0" label=" " />`;
1985  }
1986  return `<?xml version="1.0" encoding="UTF-8"?>
1987<?xml-model href="https://music-encoding.org/schema/dev/mei-all.rng" type="application/xml" schematypens="http://relaxng.org/ns/structure/1.0"?>
1988<?xml-model href="https://music-encoding.org/schema/dev/mei-all.rng" type="application/xml" schematypens="http://purl.oclc.org/dsdl/schematron"?>
1989<mei xmlns="http://www.music-encoding.org/ns/mei" meiversion="5.0">
1990    <meiHead>
1991        <fileDesc>
1992            <titleStmt>
1993                <title>${title}</title>
1994                <title type="subtitle">Subtitle</title>
1995                <respStmt>
1996                    <persName role="composer">Composer</persName>
1997                    <persName role="lyricist">Lyricist</persName>
1998                </respStmt>
1999            </titleStmt>
2000            <pubStmt/>
2001        </fileDesc>
2002    </meiHead>
2003    <music>
2004        <body>
2005            <mdiv>
2006                <score>
2007                    <scoreDef>
2008                        <staffGrp>
2009${staffGrp}
2010                        </staffGrp>
2011                    </scoreDef>
2012                    <section>${measures}
2013                    </section>
2014                </score>
2015            </mdiv>
2016        </body>
2017    </music>
2018</mei>`;
2019};
2020const getDurationInBeats$1 = (verovioDur) => {
2021  switch (verovioDur) {
2022    case "1":
2023      return 4;
2024    case "2":
2025      return 2;
2026    case "4":
2027      return 1;
2028    case "8":
2029      return 0.5;
2030    case "16":
2031      return 0.25;
2032    case "32":
2033      return 0.125;
2034    case "64":
2035      return 0.0625;
2036    default:
2037      return 1;
2038  }
2039};
2040const getMidiFromMEI$1 = (pname, oct, accid) => {
2041  const basePitches = {
2042    "c": 0,
2043    "d": 2,
2044    "e": 4,
2045    "f": 5,
2046    "g": 7,
2047    "a": 9,
2048    "b": 11
2049  };
2050  let midi = (oct + 1) * 12 + basePitches[pname.toLowerCase()];
2051  if (accid === "s") midi += 1;
2052  if (accid === "f") midi -= 1;
2053  if (accid === "ss" || accid === "xs") midi += 2;
2054  if (accid === "ff" || accid === "df") midi -= 2;
2055  return midi;
2056};
2057const getMEIFromMidi = (midiNote) => {
2058  const noteNames2 = ["c", "c", "d", "d", "e", "f", "f", "g", "g", "a", "a", "b"];
2059  const accids = ["", "s", "", "s", "", "", "s", "", "s", "", "s", ""];
2060  const pitchClass = midiNote % 12;
2061  const oct = Math.floor(midiNote / 12) - 1;
2062  return {
2063    pname: noteNames2[pitchClass],
2064    oct,
2065    accid: accids[pitchClass] || void 0
2066  };
2067};
2068const getBeats$1 = (dur, dots, num, numbase) => {
2069  let base = 1;
2070  switch (dur) {
2071    case "1":
2072      base = 4;
2073      break;
2074    case "2":
2075      base = 2;
2076      break;
2077    case "4":
2078      base = 1;
2079      break;
2080    case "8":
2081      base = 0.5;
2082      break;
2083    case "16":
2084      base = 0.25;
2085      break;
2086    case "32":
2087      base = 0.125;
2088      break;
2089    case "64":
2090      base = 0.0625;
2091      break;
2092  }
2093  if (dots) {
2094    const numDots = parseInt(dots);
2095    if (numDots === 1) base *= 1.5;
2096    else if (numDots === 2) base *= 1.75;
2097  }
2098  if (num && numbase) {
2099    const n = parseInt(num);
2100    const nb = parseInt(numbase);
2101    if (n > 0 && nb > 0) {
2102      base = base * nb / n;
2103    }
2104  }
2105  return base;
2106};
2107const generateRestsForDuration = (doc, beats) => {
2108  const rests = [];
2109  const ns = "http://www.music-encoding.org/ns/mei";
2110  let remaining = beats;
2111  const restTypes = [
2112    { beats: 4, dur: "1", dots: 0 },
2113    { beats: 3, dur: "2", dots: 1 },
2114    { beats: 2, dur: "2", dots: 0 },
2115    { beats: 1.5, dur: "4", dots: 1 },
2116    { beats: 1, dur: "4", dots: 0 },
2117    { beats: 0.75, dur: "8", dots: 1 },
2118    { beats: 0.5, dur: "8", dots: 0 },
2119    { beats: 0.375, dur: "16", dots: 1 },
2120    { beats: 0.25, dur: "16", dots: 0 },
2121    { beats: 0.125, dur: "32", dots: 0 },
2122    { beats: 0.0625, dur: "64", dots: 0 }
2123  ];
2124  while (remaining > 1e-3) {
2125    const match = restTypes.find((r) => r.beats <= remaining + 1e-3);
2126    if (!match) break;
2127    const restEl = doc.createElementNS(ns, "rest");
2128    restEl.setAttribute("xml:id", "r-" + Math.random().toString(36).substring(2, 9));
2129    restEl.setAttribute("dur", match.dur);
2130    if (match.dots > 0) {
2131      restEl.setAttribute("dots", match.dots.toString());
2132    }
2133    rests.push(restEl);
2134    remaining -= match.beats;
2135  }
2136  return rests;
2137};
2138const consolidateMeiRests = (doc) => {
2139  const layers = Array.from(doc.querySelectorAll("layer"));
2140  const ns = "http://www.music-encoding.org/ns/mei";
2141  layers.forEach((layer) => {
2142    const noteEvents = [];
2143    let currentBeat = 0;
2144    const children = Array.from(layer.children);
2145    children.forEach((child) => {
2146      const tmpStart = child.getAttribute("tmp-start-beat");
2147      const startBeat = tmpStart ? parseFloat(tmpStart) : currentBeat;
2148      child.removeAttribute("tmp-start-beat");
2149      if (child.tagName === "note") {
2150        const durAttr = child.getAttribute("dur") || "4";
2151        const dotsAttr = child.getAttribute("dots") || null;
2152        const duration = getBeats$1(durAttr, dotsAttr);
2153        console.log("EDITOR_LOG: parsed child note:", child.getAttribute("xml:id"), "durAttr =", durAttr, "duration =", duration);
2154        noteEvents.push({
2155          element: child,
2156          pname: child.getAttribute("pname") || "c",
2157          oct: child.getAttribute("oct") || "4",
2158          accid: child.getAttribute("accid") || void 0,
2159          startBeat,
2160          duration,
2161          tie: child.getAttribute("tie") || void 0
2162        });
2163        if (!tmpStart) {
2164          currentBeat += duration;
2165        }
2166      } else if (child.tagName === "chord") {
2167        const durAttr = child.getAttribute("dur") || "4";
2168        const dotsAttr = child.getAttribute("dots") || null;
2169        const duration = getBeats$1(durAttr, dotsAttr);
2170        const notes = Array.from(child.querySelectorAll("note"));
2171        notes.forEach((n) => {
2172          noteEvents.push({
2173            element: n,
2174            pname: n.getAttribute("pname") || "c",
2175            oct: n.getAttribute("oct") || "4",
2176            accid: n.getAttribute("accid") || void 0,
2177            startBeat,
2178            duration,
2179            tie: n.getAttribute("tie") || child.getAttribute("tie") || void 0
2180          });
2181        });
2182        if (!tmpStart) {
2183          currentBeat += duration;
2184        }
2185      } else if (child.tagName === "rest" || child.tagName === "mRest") {
2186        const durAttr = child.getAttribute("dur") || "4";
2187        const dotsAttr = child.getAttribute("dots") || null;
2188        const duration = child.tagName === "mRest" ? 4 : getBeats$1(durAttr, dotsAttr);
2189        if (!tmpStart) {
2190          currentBeat += duration;
2191        }
2192      }
2193    });
2194    while (layer.firstChild) {
2195      layer.removeChild(layer.firstChild);
2196    }
2197    if (noteEvents.length === 0) {
2198      if (layer.getAttribute("n") === "1") {
2199        const mRestEl = doc.createElementNS(ns, "mRest");
2200        const layerId = layer.getAttribute("xml:id") || "";
2201        const mRestId = layerId.replace("l-", "mr-") || "mr-" + Math.random().toString(36).substring(2, 9);
2202        mRestEl.setAttribute("xml:id", mRestId);
2203        layer.appendChild(mRestEl);
2204      } else {
2205        layer.remove();
2206      }
2207      return;
2208    }
2209    noteEvents.sort((a, b) => {
2210      if (Math.abs(a.startBeat - b.startBeat) > 1e-3) {
2211        return a.startBeat - b.startBeat;
2212      }
2213      return a.pname.localeCompare(b.pname);
2214    });
2215    const uniqueStartBeats = Array.from(new Set(noteEvents.map((e) => Math.round(e.startBeat * 1e3) / 1e3))).sort((a, b) => a - b);
2216    for (let i = 0; i < uniqueStartBeats.length - 1; i++) {
2217      const currentStart = uniqueStartBeats[i];
2218      const nextStart = uniqueStartBeats[i + 1];
2219      const currentGroup = noteEvents.filter((e) => Math.abs(e.startBeat - currentStart) < 1e-3);
2220      const maxDur = Math.max(...currentGroup.map((g) => g.duration));
2221      if (currentStart + maxDur > nextStart) {
2222        const allowedDur = nextStart - currentStart;
2223        currentGroup.forEach((g) => {
2224          g.duration = allowedDur;
2225        });
2226      }
2227    }
2228    const groupedEvents = [];
2229    noteEvents.forEach((evt) => {
2230      let grp = groupedEvents.find((g) => Math.abs(g.startBeat - evt.startBeat) < 1e-3);
2231      if (!grp) {
2232        grp = { startBeat: evt.startBeat, duration: evt.duration, notes: [] };
2233        groupedEvents.push(grp);
2234      }
2235      grp.notes.push(evt);
2236      grp.duration = evt.duration;
2237    });
2238    groupedEvents.sort((a, b) => a.startBeat - b.startBeat);
2239    let rebuildBeat = 0;
2240    const measureBeats = 4;
2241    groupedEvents.forEach((grp) => {
2242      const gap = grp.startBeat - rebuildBeat;
2243      if (gap > 1e-3) {
2244        const rests = generateRestsForDuration(doc, gap);
2245        rests.forEach((r) => layer.appendChild(r));
2246      }
2247      const { dur: durAttr, dots: dotsAttr } = getMeiDurAttr(grp.duration);
2248      console.log("EDITOR_LOG: grp.duration =", grp.duration, "durAttr =", durAttr, "dotsAttr =", dotsAttr);
2249      if (grp.notes.length > 1) {
2250        const chord = doc.createElementNS(ns, "chord");
2251        chord.setAttribute("xml:id", "chord-" + Math.random().toString(36).substr(2, 9));
2252        chord.setAttribute("dur", durAttr);
2253        if (dotsAttr) chord.setAttribute("dots", dotsAttr);
2254        grp.notes.forEach((n) => {
2255          const noteEl = n.element;
2256          noteEl.setAttribute("xml:id", noteEl.getAttribute("xml:id") || "note-" + Math.random().toString(36).substr(2, 9));
2257          noteEl.setAttribute("pname", n.pname);
2258          noteEl.setAttribute("oct", n.oct);
2259          if (n.accid) noteEl.setAttribute("accid", n.accid);
2260          else noteEl.removeAttribute("accid");
2261          noteEl.removeAttribute("dur");
2262          noteEl.removeAttribute("dots");
2263          if (n.tie) {
2264            noteEl.setAttribute("tie", n.tie);
2265          } else {
2266            noteEl.removeAttribute("tie");
2267          }
2268          chord.appendChild(noteEl);
2269        });
2270        layer.appendChild(chord);
2271      } else {
2272        const n = grp.notes[0];
2273        const noteEl = n.element;
2274        noteEl.setAttribute("xml:id", noteEl.getAttribute("xml:id") || "note-" + Math.random().toString(36).substr(2, 9));
2275        noteEl.setAttribute("pname", n.pname);
2276        noteEl.setAttribute("oct", n.oct);
2277        if (n.accid) noteEl.setAttribute("accid", n.accid);
2278        else noteEl.removeAttribute("accid");
2279        noteEl.setAttribute("dur", durAttr);
2280        if (dotsAttr) noteEl.setAttribute("dots", dotsAttr);
2281        else noteEl.removeAttribute("dots");
2282        if (n.tie) noteEl.setAttribute("tie", n.tie);
2283        else noteEl.removeAttribute("tie");
2284        layer.appendChild(noteEl);
2285      }
2286      rebuildBeat = grp.startBeat + grp.duration;
2287    });
2288    const remaining = measureBeats - rebuildBeat;
2289    if (remaining > 1e-3) {
2290      const rests = generateRestsForDuration(doc, remaining);
2291      rests.forEach((r) => layer.appendChild(r));
2292    }
2293  });
2294};
2295const wrapMeiTuplets = (doc) => {
2296  const ns = "http://www.music-encoding.org/ns/mei";
2297  const layersAll = Array.from(doc.querySelectorAll("layer"));
2298  layersAll.forEach((layer) => {
2299    const tuplets = Array.from(layer.querySelectorAll("tuplet"));
2300    tuplets.forEach((t) => {
2301      const children2 = Array.from(t.children);
2302      children2.forEach((child) => {
2303        layer.insertBefore(child, t);
2304      });
2305      t.remove();
2306    });
2307    const children = Array.from(layer.children);
2308    let tupletGroup = [];
2309    let tupletSum = 0;
2310    const processTupletGroup = () => {
2311      if (tupletGroup.length > 0 && Math.abs(tupletSum - 1) < 0.15) {
2312        const tupletEl = doc.createElementNS(ns, "tuplet");
2313        tupletEl.setAttribute("num", "3");
2314        tupletEl.setAttribute("numbase", "2");
2315        tupletEl.setAttribute("xml:id", "tup-" + Math.random().toString(36).substring(2, 9));
2316        layer.insertBefore(tupletEl, tupletGroup[0]);
2317        tupletGroup.forEach((el) => {
2318          el.removeAttribute("num");
2319          el.removeAttribute("numbase");
2320          tupletEl.appendChild(el);
2321        });
2322      } else if (tupletGroup.length > 0) {
2323        tupletGroup.forEach((el) => {
2324          el.setAttribute("num", "3");
2325          el.setAttribute("numbase", "2");
2326        });
2327      }
2328      tupletGroup = [];
2329      tupletSum = 0;
2330    };
2331    children.forEach((child) => {
2332      if (child.tagName === "note" || child.tagName === "rest" || child.tagName === "chord") {
2333        let dur = child.getAttribute("dur") || "4";
2334        let dots = child.getAttribute("dots") || null;
2335        if (child.tagName === "chord") {
2336          const firstNote = child.querySelector("note");
2337          if (firstNote) {
2338            dur = firstNote.getAttribute("dur") || "4";
2339            dots = firstNote.getAttribute("dots") || null;
2340          }
2341        }
2342        const num = child.getAttribute("num");
2343        const numbase = child.getAttribute("numbase");
2344        const isTripletAttr = num === "3" && numbase === "2";
2345        const beatDur = getBeats$1(dur, dots, num, numbase);
2346        const isTriplet = isTripletAttr || Math.abs(beatDur - 0.333) < 0.05 || Math.abs(beatDur - 0.666) < 0.05;
2347        if (isTriplet) {
2348          tupletGroup.push(child);
2349          tupletSum += beatDur;
2350          if (tupletSum >= 0.95) {
2351            processTupletGroup();
2352          }
2353        } else {
2354          processTupletGroup();
2355        }
2356      } else {
2357        processTupletGroup();
2358      }
2359    });
2360    processTupletGroup();
2361  });
2362};
2363const getMeiDurAttr = (beats) => {
2364  const approx = (val) => Math.abs(beats - val) < 5e-3;
2365  if (beats >= 4) return { dur: "1" };
2366  if (beats >= 3.5) return { dur: "1", dots: "2" };
2367  if (beats >= 3) return { dur: "2", dots: "1" };
2368  if (beats >= 2.75) return { dur: "2", dots: "2" };
2369  if (beats >= 2) return { dur: "2" };
2370  if (beats >
2370= 1.75) return { dur: "4", dots: "2" };
2371  if (beats >= 1.5) return { dur: "4", dots: "1" };
2372  if (approx(1.333)) return { dur: "2" };
2373  if (beats >= 1) return { dur: "4" };
2374  if (beats >= 0.875) return { dur: "8", dots: "2" };
2375  if (beats >= 0.75) return { dur: "8", dots: "1" };
2376  if (approx(0.666)) return { dur: "4" };
2377  if (beats >= 0.5) return { dur: "8" };
2378  if (beats >= 0.4375) return { dur: "16", dots: "2" };
2379  if (beats >= 0.375) return { dur: "16", dots: "1" };
2380  if (approx(0.333)) return { dur: "8" };
2381  if (beats >= 0.25) return { dur: "16" };
2382  if (beats >= 0.21875) return { dur: "32", dots: "2" };
2383  if (beats >= 0.125) return { dur: "32" };
2384  if (beats >= 0.109375) return { dur: "64", dots: "2" };
2385  return { dur: "64" };
2386};
2387const convertMidiToMEI = (track, ppq = 480) => {
2388  const parser = new DOMParser();
2389  const isPiano = track.instrument.name.toLowerCase().includes("piano");
2390  const doc = parser.parseFromString(createEmptyMEI("Title", track.channel === 9, isPiano), "application/xml");
2391  const ns = "http://www.music-encoding.org/ns/mei";
2392  const processedNotes = [];
2393  const flatNotes = [...track.notes].sort((a, b) => a.ticks - b.ticks);
2394  for (let i = 0; i < flatNotes.length; i++) {
2395    const n = flatNotes[i];
2396    let startBeat = n.ticks / ppq;
2397    let durBeats = n.durationTicks / ppq;
2398    if (i < flatNotes.length - 1) {
2399      const nextN = flatNotes[i + 1];
2400      const nextStartBeat = nextN.ticks / ppq;
2401      const endBeat = startBeat + durBeats;
2402      const gap = nextStartBeat - endBeat;
2403      if (gap > 0 && gap <= 0.55) {
2404        durBeats += gap;
2405      }
2406    }
2407    const snapToGrid = (val) => {
2408      const nearest16th = Math.round(val * 4) / 4;
2409      const nearestTriplet = Math.round(val * 3) / 3;
2410      if (Math.abs(val - nearestTriplet) < Math.abs(val - nearest16th) && Math.abs(val - nearestTriplet) < 0.05) {
2411        return nearestTriplet;
2412      }
2413      return nearest16th;
2414    };
2415    startBeat = snapToGrid(startBeat);
2416    durBeats = snapToGrid(durBeats);
2417    if (durBeats <= 0.01) durBeats = 0.25;
2418    let staff = 1;
2419    if (isPiano && track.channel !== 9) {
2420      if (n.midi < 60) staff = 2;
2421    }
2422    processedNotes.push({ ...n, _beatTime: startBeat, _beatDuration: durBeats, staff, _originalIdx: i });
2423  }
2424  const splitRhythms = (notes) => {
2425    const splitNotes = [];
2426    notes.forEach((note) => {
2427      let currentBeat = note._beatTime;
2428      let remainingDur = note._beatDuration;
2429      const measureBeats = 4;
2430      const parts = [];
2431      while (remainingDur > 1e-3) {
2432        const beatInMeasure = currentBeat % measureBeats;
2433        let maxDur = measureBeats - beatInMeasure;
2434        if (beatInMeasure < 2 && beatInMeasure + remainingDur > 2) {
2435          if (!(beatInMeasure === 0 && remainingDur >= 4) && !(beatInMeasure === 0 && remainingDur === 3)) {
2436            maxDur = Math.min(maxDur, 2 - beatInMeasure);
2437          }
2438        }
2439        const isOffBeat = beatInMeasure % 1 > 1e-3;
2440        if (isOffBeat && remainingDur > 0.5) {
2441          const nextBeat = Math.ceil(beatInMeasure);
2442          maxDur = Math.min(maxDur, nextBeat - beatInMeasure);
2443        }
2444        const durInThisPart = Math.min(remainingDur, maxDur);
2445        parts.push({ ...note, _beatTime: currentBeat, _beatDuration: durInThisPart });
2446        currentBeat += durInThisPart;
2447        remainingDur -= durInThisPart;
2448      }
2449      if (parts.length > 1) {
2450        for (let i = 0; i < parts.length; i++) {
2451          if (i === 0) parts[i].tie = "i";
2452          else if (i === parts.length - 1) parts[i].tie = "t";
2453          else parts[i].tie = "m";
2454        }
2455      }
2456      splitNotes.push(...parts);
2457    });
2458    return splitNotes;
2459  };
2460  const tiedNotes = splitRhythms(processedNotes);
2461  const stavesNotes = {
2462    1: { 1: [], 2: [], 3: [], 4: [] },
2463    2: { 1: [], 2: [], 3: [], 4: [] }
2464  };
2465  tiedNotes.sort((a, b) => {
2466    if (Math.abs(a._beatTime - b._beatTime) < 1e-3) return b._beatDuration - a._beatDuration;
2467    return a._beatTime - b._beatTime;
2468  });
2469  tiedNotes.forEach((note) => {
2470    const staffIdx = note.staff;
2471    const voice = note.voice || 1;
2472    const safeVoice = Math.max(1, Math.min(4, voice));
2473    stavesNotes[staffIdx][safeVoice].push(note);
2474  });
2475  let maxBeat = 0;
2476  tiedNotes.forEach((n) => {
2477    maxBeat = Math.max(maxBeat, n._beatTime + n._beatDuration);
2478  });
2479  const maxMeasureNum = Math.max(20, Math.floor(maxBeat / 4) + 1);
2480  const section = doc.querySelector("section");
2481  if (section) {
2482    for (let m = 1; m <= maxMeasureNum; m++) {
2483      let measure = doc.querySelector(`measure[n="${m}"]`);
2484      if (!measure) {
2485        measure = doc.createElementNS(ns, "measure");
2486        measure.setAttribute("n", m.toString());
2487        measure.setAttribute("xml:id", `m-${m}`);
2488        section.appendChild(measure);
2489      }
2490      const staffNums = isPiano ? [1, 2] : [1];
2491      staffNums.forEach((staffNum) => {
2492        let staff = measure.querySelector(`staff[n="${staffNum}"]`);
2493        if (!staff) {
2494          staff = doc.createElementNS(ns, "staff");
2495          staff.setAttribute("n", staffNum.toString());
2496          staff.setAttribute("xml:id", `s-${m}-${staffNum}`);
2497          measure.appendChild(staff);
2498        }
2499        const layers = stavesNotes[staffNum];
2500        for (let l = 1; l <= 4; l++) {
2501          const notesInLayer = layers[l];
2502          if (notesInLayer.length === 0) continue;
2503          const layerN = l.toString();
2504          let layer = staff.querySelector(`layer[n="${layerN}"]`);
2505          if (!layer) {
2506            layer = doc.createElementNS(ns, "layer");
2507            layer.setAttribute("n", layerN);
2508            layer.setAttribute("xml:id", `l-${m}-${staffNum}-${layerN}`);
2509            const mRest = doc.createElementNS(ns, "mRest");
2510            mRest.setAttribute("xml:id", `mr-${m}-${staffNum}-${layerN}`);
2511            layer.appendChild(mRest);
2512            staff.appendChild(layer);
2513          }
2514        }
2515      });
2516    }
2517  }
2518  Object.keys(stavesNotes).forEach((staffKey) => {
2519    const staffNum = parseInt(staffKey);
2520    const layers = stavesNotes[staffNum];
2521    Object.keys(layers).forEach((layerKey) => {
2522      const layerNum = parseInt(layerKey);
2523      const notesInLayer = layers[layerNum];
2524      if (notesInLayer.length === 0) return;
2525      const layerN = layerNum.toString();
2526      const notesByTime = {};
2527      notesInLayer.forEach((note) => {
2528        const timeKey = note._beatTime.toFixed(3);
2529        if (!notesByTime[timeKey]) notesByTime[timeKey] = [];
2530        notesByTime[timeKey].push(note);
2531      });
2532      const sortedTimes = Object.keys(notesByTime).map(Number).sort((a, b) => a - b);
2533      sortedTimes.forEach((time) => {
2534        const notes = notesByTime[time.toFixed(3)];
2535        if (notes.length === 0) return;
2536        const startBeat = time;
2537        const measureNum = Math.floor(startBeat / 4) + 1;
2538        const beatInMeasure = startBeat % 4;
2539        const measure = doc.querySelector(`measure[n="${measureNum}"]`);
2540        if (!measure) return;
2541        const staff = measure.querySelector(`staff[n="${staffNum}"]`);
2542        if (!staff) return;
2543        const layer = staff.querySelector(`layer[n="${layerN}"]`);
2544        if (!layer) return;
2545        const mRest = layer.querySelector("mRest");
2546        if (mRest) mRest.remove();
2547        if (notes.length > 1) {
2548          const chord = doc.createElementNS(ns, "chord");
2549          chord.setAttribute("xml:id", "chord-" + Math.random().toString(36).substr(2, 9));
2550          const { dur: chordDur, dots: chordDots } = getMeiDurAttr(notes[0]._beatDuration);
2551          chord.setAttribute("dur", chordDur);
2552          if (chordDots) chord.setAttribute("dots", chordDots);
2553          chord.setAttribute("tmp-start-beat", beatInMeasure.toString());
2554          chord.setAttribute("tmp-beat-duration", notes[0]._beatDuration.toString());
2555          notes.forEach((note) => {
2556            const noteEl = doc.createElementNS(ns, "note");
2557            noteEl.setAttribute("xml:id", "note-" + Math.random().toString(36).substr(2, 9));
2558            if (track.channel === 9) {
2559              const mapped = DRUM_MAP[note.midi] || { pname: "c", oct: 5, shape: "diamond" };
2560              noteEl.setAttribute("pname", mapped.pname);
2561              noteEl.setAttribute("oct", mapped.oct.toString());
2562              if (mapped.shape) noteEl.setAttribute("head.shape", mapped.shape);
2563            } else {
2564              const meiPitch = getMEIFromMidi(note.midi);
2565              noteEl.setAttribute("pname", meiPitch.pname);
2566              noteEl.setAttribute("oct", meiPitch.oct.toString());
2567              if (meiPitch.accid) noteEl.setAttribute("accid", meiPitch.accid);
2568            }
2569            if (note.tie) noteEl.setAttribute("tie", note.tie);
2570            noteEl.setAttribute("tmp-midi-idx", note._originalIdx.toString());
2571            chord.appendChild(noteEl);
2572          });
2573          layer.appendChild(chord);
2574        } else {
2575          const note = notes[0];
2576          const noteEl = doc.createElementNS(ns, "note");
2577          noteEl.setAttribute("xml:id", "note-" + Math.random().toString(36).substr(2, 9));
2578          const { dur: noteDur, dots: noteDots } = getMeiDurAttr(note._beatDuration);
2579          noteEl.setAttribute("dur", noteDur);
2580          if (noteDots) noteEl.setAttribute("dots", noteDots);
2581          noteEl.setAttribute("tmp-start-beat", beatInMeasure.toString());
2582          noteEl.setAttribute("tmp-beat-duration", note._beatDuration.toString());
2583          if (track.channel === 9) {
2584            const mapped = DRUM_MAP[note.midi] || { pname: "c", oct: 5, shape: "diamond" };
2585            noteEl.setAttribute("pname", mapped.pname);
2586            noteEl.setAttribute("oct", mapped.oct.toString());
2587            if (mapped.shape) noteEl.setAttribute("head.shape", mapped.shape);
2588          } else {
2589            const meiPitch = getMEIFromMidi(note.midi);
2590            noteEl.setAttribute("pname", meiPitch.pname);
2591            noteEl.setAttribute("oct", meiPitch.oct.toString());
2592            if (meiPitch.accid) noteEl.setAttribute("accid", meiPitch.accid);
2593          }
2594          if (note.tie) noteEl.setAttribute("tie", note.tie);
2595          noteEl.setAttribute("tmp-midi-idx", note._originalIdx.toString());
2596          layer.appendChild(noteEl);
2597        }
2598      });
2599    });
2600  });
2601  const layersAll = Array.from(doc.querySelectorAll("layer"));
2602  layersAll.forEach((layer) => {
2603    const children = Array.from(layer.children);
2604    let tupletGroup = [];
2605    let tupletSum = 0;
2606    const processTupletGroup = () => {
2607      if (tupletGroup.length > 0 && Math.abs(tupletSum - 1) < 0.1) {
2608        const tupletEl = doc.createElementNS(ns, "tuplet");
2609        tupletEl.setAttribute("num", "3");
2610        tupletEl.setAttribute("numbase", "2");
2611        layer.insertBefore(tupletEl, tupletGroup[0]);
2612        tupletGroup.forEach((el) => tupletEl.appendChild(el));
2613      }
2614      tupletGroup = [];
2615      tupletSum = 0;
2616    };
2617    children.forEach((child) => {
2618      const beatDurStr = child.getAttribute("tmp-beat-duration");
2619      if (beatDurStr) {
2620        const beatDur = parseFloat(beatDurStr);
2621        const isTriplet = Math.abs(beatDur - 0.333) < 0.05 || Math.abs(beatDur - 0.666) < 0.05;
2622        if (isTriplet) {
2623          tupletGroup.push(child);
2624          tupletSum += beatDur;
2625          if (tupletSum >= 0.95) {
2626            processTupletGroup();
2627          }
2628        } else {
2629          processTupletGroup();
2630        }
2631      } else {
2632        processTupletGroup();
2633      }
2634    });
2635    processTupletGroup();
2636  });
2637  consolidateMeiRests(doc);
2638  const allNotes = Array.from(doc.querySelectorAll("note"));
2639  const tieStartNotes = allNotes.filter((n) => n.getAttribute("tie") === "i" || n.getAttribute("tie") === "m");
2640  tieStartNotes.forEach((startNote) => {
2641    const startId = startNote.getAttribute("xml:id");
2642    const pname = startNote.getAttribute("pname");
2643    const oct = startNote.getAttribute("oct");
2644    const tmpIdx = startNote.getAttribute("tmp-midi-idx");
2645    const startIndex = allNotes.indexOf(startNote);
2646    let endNote = null;
2647    for (let i = startIndex + 1; i < allNotes.length; i++) {
2648      const n = allNotes[i];
2649      if (n.getAttribute("pname") === pname && n.getAttribute("oct") === oct && n.getAttribute("tmp-midi-idx") === tmpIdx) {
2650        if (n.getAttribute("tie") === "m" || n.getAttribute("tie") === "t") {
2651          endNote = n;
2652          break;
2653        }
2654      }
2655    }
2656    if (endNote && startId && endNote.getAttribute("xml:id")) {
2657      const tieEl = doc.createElementNS(ns, "tie");
2658      tieEl.setAttribute("startid", "#" + startId);
2659      tieEl.setAttribute("endid", "#" + endNote.getAttribute("xml:id"));
2660      const m = startNote.closest("measure");
2661      if (m) m.appendChild(tieEl);
2662    }
2663  });
2664  allNotes.forEach((n) => {
2665    n.removeAttribute("tmp-midi-idx");
2666    n.removeAttribute("tmp-beat-duration");
2667  });
2668  const allChords = Array.from(doc.querySelectorAll("chord"));
2669  allChords.forEach((c) => c.removeAttribute("tmp-beat-duration"));
2670  const serializer = new XMLSerializer();
2671  return serializer.serializeToString(doc);
2672};
2673const getMidiNotesFromBase64 = (base64Midi, originalPpq = 480) => {
2674  const binaryString = window.atob(base64Midi);
2675  const len = binaryString.length;
2676  const bytes = new Uint8Array(len);
2677  for (let i = 0; i < len; i++) {
2678    bytes[i] = binaryString.charCodeAt(i);
2679  }
2680  const parsedMidi = new Midi.Midi(bytes.buffer);
2681  const notes = [];
2682  parsedMidi.tracks.forEach((track) => {
2683    track.notes.forEach((note) => {
2684      const scalingFactor = originalPpq / parsedMidi.header.ppq;
2685      const ticks = Math.round(note.ticks * scalingFactor);
2686      const durationTicks = Math.round(note.durationTicks * scalingFactor);
2687      notes.push({
2688        midi: note.midi,
2689        ticks,
2690        durationTicks,
2691        velocity: note.velocity,
2692        voice: note.voice || 1
2693      });
2694    });
2695  });
2696  return notes;
2697};
2698const getPitchFromY = (meiDoc, yOffset, snappedBeat) => {
2699  const step = 38 - yOffset;
2700  const pitchMap = ["c", "d", "e", "f", "g", "a", "b"];
2701  const pitch = pitchMap[(step % 7 + 7) % 7];
2702  const oct = Math.floor(step / 7);
2703  return `${pitch}${oct}`;
2704};
2705const ScoreSyncManager = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
2706  __proto__: null,
2707  DRUM_MAP,
2708  REVERSE_DRUM_MAP,
2709  consolidateMeiRests,
2710  convertMidiToMEI,
2711  createEmptyMEI,
2712  generateRestsForDuration,
2713  getBeats: getBeats$1,
2714  getDurationInBeats: getDurationInBeats$1,
2715  getMEIFromMidi,
2716  getMeiDurAttr,
2717  getMidiFromMEI: getMidiFromMEI$1,
2718  getMidiNotesFromBase64,
2719  getPitchFromY,
2720  wrapMeiTuplets
2721}, Symbol.toStringTag, { value: "Module" }));
2722const useScoreEditorEngine = (props) => {
2723  const { initialMei, dimensions, zoom, isPercussion, onNoteAdded, onNoteDeleted, onMeiUpdate, showMeasureNumbers = true, currentPage = 1, layoutMode = "linear" } = props;
2724  const [toolkit, setToolkit] = reactExports.useState(null);
2725  const [svg, setSvg] = reactExports.useState("");
2726  const [isReady, setIsReady] = reactExports.useState(false);
2727  const currentMeiRef = reactExports.useRef(initialMei || createEmptyMEI("Title", isPercussion));
2728  const onMeiUpdateRef = reactExports.useRef(onMeiUpdate);
2729  const [historyState, setHistoryState] = reactExports.useState(() => {
2730    const startMei = initialMei || createEmptyMEI("Title", isPercussion);
2731    return {
2732      history: [startMei],
2733      index: 0
2734    };
2735  });
2736  const pushToHistory = reactExports.useCallback((mei) => {
2737    setHistoryState((prev) => {
2738      if (prev.index >= 0 && prev.history[prev.index] === mei) return prev;
2739      const newHistory = prev.history.slice(0, prev.index + 1);
2740      let updatedHistory = [...newHistory, mei];
2741      let newIndex = newHistory.length;
2742      if (updatedHistory.length > 100) {
2743        updatedHistory = updatedHistory.slice(updatedHistory.length - 100);
2744        newIndex = updatedHistory.length - 1;
2745      }
2746      return {
2747        history: updatedHistory,
2748        index: newIndex
2749      };
2750    });
2751  }, []);
2752  reactExports.useEffect(() => {
2753    onMeiUpdateRef.current = onMeiUpdate;
2754  }, [onMeiUpdate]);
2755  const updateSvg = reactExports.useCallback(() => {
2756    if (!toolkit) return;
2757    const pageCount = toolkit.getPageCount();
2758    if (pageCount > 0) {
2759      const pageToRender = Math.min(pageCount, Math.max(1, currentPage));
2760      const render = toolkit.renderToSVG(pageToRender, {});
2761      setSvg(render);
2762    }
2763  }, [toolkit, currentPage]);
2764  reactExports.useEffect(() => {
2765    let mounted = true;
2766    const injectScript = () => {
2767      return new Promise((resolve, reject) => {
2768        const existing = document.querySelector('script[src*="verovio"]');
2769        if (window.verovio) {
2770          resolve();
2771          return;
2772        }
2773        if (existing) {
2774          resolve();
2775          return;
2776        }
2777        const script = document.createElement("script");
2778        script.src = "/libs/verovio/verovio-toolkit-wasm.js";
2779        script.async = true;
2780        script.onload = () => {
2781          script.dataset.loaded = "true";
2782          resolve();
2783        };
2784        script.onerror = () => reject(new Error("Failed to download verovio-toolkit-wasm.js"));
2785        document.head.appendChild(script);
2786      });
2787    };
2788    const initVerovio = async () => {
2789      try {
2790        await injectScript();
2791      } catch (e) {
2792        console.error("Verovio script download failed:", e);
2793        return;
2794      }
2795      let attempts = 0;
2796      while (attempts < 100) {
2797        if (!mounted) return;
2798        const v2 = window.verovio;
2799        if (v2 && typeof v2 === "object") break;
2800        await new Promise((r) => setTimeout(r, 100));
2801        attempts++;
2802      }
2803      const v = window.verovio;
2804      if (!v) {
2805        console.error("Verovio not loaded on window after timeout.");
2806        return;
2807      }
2808      try {
2809        let module = v.module;
2810        if (module && typeof module.then === "function") module = await module;
2811        else if (typeof module === "function") module = await module();
2812        if (module && !module.cwrap) {
2813          let waitAttempts = 0;
2814          while (!module.cwrap && waitAttempts < 100) {
2815            if (!mounted) return;
2816            await new Promise((r) => setTimeout(r, 50));
2817            waitAttempts++;
2818          }
2819        }
2820        const ToolkitClass = v.toolkit;
2821        let tk = null;
2822        if (!module || !module.cwrap) {
2823          try {
2824            tk = new ToolkitClass();
2825          } catch (err) {
2826            console.error("No-args instantiation failed:", err);
2827          }
2828        }
2829        if (!tk) {
2830          let initAttempts = 0;
2831          while (initAttempts < 10) {
2832            try {
2833              tk = new ToolkitClass(module);
2834              if (tk && typeof tk.getVersion === "function") break;
2835            } catch (err) {
2836              if (initAttempts >= 9) throw err;
2837              await new Promise((r) => setTimeout(r, 200));
2838              initAttempts++;
2839            }
2840          }
2841        }
2842        if (layoutMode === "page") {
2843          tk.setOptions({
2844            pageHeight: 2970,
2845            pageWidth: 2100,
2846            pageMarginLeft: 100,
2847            pageMarginRight: 100,
2848            pageMarginTop: 100,
2849            pageMarginBottom: 100,
2850            scale: 35,
2851            spacingLinear: 0.3,
2852            spacingNonLinear: 0.45,
2853            spacingDurDetection: true,
2854            breaks: "encoded",
2855            header: "none",
2856            footer: "none",
2857            justifyLastSystem: true,
2858            minLastJustification: 0,
2859            adjustPageHeight: true,
2860            adjustPageWidth: false,
2861            svgBoundingBoxes: true,
2862            mnumInterval: showMeasureNumbers ? 1 : 0
2863          });
2864        } else {
2865          tk.setOptions({
2866            scale: 40,
2867            font: "Leland",
2868            header: "auto",
2869            footer: "none",
2870            pageMarginTop: 195,
2871            pageMarginBottom: 195,
2872            pageMarginLeft: 150,
2873            pageMarginRight: 150,
2874            pageWidth: 2100,
2875            pageHeight: 2970,
2876            adjustPageHeight: true,
2877            adjustPageWidth: false,
2878            svgBoundingBoxes: true,
2879            spacingSystem: 14,
2880            spacingStaff: 10,
2881            justifyVertically: true,
2882            breaks: "line",
2883            mnumInterval: showMeasureNumbers ? 1 : 0
2884          });
2885        }
2886        if (mounted) {
2887          setToolkit(tk);
2888        }
2889      } catch (err) {
2890        console.error("Failed to init Verovio Editor Toolkit", err);
2891      }
2892    };
2893    initVerovio();
2894    return () => {
2895      mounted = false;
2896    };
2897  }, []);
2898  reactExports.useEffect(() => {
2899    if (!toolkit) return;
2900    toolkit.setOptions({
2901      mnumInterval: showMeasureNumbers ? 1 : 0
2902    });
2903    toolkit.redoLayout();
2904    updateSvg();
2905  }, [toolkit, showMeasureNumbers, updateSvg]);
2906  reactExports.useEffect(() => {
2907    if (!toolkit) return;
2908    let processedMei = currentMeiRef.current;
2909    if (processedMei && !processedMei.includes("<label>")) {
2910      processedMei = processedMei.replace(/(<staffDef\b[^>]*>)/g, "$1<label>&#160;</label>");
2911      currentMeiRef.current = processedMei;
2912    }
2913    const success = toolkit.loadData(processedMei);
2914    if (success === true || success === 1) {
2915      setIsReady(true);
2916      updateSvg();
2917      setHistoryState({
2918        history: [processedMei],
2919        index: 0
2920      });
2921    }
2922  }, [toolkit]);
2923  reactExports.useEffect(() => {
2924    if (!toolkit) return;
2925    let targetMei = initialMei || createEmptyMEI();
2926    if (targetMei && !targetMei.includes("<label>")) {
2927      targetMei = targetMei.replace(/(<staffDef\b[^>]*>)/g, "$1<label>&#160;</label>");
2928    }
2929    if (targetMei !== currentMeiRef.current) {
2930      currentMeiRef.current = targetMei;
2931      const success = toolkit.loadData(targetMei);
2932      if (success === true || success === 1) {
2933        setIsReady(true);
2934        updateSvg();
2935        setHistoryState((prev) => {
2936          if (prev.history.includes(targetMei)) return prev;
2937          return {
2938            history: [targetMei],
2939            index: 0
2940          };
2941        });
2942      }
2943    }
2944  }, [initialMei, toolkit, updateSvg]);
2945  reactExports.useEffect(() => {
2946    if (!toolkit || !isReady) return;
2947    toolkit.setOptions({ scale: layoutMode === "page" ? 19 : 45 });
2948    toolkit.redoLayout();
2949    updateSvg();
2950  }, [toolkit, isReady, updateSvg, layoutMode]);
2951  const performAction = reactExports.useCallback((action) => {
2952    var _a;
2953    if (!toolkit) return false;
2954    try {
2955      const res = toolkit.edit(action);
2956      if (res) {
2957        const mei = toolkit.getMEI();
2958        const parser = new DOMParser();
2959        const doc = parser.parseFromString(mei, "application/xml");
2960        consolidateMeiRests(doc);
2961        wrapMeiTuplets(doc);
2962        const serializer = new XMLSerializer();
2963        const newMei = serializer.serializeToString(doc);
2964        toolkit.loadData(newMei);
2965        currentMeiRef.current = newMei;
2966        updateSvg();
2967        (_a = onMeiUpdateRef.current) == null ? void 0 : _a.call(onMeiUpdateRef, newMei);
2968        pushToHistory(newMei);
2969        return true;
2970      }
2971    } catch (e) {
2972      console.error("Verovio Editor Action failed", e);
2973    }
2974    return false;
2975  }, [toolkit, updateSvg, pushToHistory]);
2976  const insertNote = reactExports.useCallback((elementId, actionType, noteData) => {
2977    const action = {
2978      action: actionType || "insert",
2979      param: {
2980        startid: elementId,
2981        elementType: noteData.elementType || "note",
2982        ...noteData
2983      }
2984    };
2985    return performAction(action);
2986  }, [performAction]);
2987  const updateMei = reactExports.useCallback((modifier) => {
2988    var _a;
2989    if (!toolkit) return false;
2990    try {
2991      const parser = new DOMParser();
2992      const doc = parser.parseFromString(currentMeiRef.current, "application/xml");
2993      modifier(doc);
2994      consolidateMeiRests(doc);
2995      wrapMeiTuplets(doc);
2996      const serializer = new XMLSerializer();
2997      const newMei = serializer.serializeToString(doc);
2998      if (newMei.includes('dur="32"')) {
2999        console.log("EDITOR_LOG: dur=32 is present in newMei!");
3000      } else {
3001        console.log("EDITOR_LOG: dur=32 is NOT present in newMei!");
3002      }
3003      const success = toolkit.loadData(newMei);
3004      if (success === true || success === 1) {
3005        currentMeiRef.current = newMei;
3006        updateSvg();
3007        (_a = onMeiUpdateRef.current) == null ? void 0 : _a.call(onMeiUpdateRef, newMei);
3008        pushToHistory(newMei);
3009        return true;
3010      }
3011    } catch (err) {
3012      console.error("Failed to update MEI", err);
3013    }
3014    return false;
3015  }, [toolkit, updateSvg, pushToHistory]);
3016  const deleteElement = reactExports.useCallback((elementId) => {
3017    const action = {
3018      action: "delete",
3019      param: { startid: elementId }
3020    };
3021    const success = performAction(action);
3022    if (success) return true;
3023    return updateMei((doc) => {
3024      const getXmlElById = (xmlDoc, id) => {
3025        return xmlDoc.getElementById(id) || xmlDoc.querySelector(`[id="${id}"]`) || xmlDoc.querySelector(`[xml\\:id="${id}"]`) || xmlDoc.querySelector(`[*|id="${id}"]`) || Array.from(xmlDoc.getElementsByTagName("*")).find((el) => el.getAttribute("xml:id") === id) || null;
3026      };
3027      const noteEl = getXmlElById(doc, elementId);
3028      if (noteEl) {
3029        const parent = noteEl.parentNode;
3030        noteEl.remove();
3031        if (parent && parent.tagName === "chord") {
3032          const remainingNotes = Array.from(parent.querySelectorAll("note"));
3033          if (remainingNotes.length === 1) {
3034            const singleNote = remainingNotes[0];
3035            const chordDur = parent.getAttribute("dur");
3036            const chordDots = parent.getAttribute("dots");
3037            if (chordDur) singleNote.setAttribute("dur", chordDur);
3038            if (chordDots) singleNote.setAttribute("dots", chordDots);
3039            const chordParent = parent.parentNode;
3040            if (chordParent) {
3041              chordParent.insertBefore(singleNote, parent);
3042              parent.remove();
3043            }
3044          } else if (remainingNotes.length === 0) {
3045            parent.remove();
3046          }
3047        }
3048      }
3049    });
3050  }, [performAction, updateMei]);
3051  const setAttributes = reactExports.useCallback((elementId, attributes) => {
3052    const action = {
3053      action: "set",
3054      param: {
3055        elementId,
3056        attributes
3057      }
3058    };
3059    return performAction(action);
3060  }, [performAction]);
3061  const undo = reactExports.useCallback(() => {
3062    if (!toolkit) return false;
3063    let undone = false;
3064    setHistoryState((prev) => {
3065      var _a;
3066      if (prev.index > 0) {
3067        const newIndex = prev.index - 1;
3068        const prevMei = prev.history[newIndex];
3069        toolkit.loadData(prevMei);
3070        currentMeiRef.current = prevMei;
3071        updateSvg();
3072        (_a = onMeiUpdateRef.current) == null ? void 0 : _a.call(onMeiUpdateRef, prevMei);
3073        undone = true;
3074        return {
3075          ...prev,
3076          index: newIndex
3077        };
3078      }
3079      return prev;
3080    });
3081    return undone;
3082  }, [toolkit, updateSvg]);
3083  const redo = reactExports.useCallback(() => {
3084    if (!toolkit) return false;
3085    let redone = false;
3086    setHistoryState((prev) => {
3087      var _a;
3088      if (prev.index < prev.history.length - 1) {
3089        const newIndex = prev.index + 1;
3090        const nextMei = prev.history[newIndex];
3091        toolkit.loadData(nextMei);
3092        currentMeiRef.current = nextMei;
3093        updateSvg();
3094        (_a = onMeiUpdateRef.current) == null ? void 0 : _a.call(onMeiUpdateRef, nextMei);
3095        redone = true;
3096        return {
3097          ...prev,
3098          index: newIndex
3099        };
3100      }
3101      return prev;
3102    });
3103    return redone;
3104  }, [toolkit, updateSvg]);
3105  const canUndo = historyState.index > 0;
3106  const canRedo = historyState.index < historyState.history.length - 1;
3107  const changeClef = reactExports.useCallback((staffNum, clefShape, clefLine) => {
3108    return updateMei((doc) => {
3109      const staffDefs = Array.from(doc.querySelectorAll("staffDef"));
3110      staffDefs.forEach((sd) => {
3111        if (sd.getAttribute("n") === staffNum.toString()) {
3112          sd.setAttribute("clef.shape", clefShape);
3113          sd.setAttribute("clef.line", clefLine.toString());
3114        }
3115      });
3116    });
3117  }, [updateMei]);
3118  const insertBarline = reactExports.useCallback((barlineType, selectedNoteId) => {
3119    return updateMei((doc) => {
3120      let measureEl = null;
3121      if (selectedNoteId) {
3122        const noteEl = doc.getElementById(selectedNoteId) || doc.querySelector(`[xml\\:id="${selectedNoteId}"]`);
3123        if (noteEl) {
3124          measureEl = noteEl.closest("measure");
3125        }
3126      }
3127      if (!measureEl) {
3128        const measures = Array.from(doc.querySelectorAll("measure"));
3129        measureEl = measures[measures.length - 1];
3130      }
3131      if (measureEl) {
3132        measureEl.setAttribute("right", barlineType);
3133      }
3134    });
3135  }, [updateMei]);
3136  return {
3137    toolkit,
3138    isReady,
3139    svg,
3140    currentMei: currentMeiRef.current,
3141    insertNote,
3142    deleteElement,
3143    setAttributes,
3144    updateMei,
3145    updateSvg,
3146    undo,
3147    redo,
3148    canUndo,
3149    canRedo,
3150    changeClef,
3151    insertBarline
3152  };
3153};
3154const getSnappingOffset = (clientY, staffNode) => {
3155  const staffLines = Array.from(staffNode.querySelectorAll("path"));
3156  if (staffLines.length < 5) return 0;
3157  const pathsWithWidth = staffLines.map((line) => {
3158    const rect = line.getBoundingClientRect();
3159    return { line, width: rect.width, rect };
3160  }).sort((a, b) => b.width - a.width);
3161  const rects = pathsWithWidth.slice(0, 5).map((item) => item.rect).sort((a, b) => a.top - b.top);
3162  if (rects.length === 5) {
3163    const topLineY = rects[0].top;
3164    const bottomLineY = rects[4].top;
3165    const avgGap = (bottomLineY - topLineY) / 4;
3166    const stepSize = avgGap / 2;
3167    const relativeY = clientY - topLineY;
3168    return Math.round(relativeY / stepSize);
3169  }
3170  return 0;
3171};
3172const getMidiFromMEI = (pname, oct, accid) => {
3173  const basePitches = {
3174    "c": 0,
3175    "d": 2,
3176    "e": 4,
3177    "f": 5,
3178    "g": 7,
3179    "a": 9,
3180    "b": 11
3181  };
3182  let midi = (oct + 1) * 12 + basePitches[pname.toLowerCase()];
3183  if (accid === "s") midi += 1;
3184  if (accid === "f") midi -= 1;
3185  if (accid === "ss") midi += 2;
3186  if (accid === "ff") midi -= 2;
3187  return midi;
3188};
3189const noteNames = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
3190const getNoteName = (midiNote) => {
3191  const octave = Math.floor(midiNote / 12) - 1;
3192  const name = noteNames[midiNote % 12];
3193  return `${name}${octave}`;
3194};
3195const GM_PERCUSSION_MAP = {
3196  35: "Acoustic Bass Drum",
3197  36: "Bass Drum 1",
3198  37: "Side Stick",
3199  38: "Acoustic Snare",
3200  39: "Hand Clap",
3201  40: "Electric Snare",
3202  41: "Low Floor Tom",
3203  42: "Closed Hi Hat",
3204  43: "High Floor Tom",
3205  44: "Pedal Hi-Hat",
3206  45: "Low Tom",
3207  46: "Open Hi-Hat",
3208  47: "Low-Mid Tom",
3209  48: "Hi-Mid Tom",
3210  49: "Crash Cymbal 1",
3211  50: "High Tom",
3212  51: "Ride Cymbal 1",
3213  52: "Chinese Cymbal",
3214  53: "Ride Bell",
3215  54: "Tambourine",
3216  55: "Splash Cymbal",
3217  56: "Cowbell",
3218  57: "Crash Cymbal 2",
3219  58: "Vibraslap",
3220  59: "Ride Cymbal 2",
3221  60: "Hi Bongo",
3222  61: "Low Bongo",
3223  62: "Mute Hi Conga",
3224  63: "Open Hi Conga",
3225  64: "Low Conga",
3226  65: "High Timbale",
3227  66: "Low Timbale",
3228  67: "High Agogo",
3229  68: "Low Agogo",
3230  69: "Cabasa",
3231  70: "Maracas",
3232  71: "Short Whistle",
3233  72: "Long Whistle",
3234  73: "Short Guiro",
3235  74: "Long Guiro",
3236  75: "Claves",
3237  76: "Hi Wood Block",
3238  77: "Low Wood Block",
3239  78: "Mute Cuica",
3240  79: "Open Cuica",
3241  80: "Mute Triangle",
3242  81: "Open Triangle"
3243};
3244const getBeats = (dur, dots = null) => {
3245  const val = parseInt(dur || "4", 10);
3246  let base = isNaN(val) ? 1 : 4 / val;
3247  if (dots === "1") base *= 1.5;
3248  return base;
3249};
3250const getStartTimeInLayer = (layer, targetEl) => {
3251  let eventEl = targetEl;
3252  if (targetEl.parentElement && targetEl.parentElement.tagName === "chord") {
3253    eventEl = targetEl.parentElement;
3254  }
3255  let elapsed = 0;
3256  const children = Array.from(layer.children);
3257  for (const child of children) {
3258    if (child === eventEl) {
3259      break;
3260    }
3261    if (child.tagName === "note" || child.tagName === "chord" || child.tagName === "rest" || child.tagName === "mRest") {
3262      const durAttr = child.getAttribute("dur") || "4";
3263      const dotsAttr = child.getAttribute("dots") || null;
3264      elapsed += getBeats(durAttr, dotsAttr);
3265    }
3266  }
3267  return elapsed;
3268};
3269const ScoreEditorCanvas = ({
3270  svg,
3271  currentTool,
3272  zoom,
3273  mei,
3274  isPlaying,
3275  engine,
3276  currentVoice,
3277  onElementClick,
3278  onElementRightClick,
3279  onElementDrag,
3280  onMouseUp,
3281  currentAccidental,
3282  isPercussion,
3283  entryMode = "note",
3284  isDotted = false,
3285  dotsCount = 0,
3286  isTriplet = false,
3287  currentArticulation = null,
3288  selectedNoteId = null,
3289  showGrid = true
3290}) => {
3291  let editorContext = null;
3292  try {
3293    editorContext = useEditor();
3294  } catch (e) {
3295  }
3296  const setHint = (editorContext == null ? void 0 : editorContext.setHint) || (() => {
3297  });
3298  const playbackMode = (editorContext == null ? void 0 : editorContext.playbackMode) || "song";
3299  const playlistClips = (editorContext == null ? void 0 : editorContext.playlistClips) || [];
3300  const activePatternId = (editorContext == null ? void 0 : editorContext.activePatternId) || null;
3301  const activePattern = (editorContext == null ? void 0 : editorContext.activePattern) || null;
3302  const currentTime = (editorContext == null ? void 0 : editorContext.currentTime) || 0;
3303  const containerRef = reactExports.useRef(null);
3304  const playheadRef = reactExports.useRef(null);
3305  const currentTimeRef = reactExports.useRef(currentTime);
3306  reactExports.useEffect(() => {
3307    currentTimeRef.current = currentTime;
3308  }, [currentTime]);
3309  const dragTargetRef = reactExports.useRef(null);
3310  const dragStartPosRef = reactExports.useRef(null);
3311  const [ghostPos, setGhostPos] = reactExports.useState(null);
3312  const [activeGrid, setActiveGrid] = reactExports.useState([]);
3313  const beatsPerMeasure = React.useMemo(() => {
3314    if (!mei) return 4;
3315    try {
3316      const parser = new DOMParser();
3317      const doc = parser.parseFromString(mei, "application/xml");
3318      const scoreDef = doc.querySelector("scoreDef");
3319      let count = 4;
3320      let unit = 4;
3321      if (scoreDef) {
3322        const meterCount = scoreDef.getAttribute("meter.count");
3323        const meterUnit = scoreDef.getAttribute("meter.unit");
3324        if (meterCount) count = parseInt(meterCount, 10) || 4;
3325        if (meterUnit) unit = parseInt(meterUnit, 10) || 4;
3326      } else {
3327        const staffDef = doc.querySelector("staffDef");
3328        if (staffDef) {
3329          const meterCount = staffDef.getAttribute("meter.count");
3330          const meterUnit = staffDef.getAttribute("meter.unit");
3331          if (meterCount) count = parseInt(meterCount, 10) || 4;
3332          if (meterUnit) unit = parseInt(meterUnit, 10) || 4;
3333        }
3334      }
3335      return count * (4 / unit);
3336    } catch (e) {
3337      console.error("Failed to parse meter from MEI", e);
3338    }
3339    return 4;
3340  }, [mei]);
3341  reactExports.useEffect(() => {
3342    if (!containerRef.current || !svg) return;
3343    const elements = containerRef.current.querySelectorAll(".selected-note");
3344    elements.forEach((el) => {
3345      el.classList.remove("selected-note");
3346      const paths = el.querySelectorAll("path, ellipse, line, rect, use, polygon");
3347      paths.forEach((p) => p.removeAttribute("fill"));
3348    });
3349    if (selectedNoteId) {
3350      const escapedId = selectedNoteId.replace(/(:|\.|\[|\]|,|=|@)/g, "\\$1");
3351      const selectedEl = containerRef.current.querySelector(`[id="${escapedId}"]`) || containerRef.current.querySelector(`[id="${selectedNoteId}"]`);
3352      if (selectedEl) {
3353        selectedEl.classList.add("selected-note");
3354        const paths = selectedEl.querySelectorAll("path, ellipse, line, rect, use, polygon");
3355        paths.forEach((p) => p.setAttribute("fill", "#4169E1"));
3356      }
3357    }
3358  }, [svg, selectedNoteId]);
3359  const meiDocRef = reactExports.useRef(null);
3360  reactExports.useEffect(() => {
3361    if (mei) {
3362      try {
3363        const parser = new DOMParser();
3364        meiDocRef.current = parser.parseFromString(mei, "text/xml");
3365      } catch (e) {
3366        console.error("Failed to parse MEI XML:", e);
3367        meiDocRef.current = null;
3368      }
3369    } else {
3370      meiDocRef.current = null;
3371    }
3372    return () => {
3373      setHint(null);
3374    };
3375  }, [mei, setHint]);
3376  const computeSnap = (clientX, measureNode, gridStep) => {
3377    const rect = measureNode.getBoundingClientRect();
3378    let startX = rect.left + 50;
3379    const elements = Array.from(measureNode.querySelectorAll(".note, .rest, .mRest, .chord"));
3380    if (elements.length > 0) {
3381      let minLeft = Infinity;
3382      elements.forEach((el) => {
3383        const elRect = el.getBoundingClientRect();
3384        if (elRect.left < minLeft && elRect.width > 0) minLeft = elRect.left + elRect.width / 2;
3385      });
3386      if (minLeft !== Infinity) startX = minLeft;
3387    }
3388    const endX = rect.right - 10;
3389    const usableWidth = endX - startX;
3390    let beat = 0;
3391    if (usableWidth > 0 && clientX > startX) {
3392      beat = (clientX - startX) / usableWidth * beatsPerMeasure;
3393    }
3394    const snappedBeat = Math.round(beat / gridStep) * gridStep;
3395    const clampedBeat = Math.max(0, Math.min(beatsPerMeasure - gridStep, snappedBeat));
3396    const snappedX = startX + clampedBeat / beatsPerMeasure * usableWidth;
3397    const gridXs = [];
3398    if (showGrid) {
3399      for (let b = 0; b <= beatsPerMeasure; b += gridStep) {
3400        if (b + gridStep <= beatsPerMeasure || b === beatsPerMeasure) {
3401          gridXs.push(startX + b / beatsPerMeasure * usableWidth);
3402        }
3403      }
3404    }
3405    return { clampedBeat, snappedX, gridXs, rect };
3406  };
3407  reactExports.useEffect(() => {
3408    if (!containerRef.current) return;
3409    const svgElement = containerRef.current.querySelector("svg");
3410    if (!svgElement) return;
3411    const tiesAndSlurs = Array.from(svgElement.querySelectorAll("g.tie, g.slur"));
3412    tiesAndSlurs.forEach((el) => {
3413      const parent = el.parentNode;
3414      if (parent) {
3415        parent.insertBefore(el, parent.firstChild);
3416      }
3417    });
3418    const style = document.getElementById("score-editor-styles") || document.createElement("style");
3419    style.id = "score-editor-styles";
3420    let voiceStyle = "";
3421    if (currentVoice !== "All" && currentVoice !== void 0) {
3422      const layerSelector = currentVoice === 1 ? 'g.layer:not([n="1"])[n]' : `g.layer:not([n="${currentVoice}"])`;
3423      voiceStyle = `
3424                .score-editor-canvas-container svg ${layerSelector} .note,
3425                .score-editor-canvas-container svg ${layerSelector} .rest,
3426                .score-editor-canvas-container svg ${layerSelector} .mRest,
3427                .score-editor-canvas-container svg ${layerSelector} .chord {
3428                    opacity: 0.25;
3429                    pointer-events: none;
3430                }
3431            `;
3432    }
3433    style.innerHTML = `
3434            .score-editor-canvas-container svg { width: 100%; height: auto; display: block; }
3435            .score-editor-canvas-container svg g.measure, .score-editor-canvas-container svg g.staff, .score-editor-canvas-container svg g.layer { pointer-events: bounding-box; cursor: inherit; }
3436            .score-editor-canvas-container svg .bounding-box { pointer-events: all; fill: transparent; cursor: inherit; }
3437            ${voiceStyle}
3438        `;
3439    if (!document.getElementById("score-editor-styles")) {
3440      document.head.appendChild(style);
3441    }
3442    if (currentTool === "erase") {
3443      svgElement.style.cursor = "crosshair";
3444    } else if (currentTool.startsWith("note_")) {
3445      svgElement.style.cursor = "none";
3446    } else {
3447      svgElement.style.cursor = "default";
3448    }
3449  }, [svg, currentTool, currentVoice]);
3450  reactExports.useEffect(() => {
3451    if (!isPlaying || !engine || !containerRef.current) return;
3452    let animationFrameId;
3453    const updatePlayhead = () => {
3454      var _a, _b, _c;
3455      try {
3456        const timeSeconds = engine.isPlaying() ? engine.getCurrentTime() : currentTimeRef.current;
3457        if (playheadRef.current && containerRef.current) {
3458          const ticks = engine.secondsToTicks(timeSeconds);
3459          let showPlayhead = false;
3460          let targetTicks = 0;
3461          let patternLength = 0;
3462          if (activePattern) {
3463            const maxNoteTick = activePattern.notes.reduce((max, n) => Math.max(max, n.ticks + n.durationTicks), 0);
3464            const ppq2 = ((_b = (_a = engine == null ? void 0 : engine.midi) == null ? void 0 : _a.header) == null ? void 0 : _b.ppq) || 96;
3465            const ticksPerBar = ppq2 * 4;
3466            patternLength = activePattern.durationTicks || Math.max(ticksPerBar * 4, Math.ceil(maxNoteTick / ticksPerBar) * ticksPerBar);
3467          }
3468          if (playbackMode === "pat") {
3469            showPlayhead = true;
3470            targetTicks = patternLength > 0 ? ticks % patternLength : ticks;
3471          } else {
3472            const playingClip = playlistClips.find(
3473              (clip) => clip.patternId === activePatternId && !clip.isMuted && ticks >= clip.startTick && ticks < clip.startTick + clip.durationTicks
3474            );
3475            if (playingClip) {
3476              showPlayhead = true;
3477              targetTicks = patternLength > 0 ? (ticks - playingClip.startTick + playingClip.offsetTicks) % patternLength : ticks - playingClip.startTick + playingClip.offsetTicks;
3478            }
3479          }
3480          if (!showPlayhead) {
3481            playheadRef.current.style.display = "none";
3482            animationFrameId = requestAnimationFrame(updatePlayhead);
3483            return;
3484          }
3485          const timeSignatures = engine.midi.header.timeSignatures;
3486          const ppq = engine.midi.header.ppq;
3487          let measureCount = 0;
3488          let lastTick = 0;
3489          let num = 4, den = 4;
3490          for (const ts of timeSignatures) {
3491            if (ts.ticks > targetTicks) break;
3492            const ticksDiff2 = ts.ticks - lastTick;
3493            const ticksPerMeasure2 = ppq * (4 / den) * num;
3494            if (ticksPerMeasure2 > 0) {
3495              measureCount += Math.floor(ticksDiff2 / ticksPerMeasure2);
3496            }
3497            num = ts.timeSignature[0];
3498            den = ts.timeSignature[1];
3499            lastTick = ts.ticks;
3500          }
3501          const ticksDiff = targetTicks - lastTick;
3502          const ticksPerMeasure = ppq * (4 / den) * num;
3503          let measureProgress = 0;
3504          if (ticksPerMeasure > 0) {
3505            measureCount += Math.floor(ticksDiff / ticksPerMeasure);
3506            measureProgress = ticksDiff % ticksPerMeasure / ticksPerMeasure;
3507          }
3508          const measureNum = measureCount + 1;
3509          const measureNode = containerRef.current.querySelector(`g.measure[n="${measureNum}"]`);
3510          if (measureNode) {
3511            const rect = measureNode.getBoundingClientRect();
3512            const containerRect = containerRef.current.getBoundingClientRect();
3513            let startX = rect.left + 50;
3514            const elements = Array.from(measureNode.querySelectorAll(".note, .rest, .mRest, .chord"));
3515            if (elements.length > 0) {
3516              let minLeft = Infinity;
3517              elements.forEach((el) => {
3518                const elRect = el.getBoundingClientRect();
3519                if (elRect.left < minLeft && elRect.width > 0) minLeft = elRect.left + elRect.width / 2;
3520              });
3521              if (minLeft !== Infinity) startX = minLeft;
3522            }
3523            const endX = rect.right - 10;
3524            const playheadX = startX + measureProgress * (endX - startX);
3525            const localX = (playheadX - containerRect.left) / zoom;
3526            const localTop = (rect.top - containerRect.top) / zoom;
3527            const localBottom = (rect.bottom - containerRect.top) / zoom;
3528            playheadRef.current.style.display = "block";
3529            playheadRef.current.style.left = `${localX}px`;
3530            playheadRef.current.style.top = `${localTop}px`;
3531            playheadRef.current.style.height = `${localBottom - localTop}px`;
3532            const scrollContainer = (_c = containerRef.current.parentElement) == null ? void 0 : _c.parentElement;
3533            if (scrollContainer) {
3534              const systemNode = measureNode.closest("g.system");
3535              const containerRect2 = scrollContainer.getBoundingClientRect();
3536              if (systemNode) {
3537                const sysRect = systemNode.getBoundingClientRect();
3538                const isSystemVisible = sysRect.top >= containerRect2.top + 20 && sysRect.bottom <= containerRect2.bottom - 20;
3539                if (!isSystemVisible) {
3540                  const relativeTop = sysRect.top - containerRect2.top + scrollContainer.scrollTop;
3541                  const targetTop = relativeTop - 40;
3542                  scrollContainer.scrollTo({
3543                    top: Math.max(0, targetTop),
3544                    behavior: "smooth"
3545                  });
3546                }
3547              } else {
3548                const relativeTop = rect.top - containerRect2.top + scrollContainer.scrollTop;
3549                const targetTop = relativeTop - scrollContainer.clientHeight / 2;
3550                scrollContainer.scrollTo({
3551                  top: Math.max(0, targetTop),
3552                  behavior: "smooth"
3553                });
3554              }
3555            }
3556          } else {
3557            playheadRef.current.style.display = "none";
3558          }
3559        }
3560      } catch (e) {
3561      }
3562      animationFrameId = requestAnimationFrame(updatePlayhead);
3563    };
3564    animationFrameId = requestAnimationFrame(updatePlayhead);
3565    return () => {
3566      cancelAnimationFrame(animationFrameId);
3567      if (playheadRef.current) {
3568        playheadRef.current.style.display = "none";
3569      }
3570    };
3571  }, [isPlaying, engine, zoom, playbackMode, playlistClips, activePatternId, activePattern]);
3572  const handleMouseDown = (e) => {
3573    const target = e.target;
3574    let gNode = target.closest("g[id]");
3575    let elementId = gNode == null ? void 0 : gNode.getAttribute("id");
3576    let className = (gNode == null ? void 0 : gNode.getAttribute("class")) || "";
3577    if (!elementId && containerRef.current) {
3578      const measures = Array.from(containerRef.current.querySelectorAll("g.measure"));
3579      let closestMeasure = null;
3580      let minDistance = Infinity;
3581      for (const measure of measures) {
3582        const rect = measure.getBoundingClientRect();
3583        const isWithinX = e.clientX >= rect.left && e.clientX <= rect.right;
3584        const isWithinY = e.clientY >= rect.top && e.clientY <= rect.bottom;
3585        if (isWithinX && isWithinY) {
3586          closestMeasure = measure;
3587          break;
3588        }
3589        const centerX = rect.left + rect.width / 2;
3590        const centerY = rect.top + rect.height / 2;
3591        const dist = Math.hypot(e.clientX - centerX, e.clientY - centerY);
3592        if (dist < minDistance) {
3593          minDistance = dist;
3594          closestMeasure = measure;
3595        }
3596      }
3597      if (closestMeasure) {
3598        gNode = closestMeasure;
3599        elementId = gNode.getAttribute("id");
3600        className = gNode.getAttribute("class") || "";
3601      }
3602    }
3603    if (!elementId) return;
3604    if (currentTool === "select" && className.includes("note")) {
3605      dragTargetRef.current = elementId;
3606      dragStartPosRef.current = { x: e.clientX, y: e.clientY };
3607      gNode.style.opacity = "0.5";
3608    }
3609    let yOffset = void 0;
3610    if (currentTool.startsWith("note_") && containerRef.current) {
3611      let staffNode = target.closest("g.staff");
3612      if (!staffNode) {
3613        const measureNode = target.closest("g.measure");
3614        if (measureNode) {
3615          staffNode = measureNode.querySelector("g.staff");
3616        }
3617      }
3618      if (!staffNode) {
3619        const staves = Array.from(containerRef.current.querySelectorAll("g.staff"));
3620        if (staves.length > 0) {
3621          let closestStaff = staves[0];
3622          let minDistance = Infinity;
3623          for (const staff of staves) {
3624            const rect = staff.getBoundingClientRect();
3625            const staffCenter = rect.top + rect.height / 2;
3626            const dist = Math.abs(e.clientY - staffCenter);
3627            if (dist < minDistance) {
3628              minDistance = dist;
3629              closestStaff = staff;
3630            }
3631          }
3632          staffNode = closestStaff;
3633        }
3634      }
3635      if (staffNode) {
3636        yOffset = getSnappingOffset(e.clientY, staffNode);
3637      }
3638    }
3639    let snappedBeat = void 0;
3640    if (currentTool.startsWith("note_") && containerRef.current) {
3641      const measureNode = target.closest("g.measure") || (className.includes("measure") ? gNode : null);
3642      if (measureNode) {
3643        const durMap = {
3644          "note_whole": "1",
3645          "note_half": "2",
3646          "note_quarter": "4",
3647          "note_eighth": "8",
3648          "note_sixteenth": "16"
3649        };
3650        const dur = durMap[currentTool] || "4";
3651        let beatDur = getDurationInBeats(dur);
3652        if (isTriplet) {
3653          beatDur /= 1.5;
3654        }
3655        if (isDotted) {
3656          beatDur *= 1.5;
3657        }
3658        const gridStep = showGrid ? beatDur >= 1 ? 1 : beatDur : 1e-3;
3659        const snapInfo = computeSnap(e.clientX, measureNode, gridStep);
3660        snappedBeat = snapInfo.clampedBeat;
3661      }
3662    }
3663    onElementClick(elementId, e, yOffset, snappedBeat);
3664  };
3665  const handleContextMenu = (e) => {
3666    e.preventDefault();
3667    const target = e.target;
3668    const gNode = target.closest("g[id]");
3669    const elementId = gNode == null ? void 0 : gNode.getAttribute("id");
3670    if (!elementId) return;
3671    const className = (gNode == null ? void 0 : gNode.getAttribute("class")) || "";
3672    onElementRightClick(elementId, className, e);
3673  };
3674  const handleMouseMove = (e) => {
3675    if (dragTargetRef.current && dragStartPosRef.current) ;
3676    let hintSet = false;
3677    if (containerRef.current) {
3678      const target = e.target;
3679      const gNode = target.closest("g[id]");
3680      const elementId = gNode == null ? void 0 : gNode.getAttribute("id");
3681      if (elementId && meiDocRef.current) {
3682        const getXmlElById = (doc, id) => {
3683          return doc.querySelector(`[id="${id}"]`) || doc.querySelector(`[xml\\:id="${id}"]`) || doc.querySelector(`[*|id="${id}"]`) || Array.from(doc.getElementsByTagName("*")).find((el) => el.getAttribute("xml:id") === id) || null;
3684        };
3685        const noteEl = getXmlElById(meiDocRef.current, elementId);
3686        if (noteEl && noteEl.tagName === "note") {
3687          const pname = noteEl.getAttribute("pname") || "c";
3688          const oct = parseInt(noteEl.getAttribute("oct") || "4", 10);
3689          const accid = noteEl.getAttribute("accid") || void 0;
3690          const midiVal = getMidiFromMEI(pname, oct, accid);
3691          const noteNameStr = isPercussion ? GM_PERCUSSION_MAP[midiVal] || `Drum ${midiVal}` : getNoteName(midiVal);
3692          const layer = noteEl.closest("layer");
3693          const measure = noteEl.closest("measure");
3694          const measureNum = measure ? parseInt(measure.getAttribute("n") || "1", 10) : 1;
3695          let elapsedBeats = 0;
3696          if (layer) {
3697            let eventEl = noteEl;
3698            if (noteEl.parentElement && noteEl.parentElement.tagName === "chord") {
3699              eventEl = noteEl.parentElement;
3700            }
3701            elapsedBeats = getStartTimeInLayer(layer, eventEl);
3702          }
3703          const durAttr = noteEl.getAttribute("dur") || "4";
3704          const dotsAttr = noteEl.getAttribute("dots") || null;
3705          const durationInBeats = getBeats(durAttr, dotsAttr);
3706          const bar = measureNum;
3707          const step = Math.floor(elapsedBeats * 4) + 1;
3708          const tick = Math.floor(elapsedBeats * 4 % 1 * 24);
3709          const posStr = `${bar}:${String(step).padStart(2, "0")}:${String(tick).padStart(2, "0")}`;
3710          const durBar = Math.floor(durationInBeats / 4);
3711          const durStep = Math.floor(durationInBeats % 4 * 4);
3712          const durTick = Math.floor(durationInBeats % 4 * 4 % 1 * 24);
3713          const durStr = `${durBar}:${String(durStep).padStart(2, "0")}:${String(durTick).padStart(2, "0")}`;
3714          setHint({
3715            label: isPercussion ? `Percussion Note` : `Note: ${noteNameStr}`,
3716            positionText: `${posStr} for ${durStr}`,
3717            noteText: `${noteNameStr} / ${midiVal}`,
3718            iconType: "note"
3719          });
3720          hintSet = true;
3721        }
3722      }
3723      if (!hintSet) {
3724        const measureNode = target.closest("g.measure");
3725        let staffNode = target.closest("g.staff");
3726        if (!staffNode && measureNode) {
3727          staffNode = measureNode.querySelector("g.staff");
3728        }
3729        if (measureNode && staffNode) {
3730          const offset = getSnappingOffset(e.clientY, staffNode);
3731          const measureNum = parseInt(measureNode.getAttribute("n") || "1", 10);
3732          const durMap = {
3733            "note_whole": "1",
3734            "note_half": "2",
3735            "note_quarter": "4",
3736            "note_eighth": "8",
3737            "note_sixteenth": "16",
3738            "rest_whole": "1",
3739            "rest_half": "2",
3740            "rest_quarter": "4",
3741            "rest_eighth": "8",
3742            "rest_sixteenth": "16"
3743          };
3744          const dur = durMap[currentTool] || "4";
3745          let beatDur = getDurationInBeats(dur);
3746          if (isTriplet) {
3747            beatDur /= 1.5;
3748          }
3749          if (isDotted) {
3750            beatDur *= 1.5;
3751          }
3752          const gridStep = showGrid ? beatDur >= 1 ? 1 : beatDur : 1e-3;
3753          const snapInfo = computeSnap(e.clientX, measureNode, gridStep);
3754          let midiVal = 60;
3755          if (isPercussion) {
3756            if (currentAccidental && ["kick", "snare", "hihat", "cymbal"].includes(currentAccidental)) {
3757              if (currentAccidental === "kick") midiVal = 36;
3758              else if (currentAccidental === "snare") midiVal = 38;
3759              else if (currentAccidental === "hihat") midiVal = 42;
3760              else if (currentAccidental === "cymbal") midiVal = 49;
3761            } else {
3762              midiVal = 38;
3763            }
3764          } else {
3765            const step2 = 38 - offset;
3766            const pitchMap = ["c", "d", "e", "f", "g", "a", "b"];
3767            const pitch = pitchMap[(step2 % 7 + 7) % 7];
3768            const oct = Math.floor(step2 / 7);
3769            const accid = currentAccidental === "sharp" ? "s" : currentAccidental === "flat" ? "f" : currentAccidental === "natural" ? "n" : void 0;
3770            midiVal = getMidiFromMEI(pitch, oct, accid);
3771          }
3772          const noteNameStr = isPercussion ? GM_PERCUSSION_MAP[midiVal] || `Drum ${midiVal}` : getNoteName(midiVal);
3773          const bar = measureNum;
3774          const step = Math.floor(snapInfo.clampedBeat * 4) + 1;
3775          const tick = Math.floor(snapInfo.clampedBeat * 4 % 1 * 24);
3776          const posStr = `${bar}:${String(step).padStart(2, "0")}
3776:${String(tick).padStart(2, "0")}`;
3777          setHint({
3778            label: `Score: ${currentTool.startsWith("rest_") ? "Place Rest" : currentTool.startsWith("note_") ? "Place Note" : currentTool}`,
3779            positionText: posStr,
3780            noteText: `${noteNameStr} / ${midiVal}`,
3781            iconType: currentTool
3782          });
3783          hintSet = true;
3784        }
3785      }
3786    }
3787    if (!hintSet) {
3788      setHint(null);
3789    }
3790    if (currentTool.startsWith("note_") && containerRef.current) {
3791      const isOverPaper = containerRef.current.contains(e.target);
3792      if (isOverPaper) {
3793        containerRef.current.style.cursor = "none";
3794        const target = e.target;
3795        let staffNode = target.closest("g.staff");
3796        if (!staffNode) {
3797          const measureNode = target.closest("g.measure");
3798          if (measureNode) {
3799            staffNode = measureNode.querySelector("g.staff");
3800          }
3801        }
3802        if (!staffNode) {
3803          const staves = Array.from(containerRef.current.querySelectorAll("g.staff"));
3804          if (staves.length > 0) {
3805            let closestStaff = staves[0];
3806            let minDistance = Infinity;
3807            for (const staff of staves) {
3808              const rect = staff.getBoundingClientRect();
3809              const staffCenter = rect.top + rect.height / 2;
3810              const dist = Math.abs(e.clientY - staffCenter);
3811              if (dist < minDistance) {
3812                minDistance = dist;
3813                closestStaff = staff;
3814              }
3815            }
3816            staffNode = closestStaff;
3817          }
3818        }
3819        const containerRect = containerRef.current.getBoundingClientRect();
3820        if (staffNode) {
3821          const offset = getSnappingOffset(e.clientY, staffNode);
3822          const staffLines = Array.from(staffNode.querySelectorAll(":scope > path"));
3823          const rects = staffLines.map((line) => line.getBoundingClientRect()).sort((a, b) => a.top - b.top);
3824          if (rects.length === 5) {
3825            const topLineY = rects[0].top;
3826            const bottomLineY = rects[4].top;
3827            const avgGap = (bottomLineY - topLineY) / 4;
3828            const stepSize = avgGap / 2;
3829            const snappedY = topLineY + offset * stepSize;
3830            let snappedX = e.clientX;
3831            const measureNode = staffNode.closest("g.measure");
3832            if (measureNode) {
3833              const durMap = {
3834                "note_whole": "1",
3835                "note_half": "2",
3836                "note_quarter": "4",
3837                "note_eighth": "8",
3838                "note_sixteenth": "16"
3839              };
3840              const dur = durMap[currentTool] || "4";
3841              let beatDur = getDurationInBeats(dur);
3842              if (isTriplet) {
3843                beatDur /= 1.5;
3844              }
3845              if (isDotted) {
3846                beatDur *= 1.5;
3847              }
3848              const gridStep = showGrid ? beatDur >= 1 ? 1 : beatDur : 1e-3;
3849              const snapInfo = computeSnap(e.clientX, measureNode, gridStep);
3850              snappedX = snapInfo.snappedX;
3851              const localGrid = snapInfo.gridXs.map((gx) => ({
3852                x: (gx - containerRect.left) / zoom,
3853                top: (snapInfo.rect.top - containerRect.top) / zoom,
3854                bottom: (snapInfo.rect.bottom - containerRect.top) / zoom
3855              }));
3856              setActiveGrid(localGrid);
3857            } else {
3858              setActiveGrid([]);
3859            }
3860            const localX2 = (snappedX - containerRect.left) / zoom;
3861            const localY2 = (snappedY - containerRect.top) / zoom;
3862            setGhostPos({ x: localX2, y: localY2, visible: true, offset });
3863            return;
3864          }
3865        }
3866        const localX = (e.clientX - containerRect.left) / zoom;
3867        const localY = (e.clientY - containerRect.top) / zoom;
3868        setGhostPos({ x: localX, y: localY, visible: true, offset: 0 });
3869        setActiveGrid([]);
3870      } else {
3871        containerRef.current.style.cursor = "default";
3872        setGhostPos(null);
3873        setActiveGrid([]);
3874      }
3875    } else {
3876      if (containerRef.current) {
3877        containerRef.current.style.cursor = currentTool === "erase" ? "crosshair" : "default";
3878      }
3879      setGhostPos(null);
3880      setActiveGrid([]);
3881    }
3882  };
3883  const handleMouseUp = (e) => {
3884    if (onMouseUp) onMouseUp();
3885    if (dragTargetRef.current && dragStartPosRef.current) {
3886      const dy = e.clientY - dragStartPosRef.current.y;
3887      if (Math.abs(dy) > 10) {
3888        onElementDrag(dragTargetRef.current, dy, e.clientY);
3889      }
3890      if (containerRef.current) {
3891        const gNode = containerRef.current.querySelector(`g[id="${dragTargetRef.current}"]`);
3892        if (gNode) gNode.style.opacity = "1";
3893      }
3894      dragTargetRef.current = null;
3895      dragStartPosRef.current = null;
3896    }
3897  };
3898  const handleMouseLeave = (e) => {
3899    handleMouseUp(e);
3900    setGhostPos(null);
3901    setActiveGrid([]);
3902    setHint(null);
3903    if (containerRef.current) {
3904      containerRef.current.style.cursor = "default";
3905    }
3906  };
3907  const renderGhostSVG = (tool) => {
3908    const color = "#4169E1";
3909    const offset = (ghostPos == null ? void 0 : ghostPos.offset) || 0;
3910    const renderLedgerLines = () => {
3911      const lines = [];
3912      if (offset <= -2) {
3913        for (let L = -2; L >= offset; L -= 2) {
3914          const cy = (L - offset) * 4.5;
3915          lines.push(
3916            /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: "-12", y1: cy, x2: "12", y2: cy, stroke: color, strokeWidth: "1.5" }, `ledger-${L}`)
3917          );
3918        }
3919      } else if (offset >= 10) {
3920        for (let L = 10; L <= offset; L += 2) {
3921          const cy = (L - offset) * 4.5;
3922          lines.push(
3923            /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: "-12", y1: cy, x2: "12", y2: cy, stroke: color, strokeWidth: "1.5" }, `ledger-${L}`)
3924          );
3925        }
3926      }
3927      return lines;
3928    };
3929    const isRest = entryMode === "rest";
3930    const renderDot = () => {
3931      const numDots = dotsCount > 0 ? dotsCount : isDotted ? 1 : 0;
3932      if (numDots === 0) return null;
3933      if (numDots === 2) {
3934        return /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { children: [
3935          /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "12", cy: "0", r: "1.5", fill: color }),
3936          /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "16", cy: "0", r: "1.5", fill: color })
3937        ] });
3938      }
3939      return /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "12", cy: "0", r: "1.5", fill: color });
3940    };
3941    const renderArticulation = () => {
3942      if (!currentArticulation || isRest) return null;
3943      const yPos = 10;
3944      switch (currentArticulation) {
3945        case "staccato":
3946          return /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "0", cy: yPos, r: "1.5", fill: color });
3947        case "accent":
3948          return /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: `M -4 ${yPos - 2} L 4 ${yPos} L -4 ${yPos + 2}`, fill: "none", stroke: color, strokeWidth: "1.5" });
3949        case "tenuto":
3950          return /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: "-5", y1: yPos, x2: "5", y2: yPos, stroke: color, strokeWidth: "1.5" });
3951        case "fermata":
3952          return /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { transform: `translate(0, ${yPos - 2})`, children: [
3953            /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M -6 0 Q 0 -8 6 0", fill: "none", stroke: color, strokeWidth: "1.5" }),
3954            /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "0", cy: "-2", r: "1", fill: color })
3955          ] });
3956        default:
3957          return null;
3958      }
3959    };
3960    if (isRest) {
3961      return /* @__PURE__ */ jsxRuntimeExports.jsxs("svg", { width: "24", height: "60", viewBox: "-12 -30 24 60", style: { display: "block" }, children: [
3962        renderLedgerLines(),
3963        tool === "note_whole" && /* @__PURE__ */ jsxRuntimeExports.jsx("rect", { x: "-5", y: "-5", width: "10", height: "6", fill: color }),
3964        tool === "note_half" && /* @__PURE__ */ jsxRuntimeExports.jsx("rect", { x: "-5", y: "-1", width: "10", height: "6", fill: color }),
3965        tool === "note_quarter" && /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M -3 -8 L 1 -2 L -2 4 L 2 10", stroke: color, strokeWidth: "2", fill: "none" }),
3966        tool === "note_eighth" && /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { children: [
3967          /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "-2", cy: "-4", r: "2", fill: color }),
3968          /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M -2 -4 Q 2 -2 -2 6 L 1 12", stroke: color, strokeWidth: "1.5", fill: "none" })
3969        ] }),
3970        tool === "note_sixteenth" && /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { children: [
3971          /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "-2", cy: "-8", r: "2", fill: color }),
3972          /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M -2 -8 Q 2 -6 -2 2", stroke: color, strokeWidth: "1.5", fill: "none" }),
3973          /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "-3", cy: "-2", r: "2", fill: color }),
3974          /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M -3 -2 Q 1 0 -3 8 L 0 14", stroke: color, strokeWidth: "1.5", fill: "none" })
3975        ] }),
3976        renderDot()
3977      ] });
3978    }
3979    if (tool === "note_whole") {
3980      return /* @__PURE__ */ jsxRuntimeExports.jsxs("svg", { width: "24", height: "60", viewBox: "-12 -30 24 60", style: { display: "block" }, children: [
3981        renderLedgerLines(),
3982        /* @__PURE__ */ jsxRuntimeExports.jsx("ellipse", { cx: "0", cy: "0", rx: "8", ry: "5", transform: "rotate(-20)", stroke: color, strokeWidth: "2", fill: "none" }),
3983        renderDot(),
3984        renderArticulation()
3985      ] });
3986    }
3987    const isHollow = tool === "note_half";
3988    return /* @__PURE__ */ jsxRuntimeExports.jsxs("svg", { width: "24", height: "60", viewBox: "-12 -30 24 60", style: { display: "block" }, children: [
3989      renderLedgerLines(),
3990      /* @__PURE__ */ jsxRuntimeExports.jsx(
3991        "ellipse",
3992        {
3993          cx: "0",
3994          cy: "0",
3995          rx: "7",
3996          ry: "4.5",
3997          transform: "rotate(-20)",
3998          stroke: color,
3999          strokeWidth: isHollow ? 1.5 : 0,
4000          fill: isHollow ? "none" : color
4001        }
4002      ),
4003      /* @__PURE__ */ jsxRuntimeExports.jsx(
4004        "line",
4005        {
4006          x1: "6",
4007          y1: "0",
4008          x2: "6",
4009          y2: "-22",
4010          stroke: color,
4011          strokeWidth: "1.5"
4012        }
4013      ),
4014      tool === "note_eighth" && /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M 6 -22 Q 12 -15 11 -8 Q 9 -12 6 -14", fill: color }),
4015      tool === "note_sixteenth" && /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { children: [
4016        /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M 6 -22 Q 12 -15 11 -8 Q 9 -12 6 -14", fill: color }),
4017        /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M 6 -16 Q 12 -9 11 -2 Q 9 -6 6 -8", fill: color })
4018      ] }),
4019      renderDot(),
4020      renderArticulation()
4021    ] });
4022  };
4023  return /* @__PURE__ */ jsxRuntimeExports.jsxs(
4024    "div",
4025    {
4026      ref: containerRef,
4027      className: "score-editor-canvas-container relative bg-[#FFFDF6] shadow-2xl rounded-sm border border-zinc-200",
4028      style: { minWidth: "800px", minHeight: "1132px" },
4029      onMouseDown: handleMouseDown,
4030      onContextMenu: handleContextMenu,
4031      onMouseMove: handleMouseMove,
4032      onMouseUp: handleMouseUp,
4033      onMouseLeave: handleMouseLeave,
4034      children: [
4035        /* @__PURE__ */ jsxRuntimeExports.jsx(
4036          "div",
4037          {
4038            dangerouslySetInnerHTML: { __html: svg || '<div class="text-zinc-400 p-8 text-center">Loading Engine...</div>' }
4039          }
4040        ),
4041        /* @__PURE__ */ jsxRuntimeExports.jsx("div", { ref: playheadRef, style: {
4042          position: "absolute",
4043          display: "none",
4044          width: "2px",
4045          backgroundColor: "rgba(255, 69, 58, 0.8)",
4046          boxShadow: "0 0 4px rgba(255, 69, 58, 0.5)",
4047          pointerEvents: "none",
4048          zIndex: 999997,
4049          transition: "none"
4050        } }),
4051        ghostPos && currentTool.startsWith("note_") && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { style: {
4052          position: "absolute",
4053          left: ghostPos.x,
4054          top: ghostPos.y,
4055          transform: "translate(-50%, -50%)",
4056          pointerEvents: "none",
4057          opacity: ghostPos.visible ? 0.6 : 0,
4058          zIndex: 999999
4059        }, children: renderGhostSVG(currentTool) }),
4060        currentTool.startsWith("note_") && activeGrid.map((gridLine, idx) => /* @__PURE__ */ jsxRuntimeExports.jsx("div", { style: {
4061          position: "absolute",
4062          left: gridLine.x,
4063          top: gridLine.top,
4064          height: gridLine.bottom - gridLine.top,
4065          borderLeft: "2px dashed rgba(65, 105, 225, 0.4)",
4066          pointerEvents: "none",
4067          zIndex: 999998
4068        } }, `grid-${idx}`))
4069      ]
4070    }
4071  );
4072};
4073const getDurationInBeats = (verovioDur) => {
4074  switch (verovioDur) {
4075    case "1":
4076      return 4;
4077    case "2":
4078      return 2;
4079    case "4":
4080      return 1;
4081    case "8":
4082      return 0.5;
4083    case "16":
4084      return 0.25;
4085    default:
4086      return 1;
4087  }
4088};
4089export {
4090  EditorProvider as E,
4091  ScoreEditorCanvas as S,
4092  useScoreEditorEngine as a,
4093  getMidiFromMEI$1 as b,
4094  convertMidiToMEI as c,
4095  getMeiDurAttr as d,
4096  getBeats$1 as e,
4097  getDurationInBeats$1 as f,
4098  getMidiNotesFromBase64 as g,
4099  ScoreSyncManager as h,
4100  useEditor as u
4101};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.