PageSourceSearch

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

js hamienet.com collected 2026-10-02 05:00:59 UTC 169,746 bytes, 3,485 lines download raw bytes

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

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.