PageSourceSearch

https://www.hamienet.com/assets/SheetMusicViewer-CoFH8B_z.js

js hamienet.com collected 2026-09-24 19:00:00 UTC 163,491 bytes, 3,387 lines download raw bytes

1const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/vendor-pdf-TxEta_LX.js","assets/vendor-react-three-MDLA5jog.js","assets/vendor-react-Bay9Y7Lb.js","assets/vendor-dVNaLJx_.js","assets/vendor-i18n-R-bMoYp_.js","assets/vendor-DqKRdbjz.css"])))=>i.map(i=>d[i]);
2import { _ as __vitePreload } from "./vendor-react-three-MDLA5jog.js";
3import { r as reactExports, j as jsxRuntimeExports, R as React } from "./vendor-react-Bay9Y7Lb.js";
4import { bo as Monitor, dq as ZoomOut, dr as ZoomIn, cz as SlidersHorizontal, cs as BookOpen, bg as Music, cF as Keyboard, aW as X, cd as Ellipsis, bK as ThumbsUp, bL as Clock, bN as Library, b6 as LoaderCircle, c$ as Download, cY as ChevronLeft, bE as ChevronRight, cj as Layers, fG as Map$1, gb as Pill, ba as Info, bH as Hash, e0 as Palette, dn as MousePointer2, cO as GraduationCap } from "./vendor-dVNaLJx_.js";
5import { u as useSoundfontKeyboard, a as useKeyboardInput, b as getMidiFromKeyboardClick, r as renderSharedKeyboard } from "./SharedKeyboard-B3r3Jaho.js";
6import "./vendor-i18n-R-bMoYp_.js";
7import "./useMidiKeyboard-DlyEIN8T.js";
8import "./wasm-synth-adapter-BrqgBX1y.js";
9import "./vendor-spessasynth-DH8n6o_V.js";
10const useSheetMusicEngine = (props) => {
11  const {
12    xmlData: initialXmlData,
13    xmlString: initialXmlString,
14    scorePath,
15    quantizedScorePaths,
16    midiId,
17    isMelodyMode,
18    melodySource,
19    title,
20    processedInstruments,
21    trackVisibility,
22    dimensions,
23    layoutMode,
24    scale,
25    quantization,
26    density,
27    showMeasureNumbers,
28    visibleTrackCount,
29    showChords,
30    showRoman,
31    harmonyChordsUrl,
32    harmonyRomanUrl,
33    measureMap = [],
34    sections = [],
35    duration
36  } = props;
37  const [verovioToolkit, setVerovioToolkit] = reactExports.useState(null);
38  const [loading, setLoading] = reactExports.useState(() => {
39    return !!(initialXmlData || initialXmlString || scorePath || quantizedScorePaths);
40  });
41  const [loadingProgress, setLoadingProgress] = reactExports.useState({
42    percent: 15,
43    stage: "Initializing score engine..."
44  });
45  const [isDataLoaded, setIsDataLoaded] = reactExports.useState(false);
46  const [pageCount, setPageCount] = reactExports.useState(0);
47  const [currentPage, setCurrentPage] = reactExports.useState(1);
48  const [svgContent, setSvgContent] = reactExports.useState("");
49  const [svgHeight, setSvgHeight] = reactExports.useState(0);
50  const [syncPoints, setSyncPoints] = reactExports.useState([]);
51  const [internalXmlData, setInternalXmlData] = reactExports.useState(null);
52  const [harmonyData, setHarmonyData] = reactExports.useState({});
53  const currentSourceRef = reactExports.useRef(null);
54  const finalXmlDataRef = reactExports.useRef(null);
55  const availableIdsRef = reactExports.useRef(/* @__PURE__ */ new Set());
56  const measureIdMapRef = reactExports.useRef({});
57  const lastOptionsRef = reactExports.useRef("");
58  const lastRenderedPageRef = reactExports.useRef(0);
59  const trackVisibilityStr = JSON.stringify(trackVisibility);
60  reactExports.useEffect(() => {
61    let mounted = true;
62    const injectScript = () => {
63      return new Promise((resolve, reject) => {
64        const existing = document.querySelector('script[src*="verovio"]');
65        if (window.verovio) {
66          resolve();
67          return;
68        }
69        if (existing) {
70          existing.addEventListener("load", () => resolve());
71          existing.addEventListener("error", () => reject(new Error("Verovio script load error")));
72          if (existing.dataset.loaded === "true") resolve();
73          return;
74        }
75        const script = document.createElement("script");
76        script.src = "/libs/verovio/verovio-toolkit-wasm.js";
77        script.async = true;
78        script.onload = () => {
79          script.dataset.loaded = "true";
80          resolve();
81        };
82        script.onerror = () => reject(new Error("Failed to download verovio-toolkit-wasm.js"));
83        document.head.appendChild(script);
84      });
85    };
86    const init = async () => {
87      try {
88        await injectScript();
89      } catch (e) {
90        console.error("[SheetMusicEngine] Verovio script download failed:", e);
91        if (mounted) setLoading(false);
92        return;
93      }
94      let attempts = 0;
95      while (attempts < 300) {
96        if (!mounted) return;
97        const v = window.verovio;
98        if (v && typeof v === "object") break;
99        await new Promise((r) => setTimeout(r, 100));
100        attempts++;
101      }
102      const verovio = window.verovio;
103      if (!verovio || typeof verovio !== "object") {
104        console.error("[SheetMusicEngine] Verovio global object not found after timeout. typeof window.verovio:", typeof window.verovio);
105        if (mounted) setLoading(false);
106        return;
107      }
108      if (window.verovioToolkit) {
109        if (mounted) setVerovioToolkit(window.verovioToolkit);
110        return;
111      }
112      try {
113        let module = verovio.module;
114        const ToolkitClass = verovio.toolkit;
115        if (!ToolkitClass) {
116          throw new Error("Verovio toolkit class not found");
117        }
118        console.log("[SheetMusicEngine] Initializing with module type:", typeof module);
119        if (module && typeof module.then === "function") {
120          console.log("[SheetMusicEngine] Awaiting Verovio module promise...");
121          module = await module;
122        } else if (typeof module === "function") {
123          console.log("[SheetMusicEngine] Calling Verovio module factory...");
124          module = await module();
125        }
126        if (module && !module.calledRun) {
127          await new Promise((resolve) => {
128            const prev = module.onRuntimeInitialized;
129            module.onRuntimeInitialized = () => {
130              if (typeof prev === "function") prev();
131              resolve();
132            };
133            let waitCount = 0;
134            const checkInterval = setInterval(() => {
135              if (module.calledRun || waitCount > 100) {
136                clearInterval(checkInterval);
137                resolve();
138              }
139              waitCount++;
140            }, 50);
141          });
142        }
143        let tk = null;
144        let initAttempts = 0;
145        while (initAttempts < 30) {
146          try {
147            tk = new ToolkitClass();
148            if (tk && typeof tk.getVersion === "function") break;
149          } catch {
150            try {
151              tk = new ToolkitClass(module);
152              if (tk && typeof tk.getVersion === "function") break;
153            } catch {
154            }
155          }
156          await new Promise((r) => setTimeout(r, 100));
157          initAttempts++;
158        }
159        if (tk && typeof tk.getVersion === "function") {
160          console.log("[SheetMusicEngine] Verovio initialized successfully. Version:", tk.getVersion());
161          window.verovioToolkit = tk;
162          if (mounted) setVerovioToolkit(tk);
163        } else {
164          throw new Error("Failed to instantiate Verovio toolkit after retries");
165        }
166      } catch (e) {
167        console.error("[SheetMusicEngine] Failed to init Verovio:", e);
168        let fallbackSuccess = false;
169        if (window.createVerovioModule) {
170          try {
171            console.log("[SheetMusicEngine] Attempting fallback via createVerovioModule...");
172            const module = await window.createVerovioModule();
173            const tk = new window.verovio.toolkit(module);
174            if (mounted) {
175              setVerovioToolkit(tk);
176              fallbackSuccess = true;
177            }
178          } catch (fallbackErr) {
179            console.error("[SheetMusicEngine] Fallback failed:", fallbackErr);
180          }
181        }
182        if (!fallbackSuccess && mounted) {
183          setLoading(false);
184        }
185      }
186    };
187    init();
188    return () => {
189      mounted = false;
190    };
191  }, []);
192  const renderPage = reactExports.useCallback((page) => {
193    if (!verovioToolkit || !isDataLoaded) return;
194    try {
195      const svg = verovioToolkit.renderToSVG(page, {});
196      if (svg) {
197        setSvgContent(svg);
198        const vbMatch = svg.match(/viewBox="[-\d.]+\s+[-\d.]+\s+[-\d.]+\s+([-\d.]+)"/);
199        if (vbMatch) setSvgHeight(parseFloat(vbMatch[1]));
200        lastRenderedPageRef.current = page;
201      }
202    } catch (e) {
203      console.error("[SheetMusicEngine] Render Page Error:", e);
204    }
205  }, [verovioToolkit, isDataLoaded]);
206  reactExports.useEffect(() => {
207    let active = true;
208    const fetchScore = async () => {
209      let url = scorePath;
210      if (isMelodyMode) {
211        if (melodySource === "studio") {
212          url = (quantizedScorePaths == null ? void 0 : quantizedScorePaths.studio_melody) || (quantizedScorePaths == null ? void 0 : quantizedScorePaths.musescore_melody) || (quantizedScorePaths == null ? void 0 : quantizedScorePaths.melody) || scorePath;
213        } else {
214          url = (quantizedScorePaths == null ? void 0 : quantizedScorePaths.melody) || (quantizedScorePaths == null ? void 0 : quantizedScorePaths.studio_melody) || (quantizedScorePaths == null ? void 0 : quantizedScorePaths.musescore_melody) || scorePath;
215        }
216      } else {
217        if (quantization === 8) url = (quantizedScorePaths == null ? void 0 : quantizedScorePaths.q8) || (quantizedScorePaths == null ? void 0 : quantizedScorePaths.engraved) || (quantizedScorePaths == null ? void 0 : quantizedScorePaths.studio) || scorePath;
218        else if (quantization === 16) url = (quantizedScorePaths == null ? void 0 : quantizedScorePaths.q16) || (quantizedScorePaths == null ? void 0 : quantizedScorePaths.engraved) || (quantizedScorePaths == null ? void 0 : quantizedScorePaths.studio) || scorePath;
219        else if (quantization === 300) url = (quantizedScorePaths == null ? void 0 : quantizedScorePaths.engraved) || (quantizedScorePaths == null ? void 0 : quantizedScorePaths.studio) || scorePath;
220        else if (quantization === 500) url = (quantizedScorePaths == null ? void 0 : quantizedScorePaths.studio) || (quantizedScorePaths == null ? void 0 : quantizedScorePaths.musescore) || (quantizedScorePaths == null ? void 0 : quantizedScorePaths.engraved) || scorePath;
221        else if (quantization === 600) url = (quantizedScorePaths == null ? void 0 : quantizedScorePaths.ly) || (quantizedScorePaths == null ? void 0 : quantizedScorePaths.engraved) || (quantizedScorePaths == null ? void 0 : quantizedScorePaths.studio) || scorePath;
222        else if (quantization === 700) url = (quantizedScorePaths == null ? void 0 : quantizedScorePaths.abc) || (quantizedScorePaths == null ? void 0 : quantizedScorePaths.engraved) || (quantizedScorePaths == null ? void 0 : quantizedScorePaths.studio) || scorePath;
223        else if (quantization === 800) url = (quantizedScorePaths == null ? void 0 : quantizedScorePaths.krn) || (quantizedScorePaths == null ? void 0 : quantizedScorePaths.studio) || (quantizedScorePaths == null ? void 0 : quantizedScorePaths.engraved) || scorePath;
224        else url = (quantizedScorePaths == null ? void 0 : quantizedScorePaths.engraved) || (quantizedScorePaths == null ? void 0 : quantizedScorePaths.studio) || scorePath;
225      }
226      if (!url && !initialXmlString && !initialXmlData) {
227        if (active) {
228          setLoading(false);
229          setIsDataLoaded(false);
230        }
231        return;
232      }
233      if (active) {
234        setLoading(true);
235        setLoadingProgress({ percent: 30, stage: "Downloading sheet music notation..." });
236        setIsDataLoaded(false);
237        setInternalXmlData("");
238      }
239      if (url) {
240        try {
241          console.log("[SheetMusicEngine] Fetching score from:", url);
242          const res = await fetch(url);
243          if (!res.ok) throw new Error(`HTTP ${res.status}`);
244          let text = await res.text();
245          const hasNotes = text.includes("<note") || text.includes(":note");
246          if (!hasNotes && ((quantizedScorePaths == null ? void 0 : quantizedScorePaths.studio) || (quantizedScorePaths == null ? void 0 : quantizedScorePaths.engraved) || (quantizedScorePaths == null ? void 0 : quantizedScorePaths.q8) || (quantizedScorePaths == null ? void 0 : quantizedScorePaths.q16) || scorePath)) {
247            const fallbackUrl = url !== (quantizedScorePaths == null ? void 0 : quantizedScorePaths.studio) && (quantizedScorePaths == null ? void 0 : quantizedScorePaths.studio) || url !== (quantizedScorePaths == null ? void 0 : quantizedScorePaths.engraved) && (quantizedScorePaths == null ? void 0 : quantizedScorePaths.engraved) || url !== (quantizedScorePaths == null ? void 0 : quantizedScorePaths.q8) && (quantizedScorePaths == null ? void 0 : quantizedScorePaths.q8) || url !== (quantizedScorePaths == null ? void 0 : quantizedScorePaths.q16) && (quantizedScorePaths == null ? void 0 : quantizedScorePaths.q16) || url !== scorePath && scorePath;
248            if (fallbackUrl) {
249              console.warn(`[SheetMusicEngine] Score at ${url} contains no notes. Automatically falling back to: ${fallbackUrl}`);
250              try {
251                const fbRes = await fetch(fallbackUrl);
252                if (fbRes.ok) {
253                  text = await fbRes.text();
254                }
255              } catch (fbErr) {
256                console.error("[SheetMusicEngine] Fallback score fetch failed:", fbErr);
257              }
258            }
259          }
260          if (active) {
261            setLoadingProgress({ percent: 55, stage: "Processing score data..." });
262            setInternalXmlData(text);
263          }
264        } catch (err) {
265          console.error("[SheetMusicEngine] Failed to fetch score:", err);
266          if (active) {
267            if (initialXmlString) setInternalXmlData(initialXmlString);
268            else if (initialXmlData) setInternalXmlData(new TextDecoder().decode(initialXmlData));
269            else setLoading(false);
270          }
271        }
272      } else {
273        if (active) {
274          if (initialXmlString) setInternalXmlData(initialXmlString);
275          else if (initialXmlData) setInternalXmlData(new TextDecoder().decode(initialXmlData));
276        }
277      }
278    };
279    fetchScore();
280    return () => {
281      active = false;
282    };
283  }, [scorePath, quantizedScorePaths, quantization, isMelodyMode, melodySource, initialXmlString, initialXmlData]);
284  reactExports.useEffect(() => {
285    if (!verovioToolkit) return;
286    const loadScore = async () => {
287      const localXmlString = internalXmlData || "";
288      if (!localXmlString) {
289        return;
290      }
291      setLoading(true);
292      setLoadingProgress({ percent: 65, stage: "Processing track & notation structure..." });
293      setIsDataLoaded(false);
294      lastOptionsRef.current = "";
295      lastRenderedPageRef.current = 0;
296      try {
297        await new Promise((r) => setTimeout(r, 0));
298        const parser = new DOMParser();
299        const xmlDoc = parser.parseFromString(localXmlString, "text/xml");
300        let modified = false;
301        const realInstruments = processedInstruments.filter((instr) => {
302          const name = (instr.displayName || instr.name || "").toLowerCase();
303          return name !== "informational track";
304        });
305        const isMEI = !!xmlDoc.querySelector("mei, *|mei");
306        const allScoreParts = Array.from(xmlDoc.querySelectorAll("score-part"));
307        const allParts = Array.from(xmlDoc.querySelectorAll("part"));
308        if (isMEI) {
309          const scoreDef = xmlDoc.querySelector("scoreDef, *|scoreDef");
310          const rootStaffGrp = scoreDef ? scoreDef.querySelector("staffGrp, *|staffGrp") || scoreDef : xmlDoc;
311          let partNodes = [];
312          if (rootStaffGrp) {
313            const directChildren = Array.from(rootStaffGrp.children).filter((el) => {
314              const tag = (el.localName || el.tagName).toLowerCase().split(":").pop() || "";
315              return tag === "staffdef" || tag === "staffgrp";
316            });
317            if (directChildren.length > 0) {
318              partNodes = directChildren;
319            }
320          }
321          if (partNodes.length === 0) {
322            const allStaffDefs = Array.from(xmlDoc.querySelectorAll("staffDef, *|staffDef"));
323            const seenN = /* @__PURE__ */ new Set();
324            partNodes = allStaffDefs.filter((sd) => {
325              const n = sd.getAttribute("n");
326              if (!n || seenN.has(n)) return false;
327              seenN.add(n);
328              return true;
329            });
330          }
331          let firstVisibleStaffN = null;
332          partNodes.forEach((partNode, xmlIdx) => {
333            const instr = realInstruments[xmlIdx];
334            const isVisible = isMelodyMode ? xmlIdx === 0 : !instr ? true : trackVisibility[instr.index] !== false;
335            let displayName = instr ? instr.displayName || instr.name || "" : "";
336            if (isMelodyMode && xmlIdx === 0) {
337              displayName = melodySource === "studio" ? "Melody (Studio Engraved)" : "Melody (Standard)";
338            }
339            const isNodeStaffDef = (partNode.localName || partNode.tagName).toLowerCase().split(":").pop() === "staffdef";
340            const staffDefsInPart = Array.from(partNode.querySelectorAll("staffDef, *|staffDef"));
341            if (isNodeStaffDef && !staffDefsInPart.includes(partNode)) {
342              staffDefsInPart.unshift(partNode);
343            }
344            const partStaffNs = staffDefsInPart.map((sd) => sd.getAttribute("n")).filter(Boolean);
345            if (partStaffNs.length === 0) {
346              partStaffNs.push((xmlIdx + 1).toString());
347            }
348            if (isVisible && firstVisibleStaffN === null && partStaffNs.length > 0) {
349              firstVisibleStaffN = partStaffNs[0];
350            }
351            if (!isVisible) {
352              staffDefsInPart.forEach((sd) => sd.remove());
353              if (!isNodeStaffDef) {
354                partNode.remove();
355              }
356              partStaffNs.forEach((staffN) => {
357                xmlDoc.querySelectorAll(`staffDef[n="${staffN}"], *|staffDef[n="${staffN}"]`).forEach((el) => el.remove());
358                xmlDoc.querySelectorAll(`staff[n="${staffN}"], *|staff[n="${staffN}"]`).forEach((el) => el.remove());
359              });
360              modified = true;
361              return;
362            }
363            if (displayName) {
364              const updateLabelsOnElement = (el) => {
365                el.setAttribute("label", displayName);
366                el.setAttribute("label.abbr", displayName);
367                const tag = (el.localName || el.tagName).toLowerCase().split(":").pop() || "";
368                if (tag !== "staffdef") {
369                  const labelChildren = Array.from(el.children).filter((c) => {
370                    const cTag = (c.localName || c.tagName).toLowerCase().split(":").pop() || "";
371                    return cTag === "label";
372                  });
373                  if (labelChildren.length > 0) {
374                    labelChildren.forEach((lbl) => {
375                      lbl.textContent = displayName;
376                    });
377                  } else {
378                    try {
379                      const newLbl = xmlDoc.createElementNS ? xmlDoc.createElementNS("http://www.music-encoding.org/ns/mei", "label") : xmlDoc.createElement("label");
380                      newLbl.textContent = displayName;
381                      el.insertBefore(newLbl, el.firstChild);
382                    } catch {
383                    }
384                  }
385                  const abbrChildren = Array.from(el.children).filter((c) => {
386                    const cTag = (c.localName || c.tagName).toLowerCase().split(":").pop() || "";
387                    return cTag === "labelabbr";
388                  });
389                  abbrChildren.forEach((lbl) => {
390                    lbl.textContent = displayName;
391                  });
392                }
393              };
394              updateLabelsOnElement(partNode);
395              staffDefsInPart.forEach((sd) => {
396                if (sd !== partNode) {
397                  updateLabelsOnElement(sd);
398                }
399              });
400              modified = true;
401            }
402          });
403          if (firstVisibleStaffN !== null && firstVisibleStaffN !== "1") {
404            const allHarms = xmlDoc.querySelectorAll("harm, *|harm");
405            allHarms.forEach((harm) => {
406              if (harm.getAttribute("staff") === "1") {
407                harm.setAttribute("staff", firstVisibleStaffN);
408                modified = true;
409              }
410            });
411            const allDirs = xmlDoc.querySelectorAll("dir, *|dir");
412            allDirs.forEach((dir) => {
413              if (dir.getAttribute("staff") === "1") {
414                const text = (dir.textContent || "").trim();
415                if (text.includes("​") || text.includes("‌")) {
416                  dir.setAttribute("staff", firstVisibleStaffN);
417                  modified = true;
418                }
419              }
420            });
421          }
422          if (!showChords || !showRoman) {
423            const allHarms = xmlDoc.querySelectorAll("harm, *|harm");
424            allHarms.forEach((harm) => {
425              const text = (harm.textContent || "").trim();
426              const typeAttr = harm.getAttribute("type") || harm.getAttribute("label") || "";
427              const isRoman = typeAttr === "roman" || text.includes("‌") || /^[IViv]+[0-9]*$/.test(text);
428              const isChord = typeAttr !== "roman" && (text.includes("​") || /^[A-G][b#]?(m|maj|min|dim|aug|sus|add)?[0-9]*(\/[A-G][b#]?)?$/.test(text));
429              if (isRoman && !showRoman) {
430                harm.remove();
431                modified = true;
432              } else if (isChord && !showChords) {
433                harm.remove();
434                modified = true;
435              }
436            });
437            const allDirs = xmlDoc.querySelectorAll("dir, *|dir, anchoredText, *|anchoredText");
438            allDirs.forEach((el) => {
439              const text = (el.textContent || "").trim();
440              const isRoman = text.includes("‌") || /^[IViv]+[0-9]*$/.test(text);
441              const isChord = text.includes("​") || /^[A-G][b#]?(m|maj|min|dim|aug|sus|add)?[0-9]*(\/[A-G][b#]?)?$/.test(text);
442              const isKey = text.includes("‍") || text.includes("Key:");
443              if (isRoman && !showRoman) {
444                el.remove();
445                modified = true;
446              } else if (isChord && !showChords) {
447                el.remove();
448                modified = true;
449              } else if (isKey && !showChords && !showRoman) {
450                el.remove();
451                modified = true;
452              }
453            });
454          }
455          const tieredDirs = xmlDoc.querySelectorAll("dir, *|dir, anchoredText, *|anchoredText, harm, *|harm");
456          tieredDirs.forEach((el) => {
457            const text = (el.textContent || "").trim();
458            if (text.includes("‍") || text.includes("[Key:")) {
459              el.setAttribute("place", "above");
460              el.setAttribute("vgrp", "3000");
461              modified = true;
462            } else if (text.includes("‌")) {
463              el.setAttribute("place", "above");
464              el.setAttribute("vgrp", "2000");
465              modified = true;
466            } else if (text.includes("​")) {
467              el.setAttribute("place", "above");
468              el.setAttribute("vgrp", "1000");
469              modified = true;
470            }
471          });
472          xmlDoc.querySelectorAll("verse, *|verse").forEach((verse) => {
473            const layer = verse.closest("layer, *|layer");
474            const layerN = layer ? layer.getAttribute("n") : null;
475            if (layerN && layerN !== "1") {
476              verse.remove();
477              modified = true;
478            }
479          });
480          xmlDoc.querySelectorAll("chord, *|chord").forEach((chord) => {
481            const notes = Array.from(chord.querySelectorAll("note, *|note"));
482            let keptOne = false;
483            notes.forEach((note) => {
484              const verses = note.querySelectorAll("verse, *|verse");
485              verses.forEach((verse) => {
486                if (keptOne) {
487                  verse.remove();
488                  modified = true;
489                } else {
490                  keptOne = true;
491                }
492              });
493            });
494          });
495        } else {
496          allScoreParts.forEach((scorePart, xmlIdx) => {
497            const part = allParts[xmlIdx];
498            const instr = realInstruments[xmlIdx];
499            const isVisible = isMelodyMode ? xmlIdx === 0 : !instr ? true : trackVisibility[instr.index] !== false;
500            let displayName = instr ? instr.displayName || instr.name || "" : "";
501            if (isMelodyMode && xmlIdx === 0) {
502              displayName = melodySource === "studio" ? "Melody (Studio Engraved)" : "Melody (Standard)";
503            }
504            if (!isVisible) {
505              scorePart.remove();
506              if (part) part.remove();
507              modified = true;
508              return;
509            }
510            if (part) {
511              part.querySelectorAll("direction").forEach((el) => {
512                const text = (el.textContent || "").trim();
513                if (!showChords && text.includes("​")) {
514                  el.remove();
515                  modified = true;
516                }
517                if (!showRoman && text.includes("‌")) {
518                  el.remove();
519                  modified = true;
520                }
521                if (!showChords && !showRoman && (text.includes("‍") || text.includes("Key:"))) {
522                  el.remove();
523                  modified = true;
524                }
525                if (text.includes("‍") || text.includes("Key:")) {
526                  el.setAttribute("placement", "above");
527                  el.setAttribute("default-y", "100");
528                  modified = true;
529                } else if (text.includes("‌")) {
530                  el.setAttribute("placement", "above");
531                  el.setAttribute("default-y", "70");
532                  modified = true;
533                } else if (text.includes("​")) {
534                  el.setAttribute("placement", "above");
535                  el.setAttribute("default-y", "40");
536                  modified = true;
537                }
538              });
539              if (!showChords) {
540                part.querySelectorAll("harmony:not([type='roman'])").forEach((el) => {
541                  el.remove();
542                  modified = true;
543                });
544              }
545              if (!showRoman) {
546                part.querySelectorAll("harmony[type='roman']").forEach((el) => {
547                  el.remove();
548                  modified = true;
549                });
550              }
551              const measures = part.querySelectorAll("measure");
552              measures.forEach((measure) => {
553                let currentTime = 0;
554                const seenLyricOnsets = /* @__PURE__ */ new Set();
555                let previousNoteOnset = 0;
556                const children = Array.from(measure.childNodes);
557                children.forEach((childNode) => {
558                  var _a, _b, _c;
559                  if (childNode.nodeType !== 1) return;
560                  const el = childNode;
561                  const tagName = el.tagName.toLowerCase();
562                  if (tagName === "note") {
563                    const isChord = el.querySelector("chord") !== null;
564                    let noteOnset = 0;
565                    if (isChord) {
566                      noteOnset = previousNoteOnset;
567                    } else {
568                      noteOnset = currentTime;
569                      previousNoteOnset = currentTime;
570                    }
571                    const durationText = (_a = el.querySelector("duration")) == null ? void 0 : _a.textContent;
572                    const duration2 = durationText ? parseInt(durationText, 10) : 0;
573                    if (!isChord) {
574                      currentTime += duration2;
575                    }
576                    const lyrics = el.querySelectorAll("lyric");
577                    lyrics.forEach((lyric) => {
578                      if (seenLyricOnsets.has(noteOnset)) {
579                        lyric.remove();
580                        modified = true;
581                      } else {
582                        seenLyricOnsets.add(noteOnset);
583                      }
584                    });
585                  } else if (tagName === "backup") {
586                    const durationText = (_b = el.querySelector("duration")) == null ? void 0 : _b.textContent;
587                    const duration2 = durationText ? parseInt(durationText, 10) : 0;
588                    currentTime = Math.max(0, currentTime - duration2);
589                  } else if (tagName === "forward") {
590                    const durationText = (_c = el.querySelector("duration")) == null ? void 0 : _c.textContent;
591                    const duration2 = durationText ? parseInt(durationText, 10) : 0;
592                    currentTime += duration2;
593                  }
594                });
595              });
596            }
597            if (displayName) {
598              let partName = scorePart.querySelector("part-name");
599              if (!partName) {
600                partName = xmlDoc.createElement("part-name");
601                scorePart.appendChild(partName);
602              }
603              partName.textContent = displayName;
604              let partAbbr = scorePart.querySelector("part-abbr, part-abbreviation");
605              if (!partAbbr) {
606                partAbbr = xmlDoc.createElement("part-abbreviation");
607                scorePart.appendChild(partAbbr);
608              }
609              partAbbr.textContent = displayName;
610              scorePart.querySelectorAll("part-name-display, part-abbreviation-display").forEach((el) => el.remove());
611              modified = true;
612            }
613          });
614        }
615        const effectiveIsMelody = isMelodyMode && (melodySource === "standard" || !melodySource);
616        try {
617          const displayTitle = effectiveIsMelody && title ? `${title} (Melody)` : title || "HamieNET.com Score";
618          const scorePartwise = xmlDoc.querySelector("score-partwise");
619          const mei = xmlDoc.querySelector("mei");
620          if (scorePartwise) {
621            let work = xmlDoc.querySelector("work");
622            if (!work) {
623              work = xmlDoc.createElement("work");
624              scorePartwise.insertBefore(work, scorePartwise.firstChild);
625            }
626            let workTitle = work.querySelector("work-title");
627            if (!workTitle) {
628              workTitle = xmlDoc.createElement("work-title");
629              work.appendChild(workTitle);
630            }
631            workTitle.textContent = displayTitle;
632            modified = true;
633          } else if (mei) {
634            let meiHead = xmlDoc.querySelector("meiHead");
635            if (!meiHead) {
636              meiHead = xmlDoc.createElement("meiHead");
637              mei.insertBefore(meiHead, mei.firstChild);
638            }
639            let fileDesc = meiHead.querySelector("fileDesc");
640            if (!fileDesc) {
641              fileDesc = xmlDoc.createElement("fileDesc");
642              meiHead.appendChild(fileDesc);
643            }
644            let titleStmt = fileDesc.querySelector("titleStmt");
645            if (!titleStmt) {
646              titleStmt = xmlDoc.createElement("titleStmt");
647              fileDesc.appendChild(titleStmt);
648            }
649            let t = titleStmt.querySelector("title");
650            if (!t) {
651              t = xmlDoc.createElement("title");
652              titleStmt.appendChild(t);
653            }
654            t.textContent = displayTitle;
655            modified = true;
656          }
657        } catch (e) {
658        }
659        const ids = /* @__PURE__ */ new Set();
660        const measureIdMap = {};
661        let docMeasureIndex = 1;
662        xmlDoc.querySelectorAll("[id], [*|id]").forEach((el) => {
663          const id = el.getAttribute("xml:id") || el.getAttribute("id");
664          if (id) {
665            ids.add(id);
666            if (el.localName === "measure" || el.tagName.toLowerCase().endsWith("measure")) {
667              const rawN = el.getAttribute("n") || el.getAttribute("number");
668              const n = rawN || (docMeasureIndex++).toString();
669              if (n) {
670                measureIdMap[n.toString()] = id;
671              }
672            }
673          }
674        });
675        availableIdsRef.current = ids;
676        measureIdMapRef.current = measureIdMap;
677        const finalData = modified ? new XMLSerializer().serializeToString(xmlDoc) : localXmlString;
678        finalXmlDataRef.current = finalData;
679        setLoadingProgress({ percent: 75, stage: "Engraving sheet music layout..." });
680        await new Promise((r) => setTimeout(r, 0));
681        verovioToolkit.setOptions({
682          header: "none",
683          footer: "none",
684          font: "Bravura"
685        });
686        const success = verovioToolkit.loadData(finalData);
687        if (success === true || success === 1) {
688          const sourceKey = `${midiId}_${quantization}_${isMelodyMode}_${melodySource}`;
689          currentSourceRef.current = sourceKey;
690          try {
691            const renderedMei = verovioToolkit.getMEI ? verovioToolkit.getMEI({ scoreBased: true }) : null;
692            if (renderedMei) {
693              const meiDoc = parser.parseFromString(renderedMei, "text/xml");
694              let meiMeasureIndex = 1;
695              meiDoc.querySelectorAll("measure").forEach((el) => {
696                const id = el.getAttribute("xml:id") || el.getAttribute("id");
697                const rawN = el.getAttribute("n") || el.getAttribute("number");
698                const n = rawN || (meiMeasureIndex++).toString();
699                if (id && n) {
700                  measureIdMap[n.toString()] = id;
701                  ids.add(id);
702                }
703              });
704            }
705          } catch (e) {
706            console.warn("[SheetMusicEngine] Failed to enrich measure IDs from Verovio MEI:", e);
707          }
708          try {
709            const targetPage = Math.min(Math.max(1, currentPage), verovioToolkit.getPageCount() || 1);
710            const svg = verovioToolkit.renderToSVG(targetPage, {});
711            if (svg) {
712              setSvgContent(svg);
713              const vbMatch = svg.match(/viewBox="[-\d.]+\s+[-\d.]+\s+[-\d.]+\s+([-\d.]+)"/);
714              if (vbMatch) setSvgHeight(parseFloat(vbMatch[1]));
715            }
716            lastRenderedPageRef.current = targetPage;
717          } catch (e) {
718          }
719          setIsDataLoaded(true);
720        }
721      } catch (e) {
722        console.error("[SheetMusicEngine] Load error:", e);
723      } finally {
724        setLoading(false);
725      }
726    };
727    loadScore();
728  }, [verovioToolkit, quantization, scorePath, internalXmlData, isMelodyMode, melodySource, title, processedInstruments, trackVisibilityStr, showChords, showRoman, midiId]);
729  reactExports.useEffect(() => {
730    if (!verovioToolkit || !isDataLoaded || dimensions.width < 50) return;
731    const updateLayout = () => {
732      try {
733        const minSpacingStaff = Math.max(12, Math.round(18 * (100 / Math.max(20, scale))));
734        const minSpacingSystem = Math.max(16, minSpacingStaff + 8);
735        const calculatedSpacingStaff = Math.max(minSpacingStaff, Math.round(density / 50 * 14));
736        const calculatedSpacingSystem = Math.max(minSpacingSystem, Math.round(density / 50 * 16));
737        const calculatedSpacingLinear = Math.max(0, density / 50 * 0.25);
738        const calculatedSpacingNonLinear = Math.max(0, density / 50 * 0.6);
739        let options = {
740          scale,
741          adjustPageHeight: false,
742          adjustPageWidth: false,
743          breaks: "auto",
744          font: "Bravura",
745          mnumInterval: showMeasureNumbers ? 1 : 0,
746          pageMarginLeft: 100,
747          pageMarginRight: 100,
748          pageMarginTop: 100,
749          pageMarginBottom: 100,
750          spacingStaff: calculatedSpacingStaff,
751          spacingSystem: calculatedSpacingSystem,
752          spacingLinear: calculatedSpacingLinear,
753          spacingNonLinear: calculatedSpacingNonLinear
754        };
755        if (layoutMode === "a4") {
756          options = {
757            ...options,
758            pageWidth: 2100 * (100 / scale),
759            pageHeight: 2970 * (100 / scale),
760            pageMarginTop: 150 * (100 / scale),
761            pageMarginBottom: 50 * (100 / scale),
762            pageMarginLeft: 50 * (100 / scale),
763            pageMarginRight: 50 * (100 / scale),
764            header: "none",
765            footer: "none",
766            justifyVertically: true,
767            usePgHeaderForAll: false
768          };
769        } else if (layoutMode === "a3") {
770          options = {
771            ...options,
772            pageWidth: 4200 * (100 / scale),
773            pageHeight: 2970 * (100 / scale),
774            pageMarginTop: 150 * (100 / scale),
775            pageMarginBottom: 50 * (100 / scale),
776            pageMarginLeft: 50 * (100 / scale),
777            pageMarginRight: 50 * (100 / scale),
778            header: "none",
779            footer: "none",
780            justifyVertically: true,
781            usePgHeaderForAll: false
782          };
783        } else if (layoutMode === "screen-paged") {
784          const pWidth = Math.max(100, dimensions.width * (100 / scale));
785          options = { ...options, footer: "none", header: "none", pageWidth: pWidth, pageHeight: 100, pageMarginTop: 50, pageMarginBottom: 0, pageMarginLeft: 0, pageMarginRight: 0, adjustPageHeight: 1, justifyVertically: false };
786        } else {
787          options = { ...options, footer: "none", header: "none", pageWidth: 1e5, pageHeight: 100, pageMarginTop: 50, pageMarginBottom: 0, pageMarginLeft: 0, pageMarginRight: 0, breaks: "none", adjustPageHeight: 1, justifyVertically: false };
788        }
789        const optionsStr = JSON.stringify(options);
790        let layoutChanged = false;
791        if (optionsStr !== lastOptionsRef.current) {
792          verovioToolkit.setOptions(options);
793          if ((layoutMode === "a4" || layoutMode === "a3") && finalXmlDataRef.current) {
794            verovioToolkit.loadData(finalXmlDataRef.current);
795          } else {
796            verovioToolkit.redoLayout();
797          }
798          lastOptionsRef.current = optionsStr;
799          const newPageCount = verovioToolkit.getPageCount();
800          setPageCount(newPageCount);
801          layoutChanged = true;
802        }
803        if (layoutChanged || currentPage !== lastRenderedPageRef.current) {
804          const newPageCount = verovioToolkit.getPageCount();
805          const targetPage = Math.min(Math.max(1, currentPage), newPageCount || 1);
806          const svg = verovioToolkit.renderToSVG(targetPage, {});
807          if (svg) {
808            setSvgContent(svg);
809            const vbMatch = svg.match(/viewBox="[-\d.]+\s+[-\d.]+\s+[-\d.]+\s+([-\d.]+)"/);
810            if (vbMatch) setSvgHeight(parseFloat(vbMatch[1]));
811          }
812          lastRenderedPageRef.current = currentPage;
813        }
814      } catch (e) {
815        console.error("[SheetMusicEngine] Layout Update Error:", e);
816      }
817    };
818    updateLayout();
819  }, [verovioToolkit, isDataLoaded, layoutMode, scale, dimensions.width, dimensions.height, density, showMeasureNumbers, showChords, showRoman, currentPage]);
820  reactExports.useEffect(() => {
821    if (!verovioToolkit || !isDataLoaded) {
822      setSyncPoints([]);
823      return;
824    }
825    const points = [];
826    const measureBoundaries = [];
827    const ids = availableIdsRef.current;
828    const visualMeasureKeys = Object.keys(measureIdMapRef.current);
829    const maxAudioMeasure = measureMap.length > 0 ? Math.max(...measureMap.map((m) => m.measure)) : 0;
830    const hasVisualRepeats = visualMeasureKeys.length > 0 && maxAudioMeasure > visualMeasureKeys.length || quantization === 700;
831    if (hasVisualRepeats) {
832      let vDuration = 0;
833      try {
834        const rawTm = verovioToolkit.renderToTimemap ? verovioToolkit.renderToTimemap() : null;
835        const tm = typeof rawTm === "string" ? JSON.parse(rawTm) : rawTm;
836        if (Array.isArray(tm) && tm.length > 0) {
837          const lastEvent = tm[tm.length - 1];
838          if (lastEvent && lastEvent.tstamp > 0) {
839            vDuration = lastEvent.tstamp / 1e3;
840          }
841        }
842      } catch (e) {
843      }
844      if (vDuration <= 0) {
845        try {
846          const lastPage = verovioToolkit.getPageCount();
847          const svg = verovioToolkit.renderToSVG(lastPage, {});
848          const parser = new DOMParser();
849          const doc = parser.parseFromString(svg, "image/svg+xml");
850          const measures = doc.querySelectorAll(".measure");
851          if (measures.length > 0) {
852            const lastId = measures[measures.length - 1].getAttribute("id");
853            const lastStart = verovioToolkit.getTimeForElement(lastId) / 1e3;
854            if (lastStart > 0) {
855              const approxMeasureDur = visualMeasureKeys.length > 0 ? lastStart / visualMeasureKeys.length : 2;
856              vDuration = lastStart + approxMeasureDur;
857            }
858          }
859        } catch (e) {
860        }
861      }
862      if (vDuration <= 0) vDuration = duration || 30;
863      const numTuneLoops = duration > 0 && vDuration > 5 ? Math.max(1, Math.round(duration / vDuration)) : 1;
864      const loopAudioDur = duration > 0 ? duration / numTuneLoops : vDuration;
865      for (let loop = 0; loop < numTuneLoops; loop++) {
866        const loopStartAudio = loop * loopAudioDur;
867        const loopEndAudio = loop === numTuneLoops - 1 ? duration : (loop + 1) * loopAudioDur;
868        points.push({ vTime: 0, aTime: loopStartAudio });
869        points.push({ vTime: vDuration, aTime: loopEndAudio - 1e-3 });
870      }
871    } else {
872      measureMap.forEach((s) => {
873        if (s.beat && s.beat > 1) return;
874        const mId = s.measure.toString();
875        let vTimeMs = -1;
876        const mappedId = measureIdMapRef.current[mId];
877        if (mappedId && ids.has(mappedId)) {
878          vTimeMs = verovioToolkit.getTimeForElement(mappedId);
879        }
880        if (vTimeMs < 0 && ids.has(mId)) vTimeMs = verovioToolkit.getTimeForElement(mId);
881        if (vTimeMs < 0 && ids.has(`m-${mId}`)) vTimeMs = verovioToolkit.getTimeForElement(`m-${mId}`);
882        if (vTimeMs < 0 && ids.has(`measure-${mId}`)) vTimeMs = verovioToolkit.getTimeForElement(`measure-${mId}`);
883        if (vTimeMs < 0 && ids.has(`metric-${mId}`)) vTimeMs = verovioToolkit.getTimeForElement(`metric-${mId}`);
884        if (vTimeMs < 0) {
885          const longId = `measure-000000000${mId}`.slice(-18);
886          if (ids.has(longId)) vTimeMs = verovioToolkit.getTimeForElement(longId);
887        }
888        if (vTimeMs >= 0) {
889          measureBoundaries.push({ m: s.measure, vTime: vTimeMs / 1e3, aTime: s.time });
890        }
891      });
892      measureMap.forEach((s) => {
893        s.measure.toString();
894        const boundary = measureBoundaries.find((b) => b.m === s.measure);
895        if (boundary && (!s.beat || s.beat === 1)) {
896          points.push({ vTime: boundary.vTime, aTime: s.time });
897          return;
898        }
899        if (s.beat && s.beat > 1) {
900          const currentM = measureBoundaries.find((b) => b.m === s.measure);
901          const nextM = measureBoundaries.find((b) => b.m === s.measure + 1);
902          if (currentM && nextM) {
903            const mDurV = nextM.vTime - currentM.vTime;
904            const ratio = (s.time - currentM.aTime) / (nextM.aTime - currentM.aTime);
905            const interpolatedVTime = currentM.vTime + mDurV * ratio;
906            points.push({ vTime: interpolatedVTime, aTime: s.time });
907          } else if (currentM) {
908            points.push({ vTime: currentM.vTime + (s.beat - 1), aTime: s.time });
909          }
910        }
911      });
912      if (points.length === 0) {
913        sections.forEach((s) => {
914          var _a;
915          const mId = (_a = s.measure) == null ? void 0 : _a.toString();
916          if (!mId) return;
917          let vTimeMs = -1;
918          const mappedId = measureIdMapRef.current[mId];
919          if (mappedId && ids.has(mappedId)) {
920            vTimeMs = verovioToolkit.getTimeForElement(mappedId);
921          }
922          if (vTimeMs < 0 && ids.has(mId)) vTimeMs = verovioToolkit.getTimeForElement(mId);
923          if (vTimeMs < 0 && ids.has(`m-${mId}`)) vTimeMs = verovioToolkit.getTimeForElement(`m-${mId}`);
924          if (vTimeMs < 0 && ids.has(`measure-${mId}`)) vTimeMs = verovioToolkit.getTimeForElement(`measure-${mId}`);
925          if (vTimeMs < 0) return;
926          points.push({ vTime: vTimeMs / 1e3, aTime: s.time });
927        });
928      }
929    }
930    const uniquePoints = points.sort((a, b) => a.aTime - b.aTime).filter((p, i, arr) => i === 0 || p.aTime > arr[i - 1].aTime);
931    uniquePoints.unshift({ vTime: 0, aTime: 0 });
932    if (duration > 0 && uniquePoints.length > 0) {
933      const last = uniquePoints[uniquePoints.length - 1];
934      if (last.aTime < duration) {
935        try {
936          const lastPage = verovioToolkit.getPageCount();
937          const svg = verovioToolkit.renderToSVG(lastPage, {});
938          const parser = new DOMParser();
939          const doc = parser.parseFromString(svg, "image/svg+xml");
940          const measures = doc.querySelectorAll(".measure");
941          if (measures.length > 0) {
942            const lastId = measures[measures.length - 1].getAttribute("id");
943            const lastVTime = verovioToolkit.getTimeForElement(lastId) / 1e3;
944            uniquePoints.push({ vTime: Math.max(lastVTime, last.vTime + 0.1), aTime: duration });
945          } else {
946            uniquePoints.push({ vTime: last.vTime + (duration - last.aTime), aTime: duration });
947          }
948        } catch (e) {
949          uniquePoints.push({ vTime: last.vTime + (duration - last.aTime), aTime: duration });
950        }
951      }
952    }
953    uniquePoints.sort((a, b) => a.aTime - b.aTime);
954    setSyncPoints(uniquePoints);
955    setLoadingProgress({ percent: 100, stage: "Score ready" });
956  }, [verovioToolkit, isDataLoaded, sections, measureMap, duration, quantization]);
957  reactExports.useEffect(() => {
958    if (!isDataLoaded) return;
959    const params = new URLSearchParams(window.location.search);
960    const pageParam = params.get("page");
961    if (pageParam) {
962      const targetPage = parseInt(pageParam, 10);
963      if (!isNaN(targetPage) && targetPage > 0) {
964        setCurrentPage(targetPage);
965      }
966    }
967  }, [isDataLoaded]);
968  return {
969    verovioToolkit,
970    loading,
971    loadingProgress,
972    setLoading,
973    isDataLoaded,
974    pageCount,
975    currentPage,
976    setCurrentPage,
977    svgContent,
978    svgHeight,
979    syncPoints,
980    renderPage
981  };
982};
983const useSheetMusicPlayback = (props) => {
984  const {
985    verovioToolkit,
986    isPlaying,
987    currentTime,
988    duration,
989    audioLatency,
990    onSeek,
991    layoutMode,
992    scale,
993    pageTurnLookahead,
994    dimensions,
995    currentPage,
996    setCurrentPage,
997    pageCount,
998    renderPage,
999    syncPoints,
1000    containerRef,
1001    paperRef,
1002    cursorRef,
1003    showCursor,
1004    showHighlight,
1005    isDataLoaded = false
1006  }
1006 = props;
1007  const [pageTriggerTimes, setPageTriggerTimes] = reactExports.useState([]);
1008  const isInternalScrollRef = reactExports.useRef(false);
1009  const lastManualScrollTimeRef = reactExports.useRef(0);
1010  const lastManualPageChangeRef = reactExports.useRef(0);
1011  const lastManualPageTimeRef = reactExports.useRef(-1);
1012  reactExports.useRef(null);
1013  const lastPropTimeRef = reactExports.useRef(currentTime);
1014  const isPendingSeekScrollRef = reactExports.useRef(false);
1015  const lastSystemTopRef = reactExports.useRef(-1);
1016  const lastTimeRef = reactExports.useRef({ time: currentTime, local: performance.now() });
1017  const elementBoundsCacheRef = reactExports.useRef(/* @__PURE__ */ new Map());
1018  const lastCachedPageRef = reactExports.useRef(-1);
1019  const highlightedNoteIdsRef = reactExports.useRef(/* @__PURE__ */ new Set());
1020  const eventRangeRef = reactExports.useRef({
1021    startTime: -1,
1022    startX: 0,
1023    nextTime: 0,
1024    nextX: 0,
1025    ids: "",
1026    systemTop: 0,
1027    systemBottom: 0
1028  });
1029  const lastLayoutRef = reactExports.useRef({ scale, layoutMode, width: dimensions.width, height: dimensions.height });
1030  reactExports.useEffect(() => {
1031    lastTimeRef.current = { time: currentTime, local: performance.now() };
1032  }, [currentTime]);
1033  const renderingPageRef = reactExports.useRef(currentPage);
1034  reactExports.useEffect(() => {
1035    renderingPageRef.current = currentPage;
1036  }, [currentPage]);
1037  const getSmoothTime = reactExports.useCallback(() => {
1038    if (!isPlaying) return currentTime;
1039    const now = performance.now();
1040    const dt = (now - lastTimeRef.current.local) / 1e3;
1041    return lastTimeRef.current.time + Math.min(dt, 0.5);
1042  }, [currentTime, isPlaying]);
1043  const audioToVerovioTime = reactExports.useCallback((audioTime) => {
1044    if (syncPoints.length < 2) return audioTime;
1045    let i = 0;
1046    for (; i < syncPoints.length - 2; i++) {
1047      if (audioTime <= syncPoints[i + 1].aTime) break;
1048    }
1049    const p1 = syncPoints[i];
1050    const p2 = syncPoints[i + 1];
1051    const aSpan = p2.aTime - p1.aTime;
1052    const vSpan = p2.vTime - p1.vTime;
1053    if (aSpan <= 0) return p1.vTime;
1054    return p1.vTime + (audioTime - p1.aTime) / aSpan * vSpan;
1055  }, [syncPoints, duration]);
1056  const verovioToAudioTime = reactExports.useCallback((vTime) => {
1057    if (syncPoints.length < 2) return vTime;
1058    let i = 0;
1059    for (; i < syncPoints.length - 2; i++) {
1060      if (vTime <= syncPoints[i + 1].vTime) break;
1061    }
1062    const p1 = syncPoints[i];
1063    const p2 = syncPoints[i + 1];
1064    const vSpan = p2.vTime - p1.vTime;
1065    const aSpan = p2.aTime - p1.aTime;
1066    if (vSpan <= 0) return p1.aTime;
1067    return p1.aTime + (vTime - p1.vTime) / vSpan * aSpan;
1068  }, [syncPoints, duration]);
1069  reactExports.useEffect(() => {
1070    if (!verovioToolkit || !isDataLoaded || layoutMode === "screen-moving" || pageCount === 0) {
1071      setPageTriggerTimes([]);
1072      return;
1073    }
1074    let active = true;
1075    const triggers = [];
1076    const parser = new DOMParser();
1077    const processBatch = (startPage) => {
1078      if (!active) return;
1079      const endPage = Math.min(startPage + 4, pageCount);
1080      for (let p = startPage; p <= endPage; p++) {
1081        try {
1082          const svg = verovioToolkit.renderToSVG(p, {});
1083          const doc = parser.parseFromString(svg, "image/svg+xml");
1084          const firstMeasure = doc.querySelector(".measure");
1085          if (firstMeasure) {
1086            const id = firstMeasure.getAttribute("id");
1087            const startTime = verovioToolkit.getTimeForElement(id);
1088            if (startTime > 0) {
1089              const prevElements = verovioToolkit.getElementsAtTime(startTime - 1);
1090              if (prevElements && prevElements.measure && prevElements.measure.length > 0) {
1091                const prevMeasureId = prevElements.measure[0];
1092                const prevStartTime = verovioToolkit.getTimeForElement(prevMeasureId);
1093                if (prevStartTime > 0 && prevStartTime < startTime) {
1094                  const measureDuration = startTime - prevStartTime;
1095                  triggers[p] = startTime - measureDuration / 2;
1096                } else {
1097                  triggers[p] = startTime - 500;
1098                }
1099              } else {
1100                triggers[p] = startTime - 500;
1101              }
1102            }
1103          }
1104        } catch (e) {
1105        }
1106      }
1107      if (endPage < pageCount) {
1108        setTimeout(() => processBatch(endPage + 1), 0);
1109      } else {
1110        if (active) setPageTriggerTimes(triggers);
1111      }
1112    };
1113    processBatch(2);
1114    return () => {
1115      active = false;
1116    };
1117  }, [verovioToolkit, isDataLoaded, layoutMode, scale, pageCount]);
1118  const getElementBounds = reactExports.useCallback((id) => {
1119    var _a;
1120    const cleanId = id.replace(/-rend\d+$/, "");
1121    const cache = elementBoundsCacheRef.current;
1122    if (cache.has(cleanId)) return cache.get(cleanId);
1123    const el = (_a = containerRef.current) == null ? void 0 : _a.querySelector(`[id="${cleanId}"]`);
1124    if (!el || !paperRef.current) return null;
1125    const rect = el.getBoundingClientRect();
1126    const paperRect = paperRef.current.getBoundingClientRect();
1127    const scaleFactor = paperRect.width / (paperRef.current.offsetWidth || 1);
1128    const left = (rect.left - paperRect.left) / scaleFactor;
1129    const system = el.closest(".system");
1130    const sysRect = system == null ? void 0 : system.getBoundingClientRect();
1131    const systemTop = sysRect ? (sysRect.top - paperRect.top) / scaleFactor : (rect.top - paperRect.top) / scaleFactor;
1132    const systemBottom = systemTop + ((sysRect == null ? void 0 : sysRect.height) || rect.height) / scaleFactor;
1133    const bounds = { left, systemTop, systemBottom };
1134    cache.set(cleanId, bounds);
1135    return bounds;
1136  }, [containerRef, paperRef]);
1137  const safeGetElementsAtTime = reactExports.useCallback((timeMs) => {
1138    if (!verovioToolkit || !isDataLoaded) return null;
1139    try {
1140      return verovioToolkit.getElementsAtTime(timeMs);
1141    } catch (e) {
1142      return null;
1143    }
1144  }, [verovioToolkit, isDataLoaded]);
1145  reactExports.useEffect(() => {
1146    if (!verovioToolkit || !isDataLoaded || !containerRef.current || !paperRef.current) return;
1147    let animationFrame;
1148    const update = () => {
1149      var _a, _b, _c, _d, _e;
1150      const paperWidth = ((_a = paperRef.current) == null ? void 0 : _a.offsetWidth) || 0;
1151      const paperRect = (_b = paperRef.current) == null ? void 0 : _b.getBoundingClientRect();
1152      if (paperWidth <= 0 || !paperRect || paperRect.width <= 0) {
1153        if (isPlaying) animationFrame = requestAnimationFrame(update);
1154        return;
1155      }
1156      const layoutChanged = lastLayoutRef.current.scale !== scale || lastLayoutRef.current.layoutMode !== layoutMode || lastLayoutRef.current.width !== dimensions.width || lastLayoutRef.current.height !== dimensions.height;
1157      if (layoutChanged || renderingPageRef.current !== lastCachedPageRef.current) {
1158        lastLayoutRef.current = { scale, layoutMode, width: dimensions.width, height: dimensions.height };
1159        elementBoundsCacheRef.current.clear();
1160        lastCachedPageRef.current = renderingPageRef.current;
1161        eventRangeRef.current = {
1162          startTime: -1,
1163          startX: 0,
1164          nextTime: 0,
1165          nextX: 0,
1166          ids: "",
1167          systemTop: 0,
1168          systemBottom: 0
1169        };
1170        isPendingSeekScrollRef.current = true;
1171      }
1172      const smoothTime = getSmoothTime();
1173      let calibratedTime = audioToVerovioTime(smoothTime);
1174      const timeJump = Math.abs(currentTime - lastPropTimeRef.current);
1175      const isSeek = timeJump > 1;
1176      if (isSeek) {
1177        isPendingSeekScrollRef.current = true;
1178      }
1179      lastPropTimeRef.current = currentTime;
1180      const timeMs = Math.round((calibratedTime - audioLatency) * 1e3);
1181      if (isSeek) {
1182        eventRangeRef.current = {
1183          startTime: -1,
1184          startX: 0,
1185          nextTime: 0,
1186          nextX: 0,
1187          ids: "",
1188          systemTop: 0,
1189          systemBottom: 0
1190        };
1191      }
1192      if (isNaN(timeMs)) {
1193        if (isPlaying) animationFrame = requestAnimationFrame(update);
1194        return;
1195      }
1196      let elements = safeGetElementsAtTime(timeMs) || { page: 1, notes: [] };
1197      if (timeMs <= 0 || calibratedTime <= 0) elements = { page: 1, notes: [] };
1198      if (elements && elements.page > 0) {
1199        let targetPage = elements.page;
1200        if (layoutMode !== "screen-moving") {
1201          if (timeMs <= 0 || calibratedTime <= 0) targetPage = 1;
1202          else if (isPlaying) {
1203            if (pageTurnLookahead === -1) {
1204              const nextPage = currentPage + 1;
1205              const triggerTime = pageTriggerTimes[nextPage];
1206              if (triggerTime !== void 0 && triggerTime > 0 && timeMs >= triggerTime && nextPage <= pageCount) {
1207                targetPage = Math.max(targetPage, nextPage);
1208              }
1209            } else if (pageTurnLookahead > 0) {
1210              const lookaheadMs = pageTurnLookahead * 1e3;
1211              const lookaheadElements = safeGetElementsAtTime(timeMs + lookaheadMs);
1212              if (lookaheadElements && lookaheadElements.page > 0 && lookaheadElements.page > targetPage) {
1213                targetPage = Math.max(targetPage, lookaheadElements.page);
1214              }
1215            }
1216          }
1217          if (!isSeek && isPlaying && targetPage < currentPage) {
1218            targetPage = currentPage;
1219          }
1220        }
1221        const timeSinceManualChange = Date.now() - lastManualPageChangeRef.current;
1222        const shouldTurnPage = (isSeek || timeSinceManualChange >= 2e3) && targetPage !== currentPage && targetPage !== renderingPageRef.current && (isPlaying || isSeek);
1223        if (shouldTurnPage) {
1224          renderingPageRef.current = targetPage;
1225          setCurrentPage(targetPage);
1226          renderPage(targetPage);
1227          if (containerRef.current) containerRef.current.scrollTo({ top: 0, behavior: "smooth" });
1228          if (isPlaying) {
1229            animationFrame = requestAnimationFrame(update);
1230            return;
1231          }
1232        }
1233        const currentIds = (elements.notes || []).join(",");
1234        const range = eventRangeRef.current;
1235        let currentInterpolatedX = range.startX;
1236        if (range.nextTime > range.startTime) {
1237          const progress = Math.max(0, Math.min(1, (timeMs - range.startTime) / (range.nextTime - range.startTime)));
1238          if (range.nextX >= range.startX) currentInterpolatedX = range.startX + (range.nextX - range.startX) * progress;
1239        }
1240        const currentTrigger = pageTriggerTimes[currentPage];
1241        const isEarlyLookaheadActive = currentTrigger !== void 0 && currentTrigger > 0 && timeMs < currentTrigger;
1242        if (layoutMode !== "screen-moving" && isEarlyLookaheadActive) {
1243          if (cursorRef.current) cursorRef.current.style.display = "none";
1244          if (isPlaying) animationFrame = requestAnimationFrame(update);
1245          return;
1246        }
1247        if (currentIds !== range.ids || currentIds === "" && timeMs >= range.nextTime || timeMs < range.startTime || range.startTime === -1) {
1248          highlightedNoteIdsRef.current.forEach((id) => {
1249            var _a2;
1250            const cleanId = id.replace(/-rend\d+$/, "");
1251            const el = (_a2 = containerRef.current) == null ? void 0 : _a2.querySelector(`[id="${cleanId}"]`);
1252            if (el) el.classList.remove("highlight-note");
1253          });
1254          highlightedNoteIdsRef.current.clear();
1255          if (elements.notes && elements.notes.length > 0) {
1256            let startX = Infinity, systemTop = Infinity, systemBottom = -Infinity, found = false;
1257            elements.notes.forEach((id) => {
1258              var _a2;
1259              const cleanId = id.replace(/-rend\d+$/, "");
1260              if (showHighlight) {
1261                const el = (_a2 = containerRef.current) == null ? void 0 : _a2.querySelector(`[id="${cleanId}"]`);
1262                if (el) {
1263                  el.classList.add("highlight-note");
1264                  highlightedNoteIdsRef.current.add(cleanId);
1265                }
1266              }
1267              const bounds = getElementBounds(cleanId);
1268              if (bounds) {
1269                startX = Math.min(startX, bounds.left);
1270                systemTop = Math.min(systemTop, bounds.systemTop);
1271                systemBottom = Math.max(systemBottom, bounds.systemBottom);
1272                found = true;
1273              }
1274            });
1275            if (found) {
1276              if (range.startTime !== -1 && timeMs >= range.startTime && Math.abs(systemTop - range.systemTop) < 10 && startX < currentInterpolatedX) startX = currentInterpolatedX;
1277              let nextTime = timeMs + 500, nextX = startX + 20;
1278              for (let t = timeMs + 50; t <= timeMs + 2e3; t += 50) {
1279                const future = safeGetElementsAtTime(t);
1280                if (future && future.notes && future.notes.join(",") !== currentIds) {
1281                  nextTime = t;
1282                  let fMinX = Infinity;
1283                  future.notes.forEach((id) => {
1284                    const bounds = getElementBounds(id);
1285                    if (bounds) {
1286                      fMinX = Math.min(fMinX, bounds.left);
1287                    }
1288                  });
1289                  if (fMinX !== Infinity) {
1290                    nextX = fMinX;
1291                  }
1292                  break;
1293                }
1294              }
1295              if (nextX < startX - 100) {
1296                const cleanFirst = elements.notes[0].replace(/-rend\d+$/, "");
1297                const system = (_d = (_c = containerRef.current) == null ? void 0 : _c.querySelector(`[id="${cleanFirst}"]`)) == null ? void 0 : _d.closest(".system");
1298                const paperRect2 = paperRef.current.getBoundingClientRect();
1299                const scaleFactor = paperRect2.width / (paperRef.current.offsetWidth || 1);
1300                if (system) nextX = (system.getBoundingClientRect().right - paperRect2.left) / scaleFactor;
1301              }
1302              eventRangeRef.current = { startTime: timeMs, startX, nextTime, nextX, ids: currentIds, systemTop, systemBottom };
1303            }
1304          } else {
1305            let nextTime = timeMs + 500, nextX = range.nextX || 0, foundNext = false, systemTop = range.systemTop || 0, systemBottom = range.systemBottom || 0;
1306            if (timeMs <= 0) {
1307              const firstMeasure = (_e = containerRef.current) == null ? void 0 : _e.querySelector(".measure");
1308              if (firstMeasure) {
1309                const rect = firstMeasure.getBoundingClientRect();
1310                const paperRect2 = paperRef.current.getBoundingClientRect();
1311                const scaleFactor = paperRect2.width / (paperRef.current.offsetWidth || 1);
1312                const startX = (rect.left - paperRect2.left) / scaleFactor;
1313                const system = firstMeasure.closest(".system");
1314                const sysRect = system == null ? void 0 : system.getBoundingClientRect();
1315                systemTop = sysRect ? (sysRect.top - paperRect2.top) / scaleFactor : (rect.top - paperRect2.top) / scaleFactor;
1316                systemBottom = systemTop + ((sysRect == null ? void 0 : sysRect.height) || rect.height) / scaleFactor;
1317                for (let t = 0; t <= 5e3; t += 50) {
1318                  const future = safeGetElementsAtTime(t);
1319                  if (future && future.notes && future.notes.length > 0) {
1320                    nextTime = t;
1321                    let fMinX = Infinity;
1322                    future.notes.forEach((id) => {
1323                      const bounds = getElementBounds(id);
1324                      if (bounds) fMinX = Math.min(fMinX, bounds.left);
1325                    });
1326                    if (fMinX !== Infinity) nextX = fMinX;
1327                    break;
1328                  }
1329                }
1330                eventRangeRef.current = { startTime: 0, startX, nextTime, nextX, ids: "", systemTop, systemBottom };
1331                foundNext = true;
1332              }
1333            }
1334            if (!foundNext) {
1335              for (let t = timeMs + 50; t <= timeMs + 5e3; t += 50) {
1336                const future = safeGetElementsAtTime(t);
1337                if (future && future.notes && future.notes.length > 0) {
1338                  nextTime = t;
1339                  let fMinX = Infinity;
1340                  future.notes.forEach((id) => {
1341                    const bounds = getElementBounds(id);
1342                    if (bounds) {
1343                      fMinX = Math.min(fMinX, bounds.left);
1344                      systemTop = bounds.systemTop;
1345                      systemBottom = bounds.systemBottom;
1346                    }
1347                  });
1348                  if (fMinX !== Infinity) {
1349                    nextX = fMinX;
1350                    foundNext = true;
1351                  }
1352                  break;
1353                }
1354              }
1355              if (foundNext) {
1356                let safeStartX = currentInterpolatedX;
1357                if (Math.abs(timeMs - range.startTime) > 1e3 || range.startTime === -1) safeStartX = nextX - Math.min(200, (nextTime - timeMs) * 0.1);
1358                eventRangeRef.current = { startTime: timeMs, startX: safeStartX, nextTime, nextX, ids: "", systemTop, systemBottom };
1359              }
1360            }
1361          }
1362        }
1363        const r = eventRangeRef.current;
1364        let interpolatedX = r.startX;
1365        if (r.nextTime > r.startTime && r.nextX >= r.startX) {
1366          interpolatedX = r.startX + (r.nextX - r.startX) * Math.max(0, Math.min(1, (timeMs - r.startTime) / (r.nextTime - r.startTime)));
1367        } else if (r.nextTime > r.startTime && r.nextX < r.startX) {
1368          interpolatedX = r.startX;
1369        }
1370        if (currentIds === "" && r.nextX === r.startX) {
1371          interpolatedX = r.startX + (timeMs - r.startTime) * 0.1;
1372        }
1373        const isManualScrollActive = Date.now() - lastManualScrollTimeRef.current < 2e3;
1374        const forceSeekScroll = isPendingSeekScrollRef.current;
1375        if (layoutMode === "screen-moving" && paperRef.current && containerRef.current) {
1376          if (paperRef.current.style.transform) paperRef.current.style.transform = "";
1377          if ((isPlaying || forceSeekScroll) && !isManualScrollActive) {
1378            isInternalScrollRef.current = true;
1379            containerRef.current.scrollLeft = Math.max(0, interpolatedX - dimensions.width * 0.2);
1380            if (forceSeekScroll) isPendingSeekScrollRef.current = false;
1381          }
1382        }
1383        if (cursorRef.current) {
1384          const r2 = eventRangeRef.current;
1385          cursorRef.current.style.display = showCursor && r2.systemBottom > r2.systemTop ? "block" : "none";
1386          cursorRef.current.style.left = `${interpolatedX - 2}px`;
1387          cursorRef.current.style.top = `${r2.systemTop}px`;
1388          cursorRef.current.style.height = `${r2.systemBottom - r2.systemTop}px`;
1389        }
1390        if (layoutMode !== "screen-moving" && (isPlaying || forceSeekScroll || timeMs <= 0) && !isManualScrollActive) {
1391          const container = containerRef.current;
1392          if (container) {
1393            if (timeMs <= 0) {
1394              container.scrollTo({ top: 0, left: 0, behavior: "smooth" });
1395              lastSystemTopRef.current = -1;
1396              if (forceSeekScroll) isPendingSeekScrollRef.current = false;
1397            } else {
1398              const isOutOfBounds = interpolatedX < container.scrollLeft || interpolatedX > container.scrollLeft + container.clientWidth * 0.8;
1399              if (forceSeekScroll || isOutOfBounds) {
1400                isInternalScrollRef.current = true;
1401                container.scrollTo({
1402                  left: Math.max(0, interpolatedX - container.clientWidth * 0.2),
1403                  behavior: forceSeekScroll ? "auto" : "smooth"
1404                });
1405              }
1406              if (layoutMode !== "screen-paged" && (forceSeekScroll || r.systemTop !== lastSystemTopRef.current)) {
1407                const isVerticalOutOfBounds = r.systemTop < container.scrollTop || r.systemBottom > container.scrollTop + container.clientHeight * 0.9;
1408                if (forceSeekScroll || isVerticalOutOfBounds) {
1409                  isInternalScrollRef.current = true;
1410                  container.scrollTo({
1411                    top: Math.max(0, r.systemTop - container.clientHeight * 0.2),
1412                    behavior: forceSeekScroll ? "auto" : "smooth"
1413                  });
1414                }
1415                lastSystemTopRef.current = r.systemTop;
1416              }
1417              if (forceSeekScroll) isPendingSeekScrollRef.current = false;
1418            }
1419          }
1420        }
1421      }
1422      if (isPlaying) animationFrame = requestAnimationFrame(update);
1423    };
1424    if (isPlaying) animationFrame = requestAnimationFrame(update);
1425    else update();
1426    return () => {
1427      if (animationFrame) cancelAnimationFrame(animationFrame);
1428    };
1429  }, [isPlaying, verovioToolkit, isDataLoaded, safeGetElementsAtTime, showCursor, showHighlight, layoutMode, scale, dimensions, currentPage, renderPage, getSmoothTime, pageTurnLookahead, pageTriggerTimes, pageCount, getElementBounds, audioLatency, audioToVerovioTime]);
1430  reactExports.useEffect(() => {
1431    if (isPlaying || !verovioToolkit || !isDataLoaded) return;
1432    const frame = requestAnimationFrame(() => {
1433      var _a;
1434      const paperWidth = ((_a = paperRef.current) == null ? void 0 : _a.offsetWidth) || 0;
1435      if (paperWidth > 0 && containerRef.current && paperRef.current) {
1436        const calibratedTime = audioToVerovioTime(currentTime);
1437        const timeMs = Math.round((calibratedTime - audioLatency) * 1e3);
1438        const elements = safeGetElementsAtTime(timeMs);
1439        if (elements && elements.notes && elements.notes.length > 0) {
1440          const bounds = getElementBounds(elements.notes[0]);
1441          if (bounds && cursorRef.current) {
1442            cursorRef.current.style.display = showCursor ? "block" : "none";
1443            cursorRef.current.style.left = `${bounds.left - 2}px`;
1444            cursorRef.current.style.top = `${bounds.systemTop}px`;
1445            cursorRef.current.style.height = `${bounds.systemBottom - bounds.systemTop}px`;
1446          }
1447        }
1448      }
1449    });
1450    return () => cancelAnimationFrame(frame);
1451  }, [currentTime, isPlaying, verovioToolkit, isDataLoaded, safeGetElementsAtTime, showCursor, getElementBounds, audioLatency, audioToVerovioTime]);
1452  const handleScroll = reactExports.useCallback(() => {
1453    if (isInternalScrollRef.current) {
1454      isInternalScrollRef.current = false;
1455      return;
1456    }
1457    lastManualScrollTimeRef.current = Date.now();
1458  }, []);
1459  return {
1460    audioToVerovioTime,
1461    verovioToAudioTime,
1462    lastManualPageChangeRef,
1463    lastManualPageTimeRef,
1464    handleScroll,
1465    isInternalScrollRef,
1466    getSmoothTime
1467  };
1468};
1469const SheetMusicToolbar = (props) => {
1470  const {
1471    layoutMode,
1472    setLayoutMode,
1473    isAutoZoom,
1474    setIsAutoZoom,
1475    scale,
1476    setScale,
1477    zoomInput,
1478    setZoomInput,
1479    handleZoomChange,
1480    handleZoomBlur,
1481    handleZoomKeyDown,
1482    density,
1483    setDensity,
1484    onToggleMelody,
1485    isMelodyMode,
1486    isMMOMode,
1487    setIsMMOMode,
1488    onGameModeMute,
1489    melodyData,
1490    processedInstruments,
1491    isAnalysisStudioOpen,
1492    setIsAnalysisStudioOpen,
1493    isMenuExpanded,
1494    setIsMenuExpanded,
1495    showChords,
1496    showRoman,
1497    showTimeline,
1498    quantization,
1499    setQuantization,
1500    quantizedScorePaths,
1501    hasRecommended,
1502    handleRecommendQuantization,
1503    pageTurnLookahead,
1504    setPageTurnLookahead,
1505    melodySource,
1506    setMelodySource,
1507    handleDownloadPNG,
1508    handleDownloadPDF,
1509    loading,
1510    theme = "light"
1511  } = props;
1512  const [showDensityPopup, setShowDensityPopup] = reactExports.useState(false);
1513  const getToolbarBg = () => {
1514    if (theme === "light") return "bg-zinc-100 border-b border-zinc-200 text-zinc-800";
1515    if (theme === "fun") return "bg-[#fff1f2] border-b border-pink-200 text-pink-900";
1516    return "bg-[#050505] border-b border-white/10 text-white";
1517  };
1518  const getSelectorBg = () => {
1519    if (theme === "light") return "flex items-center gap-1 bg-zinc-200/60 border border-zinc-300/50 rounded-lg p-1";
1520    if (theme === "fun") return "flex items-center gap-1 bg-pink-100 border border-pink-200 rounded-lg p-1";
1521    return "flex items-center gap-1 bg-white/5 border border-white/5 rounded-lg p-1";
1522  };
1523  const getSelectTextClass = () => {
1524    if (theme === "light") return "bg-transparent border-none text-xs font-bold text-zinc-700 py-1 pl-1 pr-6 outline-none focus:ring-0 cursor-pointer appearance-none";
1525    if (theme === "fun") return "bg-transparent border-none text-xs font-bold text-pink-900 py-1 pl-1 pr-6 outline-none focus:ring-0 cursor-pointer appearance-none";
1526    return "bg-transparent border-none text-xs font-bold text-zinc-300 py-1 pl-1 pr-6 outline-none focus:ring-0 cursor-pointer appearance-none";
1527  };
1528  const getSelectArrow = () => {
1529    const colorHex = theme === "light" ? "%233f3f46" : theme === "fun" ? "%239d174d" : "%23a1a1aa";
1530    return `url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='${colorHex}' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e")`;
1531  };
1532  const getBorderSeparator = () => {
1533    if (theme === "light") return "flex items-center gap-2 border-l border-zinc-200 pl-4";
1534    if (theme === "fun") return "flex items-center gap-2 border-l border-pink-200 pl-4";
1535    return "flex items-center gap-2 border-l border-white/10 pl-4";
1536  };
1537  const getAutoButtonClass = () => {
1538    if (isAutoZoom) {
1539      if (theme === "fun") return "text-[9px] font-bold px-2 py-1 rounded transition-colors border bg-pink-200 text-pink-700 border-pink-300";
1540      return "text-[9px] font-bold px-2 py-1 rounded transition-colors border bg-indigo-600/25 text-indigo-400 border-indigo-500/30";
1541    }
1542    if (theme === "light") return "text-[9px] font-bold px-2 py-1 rounded transition-colors border bg-zinc-200/60 text-zinc-600 border-zinc-300 hover:bg-zinc-300 hover:text-zinc-800";
1543    if (theme === "fun") return "text-[9px] font-bold px-2 py-1 rounded transition-colors border bg-pink-50 text-pink-800 border-pink-200 hover:bg-pink-100 hover:text-pink-900";
1544    return "text-[9px] font-bold px-2 py-1 rounded transition-colors border bg-white/5 text-zinc-400 border-white/5 hover:bg-white/10 hover:text-white";
1545  };
1546  const getIconButtonClass = (isActive = false) => {
1547    if (isActive) {
1548      if (theme === "fun") return "p-1.5 bg-pink-100 text-pink-800 rounded-md transition-colors border border-pink-200";
1549      return "p-1.5 bg-indigo-600/20 text-indigo-400 rounded-md transition-colors border border-indigo-500/30";
1550    }
1551    if (theme === "light") return "p-1.5 hover:bg-zinc-200/60 text-zinc-600 hover:text-zinc-900 rounded-md transition-colors";
1552    if (theme === "fun") return "p-1.5 hover:bg-pink-100/60 text-pink-800 hover:text-pink-950 rounded-md transition-colors";
1553    return "p-1.5 hover:bg-white/10 text-zinc-400 hover:text-white transition-colors rounded-md";
1554  };
1555  const getZoomInputClass = () => {
1556    if (theme === "light") return "w-12 text-center text-xs font-bold bg-white text-zinc-700 border border-zinc-300 rounded py-1 focus:ring-1 focus:ring-indigo-500 outline-none";
1557    if (theme === "fun") return "w-12 text-center text-xs font-bold bg-white text-pink-850 border border-pink-200 rounded py-1 focus:ring-1 focus:ring-pink-500 outline-none";
1558    return "w-12 text-center text-xs font-bold bg-white/5 text-zinc-200 border border-white/5 rounded py-1 focus:ring-1 focus:ring-indigo-500 outline-none";
1559  };
1560  const getDensityPopupBg = () => {
1561    if (theme === "light") return "absolute top-full mt-2 left-1/2 -translate-x-1/2 bg-white border border-zinc-200 shadow-2xl rounded-xl p-4 z-50 w-64 flex flex-col gap-3 animate-in fade-in slide-in-from-top-2";
1562    if (theme === "fun") return "absolute top-full mt-2 left-1/2 -translate-x-1/2 bg-[#fff1f2] border border-pink-200 shadow-2xl rounded-xl p-4 z-50 w-64 flex flex-col gap-3 animate-in fade-in slide-in-from-top-2";
1563    return "absolute top-full mt-2 left-1/2 -translate-x-1/2 bg-zinc-900 border border-white/10 shadow-2xl rounded-xl p-4 z-50 w-64 flex flex-col gap-3 animate-in fade-in slide-in-from-top-2 backdrop-blur-xl";
1564  };
1565  const getDensityPopupText = () => {
1566    if (theme === "light") return "text-xs font-bold text-zinc-700";
1567    if (theme === "fun") return "text-xs font-bold text-pink-900";
1568    return "text-xs font-bold text-zinc-200";
1569  };
1570  const getDensityPopupPill = () => {
1571    if (theme === "light") return "text-[10px] font-bold px-2 py-0.5 rounded-full bg-zinc-100 text-zinc-500 border border-zinc-200";
1572    if (theme === "fun") return "text-[10px] font-bold px-2 py-0.5 rounded-full bg-pink-100 text-pink-700 border border-pink-200";
1573    return "text-[10px] font-bold px-2 py-0.5 rounded-full bg-white/5 text-zinc-400 border-white/5";
1574  };
1575  const getRangeTrackClass = () => {
1576    if (theme === "light") return "w-full accent-indigo-600 h-1 bg-zinc-200 rounded-lg appearance-none cursor-pointer";
1577    if (theme === "fun") return "w-full accent-pink-500 h-1 bg-pink-200 rounded-lg appearance-none cursor-pointer";
1578    return "w-full accent-indigo-500 h-1 bg-white/10 rounded-lg appearance-none cursor-pointer";
1579  };
1580  const getToggleGroupBg = () => {
1581    if (theme === "light") return "flex bg-zinc-200/60 rounded-md border border-zinc-300/40 p-0.5";
1582    if (theme === "fun") return "flex bg-pink-100 rounded-md border border-pink-200 p-0.5";
1583    return "flex bg-zinc-950/60 rounded-md border border-white/5 p-0.5";
1584  };
1585  const getToggleItemClass = (isActive, isOrange = false) => {
1586    const base = "flex items-center gap-1.5 px-3 py-1.5 text-xs font-bold rounded-md transition-all";
1587    if (isActive) {
1588      if (theme === "fun") {
1589        return `${base} bg-gradient-to-r from-pink-500 to-indigo-500 text-white shadow-lg shadow-indigo-500/20`;
1590      }
1591      return `${base} ${isOrange ? "bg-orange-600 border-orange-500/30" : "bg-indigo-600 border-indigo-500/30"} text-white shadow-lg ${isOrange ? "shadow-orange-500/20" : "shadow-indigo-500/20"}`;
1592    }
1593    if (theme === "light") return `${base} text-zinc-600 hover:bg-white/60 hover:text-zinc-900`;
1594    if (theme === "fun") return `${base} text-pink-800 hover:bg-pink-200/50 hover:text-pink-950`;
1595    return `${base} text-zinc-400 hover:bg-white/5 hover:text-white`;
1596  };
1597  const getOptionsToggleBtn = () => {
1598    if (isMenuExpanded) {
1599      if (theme === "fun") return "p-1.5 rounded-md border transition-all bg-pink-200 border-pink-300 text-pink-850";
1600      return "p-1.5 rounded-md border transition-all bg-white/10 border-white/20 text-white";
1601    }
1602    if (theme === "light") return "p-1.5 rounded-md border transition-all bg-zinc-200/60 border-zinc-300 text-zinc-600 hover:bg-zinc-300/60 hover:text-zinc-800";
1603    if (theme === "fun") return "p-1.5 rounded-md border transition-all bg-pink-50 border-pink-200 text-pink-800 hover:bg-pink-100 hover:text-pink-900";
1604    return "p-1.5 rounded-md border transition-all bg-white/5 border-white/5 text-zinc-400 hover:bg-white/10 hover:text-white";
1605  };
1606  const getSubmenuBg = () => {
1607    if (theme === "light") return "flex flex-wrap items-center justify-between gap-4 px-4 py-2.5 bg-zinc-50 border-b border-t border-zinc-200/80 shadow-inner z-10 shrink-0 animate-in slide-in-from-top-2 duration-200";
1608    if (theme === "fun") return "flex flex-wrap items-center justify-between gap-4 px-4 py-2.5 bg-[#fffafb] border-b border-t border-pink-200 shadow-inner z-10 shrink-0 animate-in slide-in-from-top-2 duration-200";
1609    return "flex flex-wrap items-center justify-between gap-4 px-4 py-2.5 bg-zinc-950/40 border-b border-t border-white/10 shadow-inner z-10 shrink-0 animate-in slide-in-from-top-2 duration-200";
1610  };
1611  const getSubmenuLabelClass = () => {
1612    if (theme === "light") return "text-xs font-bold text-zinc-500 uppercase tracking-wider";
1613    if (theme === "fun") return "text-xs font-bold text-pink-800 uppercase tracking-wider";
1614    return "text-xs font-bold text-zinc-400 uppercase tracking-wider";
1615  };
1616  const getSubmenuSelectClass = () => {
1617    if (theme === "light") return "bg-white text-zinc-700 border border-zinc-300 rounded text-xs font-bold py-1 px-2 outline-none focus:ring-1 focus:ring-indigo-500 cursor-pointer";
1618    if (theme === "fun") return "bg-white text-pink-850 border border-pink-200 rounded text-xs font-bold py-1 px-2 outline-none focus:ring-1 focus:ring-pink-500 cursor-pointer";
1619    return "bg-zinc-950 text-zinc-200 border border-white/10 rounded text-xs font-bold py-1 px-2 outline-none focus:ring-1 focus:ring-indigo-500 cursor-pointer";
1620  };
1621  const getSubmenuSelectOptionClass = () => {
1622    if (theme === "light") return "bg-white text-zinc-700";
1623    return "bg-zinc-900 text-zinc-200";
1624  };
1625  const getStudyViewBtnClass = () => {
1626    const isActive = isAnalysisStudioOpen || showChords || showRoman || showTimeline;
1627    if (isActive) {
1628      return "flex items-center gap-2 px-3 py-1.5 text-xs font-bold rounded-md transition-all border bg-amber-500/20 text-amber-400 border-amber-500/30 shadow-sm shadow-amber-500/10";
1629    }
1630    if (theme === "light") return "flex items-center gap-2 px-3 py-1.5 text-xs font-bold rounded-md transition-all border bg-zinc-200/60 border-zinc-300 text-zinc-600 hover:bg-zinc-300 hover:text-zinc-800";
1631    if (theme === "fun") return "flex items-center gap-2 px-3 py-1.5 text-xs font-bold rounded-md transition-all border bg-white/5 border-white/10 text-white hover:bg-white/15";
1632    return "flex items-center gap-2 px-3 py-1.5 text-xs font-bold rounded-md transition-all border bg-white/5 text-zinc-400 border-white/5 hover:bg-white/10 hover:text-white";
1633  };
1634  const getPNGBtnClass = () => {
1635    if (theme === "light") return "flex items-center gap-1.5 px-3 py-1.5 bg-white border border-zinc-300 text-zinc-600 hover:bg-zinc-100 disabled:bg-zinc-200 disabled:text-zinc-400 rounded-lg text-xs font-bold transition-all shadow-sm";
1636    if (theme === "fun") return "flex items-center gap-1.5 px-3 py-1.5 bg-white/5 border border-white/10 text-white/90 hover:bg-white/15 disabled:bg-zinc-800 disabled:text-zinc-500 rounded-lg text-xs font-bold transition-all shadow-sm";
1637    return "flex items-center gap-1.5 px-3 py-1.5 bg-white/5 border border-white/5 hover:bg-white/10 disabled:bg-zinc-800 disabled:text-zinc-500 text-zinc-300 hover:text-white rounded-lg text-xs font-bold transition-all shadow-sm";
1638  };
1639  const getPDFBtnClass = () => {
1640    if (theme === "fun") return "flex items-center gap-1.5 px-3 py-1.5 bg-pink-600 hover:bg-pink-700 disabled:bg-pink-800 text-white border border-pink-700 rounded-lg text-xs font-bold transition-all shadow-lg shadow-pink-500/20";
1641    return "flex items-center gap-1.5 px-3 py-1.5 bg-indigo-600 hover:bg-indigo-700 disabled:bg-indigo-800 text-white border border-indigo-700 rounded-lg text-xs font-bold transition-all shadow-lg shadow-indigo-500/20";
1642  };
1643  return /* @__PURE__ */ jsxRuntimeExports.jsxs(React.Fragment, { children: [
1644    /* @__PURE__ */ jsxRuntimeExports.jsxs(
1645      "div",
1646      {
1647        className: `flex items-center justify-between px-4 py-2 ${getToolbarBg()} z-20 shrink-0 relative`,
1648        onClick: (e) => e.stopPropagation(),
1649        children: [
1650          /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-4", children: [
1651            /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: getSelectorBg(), children: [
1652              /* @__PURE__ */ jsxRuntimeExports.jsx(Monitor, { size: 14, className: "text-zinc-400 ml-1" }),
1653              /* @__PURE__ */ jsxRuntimeExports.jsxs(
1654                "select",
1655                {
1656                  value: layoutMode,
1657                  onChange: (e) => {
1658                    const newMode = e.target.value;
1659                    setLayoutMode(newMode);
1660                    setIsAutoZoom(true);
1661                    let targetScale = scale;
1662                    if (newMode === "a4") targetScale = 50;
1663                    else if (newMode === "a3") targetScale = 25;
1664                    setScale(targetScale);
1665                    setZoomInput(targetScale.toString());
1666                  },
1667                  className: getSelectTextClass(),
1668                  style: { backgroundImage: getSelectArrow(), backgroundPosition: "right 0.25rem center", backgroundRepeat: "no-repeat", backgroundSize: "1.5em 1.5em" },
1669                  children: [
1670                    /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "screen-moving", className: getSubmenuSelectOptionClass(), children: "Web (Moving)" }),
1671                    /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "screen-paged", className: getSubmenuSelectOptionClass(), children: "Web (Paged)" }),
1672                    /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "a4", className: getSubmenuSelectOptionClass(), children: "A4 Print" }),
1673                    /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "a3", className: getSubmenuSelectOptionClass(), children: "A3 Print" })
1674                  ]
1675                }
1676              )
1677            ] }),
1678            /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: getBorderSeparator(), children: [
1679              /* @__PURE__ */ jsxRuntimeExports.jsx(
1680                "button",
1681                {
1682                  onClick: () => setIsAutoZoom(!isAutoZoom),
1683                  className: getAutoButtonClass(),
1684                  title: isAutoZoom ? "Auto Zoom: ON" : "Auto Zoom: OFF",
1685                  children: "AUTO"
1686                }
1687              ),
1688              /* @__PURE__ */ jsxRuntimeExports.jsx(
1689                "button",
1690                {
1691                  onClick: () => {
1692                    setIsAutoZoom(false);
1693                    setScale((prev) => {
1694                      const next = Math.max(10, prev - 5);
1695                      setZoomInput(next.toString());
1696                      return next;
1697                    });
1698                  },
1699                  className: getIconButtonClass(false),
1700                  children: /* @__PURE__ */ jsxRuntimeExports.jsx(ZoomOut, { size: 16 })
1701                }
1702              ),
1703              /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1", children: [
1704                /* @__PURE__ */ jsxRuntimeExports.jsx(
1705                  "input",
1706                  {
1707                    type: "text",
1708                    value: zoomInput,
1709                    onChange: handleZoomChange,
1710                    onBlur: handleZoomBlur,
1711                    onKeyDown: handleZoomKeyDown,
1712                    className: getZoomInputClass()
1713                  }
1714                ),
1715                /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] font-bold text-zinc-500", children: "%" })
1716              ] }),
1717              /* @__PURE__ */ jsxRuntimeExports.jsx(
1718                "button",
1719                {
1720                  onClick: () => {
1721                    setIsAutoZoom(false);
1722                    setScale((prev) => {
1723                      const next = Math.min(500, prev + 5);
1724                      setZoomInput(next.toString());
1725                      return next;
1726                    });
1727                  },
1728                  className: getIconButtonClass(false),
1729                  children: /* @__PURE__ */ jsxRuntimeExports.jsx(ZoomIn, { size: 16 })
1730                }
1731              ),
1732              /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative", children: [
1733                /* @__PURE__ */ jsxRuntimeExports.jsx(
1734                  "button",
1735                  {
1736                    onClick: () => setShowDensityPopup(!showDensityPopup),
1737                    className: getIconButtonClass(showDensityPopup),
1738                    title: "Adjust Display Density",
1739                    children: /* @__PURE__ */ jsxRuntimeExports.jsx(SlidersHorizontal, { size: 16 })
1740                  }
1741                ),
1742                showDensityPopup && /* @__PURE__ */ jsxRuntimeExports.jsxs(React.Fragment, { children: [
1743                  /* @__PURE__ */ jsxRuntimeExports.jsx(
1744                    "div",
1745                    {
1746                      className: "fixed inset-0 z-40",
1747                      onClick: () => setShowDensityPopup(false)
1748                    }
1749                  ),
1750                  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: getDensityPopupBg(), children: [
1751                    /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between items-center", children: [
1752                      /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: getDensityPopupText(), children: "Display Density" }),
1753                      /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: getDensityPopupPill(), children: density < 30 ? "Compact" : density > 70 ? "Expanded" : "Normal" })
1754                    ] }),
1755                    /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-1", children: [
1756                      /* @__PURE__ */ jsxRuntimeExports.jsx(
1757                        "input",
1758                        {
1759                          type: "range",
1760                          min: "0",
1761                          max: "100",
1762                          value: density,
1763                          onChange: (e) => setDensity(parseInt(e.target.value)),
1764                          className: getRangeTrackClass()
1765                        }
1766                      ),
1767                      /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between text-[10px] text-zinc-500 font-medium px-1 mt-1", children: [
1768                        /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Compact" }),
1769                        /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Normal" }),
1770                        /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Expanded" })
1771                      ] })
1772                    ] })
1773                  ] })
1774                ] })
1775              ] })
1776            ] })
1777          ] }),
1778          /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [
1779            onToggleMelody && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: getToggleGroupBg(), children: [
1780              /* @__PURE__ */ jsxRuntimeExports.jsxs(
1781                "button",
1782                {
1783                  onClick: isMelodyMode ? onToggleMelody : void 0,
1784                  className: getToggleItemClass(!isMelodyMode),
1785                  children: [
1786                    /* @__PURE__ */ jsxRuntimeExports.jsx(BookOpen, { size: 14 }),
1787                    "Full Score"
1788                  ]
1789                }
1790              ),
1791              /* @__PURE__ */ jsxRuntimeExports.jsxs(
1792                "button",
1793                {
1794                  onClick: !isMelodyMode ? onToggleMelody : void 0,
1795                  className: getToggleItemClass(isMelodyMode),
1796                  children: [
1797                    /* @__PURE__ */ jsxRuntimeExports.jsx(Music, { size: 14 }),
1798                    "Melody"
1799                  ]
1800                }
1801              )
1802            ] }),
1803            isMelodyMode && onGameModeMute && /* @__PURE__ */ jsxRuntimeExports.jsxs(
1804              "button",
1805              {
1806                onClick: () => {
1807                  setIsMMOMode(!isMMOMode);
1808                },
1809                className: getToggleItemClass(isMMOMode, true),
1810                title: isMMOMode ? "Disable MMO Play-Along" : "Enable MMO Play-Along",
1811                children: [
1812                  /* @__PURE__ */ jsxRuntimeExports.jsx(Keyboard, { size: 14 }),
1813                  "Play-Along"
1814                ]
1815              }
1816            ),
1817            /* @__PURE__ */ jsxRuntimeExports.jsx(
1818              "button",
1819              {
1820                onClick: () => setIsMenuExpanded(!isMenuExpanded),
1821                className: getOptionsToggleBtn(),
1822                title: "More Options",
1823                children: isMenuExpanded ? /* @__PURE__ */ jsxRuntimeExports.jsx(X, { size: 18 }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Ellipsis, { size: 18 })
1824              }
1825            )
1826          ] })
1827        ]
1828      }
1829    ),
1830    isMenuExpanded && /* @__PURE__ */ jsxRuntimeExports.jsxs(
1831      "div",
1832      {
1833        className: getSubmenuBg(),
1834        onClick: (e) => e.stopPropagation(),
1835        children: [
1836          /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-wrap items-center gap-4", children: [
1837            !isMelodyMode && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [
1838              /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: getSubmenuLabelClass(), children: "Quantize:" }),
1839              /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1", children: [
1840                /* @__PURE__ */ jsxRuntimeExports.jsxs(
1841                  "select",
1842                  {
1843                    value: quantization,
1844                    onChange: (e) => setQuantization(parseInt(e.target.value)),
1845                    className: getSubmenuSelectClass(),
1846                    children: [
1847                      /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: 0, className: getSubmenuSelectOptionClass(), children: "Off (Raw)" }),
1848                      (quantizedScorePaths == null ? void 0 : quantizedScorePaths.q8) && /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: 8, className: getSubmenuSelectOptionClass(), children: "Simple (8th)" }),
1849                      (quantizedScorePaths == null ? void 0 : quantizedScorePaths.q16) && /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: 16, className: getSubmenuSelectOptionClass(), children: "Standard (16th)" }),
1850                      (quantizedScorePaths == null ? void 0 : quantizedScorePaths.engraved) && /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: 300, className: getSubmenuSelectOptionClass(), children: "Community Standard" }),
1851                      /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: 500, className: getSubmenuSelectOptionClass(), children: "Studio Engraved" }),
1852                      (quantizedScorePaths == null ? void 0 : quantizedScorePaths.ly) && /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: 600, className: getSubmenuSelectOptionClass(), children: "LilyPond Score" }),
1853                      (quantizedScorePaths == null ? void 0 : quantizedScorePaths.abc) && /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: 700, className: getSubmenuSelectOptionClass(), children: "The Session (ABC)" }),
1854                      (quantizedScorePaths == null ? void 0 : quantizedScorePaths.krn) && /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: 800, className: getSubmenuSelectOptionClass(), children: "KernScores (Humdrum)" })
1855                    ]
1856                  }
1857                ),
1858                quantization > 0 && /* @__PURE__ */ jsxRuntimeExports.jsx(
1859                  "button",
1860                  {
1861                    onClick: handleRecommendQuantization,
1862                    className: `p-1.5 rounded-md border transition-colors ${hasRecommended ? "bg-emerald-500/20 border-emerald-500/30 text-emerald-400" : theme === "light" ? "bg-zinc-200/60 border-zinc-300 text-zinc-500 hover:text-indigo-600" : "bg-white/5 border-white/5 text-zinc-400 hover:text-indigo-400"}`,
1863                    title: hasRecommended ? "Recommended!" : "Recommend this quantization",
1864                    children: /* @__PURE__ */ jsxRuntimeExports.jsx(ThumbsUp, { size: 14, className: hasRecommended ? "fill-emerald-400" : "" })
1865                  }
1866                )
1867              ] })
1868            ] }),
1869            layoutMode === "screen-paged" && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: getBorderSeparator(), children: [
1870              /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: `${getSubmenuLabelClass()} flex items-center gap-1`, children: [
1871                /* @__PURE__ */ jsxRuntimeExports.jsx(Clock, { size: 12 }),
1872                " Lookahead:"
1873              ] }),
1874              /* @__PURE__ */ jsxRuntimeExports.jsxs(
1875                "select",
1876                {
1877                  value: pageTurnLookahead,
1878                  onChange: (e) => {
1879                    const val = parseFloat(e.target.value);
1880                    setPageTurnLookahead(val);
1881                    localStorage.setItem("sheet_page_turn_lookahead", val.toString());
1882                  },
1883                  className: getSubmenuSelectClass(),
1884                  children: [
1885                    /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: -1, className: getSubmenuSelectOptionClass(), children: "Half Measure" }),
1886                    /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: 0, className: getSubmenuSelectOptionClass(), children: "Exact Page Turn" }),
1887                    /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: 0.5, className: getSubmenuSelectOptionClass(), children: "0.5s Lookahead" }),
1888                    /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: 1, className: getSubmenuSelectOptionClass(), children: "1.0s Lookahead" }),
1889                    /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: 1.5, className: getSubmenuSelectOptionClass(), children: "1.5s Lookahead" }),
1890                    /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: 2, className: getSubmenuSelectOptionClass(), children: "2.0s Lookahead" }),
1891                    /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: 3, className: getSubmenuSelectOptionClass(), children: "3.0s Lookahead" })
1892                  ]
1893                }
1894              )
1895            ] }),
1896            isMelodyMode && setMelodySource && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: getBorderSeparator(), children: [
1897              /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: getSubmenuLabelClass(), children: "Melody Source:" }),
1898              /* @__PURE__ */ jsxRuntimeExports.jsxs(
1899                "select",
1900                {
1901                  value: melodySource || "standard",
1902                  onChange: (e) => setMelodySource(e.target.value),
1903                  className: getSubmenuSelectClass(),
1904                  children: [
1905                    /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "standard", className: getSubmenuSelectOptionClass(), children: "Standard (Score Heuristics)" }),
1906                    ((quantizedScorePaths == null ? void 0 : quantizedScorePaths.studio_melody) || (quantizedScorePaths == null ? void 0 : quantizedScorePaths.musescore_melody)) && /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "studio", className: getSubmenuSelectOptionClass(), children: "Studio Engraved (Extraction)" })
1907                  ]
1908                }
1909              )
1910            ] })
1911          ] }),
1912          /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-wrap items-center gap-4", children: [
1913            /* @__PURE__ */ jsxRuntimeExports.jsxs(
1914              "button",
1915              {
1916                onClick: () => setIsAnalysisStudioOpen(!isAnalysisStudioOpen),
1917                className: getStudyViewBtnClass(),
1918                title: "Study View",
1919                children: [
1920                  /* @__PURE__ */ jsxRuntimeExports.jsx(Library, { size: 14, className: isAnalysisStudioOpen || showChords || showRoman ? "text-amber-500" : "" }),
1921                  "Study View"
1922                ]
1923              }
1924            ),
1925            /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: getBorderSeparator(), children: [
1926              /* @__PURE__ */ jsxRuntimeExports.jsxs(
1927                "button",
1928                {
1929                  onClick: handleDownloadPNG,
1930                  disabled: loading,
1931                  className: getPNGBtnClass(),
1932                  children: [
1933                    loading ? /* @__PURE__ */ jsxRuntimeExports.jsx(LoaderCircle, { size: 14, className: "animate-spin" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Download, { size: 14 }),
1934                    "PNG"
1935                  ]
1936                }
1937              ),
1938              /* @__PURE__ */ jsxRuntimeExports.jsxs(
1939                "button",
1940                {
1941                  onClick: handleDownloadPDF,
1942                  disabled: loading,
1943                  className: getPDFBtnClass(),
1944                  children: [
1945                    loading ? /* @__PURE__ */ jsxRuntimeExports.jsx(LoaderCircle, { size: 14, className: "animate-spin" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Download, { size: 14 }),
1946                    "PDF"
1947                  ]
1948                }
1949              )
1950            ] })
1951          ] })
1952        ]
1953      }
1954    )
1955  ] });
1956};
1957const SheetMusicCanvas = (props) => {
1958  const {
1959    containerRef,
1960    paperRef,
1961    cursorRef,
1962    svgContent,
1963    loading,
1964    loadingProgress,
1965    isDataLoaded,
1966    pageCount,
1967    currentPage,
1968    setCurrentPage,
1969    renderPage,
1970    layoutMode,
1971    dimensions,
1972    paperZoom,
1973    setPaperZoom,
1974    paperZoomInput,
1975    setPaperZoomInput,
1976    visibleTrackCount,
1977    showCursor,
1978    setIsDraggingBar,
1979    onSeek,
1980    pageInput,
1981    setPageInput,
1982    seekToPage,
1983    lastManualPageChangeRef,
1984    lastManualPageTimeRef,
1985    currentTime,
1986    handleScoreClick,
1987    handleScroll,
1988    title,
1989    midiId
1990  } = props;
1991  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 min-h-0 relative w-full h-full flex flex-col overflow-hidden", children: [
1992    /* @__PURE__ */ jsxRuntimeExports.jsxs(
1993      "div",
1994      {
1995        ref: containerRef,
1996        onClick: handleScoreClick,
1997        onScroll: handleScroll,
1998        className: `flex-1 min-h-0 w-full ${layoutMode === "screen-paged" ? "overflow-hidden" : "overflow-auto"} flex flex-col ${!layoutMode.startsWith("screen") ? "items-center bg-[#e5e7eb] p-4" : "bg-[#fdfaf5]"}`,
1999        children: [
2000          loading && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute inset-0 z-50 flex items-center justify-center bg-white/70 dark:bg-zinc-950/70 backdrop-blur-md transition-all duration-300", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col items-center gap-4 max-w-xs w-full px-6 py-5 bg-white dark:bg-zinc-900 rounded-2xl border border-zinc-200 dark:border-zinc-800 shadow-2xl", children: [
2001            /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative flex items-center justify-center w-14 h-14", children: [
2002              /* @__PURE__ */ jsxRuntimeExports.jsx(LoaderCircle, { size: 48, className: "text-indigo-600 dark:text-indigo-400 animate-spin" }),
2003              loadingProgress && /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "absolute text-[11px] font-black text-indigo-700 dark:text-indigo-300", children: [
2004                Math.round(loadingProgress.percent),
2005                "%"
2006              ] })
2007            ] }),
2008            /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "w-full self-stretch flex flex-col items-center gap-2 text-center", children: [
2009              /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs font-bold text-zinc-700 dark:text-zinc-200 tracking-wide", children: (loadingProgress == null ? void 0 : loadingProgress.stage) || (!isDataLoaded ? "Loading Score..." : !layoutMode.startsWith("screen") && pageCount > 0 ? `Page ${currentPage} of ${pageCount}` : "Engraving Score...") }),
2010              /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-full self-stretch h-2 bg-zinc-100 dark:bg-zinc-800 rounded-full overflow-hidden border border-zinc-200/50 dark:border-zinc-700/50", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
2011                "div",
2012                {
2013                  className: "h-full bg-gradient-to-r from-indigo-500 via-indigo-600 to-indigo-400 rounded-full transition-all duration-300 ease-out shadow-sm",
2014                  style: { width: `${Math.max(5, Math.min(100, (loadingProgress == null ? void 0 : loadingProgress.percent) ?? 25))}%` }
2015                }
2016              ) })
2017            ] })
2018          ] }) }),
2019          /* @__PURE__ */ jsxRuntimeExports.jsxs(
2020            "div",
2021            {
2022              ref: paperRef,
2023              className: `relative transition-transform duration-200 origin-top ${!layoutMode.startsWith("screen") ? "bg-white shadow-[0_20px_50px_rgba(0,0,0,0.15),0_10px_20px_rgba(0,0,0,0.1)] border border-zinc-200 rounded-sm" : layoutMode === "screen-paged" ? "w-full min-h-full bg-[#fdfaf5]" : "bg-[#fdfaf5] inline-block text-left align-top"}`,
2024              style: {
2025                ...layoutMode === "a4" ? {
2026                  width: "210mm",
2027                  minHeight: "297mm",
2028                  transform: `scale(${paperZoom / 100})`,
2029                  transformOrigin: "top center",
2030                  display: "flex",
2031                  flexDirection: "column",
2032                  overflow: "visible",
2033                  marginBottom: `${(paperZoom / 100 - 1) * 297}mm`
2034                } : layoutMode === "a3" ? {
2035                  width: "420mm",
2036                  minHeight: "297mm",
2037                  transform: `scale(${paperZoom / 100})`,
2038                  transformOrigin: "top center",
2039                  display: "flex",
2040                  flexDirection: "column",
2041                  overflow: "visible",
2042                  marginBottom: `${(paperZoom / 100 - 1) * 297}mm`
2043                } : layoutMode === "screen-paged" ? {
2044                  width: "100%",
2045                  height: "100%",
2046                  overflow: "hidden"
2047                } : {
2048                  width: "max-content",
2049                  height: "auto"
2050                }
2051              },
2052              children: [
2053                (layoutMode === "a4" || layoutMode === "a3") && /* @__P
2053URE__ */ jsxRuntimeExports.jsxs("div", { className: "w-full flex flex-col items-center pt-3 pb-1 px-6 text-center select-none", children: [
2054                  currentPage === 1 && /* @__PURE__ */ jsxRuntimeExports.jsx("h1", { className: "text-2xl font-serif text-zinc-900 tracking-tight leading-tight", children: title || "HamieNET.com Score" }),
2055                  currentPage > 1 && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "w-full flex justify-between border-b border-zinc-200 pb-1 text-[8pt] font-serif italic text-zinc-400 uppercase tracking-widest", children: [
2056                    /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: title }),
2057                    /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [
2058                      "Page ",
2059                      currentPage
2060                    ] })
2061                  ] })
2062                ] }),
2063                /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "relative flex-1 w-full flex items-start justify-center overflow-hidden", children: svgContent ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-full h-full flex flex-col justify-center [&>svg]:w-full [&>svg]:h-full [&>svg]:max-w-none", dangerouslySetInnerHTML: { __html: svgContent } }
2063) : !loading ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col items-center justify-center p-12 text-center my-auto min-h-[260px] select-none", children: [
2064                  /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-14 h-14 rounded-2xl bg-amber-500/10 dark:bg-amber-500/20 text-amber-600 dark:text-amber-400 flex items-center justify-center mb-3", children: /* @__PURE__ */ jsxRuntimeExports.jsx(BookOpen, { size: 28 }) }),
2065                  /* @__PURE__ */ jsxRuntimeExports.jsx("h3", { className: "text-sm font-bold text-zinc-800 dark:text-zinc-200", children: "Sheet Music Unavailable" }),
2066                  /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-zinc-500 dark:text-zinc-400 max-w-xs mt-1 leading-relaxed", children: "The notation score for this track could not be loaded or is currently queued for engraving." })
2067                ] }) : null }),
2068                (layoutMode === "a4" || layoutMode === "a3") && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-full flex items-center justify-center pt-1 pb-2 px-6 text-center select-none mt-auto", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-3", children: [
2069                  /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-[8pt] font-serif text-zinc-400 italic", children: [
2070                    "Page ",
2071                    currentPage,
2072                    " of ",
2073                    pageCount
2074                  ] }),
2075                  /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-[7pt] font-sans text-zinc-400 font-bold uppercase tracking-[0.1em]", children: [
2076                    "HamieNET.com/midi",
2077                    midiId
2078                  ] })
2079                ] }) }),
2080                /* @__PURE__ */ jsxRuntimeExports.jsx(
2081                  "div",
2082                  {
2083                    ref: cursorRef,
2084                    className: "score-playback-cursor",
2085                    style: {
2086                      display: showCursor ? "block" : "none",
2087                      position: "absolute",
2088                      width: "4px",
2089                      backgroundColor: "rgba(99, 102, 241, 0.8)",
2090                      boxShadow: "0 0 8px rgba(99, 102, 241, 0.6)",
2091                      zIndex: 50,
2092                      borderRadius: "2px",
2093                      cursor: "col-resize",
2094                      pointerEvents: "auto"
2095                    },
2096                    onMouseDown: (e) => {
2097                      e.stopPropagation();
2098                      setIsDraggingBar(true);
2099                    }
2100                  }
2101                )
2102              ]
2103            }
2104          )
2105        ]
2106      }
2107    ),
2108    (layoutMode === "a4" || layoutMode === "a3") && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "absolute bottom-6 right-6 flex flex-col gap-2 bg-white/90 backdrop-blur shadow-lg rounded-lg p-2 border border-zinc-200 z-30 animate-in fade-in slide-in-from-bottom-4", children: [
2109      /* @__PURE__ */ jsxRuntimeExports.jsx(
2110        "button",
2111        {
2112          onClick: (e) => {
2113            e.stopPropagation();
2114            const val = Math.min(400, paperZoom + 10);
2115            setPaperZoom(val);
2116            setPaperZoomInput(val.toString());
2117          },
2118          className: "p-2 hover:bg-zinc-100 rounded-md text-zinc-600 transition-colors",
2119          children: /* @__PURE__ */ jsxRuntimeExports.jsx(ZoomIn, { size: 20 })
2120        }
2121      ),
2122      /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-center gap-0.5 py-1 border-y border-zinc-100", children: [
2123        /* @__PURE__ */ jsxRuntimeExports.jsx(
2124          "input",
2125          {
2126            type: "text",
2127            value: paperZoomInput,
2128            onChange: (e) => setPaperZoomInput(e.target.value),
2129            onBlur: () => {
2130              const val = parseInt(paperZoomInput);
2131              if (!isNaN(val)) {
2132                const clamped = Math.max(10, Math.min(500, val));
2133                setPaperZoom(clamped);
2134                setPaperZoomInput(clamped.toString());
2135              } else {
2136                setPaperZoomInput(paperZoom.toString());
2137              }
2138            },
2139            className: "w-10 text-center text-[10px] font-bold bg-transparent border-none outline-none text-zinc-600"
2140          }
2141        ),
2142        /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[8px] font-bold text-zinc-400", children: "%" })
2143      ] }),
2144      /* @__PURE__ */ jsxRuntimeExports.jsx(
2145        "button",
2146        {
2147          onClick: (e) => {
2148            e.stopPropagation();
2149            const val = Math.max(10, paperZoom - 10);
2150            setPaperZoom(val);
2151            setPaperZoomInput(val.toString());
2152          },
2153          className: "p-2 hover:bg-zinc-100 rounded-md text-zinc-600 transition-colors",
2154          children: /* @__PURE__ */ jsxRuntimeExports.jsx(ZoomOut, { size: 20 })
2155        }
2156      ),
2157      /* @__PURE__ */ jsxRuntimeExports.jsx(
2158        "button",
2159        {
2160          onClick: (e) => {
2161            e.stopPropagation();
2162            setPaperZoom(100);
2163            setPaperZoomInput("100");
2164          },
2165          className: "text-[10px] font-bold text-zinc-400 hover:text-indigo-600 mt-1",
2166          children: "RESET"
2167        }
2168      )
2169    ] }),
2170    pageCount > 1 && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "absolute bottom-6 left-1/2 -translate-x-1/2 flex items-center gap-2 px-3 py-1 rounded-full bg-black/60 backdrop-blur-md border border-white/10 shadow-lg z-50 animate-in slide-in-from-bottom-4 fade-in duration-300 opacity-40 hover:opacity-100 transition-all", children: [
2171      /* @__PURE__ */ jsxRuntimeExports.jsx(
2172        "button",
2173        {
2174          onClick: (e) => {
2175            e.stopPropagation();
2176            const newPage = Math.max(1, currentPage - 1);
2177            lastManualPageChangeRef.current = Date.now();
2178            lastManualPageTimeRef.current = currentTime;
2179            setCurrentPage(newPage);
2180            renderPage(newPage);
2181            seekToPage(newPage);
2182          },
2183          disabled: currentPage === 1,
2184          className: "p-1 hover:bg-white/20 disabled:opacity-30 rounded-full transition-colors text-white/80",
2185          children: /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronLeft, { size: 14 })
2186        }
2187      ),
2188      /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1", children: [
2189        /* @__PURE__ */ jsxRuntimeExports.jsx(
2190          "input",
2191          {
2192            type: "text",
2193            value: pageInput,
2194            onChange: (e) => setPageInput(e.target.value),
2195            onBlur: () => {
2196              const val = parseInt(pageInput);
2197              if (!isNaN(val) && val >= 1 && val <= pageCount && val !== currentPage) {
2198                lastManualPageChangeRef.current = Date.now();
2199                lastManualPageTimeRef.current = currentTime;
2200                setCurrentPage(val);
2201                renderPage(val);
2202                seekToPage(val);
2203              } else {
2204                setPageInput(currentPage.toString());
2205              }
2206            },
2207            className: "w-8 h-4 text-center text-[10px] font-bold text-white/90 bg-transparent hover:bg-white/10 focus:bg-white/20 border-none outline-none transition-colors px-0 rounded-[2px]"
2208          }
2209        ),
2210        /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-[10px] font-bold text-white/90 drop-shadow-sm uppercase tracking-wider", children: [
2211          "/ ",
2212          pageCount
2213        ] })
2214      ] }),
2215      /* @__PURE__ */ jsxRuntimeExports.jsx(
2216        "button",
2217        {
2218          onClick: (e) => {
2219            e.stopPropagation();
2220            const newPage = Math.min(pageCount, currentPage + 1);
2221            lastManualPageChangeRef.current = Date.now();
2222            lastManualPageTimeRef.current = currentTime;
2223            setCurrentPage(newPage);
2224            renderPage(newPage);
2225            seekToPage(newPage);
2226          },
2227          disabled: currentPage === pageCount,
2228          className: "p-1 hover:bg-white/20 disabled:opacity-30 rounded-full transition-colors text-white/80",
2229          children: /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronRight, { size: 14 })
2230        }
2231      )
2232    ] })
2233  ] });
2234};
2235const AnalysisStudioMenu = ({
2236  isOpen,
2237  onClose,
2238  showChords,
2239  setShowChords,
2240  showRoman,
2241  setShowRoman,
2242  showTimeline,
2243  setShowTimeline,
2244  showPills,
2245  setShowPills,
2246  showMeasureNumbers,
2247  setShowMeasureNumbers,
2248  showHighlight,
2249  setShowHighlight,
2250  showCursor,
2251  setShowCursor,
2252  hasSections,
2253  hasChordsData = true,
2254  hasRomanData = true
2255}) => {
2256  const menuRef = React.useRef(null);
2257  React.useEffect(() => {
2258    if (!isOpen) return;
2259    const handleClickOutside = (event) => {
2260      var _a;
2261      if (menuRef.current && !menuRef.current.contains(event.target)) {
2262        const target = event.target;
2263        if (target.closest('[title="Study View"]') || ((_a = target.textContent) == null ? void 0 : _a.includes("Study View"))) {
2264          return;
2265        }
2266        onClose();
2267      }
2268    };
2269    const handleKeyDown = (event) => {
2270      if (event.key === "Escape") {
2271        onClose();
2272      }
2273    };
2274    document.addEventListener("mousedown", handleClickOutside);
2275    document.addEventListener("keydown", handleKeyDown);
2276    return () => {
2277      document.removeEventListener("mousedown", handleClickOutside);
2278      document.removeEventListener("keydown", handleKeyDown);
2279    };
2280  }, [isOpen, onClose]);
2281  if (!isOpen) return null;
2282  return /* @__PURE__ */ jsxRuntimeExports.jsxs(
2283    "div",
2284    {
2285      ref: menuRef,
2286      className: "absolute top-14 right-4 z-[100] w-72 bg-zinc-900/90 backdrop-blur-md border border-white/10 rounded-xl shadow-2xl overflow-hidden animate-in fade-in zoom-in duration-200 flex flex-col max-h-[calc(100%-80px)]",
2287      children: [
2288        /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-4 border-b border-white/5 flex items-center justify-between bg-zinc-950/20 shrink-0", children: [
2289          /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [
2290            /* @__PURE__ */ jsxRuntimeExports.jsx(Library, { size: 18, className: "text-amber-500" }),
2291            /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-bold text-zinc-200 tracking-tight", children: "Study View" })
2292          ] }),
2293          /* @__PURE__ */ jsxRuntimeExports.jsx(
2294            "button",
2295            {
2296              onClick: onClose,
2297              className: "p-1 hover:bg-white/10 rounded-lg transition-colors text-zinc-400 hover:text-white",
2298              children: /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronRight, { size: 18 })
2299            }
2300          )
2301        ] }),
2302        /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-2 space-y-4 overflow-y-auto flex-1 custom-scrollbar", children: [
2303          /* @__PURE__ */ jsxRuntimeExports.jsxs("section", { className: "space-y-2", children: [
2304            /* @__PURE__ */ jsxRuntimeExports.jsxs("h3", { className: "px-3 text-[10px] font-bold text-zinc-400 uppercase tracking-widest flex items-center gap-2", children: [
2305              /* @__PURE__ */ jsxRuntimeExports.jsx(Layers, { size: 10 }),
2306              " Harmonic Layers"
2307            ] }),
2308            /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-1", children: [
2309              /* @__PURE__ */ jsxRuntimeExports.jsx(
2310                ToggleItem,
2311                {
2312                  icon: /* @__PURE__ */ jsxRuntimeExports.jsx(Music, { size: 14 }),
2313                  title: "Chord Symbols",
2314                  description: hasChordsData ? "Real-time harmonic labels" : "No analysis data",
2315                  isActive: showChords,
2316                  disabled: !hasChordsData,
2317                  onClick: () => setShowChords(!showChords)
2318                }
2319              ),
2320              /* @__PURE__ */ jsxRuntimeExports.jsx(
2321                ToggleItem,
2322                {
2323                  icon: /* @__PURE__ */ jsxRuntimeExports.jsx(BookOpen, { size: 14 }),
2324                  title: "Roman Numerals",
2325                  description: hasRomanData ? "Musicological analysis" : "No analysis data",
2326                  isActive: showRoman,
2327                  disabled: !hasRomanData,
2328                  onClick: () => setShowRoman(!showRoman)
2329                }
2330              )
2331            ] })
2332          ] }),
2333          /* @__PURE__ */ jsxRuntimeExports.jsxs("section", { className: "space-y-2", children: [
2334            /* @__PURE__ */ jsxRuntimeExports.jsxs("h3", { className: "px-3 text-[10px] font-bold text-zinc-400 uppercase tracking-widest flex items-center gap-2", children: [
2335              /* @__PURE__ */ jsxRuntimeExports.jsx(Map$1, { size: 10 }),
2336              " Structural Analysis"
2337            ] }),
2338            /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-1", children: [
2339              /* @__PURE__ */ jsxRuntimeExports.jsx(
2340                ToggleItem,
2341                {
2342                  icon: /* @__PURE__ */ jsxRuntimeExports.jsx(Map$1, { size: 14 }),
2343                  title: "Interactive Timeline",
2344                  description: "Navigable structural segments",
2345                  isActive: showTimeline,
2346                  disabled: !hasSections,
2347                  onClick: () => setShowTimeline(!showTimeline)
2348                }
2349              ),
2350              /* @__PURE__ */ jsxRuntimeExports.jsx(
2351                ToggleItem,
2352                {
2353                  icon: /* @__PURE__ */ jsxRuntimeExports.jsx(Pill, { size: 14 }),
2354                  title: "Section Labels",
2355                  description: "Floating contextual pills",
2356                  isActive: showPills,
2357                  disabled: !hasSections,
2358                  onClick: () => setShowPills(!showPills)
2359                }
2360              )
2361            ] }),
2362            !hasSections && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "px-3 flex items-start gap-2 text-[10px] text-zinc-400 bg-amber-500/10 p-2 rounded-lg border border-amber-500/20", children: [
2363              /* @__PURE__ */ jsxRuntimeExports.jsx(Info, { size: 12, className: "shrink-0 mt-0.5" }),
2364              /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "No musicological markers detected for this track." })
2365            ] })
2366          ] }),
2367          /* @__PURE__ */ jsxRuntimeExports.jsxs("section", { className: "space-y-2 border-t border-white/5 pt-3", children: [
2368            /* @__PURE__ */ jsxRuntimeExports.jsxs("h3", { className: "px-3 text-[10px] font-bold text-zinc-400 uppercase tracking-widest flex items-center gap-2", children: [
2369              /* @__PURE__ */ jsxRuntimeExports.jsx(Hash, { size: 10 }),
2370              " Notation Guides"
2371            ] }),
2372            /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-1", children: [
2373              /* @__PURE__ */ jsxRuntimeExports.jsx(
2374                ToggleItem,
2375                {
2376                  icon: /* @__PURE__ */ jsxRuntimeExports.jsx(Hash, { size: 14 }),
2377                  title: "Measure Numbers",
2378                  isActive: showMeasureNumbers,
2379                  onClick: () => setShowMeasureNumbers(!showMeasureNumbers)
2380                }
2381              ),
2382              /* @__PURE__ */ jsxRuntimeExports.jsx(
2383                ToggleItem,
2384                {
2385                  icon: /* @__PURE__ */ jsxRuntimeExports.jsx(Palette, { size: 14 }),
2386                  title: "Color Highlighting",
2387                  isActive: showHighlight,
2388                  onClick: () => setShowHighlight(!showHighlight)
2389                }
2390              ),
2391              /* @__PURE__ */ jsxRuntimeExports.jsx(
2392                ToggleItem,
2393                {
2394                  icon: /* @__PURE__ */ jsxRuntimeExports.jsx(MousePointer2, { size: 14 }),
2395                  title: "Playback Cursor",
2396                  isActive: showCursor,
2397                  onClick: () => setShowCursor(!showCursor)
2398                }
2399              )
2400            ] })
2401          ] })
2402        ] }),
2403        /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "p-3 bg-zinc-950/40 text-[9px] text-zinc-500 text-center italic shrink-0 border-t border-white/5", children: "HamieNET Research-Grade Educational Tool" })
2404      ]
2405    }
2406  );
2407};
2408const ToggleItem = ({ icon, title, description, isActive, disabled, onClick }) => /* @__PURE__ */ jsxRuntimeExports.jsxs(
2409  "button",
2410  {
2411    onClick,
2412    disabled,
2413    className: `w-full flex items-center justify-between p-2.5 rounded-lg transition-all ${disabled ? "opacity-40 cursor-not-allowed" : "hover:bg-white/5"}`,
2414    children: [
2415      /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-3", children: [
2416        /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `p-1.5 rounded-md ${isActive ? "bg-amber-500/20 text-amber-400" : "bg-white/5 text-zinc-500"}`, children: icon }),
2417        /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-left", children: [
2418          /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-xs font-semibold text-zinc-200", children: title }),
2419          description && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-[10px] text-zinc-500", children: description })
2420        ] })
2421      ] }),
2422      /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `w-10 h-5 rounded-full relative transition-colors ${isActive ? "bg-amber-500" : "bg-white/10"}`, children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `absolute top-1 w-3 h-3 bg-white rounded-full transition-all ${isActive ? "left-6" : "left-1 shadow-sm"}` }) })
2423    ]
2424  }
2425);
2426const sectionColors = {
2427  "exposition": "bg-indigo-600",
2428  "development": "bg-rose-600",
2429  "recapitulation": "bg-emerald-600",
2430  "coda": "bg-amber-600",
2431  "intro": "bg-slate-500",
2432  "verse": "bg-blue-600",
2433  "chorus": "bg-purple-600",
2434  "bridge": "bg-orange-600",
2435  "solo": "bg-red-600",
2436  "phrase": "bg-zinc-600",
2437  "part": "bg-zinc-600"
2438};
2439const StructuralTimeline = ({
2440  sections,
2441  duration,
2442  currentTime,
2443  onSeek,
2444  onSeekSection
2445}) => {
2446  if (!sections || sections.length === 0) return null;
2447  const maxSectionTime = Math.max(...sections.map((s) => s.time));
2448  const effectiveDuration = duration > 0 ? duration : maxSectionTime + 10;
2449  return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-full h-9 bg-zinc-900 border-b border-white/10 flex items-stretch overflow-hidden group shadow-lg relative", children: sections.map((section, index) => {
2450    const nextSection = sections[index + 1];
2451    const sectionDuration = (nextSection ? nextSection.time : effectiveDuration) - section.time;
2452    const widthPercent = sectionDuration / effectiveDuration * 100;
2453    const safeName = (section == null ? void 0 : section.label) || (section == null ? void 0 : section.name) || "";
2454    const nameKey = safeName.toLowerCase();
2455    const colorKey = Object.keys(sectionColors).find((k) => nameKey.includes(k));
2456    const colorClass = colorKey ? sectionColors[colorKey] : "bg-zinc-700";
2457    const isCurrent = currentTime >= section.time && (nextSection ? currentTime < nextSection.time : true);
2458    return /* @__PURE__ */ jsxRuntimeExports.jsxs(
2459      "button",
2460      {
2461        onClick: () => {
2462          if (onSeekSection) {
2463            onSeekSection(section);
2464          } else if (typeof section.time === "number" && isFinite(section.time)) {
2465            console.log(`[Timeline] Seeking to measure ${section.measure} at time ${section.time}`);
2466            onSeek(section.time);
2467          } else {
2468            console.warn(`[Timeline] Cannot seek: Invalid time for measure ${section.measure}`, section);
2469          }
2470        },
2471        className: `relative h-full transition-all group/seg border-r border-black/20 last:border-0 overflow-hidden flex flex-col justify-center ${isCurrent ? "flex-[1.5] opacity-100 ring-1 ring-inset ring-white/30 z-10" : "flex-1 opacity-80 hover:opacity-100"}`,
2472        style: { width: `${widthPercent}%` },
2473        title: `${safeName} (Measure ${section.measure})`,
2474        children: [
2475          /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `absolute inset-0 ${colorClass}` }),
2476          isCurrent && /* @__PURE__ */ jsxRuntimeExports.jsx(
2477            "div",
2478            {
2479              className: "absolute inset-y-0 left-0 bg-white/20",
2480              style: {
2481                width: `${Math.min(100, Math.max(0, (currentTime - section.time) / sectionDuration * 100))}%`
2482              }
2483            }
2484          ),
2485          /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative z-10 px-2 h-full flex flex-col justify-center text-white drop-shadow-sm", children: [
2486            /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] font-black tracking-wider uppercase truncate leading-tight", children: safeName }),
2487            /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-[9px] font-bold opacity-90 leading-none", children: [
2488              "m.",
2489              section.measure
2490            ] })
2491          ] }),
2492          isCurrent && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute top-0 inset-x-0 h-0.5 bg-white/70 shadow-[0_0_8px_white]" })
2493        ]
2494      },
2495      index
2496    );
2497  }) });
2498};
2499const SectionPill = ({ currentSection }) => {
2500  const [visible, setVisible] = reactExports.useState(false);
2501  const [displayText, setDisplayText] = reactExports.useState("");
2502  reactExports.useEffect(() => {
2503    if (currentSection) {
2504      setDisplayText(currentSection);
2505      setVisible(true);
2506      const timer = setTimeout(() => setVisible(false), 3e3);
2507      return () => clearTimeout(timer);
2508    } else {
2509      setVisible(false);
2510    }
2511  }, [currentSection]);
2512  if (!displayText) return null;
2513  return /* @__PURE__ */ jsxRuntimeExports.jsx(
2514    "div",
2515    {
2516      className: `absolute top-16 left-1/2 -translate-x-1/2 z-[60] pointer-events-none transition-all duration-700 ${visible ? "translate-y-0 opacity-100" : "-translate-y-8 opacity-0"}`,
2517      children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-zinc-900/90 backdrop-blur-xl text-white px-8 py-3 rounded-2xl border border-white/10 shadow-[0_20px_50px_rgba(0,0,0,0.5)] flex items-center gap-4", children: [
2518        /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative", children: [
2519          /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-3 h-3 rounded-full bg-amber-500 shadow-[0_0_15px_rgba(245,158,11,0.8)]" }),
2520          /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute inset-0 w-3 h-3 rounded-full bg-amber-500 animate-ping opacity-75" })
2521        ] }),
2522        /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col", children: [
2523          /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] text-zinc-500 font-bold tracking-[0.2em] uppercase leading-none mb-1", children: "Current Section" }),
2524          /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-sm font-black tracking-widest uppercase leading-none", children: displayText })
2525        ] })
2526      ] })
2527    }
2528  );
2529};
2530const SheetMusicViewer = (props) => {
2531  const {
2532    xmlData,
2533    scorePath,
2534    quantizedScorePaths,
2535    melodyData,
2536    trackVisibility,
2537    isPlaying,
2538    currentTime,
2539    duration,
2540    processedInstruments,
2541    recommendedQuantization,
2542    onSeek,
2543    title,
2544    selectedTrackIndex,
2545    midiId,
2546    isMelodyMode,
2547    onToggleMelody,
2548    melodySource,
2549    setMelodySource,
2550    onGameModeMute,
2551    onPlayPause,
2552    audioLatency = 0,
2553    uploaderName,
2554    xmlString,
2555    dimensions: initialDimensions,
2556    sections = [],
2557    measureMap = [],
2558    soundfont,
2559    harmonyChordsUrl,
2560    harmonyRomanUrl,
2561    theme = "light"
2562  } = props;
2563  const [harmonyAvailable, setHarmonyAvailable] = reactExports.useState({ chords: true, roman: true });
2564  const hasHarmonyChords = harmonyAvailable.chords;
2565  const hasHarmonyRoman = harmonyAvailable.roman;
2566  const [layoutMode, setLayoutMode] = reactExports.useState("screen-moving");
2567  const [scale, setScale] = reactExports.useState(45);
2568  const [zoomInput, setZoomInput] = reactExports.useState("45");
2569  const [isAutoZoom, setIsAutoZoom] = reactExports.useState(true);
2570  const [paperZoom, setPaperZoom] = reactExports.useState(100);
2571  const [paperZoomInput, setPaperZoomInput] = reactExports.useState("100");
2572  const [quantization, setQuantization] = reactExports.useState(() => {
2573    var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
2574    if (((_a = props.quantizedScorePaths) == null ? void 0 : _a.krn) || ((_b = props.scorePath) == null ? void 0 : _b.includes("_krn"))) return 800;
2575    if (((_c = props.quantizedScorePaths) == null ? void 0 : _c.studio) || ((_d = props.quantizedScorePaths) == null ? void 0 : _d.musescore) || ((_e = props.scorePath) == null ? void 0 : _e.includes("_studio")) || ((_f = props.scorePath) == null ? void 0 : _f.includes("_musescore"))) return 500;
2576    if (((_g = props.quantizedScorePaths) == null ? void 0 : _g.abc) || ((_h = props.scorePath) == null ? void 0 : _h.includes("_abc"))) return 700;
2577    if (((_i = props.quantizedScorePaths) == null ? void 0 : _i.ly) || ((_j = props.scorePath) == null ? void 0 : _j.includes("_ly"))) return 600;
2578    return 500;
2579  });
2580  const [pageInput, setPageInput] = reactExports.useState("1");
2581  const [showCursor, setShowCursor] = reactExports.useState(() => localStorage.getItem("sheet_show_cursor") !== "false");
2582  const [showHighlight, setShowHighlight] = reactExports.useState(() => localStorage.getItem("sheet_show_highlight") !== "false");
2583  const [showMeasureNumbers, setShowMeasureNumbers] = reactExports.useState(() => localStorage.getItem("sheet_show_measure_numbers") !== "false");
2584  const [showChords, setShowChords] = reactExports.useState(() => localStorage.getItem("sheet_show_chords") !== "false");
2585  const [showRoman, setShowRoman] = reactExports.useState(() => localStorage.getItem("sheet_show_roman") === "true");
2586  const [showTimeline, setShowTimeline] = reactExports.useState(false);
2587  const [showPills, setShowPills] = reactExports.useState(true);
2588  const [isMenuExpanded, setIsMenuExpanded] = reactExports.useState(false);
2589  const [isAnalysisStudioOpen, setIsAnalysisStudioOpen] = reactExports.useState(false);
2590  const [isMMOMode, setIsMMOMode] = reactExports.useState(false);
2591  const [isDraggingBar, setIsDraggingBar] = reactExports.useState(false);
2592  const [dimensions, setDimensions] = reactExports.useState(initialDimensions || { width: 0, height: 0 });
2593  const [pageTurnLookahead, setPageTurnLookahead] = reactExports.useState(() => {
2594    const saved = localStorage.getItem("sheet_page_turn_lookahead");
2595    return saved ? parseFloat(saved) : -1;
2596  });
2597  const [density, setDensity] = reactExports.useState(50);
2598  const [hasRecommended, setHasRecommended] = reactExports.useState(false);
2599  const [practiceMode, setPracticeMode] = reactExports.useState(false);
2600  const lastSatisfiedTimeRef = reactExports.useRef(-1);
2601  const waitingNotesRef = reactExports.useRef(null);
2602  reactExports.useEffect(() => {
2603    lastSatisfiedTimeRef.current = -1;
2604    waitingNotesRef.current = null;
2605  }, [practiceMode, midiId]);
2606  reactExports.useEffect(() => {
2607    if (Math.abs(currentTime - currentTimeRef.current) > 0.5) {
2608      lastSatisfiedTimeRef.current = -1;
2609      waitingNotesRef.current = null;
2610    }
2611  }, [currentTime]);
2612  const [keyboardState, setKeyboardState] = reactExports.useState("off");
2613  const keyboardCanvasRef = reactExports.useRef(null);
2614  const keyboardContainerRef = reactExports.useRef(null);
2615  reactExports.useRef(/* @__PURE__ */ new Set());
2616  const baseOctaveRef = reactExports.useRef(4);
2617  const activeKeysRef = reactExports.useRef(/* @__PURE__ */ new Map());
2618  const animationFrameRef = reactExports.useRef();
2619  const currentTimeRef = reactExports.useRef(currentTime);
2620  const isPlayingRef = reactExports.useRef(isPlaying);
2621  const melodyDataRef = reactExports.useRef(melodyData);
2622  const onGameModeMuteRef = reactExports.useRef(onGameModeMute);
2623  const processedInstrumentsRef = reactExports.useRef(processedInstruments);
2624  const selectedTrackIndexRef = reactExports.useRef(selectedTrackIndex);
2625  reactExports.useEffect(() => {
2626    currentTimeRef.current = currentTime;
2627  }, [currentTime]);
2628  reactExports.useEffect(() => {
2629    isPlayingRef.current = isPlaying;
2630  }, [isPlaying]);
2631  reactExports.useEffect(() => {
2632    melodyDataRef.current = melodyData;
2633  }, [melodyData]);
2634  reactExports.useEffect(() => {
2635    onGameModeMuteRef.current = onGameModeMute;
2636  }, [onGameModeMute]);
2637  reactExports.useEffect(() => {
2638    processedInstrumentsRef.current = processedInstruments;
2639  }, [processedInstruments]);
2640  reactExports.useEffect(() => {
2641    selectedTrackIndexRef.current = selectedTrackIndex;
2642  }, [selectedTrackIndex]);
2643  const { playNote, stopNote, stopAllNotes, previewActiveNotesRef } = useSoundfontKeyboard(selectedTrackIndex, processedInstruments, void 0, soundfont);
2644  const containerRef = reactExports.useRef(null);
2645  const paperRef = reactExports.useRef(null);
2646  const cursorRef = reactExports.useRef(null);
2647  const resizeTimeout = reactExports.useRef(null);
2648  const visibleTrackCount = reactExports.useMemo(() => {
2649    if (!processedInstruments) return 1;
2650    let count = 0;
2651    processedInstruments.forEach((instr, idx) => {
2652      const name = instr.displayName || instr.name || "";
2653      if (name !== "Informational Track" && trackVisibility[idx] !== false) count++;
2654    });
2655    return Math.max(1, count);
2656  }, [processedInstruments, trackVisibility]);
2657  const {
2658    verovioToolkit,
2659    loading,
2660    loadingProgress,
2661    setLoading,
2662    isDataLoaded,
2663    pageCount,
2664    currentPage,
2665    setCurrentPage,
2666    svgContent,
2667    syncPoints,
2668    renderPage
2669  } = useSheetMusicEngine({
2670    ...props,
2671    dimensions,
2672    layoutMode,
2673    scale,
2674    quantization,
2675    density,
2676    showMeasureNumbers,
2677    visibleTrackCount,
2678    showChords: showChords && hasHarmonyChords,
2679    showRoman: showRoman && hasHarmonyRoman,
2680    harmonyChordsUrl,
2681    harmonyRomanUrl,
2682    measureMap
2683  });
2684  const {
2685    verovioToAudioTime,
2686    lastManualPageChangeRef,
2687    lastManualPageTimeRef,
2688    handleScroll
2689  } = useSheetMusicPlayback({
2690    verovioToolkit,
2691    isPlaying,
2692    currentTime,
2693    duration,
2694    audioLatency,
2695    onSeek,
2696    layoutMode,
2697    scale,
2698    pageTurnLookahead,
2699    dimensions,
2700    currentPage,
2701    setCurrentPage,
2702    pageCount,
2703    renderPage,
2704    syncPoints,
2705    containerRef,
2706    paperRef,
2707    cursorRef,
2708    showCursor,
2709    showHighlight,
2710    isDataLoaded
2711  });
2712  useKeyboardInput({
2713    playNote,
2714    stopNote,
2715    baseOctaveRef,
2716    activeKeysRef,
2717    enabled: keyboardState !== "off"
2718  });
2719  reactExports.useEffect(() => {
2720    if (keyboardState === "off") return;
2721    const renderLoop = () => {
2722      const canvas = keyboardCanvasRef.current;
2723      const container = keyboardContainerRef.current;
2724      if (canvas && container) {
2725        const rect = container.getBoundingClientRect();
2726        if (canvas.width !== rect.width || canvas.height !== rect.height) {
2727          canvas.width = rect.width;
2728          canvas.height = rect.height;
2729        }
2730        const ctx = canvas.getContext("2d");
2731        if (ctx) {
2732          ctx.clearRect(0, 0, canvas.width, canvas.height);
2733          ctx.fillStyle = "#111";
2734          ctx.fillRect(0, 0, canvas.width, canvas.height);
2735          const activeSongNotes = /* @__PURE__ */ new Set();
2736          const currentMelData = melodyDataRef.current;
2737          const t = currentTimeRef.current;
2738          if (currentMelData && isPlayingRef.current) {
2739            currentMelData.forEach((note) => {
2740              if (t >= note.t && t <= note.t + note.d) {
2741                activeSongNotes.add(note.n);
2742              }
2743            });
2744          }
2745          if (practiceMode && currentMelData) {
2746            if (isPlayingRef.current) {
2747              const upcomingNotes = [];
2748              for (let i = 0; i < currentMelData.length; i++) {
2749                const note = currentMelData[i];
2750                if (note.t <= t + 0.05 && note.t > lastSatisfiedTimeRef.current + 0.02) {
2751                  upcomingNotes.push(note);
2752                }
2753                if (note.t > t + 0.2) break;
2754              }
2755              if (upcomingNotes.length > 0) {
2756                const chordTime = upcomingNotes[0].t;
2757                const chordNotes = upcomingNotes.filter((n) => Math.abs(n.t - chordTime) < 0.05);
2758                const targetNoteNumbers = new Set(chordNotes.map((n) => n.n));
2759                if (targetNoteNumbers.size > 0) {
2760                  let allPressed = true;
2761                  for (const midiNum of targetNoteNumbers) {
2762                    if (!previewActiveNotesRef.current.has(midiNum)) {
2763                      allPressed = false;
2764                      break;
2765                    }
2766                  }
2767                  if (allPressed) {
2768                    lastSatisfiedTimeRef.current = chordTime;
2769                    waitingNotesRef.current = null;
2770                  } else {
2771                    waitingNotesRef.current = targetNoteNumbers;
2772                    if (onPlayPause) onPlayPause(false);
2773                    if (onSeek) onSeek(chordTime);
2774                  }
2775                }
2776              }
2777            } else if (waitingNotesRef.current) {
2778              let allPressed = true;
2779              for (const midiNum of waitingNotesRef.current) {
2780                if (!previewActiveNotesRef.current.has(midiNum)) {
2781                  allPressed = false;
2782                  break;
2783                }
2784              }
2785              if (allPressed) {
2786                const satisfiedTime = Array.from(waitingNotesRef.current)[0];
2787                const matchedNote = currentMelData.find((n) => n.n === satisfiedTime && Math.abs(n.t - t) < 0.25);
2788                const satisfiedTimeSec = matchedNote ? matchedNote.t : t;
2789                lastSatisfiedTimeRef.current = satisfiedTimeSec;
2790                waitingNotesRef.current = null;
2791                if (onPlayPause) onPlayPause(true);
2792              }
2793            }
2794          } else {
2795            waitingNotesRef.current = null;
2796          }
2797          renderSharedKeyboard(ctx, canvas.width, canvas.height, canvas.height, /* @__PURE__ */ new Set([...activeSongNotes, ...previewActiveNotesRef.current]), keyboardState === "on_pc" ? "pc" : keyboardState === "on_notes" ? "notes" : "off", baseOctaveRef.current, waitingNotesRef.current || void 0);
2798        }
2799      }
2800      animationFrameRef.current = requestAnimationFrame(renderLoop);
2801    };
2802    renderLoop();
2803    return () => {
2804      if (animationFrameRef.current) cancelAnimationFrame(animationFrameRef.current);
2805    };
2806  }, [keyboardState, previewActiveNotesRef, practiceMode, onPlayPause, onSeek]);
2807  reactExports.useEffect(() => {
2808    if (isMMOMode && keyboardState === "off") {
2809      setKeyboardState("on_pc");
2810    } else if (!isMMOMode && keyboardState !== "off") {
2811      setKeyboardState("off");
2812    }
2813  }, [isMMOMode]);
2814  reactExports.useEffect(() => {
2815    const onGameModeMuteFn = onGameModeMuteRef.current;
2816    if (!onGameModeMuteFn) return;
2817    if (!isMMOMode) {
2818      console.log("[SheetMusicViewer] Play-Along Ended, Unmuting All");
2819      onGameModeMuteFn(null);
2820      return;
2821    }
2822    let lastMutedStr = "";
2823    const checkForMuteUpdates = () => {
2824      const time = currentTimeRef.current;
2825      const currentMelData = melodyDataRef.current;
2826      const currentInstruments = processedInstrumentsRef.current;
2827      selectedTrackIndexRef.current;
2828      const onMuteFn = onGameModeMuteRef.current;
2829      if (!onMuteFn) return;
2830      const activeTracks = /* @__PURE__ */ new Set();
2831      const nextTracks = /* @__PURE__ */ new Set();
2832      let nextTime = Infinity;
2833      let bestIdx = -1;
2834      if (currentMelData && currentMelData.length > 0 && currentInstruments) {
2835        for (const note of currentMelData) {
2836          let idx = -1;
2837          if (note.trackIdx !== void 0 && note.trackIdx !== null) {
2838            const num = Number(note.trackIdx);
2839            if (!isNaN(num) && num >= 0 && num < currentInstruments.length) {
2840              idx = num;
2841            }
2842          }
2843          if (idx === -1) {
2844            const name = note.i;
2845            idx = currentInstruments.findIndex(
2846              (inst) => inst.originalName === name || inst.displayName === name || inst.name === name
2847            );
2848          }
2849          if (idx === -1) continue;
2850          if (note.t <= time && note.t + note.d > time) {
2851            activeTracks.add(idx);
2852            if (bestIdx === -1) bestIdx = idx;
2853          }
2854          if (note.t > time) {
2855            if (nextTime === Infinity) nextTime = note.t;
2856            if (note.t === nextTime) {
2857              nextTracks.add(idx);
2858            }
2859          }
2860          if (nextTime !== Infinity && note.t > nextTime) break;
2861        }
2862      }
2863      const finalMuteTracks = activeTracks.size > 0 ? activeTracks : nextTracks;
2864      if (bestIdx === -1 && nextTracks.size > 0) {
2865        bestIdx = Array.from(nextTracks)[0];
2866      }
2867      const newMuted = Array.from(finalMuteTracks);
2868      newMuted.sort();
2869      const newMutedStr = newMuted.join(",");
2870      if (newMutedStr !== lastMutedStr) {
2871        lastMutedStr = newMutedStr;
2872        const trackNames = newMuted.map((idx) => {
2873          const track = currentInstruments && currentInstruments[idx];
2874          return track ? track.displayName || track.originalName || track.name : "Unknown";
2875        }).join(", ");
2876        let noteDetail = activeTracks.size > 0 ? " (Active Notes)" : " (Rests / Sticky Next Notes)";
2877        console.log(`%c[SheetMusicViewer] Play-Along Mute Handoff: Tracks [${newMutedStr}] ("${trackNames}")${noteDetail} at currentTime=${time.toFixed(2)}s`, "color: #ff9800; font-weight: bold; font-size: 11px;");
2878        onMuteFn(newMuted.length > 0 ? newMuted : [0]);
2879      }
2880    };
2881    const interval = setInterval(checkForMuteUpdates, 100);
2882    checkForMuteUpdates();
2883    return () => {
2884      clearInterval(interval);
2885      console.log("[SheetMusicViewer] Cleanup: Unmuting All");
2886      const onMuteFn = onGameModeMuteRef.current;
2887      if (onMuteFn) {
2888        onMuteFn(null);
2889      }
2890    };
2891  }, [isMMOMode]);
2892  reactExports.useEffect(() => {
2893    localStorage.setItem("sheet_show_chords", showChords ? "true" : "false");
2894  }, [showChords]);
2895  reactExports.useEffect(() => {
2896    localStorage.setItem("sheet_show_roman", showRoman ? "true" : "false");
2897  }, [showRoman]);
2898  reactExports.useEffect(() => {
2899    localStorage.setItem("sheet_show_cursor", showCursor ? "true" : "false");
2900  }, [showCursor]);
2901  reactExports.useEffect(() => {
2902    localStorage.setItem("sheet_show_highlight", showHighlight ? "true" : "false");
2903  }, [showHighlight]);
2904  reactExports.useEffect(() => {
2905    localStorage.setItem("sheet_show_measure_numbers", showMeasureNumbers ? "true" : "false");
2906  }, [showMeasureNumbers]);
2907  reactExports.useEffect(() => {
2908    setPageInput(currentPage.toString());
2909  }, [currentPage]);
2910  reactExports.useEffect(() => {
2911    if (!isMenuExpanded) {
2912      setIsAnalysisStudioOpen(false);
2913    }
2914  }, [isMenuExpanded]);
2915  reactExports.useEffect(() => {
2916    if (isMelodyMode) return;
2917    const hasKrn = !!(quantizedScorePaths == null ? void 0 : quantizedScorePaths.krn) || (scorePath == null ? void 0 : scorePath.includes("_krn"));
2918    const hasStudio = !!(quantizedScorePaths == null ? void 0 : quantizedScorePaths.studio) || !!(quantizedScorePaths == null ? void 0 : quantizedScorePaths.musescore) || (scorePath == null ? void 0 : scorePath.includes("_studio")) || (scorePath == null ? void 0 : scorePath.includes("_musescore"));
2919    const hasAbc = !!(quantizedScorePaths == null ? void 0 : quantizedScorePaths.abc) || (scorePath == null ? void 0 : scorePath.includes("_abc"));
2920    const hasLy = !!(quantizedScorePaths == null ? void 0 : quantizedScorePaths.ly) || (scorePath == null ? void 0 : scorePath.includes("_ly"));
2921    if (hasKrn) {
2922      setQuantization(800);
2923    } else if (hasStudio) {
2924      setQuantization(500);
2925    } else if (hasAbc) {
2926      setQuantization(700);
2927    } else if (hasLy) {
2928      setQuantization(600);
2929    } else if (recommendedQuantization) {
2930      let q = recommendedQuantization;
2931      if (q === 4) q = 8;
2932      if (q === 32) q = 16;
2933      if (q === 8 && (quantizedScorePaths == null ? void 0 : quantizedScorePaths.q8) || q === 16 && (quantizedScorePaths == null ? void 0 : quantizedScorePaths.q16)) {
2934        setQuantization(q);
2935      } else if (quantizedScorePaths == null ? void 0 : quantizedScorePaths.engraved) {
2936        setQuantization(300);
2937      } else {
2938        setQuantization(500);
2939      }
2940    } else if (quantizedScorePaths == null ? void 0 : quantizedScorePaths.engraved) {
2941      setQuantization(300);
2942    } else {
2943      setQuantization(500);
2944    }
2945  }, [midiId, recommendedQuantization, scorePath, quantizedScorePaths, isMelodyMode]);
2946  reactExports.useEffect(() => {
2947    if ((layoutMode === "a4" || layoutMode === "a3") && dimensions.width > 100 && dimensions.height > 100) {
2948      const isA3 = layoutMode === "a3";
2949      const paperW = isA3 ? 420 : 210;
2950      const paperH = 297;
2951      const availW = dimensions.width - 32;
2952      const availH = dimensions.height - 32;
2953      const paperW_px = paperW * 3.78;
2954      const paperH_px = paperH * 3.78;
2955      const zoomW = availW / paperW_px * 100;
2956      const zoomH = availH / paperH_px * 100;
2957      const bestZoom = Math.floor(Math.min(zoomW, zoomH));
2958      setPaperZoom(bestZoom);
2959      setPaperZoomInput(bestZoom.toString());
2960      const paperScale = 40;
2961      setScale(paperScale);
2962      setZoomInput(paperScale.toString());
2963    }
2964  }, [layoutMode, dimensions.width, dimensions.height]);
2965  const handleScoreClick = (e) => {
2966    const target = e.target;
2967    const noteElement = target.closest(".note");
2968    if (noteElement && verovioToolkit) {
2969      const id = noteElement.getAttribute("id");
2970      if (id) {
2971        try {
2972          const midiValsStr = verovioToolkit.getMIDIValuesForElement(id);
2973          const midiVals = typeof midiValsStr === "string" ? JSON.parse(midiValsStr) : midiValsStr;
2974          if (midiVals && typeof midiVals.pitch === "number") {
2975            playNote(midiVals.pitch);
2976            setTimeout(() => stopNote(midiVals.pitch), 500);
2977          }
2978        } catch (err) {
2979          console.error("Failed to play note", err);
2980        }
2981      }
2982    }
2983  };
2984  reactExports.useEffect(() => {
2985    if (!isDraggingBar) return;
2986    let lastRaf = null;
2987    let lastSeekTime = 0;
2988    const handleMouseMove = (e) => {
2989      if (lastRaf) cancelAnimationFrame(lastRaf);
2990      lastRaf = requestAnimationFrame(() => {
2991        if (!paperRef.current || !verovioToolkit || !onSeek) return;
2992        const now = Date.now();
2993        if (now - lastSeekTime < 50) return;
2994        const notes = paperRef.current.querySelectorAll(".note, .measure");
2995        let bestEl = null;
2996        let minDist = Infinity;
2997        for (let i = 0; i < notes.length; i++) {
2998          const n = notes[i];
2999          const rect = n.getBoundingClientRect();
3000          const distX = Math.max(rect.left - e.clientX, 0, e.clientX - rect.right);
3001          const distY = Math.max(rect.top - e.clientY, 0, e.clientY - rect.bottom);
3002          const dist = distX * distX + distY * distY;
3003          if (dist < minDist) {
3004            minDist = dist;
3005            bestEl = n;
3006          }
3007        }
3008        if (bestEl) {
3009          const id = bestEl.getAttribute("id");
3010          if (id) {
3011            const timeMs = verovioToolkit.getTimeForElement(id);
3012            if (timeMs >= 0) {
3013              const aTime = verovioToAudioTime(timeMs / 1e3);
3014              if (!isNaN(aTime) && aTime >= 0) {
3015                onSeek(aTime);
3016                lastSeekTime = now;
3017              }
3018            }
3019          }
3020        }
3021      });
3022    };
3023    const handleMouseUp = () => {
3024      setIsDraggingBar(false);
3025    };
3026    window.addEventListener("mousemove", handleMouseMove);
3027    window.addEventListener("mouseup", handleMouseUp);
3028    return () => {
3029      if (lastRaf) cancelAnimationFrame(lastRaf);
3030      window.removeEventListener("mousemove", handleMouseMove);
3031      window.removeEventListener("mouseup", handleMouseUp);
3032    };
3033  }, [isDraggingBar, verovioToolkit, onSeek, verovioToAudioTime]);
3034  reactExports.useEffect(() => {
3035    if (!containerRef.current) return;
3036    const observer = new ResizeObserver((entries) => {
3037      for (const entry of entries) {
3038        const w = entry.contentRect.width;
3039        const h = entry.contentRect.height;
3040        if (w > 100 && h > 100) {
3041          if (resizeTimeout.current) clearTimeout(resizeTimeout.current);
3042          resizeTimeout.current = setTimeout(() => {
3043            setDimensions((prev) => {
3044              if (Math.abs(prev.width - w) < 4 && Math.abs(prev.height - h) < 4) {
3045                return prev;
3046              }
3047              return { width: w, height: h };
3048            });
3049          }, 200);
3050        }
3051      }
3052    });
3053    observer.observe(containerRef.current);
3054    return () => {
3055      observer.disconnect();
3056      if (resizeTimeout.current) clearTimeout(resizeTimeout.current);
3057    };
3058  }, []);
3059  reactExports.useEffect(() => {
3060    if (!isAutoZoom || dimensions.height < 100 || !isDataLoaded) return;
3061    if (layoutMode === "a4" || layoutMode === "a3") return;
3062    let idealScale;
3063    const trackHeightUnits = 250;
3064    const totalHeightUnits = visibleTrackCount * trackHeightUnits;
3065    const availableHeight = dimensions.height - 40;
3066    idealScale = Math.floor(availableHeight / totalHeightUnits * 125);
3067    const maxAutoZoomScale = isMelodyMode ? 30 : 100;
3068    idealScale = Math.max(30, Math.min(maxAutoZoomScale, idealScale));
3069    if (idealScale !== scale) {
3070      setScale(idealScale);
3071      setZoomInput(idealScale.toString());
3072    }
3073  }, [isAutoZoom, dimensions.height, visibleTrackCount, isDataLoaded, isMelodyMode, layoutMode]);
3074  const seekToPage = (page) => {
3075    if (!verovioToolkit || !onSeek) return;
3076    const svg = verovioToolkit.renderToSVG(page, {});
3077    const parser = new DOMParser();
3078    const doc = parser.parseFromString(svg, "image/svg+xml");
3079    const measure = doc.querySelector(".measure");
3080    if (measure) {
3081      const id = measure.getAttribute("id");
3082      const vTime = verovioToolkit.getTimeForElement(id) / 1e3;
3083      const aTime = verovioToAudioTime(vTime);
3084      onSeek(aTime);
3085    }
3086  };
3087  const handleZoomChange = (e) => {
3088    setZoomInput(e.target.value);
3089    const val = parseInt(e.target.value);
3090    if (!isNaN(val)) {
3091      setIsAutoZoom(false);
3092      setScale(Math.max(10, Math.min(500, val)));
3093    }
3094  };
3095  const handleZoomBlur = () => setZoomInput(scale.toString());
3096  const handleZoomKeyDown = (e) => {
3097    if (e.key === "Enter") e.target.blur();
3098  };
3099  const handleRecommendQuantization = async () => {
3100    if (!midiId || quantization === 0) return;
3101    try {
3102      await fetch(`/api/tracks/${midiId}/quantization-recommendation`, {
3103        method: "POST",
3104        headers: { "Content-Type": "application/json" },
3105        body: JSON.stringify({ value: quantization, type: "recommend" })
3106      });
3107      setHasRecommended(true);
3108      setTimeout(() => setHasRecommended(false), 2e3);
3109    } catch (e) {
3110      console.error("Failed to recommend:", e);
3111    }
3112  };
3113  const handleDownloadPNG = async () => {
3114    if (!verovioToolkit) return;
3115    try {
3116      setLoading(true);
3117      const svg = verovioToolkit.renderToSVG(currentPage, {});
3118      const svgBlob = new Blob([svg], { type: "image/svg+xml;charset=utf-8" });
3119      const url = URL.createObjectURL(svgBlob);
3120      const img = new Image();
3121      img.onload = () => {
3122        const canvas = document.createElement("canvas");
3123        canvas.width = img.width * 2;
3124        canvas.height = img.height * 2;
3125        const ctx = canvas.getContext("2d");
3126        if (ctx) {
3127          ctx.fillStyle = "white";
3128          ctx.fillRect(0, 0, canvas.width, canvas.height);
3129          ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
3130          const pngUrl = canvas.toDataURL("image/png");
3131          const downloadLink = document.createElement("a");
3132          downloadLink.href = pngUrl;
3133          downloadLink.download = `${title || "score"}_page_${currentPage}.png`;
3134          document.body.appendChild(downloadLink);
3135          downloadLink.click();
3136          document.body.removeChild(downloadLink);
3137        }
3138        URL.revokeObjectURL(url);
3139        setLoading(false);
3140      };
3141      img.src = url;
3142    } catch (e) {
3143      console.error(e);
3144      setLoading(false);
3145    }
3146  };
3147  const handleDownloadPDF = async () => {
3148    if (!verovioToolkit) return;
3149    try {
3150      setLoading(true);
3151      const { default: jsPDF } = await __vitePreload(async () => {
3152        const { default: jsPDF2 } = await import("./vendor-pdf-TxEta_LX.js");
3153        return { default: jsPDF2 };
3154      }, true ? __vite__mapDeps([0,1,2,3,4,5]) : void 0);
3155      const pdf = new jsPDF({ orientation: layoutMode === "a3" ? "landscape" : "portrait", unit: "mm", format: layoutMode === "a3" ? "a3" : "a4" });
3156      for (let i = 1; i <= pageCount; i++) {
3157        if (i > 1) pdf.addPage();
3158        const svg = verovioToolkit.renderToSVG(i, {});
3159        const canvas = document.createElement("canvas");
3160        const img = new Image();
3161        const svgBlob = new Blob([svg], { type: "image/svg+xml;charset=utf-8" });
3162        const url = URL.createObjectURL(svgBlob);
3163        await new Promise((resolve) => {
3164          img.onload = () => {
3165            canvas.width = img.width * 2;
3166            canvas.height = img.height * 2;
3167            const ctx = canvas.getContext("2d");
3168            if (ctx) {
3169              ctx.fillStyle = "white";
3170              ctx.fillRect(0, 0, canvas.width, canvas.height);
3171              ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
3172              pdf.addImage(canvas.toDataURL("image/jpeg", 0.95), "JPEG", 0, 0, layoutMode === "a3" ? 420 : 210, layoutMode === "a3" ? 297 : 297);
3173            }
3174            URL.revokeObjectURL(url);
3175            resolve(null);
3176          };
3177          img.src = url;
3178        });
3179      }
3180      pdf.save(`${title || "score"}.pdf`);
3181    } catch (e) {
3182      console.error(e);
3183    } finally {
3184      setLoading(false);
3185    }
3186  };
3187  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col h-full w-full bg-[#fdfaf5] text-zinc-900 select-none relative overflow-hidden", children: [
3188    /* @__PURE__ */ jsxRuntimeExports.jsx(
3189      SheetMusicToolbar,
3190      {
3191        ...{
3192          layoutMode,
3193          setLayoutMode,
3194          isAutoZoom,
3195          setIsAutoZoom,
3196          scale,
3197          setScale,
3198          zoomInput,
3199          setZoomInput,
3200          handleZoomChange,
3201          handleZoomBlur,
3202          handleZoomKeyDown,
3203          density,
3204          setDensity,
3205          onToggleMelody,
3206          isMelodyMode,
3207          isMMOMode,
3208          setIsMMOMode,
3209          onGameModeMute,
3210          melodyData,
3211          processedInstruments,
3212          isAnalysisStudioOpen,
3213          setIsAnalysisStudioOpen,
3214          isMenuExpanded,
3215          setIsMenuExpanded,
3216          showChords,
3217          showRoman,
3218          showTimeline,
3219          quantization,
3220          setQuantization,
3221          quantizedScorePaths,
3222          hasRecommended,
3223          handleRecommendQuantization,
3224          pageTurnLookahead,
3225          setPageTurnLookahead,
3226          melodySource,
3227          setMelodySource,
3228          handleDownloadPNG,
3229          handleDownloadPDF,
3230          loading,
3231          theme
3232        }
3233      }
3234    ),
3235    /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 min-h-0 relative flex flex-col", children: [
3236      /* @__PURE__ */ jsxRuntimeExports.jsx(
3237        SheetMusicCanvas,
3238        {
3239          ...{
3240            containerRef,
3241            paperRef,
3242            cursorRef,
3243            svgContent,
3244            loading,
3245            loadingProgress,
3246            isDataLoaded,
3247            pageCount,
3248            currentPage,
3249            setCurrentPage,
3250            renderPage,
3251            layoutMode,
3252            dimensions,
3253            paperZoom,
3254            setPaperZoom,
3255            paperZoomInput,
3256            setPaperZoomInput,
3257            visibleTrackCount,
3258            showCursor,
3259            setIsDraggingBar,
3260            onSeek,
3261            pageInput,
3262            setPageInput,
3263            seekToPage,
3264            lastManualPageChangeRef,
3265            lastManualPageTimeRef,
3266            currentTime,
3267            handleScoreClick,
3268            handleScroll,
3269            title: title || "HamieNET.com Score",
3270            midiId
3271          }
3272        }
3273      ),
3274      showPills && sections && sections.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute top-4 left-4 z-40 flex flex-col gap-2", children: sections.map((s, i) => currentTime >= s.time && (sections[i + 1] ? currentTime < sections[i + 1].time : true) && /* @__P
3274URE__ */ jsxRuntimeExports.jsx(SectionPill, { currentSection: s.name || s.label || null }, i)) }),
3275      /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: `absolute right-4 flex flex-col gap-2 z-50 transition-all ${keyboardState !== "off" ? "bottom-4" : "bottom-16"}`, children: [
3276        keyboardState !== "off" && /* @__PURE__ */ jsxRuntimeExports.jsx(
3277          "button",
3278          {
3279            onClick: (e) => {
3280              e.stopPropagation();
3281              const nextLabel = keyboardState === "on_nolabels" ? "on_pc" : keyboardState === "on_pc" ? "on_notes" : "on_nolabels";
3282              setKeyboardState(nextLabel);
3283            },
3284            className: "p-2 rounded-full backdrop-blur-md border transition-all shadow-lg opacity-60 hover:opacity-100 bg-indigo-600/80 border-indigo-400/50 text-white",
3285            title: `Labels: ${keyboardState === "on_pc" ? "PC Keys" : keyboardState === "on_notes" ? "Note Names" : "Off"}`,
3286            children: /* @__PURE__ */ jsxRuntimeExports.jsx(Monitor, { size: 16 })
3287          }
3288        ),
3289        /* @__PURE__ */ jsxRuntimeExports.jsx(
3290          "button",
3291          {
3292            onClick: (e) => {
3293              e.stopPropagation();
3294              setKeyboardState((prev) => {
3295                const next = prev === "off" ? "on_nolabels" : prev === "on_nolabels" ? "on_pc" : prev === "on_pc" ? "on_notes" : "off";
3296                if (next === "off" && isMMOMode) {
3297                  setIsMMOMode(false);
3298                  if (onGameModeMute) onGameModeMute(null);
3299                }
3300                return next;
3301              });
3302            },
3303            className: `p-2 rounded-full backdrop-blur-md border transition-all shadow-lg opacity-40 hover:opacity-100 ${keyboardState !== "off" ? "bg-indigo-600/80 border-indigo-400/50 text-white" : "bg-black/60 border-white/10 text-white/50 hover:text-white/80"}`,
3304            title: "Toggle Keyboard",
3305            children: /* @__PURE__ */ jsxRuntimeExports.jsx(Keyboard, { size: 16 })
3306          }
3307        ),
3308        /* @__PURE__ */ jsxRuntimeExports.jsx(
3309          "button",
3310          {
3311            onClick: (e) => {
3312              e.stopPropagation();
3313              setPracticeMode((prev) => {
3314                const next = !prev;
3315                if (next) {
3316                  if (keyboardState === "off") setKeyboardState("on_pc");
3317                  setIsMMOMode(true);
3318                } else {
3319                  setIsMMOMode(false);
3320                  if (onGameModeMute) onGameModeMute(null);
3321                }
3322                return next;
3323              });
3324            },
3325            className: `p-2 rounded-full backdrop-blur-md border transition-all shadow-lg opacity-60 hover:opacity-100 ${practiceMode ? "bg-amber-600/80 border-amber-400/50 text-white shadow-lg" : "bg-black/60 border-white/10 text-white/50 hover:text-white/80"}`,
3326            title: practiceMode ? "Disable Practice Mode" : "Enable Practice Mode",
3327            children: /* @__PURE__ */ jsxRuntimeExports.jsx(GraduationCap, { size: 16 })
3328          }
3329        )
3330      ] })
3331    ] }),
3332    keyboardState !== "off" && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { ref: keyboardContainerRef, className: "w-full bg-[#050505] border-t border-zinc-800 shrink-0 relative z-40", style: { height: "15%" }, children: [
3333      /* @__PURE__ */ jsxRuntimeExports.jsx(
3334        "canvas",
3335        {
3336          ref: keyboardCanvasRef,
3337          className: "w-full h-full cursor-pointer",
3338          onPointerDown: (e) => {
3339            var _a;
3340            const rect = (_a = keyboardCanvasRef.current) == null ? void 0 : _a.getBoundingClientRect();
3341            if (rect) {
3342              const midiVal = getMidiFromKeyboardClick(e.clientX - rect.left, e.clientY - rect.top, rect.width, rect.height);
3343              if (midiVal !== null) playNote(midiVal);
3344            }
3345          },
3346          onPointerUp: stopAllNotes,
3347          onPointerLeave: stopAllNotes
3348        }
3349      ),
3350      /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => {
3351        setKeyboardState("off");
3352        if (isMMOMode) {
3353          setIsMMOMode(false);
3354          if (onGameModeMute) onGameModeMute(null);
3355        }
3356      }, className: "absolute top-2 right-2 p-1 bg-white/10 hover:bg-white/20 rounded text-white/50", children: /* @__PURE__ */ jsxRuntimeExports.jsx(X, { size: 14 }) })
3357    ] }),
3358    showTimeline && /* @__PURE__ */ jsxRuntimeExports.jsx(StructuralTimeline, { sections, currentTime, duration, onSeek }),
3359    isAnalysisStudioOpen && /* @__PURE__ */ jsxRuntimeExports.jsx(
3360      AnalysisStudioMenu,
3361      {
3362        isOpen: isAnalysisStudioOpen,
3363        onClose: () => setIsAnalysisStudioOpen(false),
3364        showChords,
3365        setShowChords,
3366        showRoman,
3367        setShowRoman,
3368        showTimeline,
3369        setShowTimeline,
3370        showPills,
3371        setShowPills,
3372        showMeasureNumbers,
3373        setShowMeasureNumbers,
3374        showHighlight,
3375        setShowHighlight,
3376        showCursor,
3377        setShowCursor,
3378        hasSections: sections && sections.length > 0,
3379        hasChordsData: hasHarmonyChords,
3380        hasRomanData: hasHarmonyRoman
3381      }
3382    )
3383  ] });
3384};
3385export {
3386  SheetMusicViewer as default
3387};

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.