@waveform-playlist/ui-components 9.0.4 → 9.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -62,6 +62,7 @@ __export(index_exports, {
62
62
  LoopRegion: () => LoopRegion,
63
63
  LoopRegionMarkers: () => LoopRegionMarkers,
64
64
  MasterVolumeControl: () => MasterVolumeControl,
65
+ PianoRollChannel: () => PianoRollChannel,
65
66
  Playhead: () => Playhead,
66
67
  PlayheadWithMarker: () => PlayheadWithMarker,
67
68
  Playlist: () => Playlist,
@@ -527,6 +528,10 @@ var defaultTheme = {
527
528
  annotationResizeHandleColor: "rgba(0, 0, 0, 0.4)",
528
529
  annotationResizeHandleActiveColor: "rgba(0, 0, 0, 0.8)",
529
530
  annotationTextItemHoverBackground: "rgba(0, 0, 0, 0.03)",
531
+ // Piano roll colors
532
+ pianoRollNoteColor: "#2a7070",
533
+ pianoRollSelectedNoteColor: "#3d9e9e",
534
+ pianoRollBackgroundColor: "#1a1a2e",
530
535
  // Spacing and sizing
531
536
  borderRadius: "4px",
532
537
  fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, sans-serif',
@@ -604,6 +609,10 @@ var darkTheme = {
604
609
  annotationResizeHandleColor: "rgba(200, 160, 120, 0.5)",
605
610
  annotationResizeHandleActiveColor: "rgba(220, 180, 140, 0.8)",
606
611
  annotationTextItemHoverBackground: "rgba(200, 160, 120, 0.08)",
612
+ // Piano roll colors
613
+ pianoRollNoteColor: "#c49a6c",
614
+ pianoRollSelectedNoteColor: "#e8c090",
615
+ pianoRollBackgroundColor: "#0d0d14",
607
616
  // Spacing and sizing
608
617
  borderRadius: "4px",
609
618
  fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, sans-serif',
@@ -615,19 +624,32 @@ var darkTheme = {
615
624
  var import_react = require("react");
616
625
  var import_jsx_runtime3 = require("react/jsx-runtime");
617
626
  var ViewportStore = class {
618
- constructor() {
619
- this._state = null;
627
+ constructor(containerEl) {
620
628
  this._listeners = /* @__PURE__ */ new Set();
629
+ this._notifyRafId = null;
621
630
  this.subscribe = (callback) => {
622
631
  this._listeners.add(callback);
623
632
  return () => this._listeners.delete(callback);
624
633
  };
625
634
  this.getSnapshot = () => this._state;
635
+ const width = containerEl?.clientWidth ?? (typeof window !== "undefined" ? window.innerWidth : 1024);
636
+ const buffer = width * 1.5;
637
+ this._state = {
638
+ scrollLeft: 0,
639
+ containerWidth: width,
640
+ visibleStart: 0,
641
+ visibleEnd: width + buffer
642
+ };
626
643
  }
627
644
  /**
628
645
  * Update viewport state. Applies a 100px scroll threshold to skip updates
629
646
  * that don't affect chunk visibility (1000px chunks with 1.5× overscan buffer).
630
647
  * Only notifies listeners when the state actually changes.
648
+ *
649
+ * Listener notification is deferred by one frame via requestAnimationFrame
650
+ * to avoid conflicting with React 19's concurrent rendering. When React
651
+ * time-slices a render across frames, synchronous useSyncExternalStore
652
+ * notifications can trigger "Should not already be working" errors.
631
653
  */
632
654
  update(scrollLeft, containerWidth) {
633
655
  const buffer = containerWidth * 1.5;
@@ -637,8 +659,19 @@ var ViewportStore = class {
637
659
  return;
638
660
  }
639
661
  this._state = { scrollLeft, containerWidth, visibleStart, visibleEnd };
640
- for (const listener of this._listeners) {
641
- listener();
662
+ if (this._notifyRafId === null) {
663
+ this._notifyRafId = requestAnimationFrame(() => {
664
+ this._notifyRafId = null;
665
+ for (const listener of this._listeners) {
666
+ listener();
667
+ }
668
+ });
669
+ }
670
+ }
671
+ cancelPendingNotification() {
672
+ if (this._notifyRafId !== null) {
673
+ cancelAnimationFrame(this._notifyRafId);
674
+ this._notifyRafId = null;
642
675
  }
643
676
  }
644
677
  };
@@ -649,7 +682,7 @@ var NULL_SNAPSHOT = () => null;
649
682
  var ScrollViewportProvider = ({ containerRef, children }) => {
650
683
  const storeRef = (0, import_react.useRef)(null);
651
684
  if (storeRef.current === null) {
652
- storeRef.current = new ViewportStore();
685
+ storeRef.current = new ViewportStore(containerRef.current);
653
686
  }
654
687
  const store = storeRef.current;
655
688
  const rafIdRef = (0, import_react.useRef)(null);
@@ -665,43 +698,27 @@ var ScrollViewportProvider = ({ containerRef, children }) => {
665
698
  measure();
666
699
  });
667
700
  }, [measure]);
701
+ (0, import_react.useLayoutEffect)(() => {
702
+ measure();
703
+ }, [measure]);
668
704
  (0, import_react.useEffect)(() => {
669
705
  const el = containerRef.current;
670
706
  if (!el) return;
671
- measure();
672
707
  el.addEventListener("scroll", scheduleUpdate, { passive: true });
673
- let userHasInteracted = false;
674
- const markInteracted = () => {
675
- userHasInteracted = true;
676
- };
677
- el.addEventListener("pointerdown", markInteracted, { once: true });
678
- el.addEventListener("keydown", markInteracted, { once: true });
679
- el.addEventListener("wheel", markInteracted, { once: true, passive: true });
680
- const resetHandler = () => {
681
- if (!userHasInteracted && el.scrollLeft !== 0) {
682
- el.scrollLeft = 0;
683
- measure();
684
- }
685
- el.removeEventListener("scroll", resetHandler);
686
- };
687
- el.addEventListener("scroll", resetHandler);
688
708
  const resizeObserver = new ResizeObserver(() => {
689
709
  scheduleUpdate();
690
710
  });
691
711
  resizeObserver.observe(el);
692
712
  return () => {
693
713
  el.removeEventListener("scroll", scheduleUpdate);
694
- el.removeEventListener("scroll", resetHandler);
695
- el.removeEventListener("pointerdown", markInteracted);
696
- el.removeEventListener("keydown", markInteracted);
697
- el.removeEventListener("wheel", markInteracted);
698
714
  resizeObserver.disconnect();
699
715
  if (rafIdRef.current !== null) {
700
716
  cancelAnimationFrame(rafIdRef.current);
701
717
  rafIdRef.current = null;
702
718
  }
719
+ store.cancelPendingNotification();
703
720
  };
704
- }, [containerRef, measure, scheduleUpdate]);
721
+ }, [containerRef, scheduleUpdate, store]);
705
722
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ViewportStoreContext.Provider, { value: store, children });
706
723
  };
707
724
  var useScrollViewport = () => {
@@ -836,8 +853,6 @@ var Waveform = import_styled_components9.default.canvas.attrs((props) => ({
836
853
  }))`
837
854
  position: absolute;
838
855
  top: 0;
839
- /* Promote to own compositing layer for smoother scrolling */
840
- will-change: transform;
841
856
  /* Disable image rendering interpolation */
842
857
  image-rendering: pixelated;
843
858
  image-rendering: crisp-edges;
@@ -874,7 +889,8 @@ var Channel = (props) => {
874
889
  const { canvasRef, canvasMapRef } = useChunkedCanvasRefs();
875
890
  const clipOriginX = useClipViewportOrigin();
876
891
  const visibleChunkIndices = useVisibleChunkIndices(length, import_core.MAX_CANVAS_WIDTH, clipOriginX);
877
- (0, import_react4.useLayoutEffect)(() => {
892
+ (0, import_react4.useEffect)(() => {
893
+ const tDraw = performance.now();
878
894
  const step = barWidth + barGap;
879
895
  for (const [canvasIdx, canvas] of canvasMapRef.current.entries()) {
880
896
  const globalPixelOffset = canvasIdx * import_core.MAX_CANVAS_WIDTH;
@@ -910,6 +926,9 @@ var Channel = (props) => {
910
926
  }
911
927
  }
912
928
  }
929
+ console.log(
930
+ `[waveform] draw ch${index}: ${canvasMapRef.current.size} chunks, ${(performance.now() - tDraw).toFixed(1)}ms`
931
+ );
913
932
  }, [
914
933
  canvasMapRef,
915
934
  data,
@@ -922,7 +941,8 @@ var Channel = (props) => {
922
941
  barWidth,
923
942
  barGap,
924
943
  drawMode,
925
- visibleChunkIndices
944
+ visibleChunkIndices,
945
+ index
926
946
  ]);
927
947
  const waveforms = visibleChunkIndices.map((i) => {
928
948
  const chunkLeft = i * import_core.MAX_CANVAS_WIDTH;
@@ -1040,7 +1060,7 @@ var ClipHeaderPresentational = ({
1040
1060
  trackName,
1041
1061
  isSelected = false
1042
1062
  }) => {
1043
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(HeaderContainer, { $interactive: false, $isSelected: isSelected, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(TrackName, { children: trackName }) });
1063
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(HeaderContainer, { $interactive: false, $isSelected: isSelected, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(TrackName, { title: trackName, children: trackName }) });
1044
1064
  };
1045
1065
  var ClipHeader = ({
1046
1066
  clipId,
@@ -1062,7 +1082,7 @@ var ClipHeader = ({
1062
1082
  "data-clip-id": clipId,
1063
1083
  $interactive: true,
1064
1084
  $isSelected: isSelected,
1065
- children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(TrackName, { children: trackName })
1085
+ children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(TrackName, { title: trackName, children: trackName })
1066
1086
  }
1067
1087
  );
1068
1088
  };
@@ -1283,7 +1303,7 @@ var Clip = ({
1283
1303
  "data-clip-container": "true",
1284
1304
  "data-track-id": trackId,
1285
1305
  onMouseDown,
1286
- ...!enableDrag ? { tabIndex: -1 } : {},
1306
+ tabIndex: -1,
1287
1307
  children: [
1288
1308
  showHeader && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1289
1309
  ClipHeader,
@@ -1393,11 +1413,140 @@ var MasterVolumeControl = ({
1393
1413
  ] });
1394
1414
  };
1395
1415
 
1396
- // src/components/Playhead.tsx
1416
+ // src/components/PianoRollChannel.tsx
1397
1417
  var import_react8 = require("react");
1398
1418
  var import_styled_components15 = __toESM(require("styled-components"));
1419
+ var import_core2 = require("@waveform-playlist/core");
1399
1420
  var import_jsx_runtime12 = require("react/jsx-runtime");
1400
- var PlayheadLine = import_styled_components15.default.div.attrs((props) => ({
1421
+ var NoteCanvas = import_styled_components15.default.canvas.attrs((props) => ({
1422
+ style: {
1423
+ width: `${props.$cssWidth}px`,
1424
+ height: `${props.$waveHeight}px`,
1425
+ left: `${props.$left}px`
1426
+ }
1427
+ }))`
1428
+ position: absolute;
1429
+ top: 0;
1430
+ image-rendering: pixelated;
1431
+ image-rendering: crisp-edges;
1432
+ `;
1433
+ var Wrapper2 = import_styled_components15.default.div.attrs((props) => ({
1434
+ style: {
1435
+ top: `${props.$waveHeight * props.$index}px`,
1436
+ width: `${props.$cssWidth}px`,
1437
+ height: `${props.$waveHeight}px`
1438
+ }
1439
+ }))`
1440
+ position: absolute;
1441
+ background: ${(props) => props.$backgroundColor};
1442
+ transform: translateZ(0);
1443
+ backface-visibility: hidden;
1444
+ `;
1445
+ var PianoRollChannel = ({
1446
+ index,
1447
+ midiNotes,
1448
+ length,
1449
+ waveHeight,
1450
+ devicePixelRatio,
1451
+ samplesPerPixel,
1452
+ sampleRate,
1453
+ clipOffsetSeconds,
1454
+ noteColor = "#2a7070",
1455
+ selectedNoteColor = "#3d9e9e",
1456
+ isSelected = false,
1457
+ transparentBackground = false,
1458
+ backgroundColor = "#1a1a2e"
1459
+ }) => {
1460
+ const { canvasRef, canvasMapRef } = useChunkedCanvasRefs();
1461
+ const clipOriginX = useClipViewportOrigin();
1462
+ const visibleChunkIndices = useVisibleChunkIndices(length, import_core2.MAX_CANVAS_WIDTH, clipOriginX);
1463
+ const { minMidi, maxMidi } = (0, import_react8.useMemo)(() => {
1464
+ if (midiNotes.length === 0) return { minMidi: 0, maxMidi: 127 };
1465
+ let min = 127, max = 0;
1466
+ for (const note of midiNotes) {
1467
+ if (note.midi < min) min = note.midi;
1468
+ if (note.midi > max) max = note.midi;
1469
+ }
1470
+ return { minMidi: Math.max(0, min - 1), maxMidi: Math.min(127, max + 1) };
1471
+ }, [midiNotes]);
1472
+ const color = isSelected ? selectedNoteColor : noteColor;
1473
+ (0, import_react8.useEffect)(() => {
1474
+ const tDraw = performance.now();
1475
+ const noteRange = maxMidi - minMidi + 1;
1476
+ const noteHeight = Math.max(2, waveHeight / noteRange);
1477
+ const pixelsPerSecond = sampleRate / samplesPerPixel;
1478
+ for (const [canvasIdx, canvas] of canvasMapRef.current.entries()) {
1479
+ const chunkPixelStart = canvasIdx * import_core2.MAX_CANVAS_WIDTH;
1480
+ const canvasWidth = canvas.width / devicePixelRatio;
1481
+ const ctx = canvas.getContext("2d");
1482
+ if (!ctx) continue;
1483
+ ctx.resetTransform();
1484
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
1485
+ ctx.imageSmoothingEnabled = false;
1486
+ ctx.scale(devicePixelRatio, devicePixelRatio);
1487
+ const chunkStartTime = chunkPixelStart * samplesPerPixel / sampleRate;
1488
+ const chunkEndTime = (chunkPixelStart + canvasWidth) * samplesPerPixel / sampleRate;
1489
+ for (const note of midiNotes) {
1490
+ const noteStart = note.time - clipOffsetSeconds;
1491
+ const noteEnd = noteStart + note.duration;
1492
+ if (noteEnd <= chunkStartTime || noteStart >= chunkEndTime) continue;
1493
+ const x = noteStart * pixelsPerSecond - chunkPixelStart;
1494
+ const w = Math.max(2, note.duration * pixelsPerSecond);
1495
+ const y = (maxMidi - note.midi) / noteRange * waveHeight;
1496
+ const alpha = 0.3 + note.velocity * 0.7;
1497
+ ctx.fillStyle = color;
1498
+ ctx.globalAlpha = alpha;
1499
+ const r = 1;
1500
+ ctx.beginPath();
1501
+ ctx.roundRect(x, y, w, noteHeight, r);
1502
+ ctx.fill();
1503
+ }
1504
+ ctx.globalAlpha = 1;
1505
+ }
1506
+ console.log(
1507
+ `[piano-roll] draw ch${index}: ${canvasMapRef.current.size} chunks, ${midiNotes.length} notes, ${(performance.now() - tDraw).toFixed(1)}ms`
1508
+ );
1509
+ }, [
1510
+ canvasMapRef,
1511
+ midiNotes,
1512
+ waveHeight,
1513
+ devicePixelRatio,
1514
+ samplesPerPixel,
1515
+ sampleRate,
1516
+ clipOffsetSeconds,
1517
+ color,
1518
+ minMidi,
1519
+ maxMidi,
1520
+ length,
1521
+ visibleChunkIndices,
1522
+ index
1523
+ ]);
1524
+ const canvases = visibleChunkIndices.map((i) => {
1525
+ const chunkLeft = i * import_core2.MAX_CANVAS_WIDTH;
1526
+ const currentWidth = Math.min(length - chunkLeft, import_core2.MAX_CANVAS_WIDTH);
1527
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1528
+ NoteCanvas,
1529
+ {
1530
+ $cssWidth: currentWidth,
1531
+ $left: chunkLeft,
1532
+ width: currentWidth * devicePixelRatio,
1533
+ height: waveHeight * devicePixelRatio,
1534
+ $waveHeight: waveHeight,
1535
+ "data-index": i,
1536
+ ref: canvasRef
1537
+ },
1538
+ `${length}-${i}`
1539
+ );
1540
+ });
1541
+ const bgColor = transparentBackground ? "transparent" : backgroundColor;
1542
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Wrapper2, { $index: index, $cssWidth: length, $waveHeight: waveHeight, $backgroundColor: bgColor, children: canvases });
1543
+ };
1544
+
1545
+ // src/components/Playhead.tsx
1546
+ var import_react9 = require("react");
1547
+ var import_styled_components16 = __toESM(require("styled-components"));
1548
+ var import_jsx_runtime13 = require("react/jsx-runtime");
1549
+ var PlayheadLine = import_styled_components16.default.div.attrs((props) => ({
1401
1550
  style: {
1402
1551
  transform: `translate3d(${props.$position}px, 0, 0)`
1403
1552
  }
@@ -1413,9 +1562,9 @@ var PlayheadLine = import_styled_components15.default.div.attrs((props) => ({
1413
1562
  will-change: transform;
1414
1563
  `;
1415
1564
  var Playhead = ({ position, color = "#ff0000" }) => {
1416
- return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(PlayheadLine, { $position: position, $color: color });
1565
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(PlayheadLine, { $position: position, $color: color });
1417
1566
  };
1418
- var PlayheadWithMarkerContainer = import_styled_components15.default.div`
1567
+ var PlayheadWithMarkerContainer = import_styled_components16.default.div`
1419
1568
  position: absolute;
1420
1569
  top: 0;
1421
1570
  left: 0;
@@ -1424,7 +1573,7 @@ var PlayheadWithMarkerContainer = import_styled_components15.default.div`
1424
1573
  pointer-events: none;
1425
1574
  will-change: transform;
1426
1575
  `;
1427
- var MarkerTriangle = import_styled_components15.default.div`
1576
+ var MarkerTriangle = import_styled_components16.default.div`
1428
1577
  position: absolute;
1429
1578
  top: -10px;
1430
1579
  left: -6px;
@@ -1434,7 +1583,7 @@ var MarkerTriangle = import_styled_components15.default.div`
1434
1583
  border-right: 7px solid transparent;
1435
1584
  border-top: 10px solid ${(props) => props.$color};
1436
1585
  `;
1437
- var MarkerLine = import_styled_components15.default.div`
1586
+ var MarkerLine = import_styled_components16.default.div`
1438
1587
  position: absolute;
1439
1588
  top: 0;
1440
1589
  left: 0;
@@ -1450,13 +1599,13 @@ var PlayheadWithMarker = ({
1450
1599
  audioStartPositionRef,
1451
1600
  samplesPerPixel,
1452
1601
  sampleRate,
1453
- controlsOffset,
1602
+ controlsOffset = 0,
1454
1603
  getAudioContextTime,
1455
1604
  getPlaybackTime
1456
1605
  }) => {
1457
- const containerRef = (0, import_react8.useRef)(null);
1458
- const animationFrameRef = (0, import_react8.useRef)(null);
1459
- (0, import_react8.useEffect)(() => {
1606
+ const containerRef = (0, import_react9.useRef)(null);
1607
+ const animationFrameRef = (0, import_react9.useRef)(null);
1608
+ (0, import_react9.useEffect)(() => {
1460
1609
  const updatePosition = () => {
1461
1610
  if (containerRef.current) {
1462
1611
  let time;
@@ -1501,35 +1650,51 @@ var PlayheadWithMarker = ({
1501
1650
  getAudioContextTime,
1502
1651
  getPlaybackTime
1503
1652
  ]);
1504
- (0, import_react8.useEffect)(() => {
1653
+ (0, import_react9.useEffect)(() => {
1505
1654
  if (!isPlaying && containerRef.current) {
1506
1655
  const time = currentTimeRef.current ?? 0;
1507
1656
  const pos = time * sampleRate / samplesPerPixel + controlsOffset;
1508
1657
  containerRef.current.style.transform = `translate3d(${pos}px, 0, 0)`;
1509
1658
  }
1510
1659
  });
1511
- return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(PlayheadWithMarkerContainer, { ref: containerRef, $color: color, children: [
1512
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(MarkerTriangle, { $color: color }),
1513
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(MarkerLine, { $color: color })
1660
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(PlayheadWithMarkerContainer, { ref: containerRef, $color: color, children: [
1661
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(MarkerTriangle, { $color: color }),
1662
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(MarkerLine, { $color: color })
1514
1663
  ] });
1515
1664
  };
1516
1665
 
1517
1666
  // src/components/Playlist.tsx
1518
- var import_styled_components16 = __toESM(require("styled-components"));
1519
- var import_react9 = require("react");
1520
- var import_jsx_runtime13 = require("react/jsx-runtime");
1521
- var Wrapper2 = import_styled_components16.default.div`
1667
+ var import_styled_components17 = __toESM(require("styled-components"));
1668
+ var import_react10 = require("react");
1669
+ var import_jsx_runtime14 = require("react/jsx-runtime");
1670
+ var Wrapper3 = import_styled_components17.default.div`
1671
+ display: flex;
1522
1672
  overflow-y: hidden;
1673
+ position: relative;
1674
+ `;
1675
+ var ControlsColumn = import_styled_components17.default.div.attrs((props) => ({
1676
+ style: { width: `${props.$width}px` }
1677
+ }))`
1678
+ flex-shrink: 0;
1679
+ overflow: hidden;
1680
+ `;
1681
+ var TimescaleGap = import_styled_components17.default.div.attrs((props) => ({
1682
+ style: { height: `${props.$height}px` }
1683
+ }))``;
1684
+ var ScrollArea = import_styled_components17.default.div`
1523
1685
  overflow-x: auto;
1686
+ overflow-y: hidden;
1687
+ overflow-anchor: none;
1688
+ flex: 1;
1524
1689
  position: relative;
1525
1690
  `;
1526
- var ScrollContainer = import_styled_components16.default.div.attrs((props) => ({
1691
+ var ScrollContainerInner = import_styled_components17.default.div.attrs((props) => ({
1527
1692
  style: props.$width !== void 0 ? { width: `${props.$width}px` } : {}
1528
1693
  }))`
1529
1694
  position: relative;
1530
1695
  background: ${(props) => props.$backgroundColor || "transparent"};
1531
1696
  `;
1532
- var TimescaleWrapper = import_styled_components16.default.div.attrs((props) => ({
1697
+ var TimescaleWrapper = import_styled_components17.default.div.attrs((props) => ({
1533
1698
  style: props.$width ? { minWidth: `${props.$width}px` } : {}
1534
1699
  }))`
1535
1700
  background: ${(props) => props.$backgroundColor || "white"};
@@ -1537,14 +1702,14 @@ var TimescaleWrapper = import_styled_components16.default.div.attrs((props) => (
1537
1702
  position: relative;
1538
1703
  overflow: hidden; /* Constrain loop region to timescale area */
1539
1704
  `;
1540
- var TracksContainer = import_styled_components16.default.div.attrs((props) => ({
1705
+ var TracksContainer = import_styled_components17.default.div.attrs((props) => ({
1541
1706
  style: props.$width !== void 0 ? { minWidth: `${props.$width}px` } : {}
1542
1707
  }))`
1543
1708
  position: relative;
1544
1709
  background: ${(props) => props.$backgroundColor || "transparent"};
1545
1710
  width: 100%;
1546
1711
  `;
1547
- var ClickOverlay = import_styled_components16.default.div`
1712
+ var ClickOverlay = import_styled_components17.default.div`
1548
1713
  position: absolute;
1549
1714
  top: 0;
1550
1715
  left: 0;
@@ -1561,7 +1726,6 @@ var Playlist = ({
1561
1726
  timescale,
1562
1727
  timescaleWidth,
1563
1728
  tracksWidth,
1564
- scrollContainerWidth,
1565
1729
  controlsWidth,
1566
1730
  onTracksClick,
1567
1731
  onTracksMouseDown,
@@ -1569,40 +1733,48 @@ var Playlist = ({
1569
1733
  onTracksMouseUp,
1570
1734
  scrollContainerRef,
1571
1735
  isSelecting,
1572
- "data-playlist-state": playlistState
1736
+ "data-playlist-state": playlistState,
1737
+ trackControlsSlots,
1738
+ timescaleGapHeight = 0
1573
1739
  }) => {
1574
- const wrapperRef = (0, import_react9.useRef)(null);
1575
- const handleRef = (0, import_react9.useCallback)(
1740
+ const scrollAreaRef = (0, import_react10.useRef)(null);
1741
+ const handleRef = (0, import_react10.useCallback)(
1576
1742
  (el) => {
1577
- wrapperRef.current = el;
1743
+ scrollAreaRef.current = el;
1578
1744
  scrollContainerRef?.(el);
1579
1745
  },
1580
1746
  [scrollContainerRef]
1581
1747
  );
1582
- return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Wrapper2, { "data-scroll-container": "true", "data-playlist-state": playlistState, ref: handleRef, children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(ScrollViewportProvider, { containerRef: wrapperRef, children: /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(ScrollContainer, { $backgroundColor: backgroundColor, $width: scrollContainerWidth, children: [
1583
- timescale && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(TimescaleWrapper, { $width: timescaleWidth, $backgroundColor: timescaleBackgroundColor, children: timescale }),
1584
- /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(TracksContainer, { $width: tracksWidth, $backgroundColor: backgroundColor, children: [
1585
- children,
1586
- (onTracksClick || onTracksMouseDown) && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1587
- ClickOverlay,
1588
- {
1589
- $controlsWidth: controlsWidth,
1590
- $isSelecting: isSelecting,
1591
- onClick: onTracksClick,
1592
- onMouseDown: onTracksMouseDown,
1593
- onMouseMove: onTracksMouseMove,
1594
- onMouseUp: onTracksMouseUp
1595
- }
1596
- )
1597
- ] })
1598
- ] }) }) });
1748
+ const showControls = controlsWidth !== void 0 && controlsWidth > 0;
1749
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(Wrapper3, { "data-playlist-state": playlistState, children: [
1750
+ showControls && /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(ControlsColumn, { $width: controlsWidth, children: [
1751
+ timescaleGapHeight > 0 && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(TimescaleGap, { $height: timescaleGapHeight }),
1752
+ trackControlsSlots
1753
+ ] }),
1754
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(ScrollArea, { "data-scroll-container": "true", ref: handleRef, children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(ScrollViewportProvider, { containerRef: scrollAreaRef, children: /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(ScrollContainerInner, { $backgroundColor: backgroundColor, $width: tracksWidth, children: [
1755
+ timescale && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(TimescaleWrapper, { $width: timescaleWidth, $backgroundColor: timescaleBackgroundColor, children: timescale }),
1756
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(TracksContainer, { $width: tracksWidth, $backgroundColor: backgroundColor, children: [
1757
+ children,
1758
+ (onTracksClick || onTracksMouseDown) && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
1759
+ ClickOverlay,
1760
+ {
1761
+ $isSelecting: isSelecting,
1762
+ onClick: onTracksClick,
1763
+ onMouseDown: onTracksMouseDown,
1764
+ onMouseMove: onTracksMouseMove,
1765
+ onMouseUp: onTracksMouseUp
1766
+ }
1767
+ )
1768
+ ] })
1769
+ ] }) }) })
1770
+ ] });
1599
1771
  };
1600
- var StyledPlaylist = (0, import_styled_components16.withTheme)(Playlist);
1772
+ var StyledPlaylist = (0, import_styled_components17.withTheme)(Playlist);
1601
1773
 
1602
1774
  // src/components/Selection.tsx
1603
- var import_styled_components17 = __toESM(require("styled-components"));
1604
- var import_jsx_runtime14 = require("react/jsx-runtime");
1605
- var SelectionOverlay = import_styled_components17.default.div.attrs((props) => ({
1775
+ var import_styled_components18 = __toESM(require("styled-components"));
1776
+ var import_jsx_runtime15 = require("react/jsx-runtime");
1777
+ var SelectionOverlay = import_styled_components18.default.div.attrs((props) => ({
1606
1778
  style: {
1607
1779
  left: `${props.$left}px`,
1608
1780
  width: `${props.$width}px`
@@ -1625,14 +1797,14 @@ var Selection = ({
1625
1797
  if (width <= 0) {
1626
1798
  return null;
1627
1799
  }
1628
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(SelectionOverlay, { $left: startPosition, $width: width, $color: color, "data-selection": true });
1800
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(SelectionOverlay, { $left: startPosition, $width: width, $color: color, "data-selection": true });
1629
1801
  };
1630
1802
 
1631
1803
  // src/components/LoopRegion.tsx
1632
- var import_react10 = require("react");
1633
- var import_styled_components18 = __toESM(require("styled-components"));
1634
- var import_jsx_runtime15 = require("react/jsx-runtime");
1635
- var LoopRegionOverlayDiv = import_styled_components18.default.div.attrs((props) => ({
1804
+ var import_react11 = require("react");
1805
+ var import_styled_components19 = __toESM(require("styled-components"));
1806
+ var import_jsx_runtime16 = require("react/jsx-runtime");
1807
+ var LoopRegionOverlayDiv = import_styled_components19.default.div.attrs((props) => ({
1636
1808
  style: {
1637
1809
  left: `${props.$left}px`,
1638
1810
  width: `${props.$width}px`
@@ -1645,7 +1817,7 @@ var LoopRegionOverlayDiv = import_styled_components18.default.div.attrs((props)
1645
1817
  z-index: 55; /* Between clips (z-index: 50) and selection (z-index: 60) */
1646
1818
  pointer-events: none;
1647
1819
  `;
1648
- var LoopMarker = import_styled_components18.default.div.attrs((props) => ({
1820
+ var LoopMarker = import_styled_components19.default.div.attrs((props) => ({
1649
1821
  style: {
1650
1822
  left: `${props.$left}px`
1651
1823
  }
@@ -1680,8 +1852,8 @@ var LoopRegion = ({
1680
1852
  if (width <= 0) {
1681
1853
  return null;
1682
1854
  }
1683
- return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_jsx_runtime15.Fragment, { children: [
1684
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
1855
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_jsx_runtime16.Fragment, { children: [
1856
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1685
1857
  LoopRegionOverlayDiv,
1686
1858
  {
1687
1859
  $left: startPosition,
@@ -1690,7 +1862,7 @@ var LoopRegion = ({
1690
1862
  "data-loop-region": true
1691
1863
  }
1692
1864
  ),
1693
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
1865
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1694
1866
  LoopMarker,
1695
1867
  {
1696
1868
  $left: startPosition,
@@ -1699,7 +1871,7 @@ var LoopRegion = ({
1699
1871
  "data-loop-marker": "start"
1700
1872
  }
1701
1873
  ),
1702
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
1874
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1703
1875
  LoopMarker,
1704
1876
  {
1705
1877
  $left: endPosition - 2,
@@ -1710,7 +1882,7 @@ var LoopRegion = ({
1710
1882
  )
1711
1883
  ] });
1712
1884
  };
1713
- var DraggableMarkerHandle = import_styled_components18.default.div.attrs((props) => ({
1885
+ var DraggableMarkerHandle = import_styled_components19.default.div.attrs((props) => ({
1714
1886
  style: {
1715
1887
  left: `${props.$left}px`
1716
1888
  }
@@ -1752,7 +1924,7 @@ var DraggableMarkerHandle = import_styled_components18.default.div.attrs((props)
1752
1924
  opacity: 1;
1753
1925
  }
1754
1926
  `;
1755
- var TimescaleLoopShade = import_styled_components18.default.div.attrs((props) => ({
1927
+ var TimescaleLoopShade = import_styled_components19.default.div.attrs((props) => ({
1756
1928
  style: {
1757
1929
  left: `${props.$left}px`,
1758
1930
  width: `${props.$width}px`
@@ -1780,12 +1952,12 @@ var LoopRegionMarkers = ({
1780
1952
  minPosition = 0,
1781
1953
  maxPosition = Infinity
1782
1954
  }) => {
1783
- const [draggingMarker, setDraggingMarker] = (0, import_react10.useState)(null);
1784
- const dragStartX = (0, import_react10.useRef)(0);
1785
- const dragStartPosition = (0, import_react10.useRef)(0);
1786
- const dragStartEnd = (0, import_react10.useRef)(0);
1955
+ const [draggingMarker, setDraggingMarker] = (0, import_react11.useState)(null);
1956
+ const dragStartX = (0, import_react11.useRef)(0);
1957
+ const dragStartPosition = (0, import_react11.useRef)(0);
1958
+ const dragStartEnd = (0, import_react11.useRef)(0);
1787
1959
  const width = Math.max(0, endPosition - startPosition);
1788
- const handleMarkerMouseDown = (0, import_react10.useCallback)(
1960
+ const handleMarkerMouseDown = (0, import_react11.useCallback)(
1789
1961
  (e, marker) => {
1790
1962
  e.preventDefault();
1791
1963
  e.stopPropagation();
@@ -1813,7 +1985,7 @@ var LoopRegionMarkers = ({
1813
1985
  },
1814
1986
  [startPosition, endPosition, minPosition, maxPosition, onLoopStartChange, onLoopEndChange]
1815
1987
  );
1816
- const handleRegionMouseDown = (0, import_react10.useCallback)(
1988
+ const handleRegionMouseDown = (0, import_react11.useCallback)(
1817
1989
  (e) => {
1818
1990
  e.preventDefault();
1819
1991
  e.stopPropagation();
@@ -1849,8 +2021,8 @@ var LoopRegionMarkers = ({
1849
2021
  if (width <= 0) {
1850
2022
  return null;
1851
2023
  }
1852
- return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_jsx_runtime15.Fragment, { children: [
1853
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
2024
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_jsx_runtime16.Fragment, { children: [
2025
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1854
2026
  TimescaleLoopShade,
1855
2027
  {
1856
2028
  $left: startPosition,
@@ -1861,7 +2033,7 @@ var LoopRegionMarkers = ({
1861
2033
  "data-loop-region-timescale": true
1862
2034
  }
1863
2035
  ),
1864
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
2036
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1865
2037
  DraggableMarkerHandle,
1866
2038
  {
1867
2039
  $left: startPosition,
@@ -1872,7 +2044,7 @@ var LoopRegionMarkers = ({
1872
2044
  "data-loop-marker-handle": "start"
1873
2045
  }
1874
2046
  ),
1875
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
2047
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1876
2048
  DraggableMarkerHandle,
1877
2049
  {
1878
2050
  $left: endPosition,
@@ -1885,13 +2057,10 @@ var LoopRegionMarkers = ({
1885
2057
  )
1886
2058
  ] });
1887
2059
  };
1888
- var TimescaleLoopCreator = import_styled_components18.default.div.attrs((props) => ({
1889
- style: {
1890
- left: `${props.$leftOffset || 0}px`
1891
- }
1892
- }))`
2060
+ var TimescaleLoopCreator = import_styled_components19.default.div`
1893
2061
  position: absolute;
1894
2062
  top: 0;
2063
+ left: 0;
1895
2064
  right: 0;
1896
2065
  height: 100%; /* Stay within timescale bounds, don't extend into tracks */
1897
2066
  cursor: crosshair;
@@ -1904,14 +2073,13 @@ var TimescaleLoopRegion = ({
1904
2073
  regionColor = "rgba(59, 130, 246, 0.3)",
1905
2074
  onLoopRegionChange,
1906
2075
  minPosition = 0,
1907
- maxPosition = Infinity,
1908
- controlsOffset = 0
2076
+ maxPosition = Infinity
1909
2077
  }) => {
1910
- const [, setIsCreating] = (0, import_react10.useState)(false);
1911
- const createStartX = (0, import_react10.useRef)(0);
1912
- const containerRef = (0, import_react10.useRef)(null);
2078
+ const [, setIsCreating] = (0, import_react11.useState)(false);
2079
+ const createStartX = (0, import_react11.useRef)(0);
2080
+ const containerRef = (0, import_react11.useRef)(null);
1913
2081
  const hasLoopRegion = endPosition > startPosition;
1914
- const handleBackgroundMouseDown = (0, import_react10.useCallback)(
2082
+ const handleBackgroundMouseDown = (0, import_react11.useCallback)(
1915
2083
  (e) => {
1916
2084
  const target = e.target;
1917
2085
  if (target.closest("[data-loop-marker-handle]") || target.closest("[data-loop-region-timescale]")) {
@@ -1942,14 +2110,13 @@ var TimescaleLoopRegion = ({
1942
2110
  },
1943
2111
  [minPosition, maxPosition, onLoopRegionChange]
1944
2112
  );
1945
- return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
2113
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1946
2114
  TimescaleLoopCreator,
1947
2115
  {
1948
2116
  ref: containerRef,
1949
- $leftOffset: controlsOffset,
1950
2117
  onMouseDown: handleBackgroundMouseDown,
1951
2118
  "data-timescale-loop-creator": true,
1952
- children: hasLoopRegion && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
2119
+ children: hasLoopRegion && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1953
2120
  LoopRegionMarkers,
1954
2121
  {
1955
2122
  startPosition,
@@ -1968,10 +2135,10 @@ var TimescaleLoopRegion = ({
1968
2135
  };
1969
2136
 
1970
2137
  // src/components/SelectionTimeInputs.tsx
1971
- var import_react12 = require("react");
2138
+ var import_react13 = require("react");
1972
2139
 
1973
2140
  // src/components/TimeInput.tsx
1974
- var import_react11 = require("react");
2141
+ var import_react12 = require("react");
1975
2142
 
1976
2143
  // src/utils/timeFormat.ts
1977
2144
  function clockFormat(seconds, decimals) {
@@ -2021,7 +2188,7 @@ function parseTime(timeStr, format) {
2021
2188
  }
2022
2189
 
2023
2190
  // src/components/TimeInput.tsx
2024
- var import_jsx_runtime16 = require("react/jsx-runtime");
2191
+ var import_jsx_runtime17 = require("react/jsx-runtime");
2025
2192
  var TimeInput = ({
2026
2193
  id,
2027
2194
  label,
@@ -2031,8 +2198,8 @@ var TimeInput = ({
2031
2198
  onChange,
2032
2199
  readOnly = false
2033
2200
  }) => {
2034
- const [displayValue, setDisplayValue] = (0, import_react11.useState)("");
2035
- (0, import_react11.useEffect)(() => {
2201
+ const [displayValue, setDisplayValue] = (0, import_react12.useState)("");
2202
+ (0, import_react12.useEffect)(() => {
2036
2203
  const formatted = formatTime(value, format);
2037
2204
  setDisplayValue(formatted);
2038
2205
  }, [value, format, id]);
@@ -2052,9 +2219,9 @@ var TimeInput = ({
2052
2219
  e.currentTarget.blur();
2053
2220
  }
2054
2221
  };
2055
- return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_jsx_runtime16.Fragment, { children: [
2056
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(ScreenReaderOnly, { as: "label", htmlFor: id, children: label }),
2057
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
2222
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_jsx_runtime17.Fragment, { children: [
2223
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(ScreenReaderOnly, { as: "label", htmlFor: id, children: label }),
2224
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
2058
2225
  BaseInput,
2059
2226
  {
2060
2227
  type: "text",
@@ -2071,15 +2238,15 @@ var TimeInput = ({
2071
2238
  };
2072
2239
 
2073
2240
  // src/components/SelectionTimeInputs.tsx
2074
- var import_jsx_runtime17 = require("react/jsx-runtime");
2241
+ var import_jsx_runtime18 = require("react/jsx-runtime");
2075
2242
  var SelectionTimeInputs = ({
2076
2243
  selectionStart,
2077
2244
  selectionEnd,
2078
2245
  onSelectionChange,
2079
2246
  className
2080
2247
  }) => {
2081
- const [timeFormat, setTimeFormat] = (0, import_react12.useState)("hh:mm:ss.uuu");
2082
- (0, import_react12.useEffect)(() => {
2248
+ const [timeFormat, setTimeFormat] = (0, import_react13.useState)("hh:mm:ss.uuu");
2249
+ (0, import_react13.useEffect)(() => {
2083
2250
  const timeFormatSelect = document.querySelector(".time-format");
2084
2251
  const handleFormatChange = () => {
2085
2252
  if (timeFormatSelect) {
@@ -2104,8 +2271,8 @@ var SelectionTimeInputs = ({
2104
2271
  onSelectionChange(selectionStart, value);
2105
2272
  }
2106
2273
  };
2107
- return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className, children: [
2108
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
2274
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className, children: [
2275
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
2109
2276
  TimeInput,
2110
2277
  {
2111
2278
  id: "audio_start",
@@ -2116,7 +2283,7 @@ var SelectionTimeInputs = ({
2116
2283
  onChange: handleStartChange
2117
2284
  }
2118
2285
  ),
2119
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
2286
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
2120
2287
  TimeInput,
2121
2288
  {
2122
2289
  id: "audio_end",
@@ -2131,14 +2298,14 @@ var SelectionTimeInputs = ({
2131
2298
  };
2132
2299
 
2133
2300
  // src/contexts/DevicePixelRatio.tsx
2134
- var import_react13 = require("react");
2135
- var import_jsx_runtime18 = require("react/jsx-runtime");
2301
+ var import_react14 = require("react");
2302
+ var import_jsx_runtime19 = require("react/jsx-runtime");
2136
2303
  function getScale() {
2137
2304
  return window.devicePixelRatio;
2138
2305
  }
2139
- var DevicePixelRatioContext = (0, import_react13.createContext)(getScale());
2306
+ var DevicePixelRatioContext = (0, import_react14.createContext)(getScale());
2140
2307
  var DevicePixelRatioProvider = ({ children }) => {
2141
- const [scale, setScale] = (0, import_react13.useState)(getScale());
2308
+ const [scale, setScale] = (0, import_react14.useState)(getScale());
2142
2309
  matchMedia(`(resolution: ${getScale()}dppx)`).addEventListener(
2143
2310
  "change",
2144
2311
  () => {
@@ -2146,13 +2313,13 @@ var DevicePixelRatioProvider = ({ children }) => {
2146
2313
  },
2147
2314
  { once: true }
2148
2315
  );
2149
- return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(DevicePixelRatioContext.Provider, { value: Math.ceil(scale), children });
2316
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(DevicePixelRatioContext.Provider, { value: Math.ceil(scale), children });
2150
2317
  };
2151
- var useDevicePixelRatio = () => (0, import_react13.useContext)(DevicePixelRatioContext);
2318
+ var useDevicePixelRatio = () => (0, import_react14.useContext)(DevicePixelRatioContext);
2152
2319
 
2153
2320
  // src/contexts/PlaylistInfo.tsx
2154
- var import_react14 = require("react");
2155
- var PlaylistInfoContext = (0, import_react14.createContext)({
2321
+ var import_react15 = require("react");
2322
+ var PlaylistInfoContext = (0, import_react15.createContext)({
2156
2323
  sampleRate: 48e3,
2157
2324
  samplesPerPixel: 1e3,
2158
2325
  zoomLevels: [1e3, 1500, 2e3, 2500],
@@ -2166,22 +2333,22 @@ var PlaylistInfoContext = (0, import_react14.createContext)({
2166
2333
  barWidth: 1,
2167
2334
  barGap: 0
2168
2335
  });
2169
- var usePlaylistInfo = () => (0, import_react14.useContext)(PlaylistInfoContext);
2336
+ var usePlaylistInfo = () => (0, import_react15.useContext)(PlaylistInfoContext);
2170
2337
 
2171
2338
  // src/contexts/Theme.tsx
2172
- var import_react15 = require("react");
2173
- var import_styled_components19 = require("styled-components");
2174
- var useTheme2 = () => (0, import_react15.useContext)(import_styled_components19.ThemeContext);
2175
-
2176
- // src/contexts/TrackControls.tsx
2177
2339
  var import_react16 = require("react");
2178
- var import_jsx_runtime19 = require("react/jsx-runtime");
2179
- var TrackControlsContext = (0, import_react16.createContext)(/* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_react16.Fragment, {}));
2180
- var useTrackControls = () => (0, import_react16.useContext)(TrackControlsContext);
2340
+ var import_styled_components20 = require("styled-components");
2341
+ var useTheme2 = () => (0, import_react16.useContext)(import_styled_components20.ThemeContext);
2181
2342
 
2182
- // src/contexts/Playout.tsx
2343
+ // src/contexts/TrackControls.tsx
2183
2344
  var import_react17 = require("react");
2184
2345
  var import_jsx_runtime20 = require("react/jsx-runtime");
2346
+ var TrackControlsContext = (0, import_react17.createContext)(/* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_react17.Fragment, {}));
2347
+ var useTrackControls = () => (0, import_react17.useContext)(TrackControlsContext);
2348
+
2349
+ // src/contexts/Playout.tsx
2350
+ var import_react18 = require("react");
2351
+ var import_jsx_runtime21 = require("react/jsx-runtime");
2185
2352
  var defaultProgress = 0;
2186
2353
  var defaultIsPlaying = false;
2187
2354
  var defaultSelectionStart = 0;
@@ -2192,8 +2359,8 @@ var defaultPlayout = {
2192
2359
  selectionStart: defaultSelectionStart,
2193
2360
  selectionEnd: defaultSelectionEnd
2194
2361
  };
2195
- var PlayoutStatusContext = (0, import_react17.createContext)(defaultPlayout);
2196
- var PlayoutStatusUpdateContext = (0, import_react17.createContext)({
2362
+ var PlayoutStatusContext = (0, import_react18.createContext)(defaultPlayout);
2363
+ var PlayoutStatusUpdateContext = (0, import_react18.createContext)({
2197
2364
  setIsPlaying: () => {
2198
2365
  },
2199
2366
  setProgress: () => {
@@ -2202,26 +2369,26 @@ var PlayoutStatusUpdateContext = (0, import_react17.createContext)({
2202
2369
  }
2203
2370
  });
2204
2371
  var PlayoutProvider = ({ children }) => {
2205
- const [isPlaying, setIsPlaying] = (0, import_react17.useState)(defaultIsPlaying);
2206
- const [progress, setProgress] = (0, import_react17.useState)(defaultProgress);
2207
- const [selectionStart, setSelectionStart] = (0, import_react17.useState)(defaultSelectionStart);
2208
- const [selectionEnd, setSelectionEnd] = (0, import_react17.useState)(defaultSelectionEnd);
2372
+ const [isPlaying, setIsPlaying] = (0, import_react18.useState)(defaultIsPlaying);
2373
+ const [progress, setProgress] = (0, import_react18.useState)(defaultProgress);
2374
+ const [selectionStart, setSelectionStart] = (0, import_react18.useState)(defaultSelectionStart);
2375
+ const [selectionEnd, setSelectionEnd] = (0, import_react18.useState)(defaultSelectionEnd);
2209
2376
  const setSelection = (start, end) => {
2210
2377
  setSelectionStart(start);
2211
2378
  setSelectionEnd(end);
2212
2379
  };
2213
- return /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(PlayoutStatusUpdateContext.Provider, { value: { setIsPlaying, setProgress, setSelection }, children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(PlayoutStatusContext.Provider, { value: { isPlaying, progress, selectionStart, selectionEnd }, children }) });
2380
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(PlayoutStatusUpdateContext.Provider, { value: { setIsPlaying, setProgress, setSelection }, children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(PlayoutStatusContext.Provider, { value: { isPlaying, progress, selectionStart, selectionEnd }, children }) });
2214
2381
  };
2215
- var usePlayoutStatus = () => (0, import_react17.useContext)(PlayoutStatusContext);
2216
- var usePlayoutStatusUpdate = () => (0, import_react17.useContext)(PlayoutStatusUpdateContext);
2382
+ var usePlayoutStatus = () => (0, import_react18.useContext)(PlayoutStatusContext);
2383
+ var usePlayoutStatusUpdate = () => (0, import_react18.useContext)(PlayoutStatusUpdateContext);
2217
2384
 
2218
2385
  // src/components/SpectrogramChannel.tsx
2219
- var import_react18 = require("react");
2220
- var import_styled_components20 = __toESM(require("styled-components"));
2221
- var import_core2 = require("@waveform-playlist/core");
2222
- var import_jsx_runtime21 = require("react/jsx-runtime");
2386
+ var import_react19 = require("react");
2387
+ var import_styled_components21 = __toESM(require("styled-components"));
2388
+ var import_core3 = require("@waveform-playlist/core");
2389
+ var import_jsx_runtime22 = require("react/jsx-runtime");
2223
2390
  var LINEAR_FREQUENCY_SCALE = (f, minF, maxF) => (f - minF) / (maxF - minF);
2224
- var Wrapper3 = import_styled_components20.default.div.attrs((props) => ({
2391
+ var Wrapper4 = import_styled_components21.default.div.attrs((props) => ({
2225
2392
  style: {
2226
2393
  top: `${props.$waveHeight * props.$index}px`,
2227
2394
  width: `${props.$cssWidth}px`,
@@ -2233,7 +2400,7 @@ var Wrapper3 = import_styled_components20.default.div.attrs((props) => ({
2233
2400
  transform: translateZ(0);
2234
2401
  backface-visibility: hidden;
2235
2402
  `;
2236
- var SpectrogramCanvas = import_styled_components20.default.canvas.attrs((props) => ({
2403
+ var SpectrogramCanvas = import_styled_components21.default.canvas.attrs((props) => ({
2237
2404
  style: {
2238
2405
  width: `${props.$cssWidth}px`,
2239
2406
  height: `${props.$waveHeight}px`,
@@ -2242,8 +2409,6 @@ var SpectrogramCanvas = import_styled_components20.default.canvas.attrs((props)
2242
2409
  }))`
2243
2410
  position: absolute;
2244
2411
  top: 0;
2245
- /* Promote to own compositing layer for smoother scrolling */
2246
- will-change: transform;
2247
2412
  image-rendering: pixelated;
2248
2413
  image-rendering: crisp-edges;
2249
2414
  `;
@@ -2273,24 +2438,24 @@ var SpectrogramChannel = ({
2273
2438
  }) => {
2274
2439
  const channelIndex = channelIndexProp ?? index;
2275
2440
  const { canvasRef, canvasMapRef } = useChunkedCanvasRefs();
2276
- const registeredIdsRef = (0, import_react18.useRef)([]);
2277
- const transferredCanvasesRef = (0, import_react18.useRef)(/* @__PURE__ */ new WeakSet());
2278
- const workerApiRef = (0, import_react18.useRef)(workerApi);
2279
- const onCanvasesReadyRef = (0, import_react18.useRef)(onCanvasesReady);
2441
+ const registeredIdsRef = (0, import_react19.useRef)([]);
2442
+ const transferredCanvasesRef = (0, import_react19.useRef)(/* @__PURE__ */ new WeakSet());
2443
+ const workerApiRef = (0, import_react19.useRef)(workerApi);
2444
+ const onCanvasesReadyRef = (0, import_react19.useRef)(onCanvasesReady);
2280
2445
  const isWorkerMode = !!(workerApi && clipId);
2281
2446
  const clipOriginX = useClipViewportOrigin();
2282
- const visibleChunkIndices = useVisibleChunkIndices(length, import_core2.MAX_CANVAS_WIDTH, clipOriginX);
2447
+ const visibleChunkIndices = useVisibleChunkIndices(length, import_core3.MAX_CANVAS_WIDTH, clipOriginX);
2283
2448
  const lut = colorLUT ?? DEFAULT_COLOR_LUT;
2284
2449
  const maxF = maxFrequency ?? (data ? data.sampleRate / 2 : 22050);
2285
2450
  const scaleFn = frequencyScaleFn ?? LINEAR_FREQUENCY_SCALE;
2286
2451
  const hasCustomFrequencyScale = Boolean(frequencyScaleFn);
2287
- (0, import_react18.useEffect)(() => {
2452
+ (0, import_react19.useEffect)(() => {
2288
2453
  workerApiRef.current = workerApi;
2289
2454
  }, [workerApi]);
2290
- (0, import_react18.useEffect)(() => {
2455
+ (0, import_react19.useEffect)(() => {
2291
2456
  onCanvasesReadyRef.current = onCanvasesReady;
2292
2457
  }, [onCanvasesReady]);
2293
- (0, import_react18.useEffect)(() => {
2458
+ (0, import_react19.useEffect)(() => {
2294
2459
  if (!isWorkerMode) return;
2295
2460
  const currentWorkerApi = workerApiRef.current;
2296
2461
  if (!currentWorkerApi || !clipId) return;
@@ -2345,15 +2510,15 @@ var SpectrogramChannel = ({
2345
2510
  const match = id.match(/chunk(\d+)$/);
2346
2511
  if (!match) {
2347
2512
  console.warn(`[spectrogram] Unexpected canvas ID format: ${id}`);
2348
- return import_core2.MAX_CANVAS_WIDTH;
2513
+ return import_core3.MAX_CANVAS_WIDTH;
2349
2514
  }
2350
2515
  const chunkIdx = parseInt(match[1], 10);
2351
- return Math.min(length - chunkIdx * import_core2.MAX_CANVAS_WIDTH, import_core2.MAX_CANVAS_WIDTH);
2516
+ return Math.min(length - chunkIdx * import_core3.MAX_CANVAS_WIDTH, import_core3.MAX_CANVAS_WIDTH);
2352
2517
  });
2353
2518
  onCanvasesReadyRef.current?.(allIds, allWidths);
2354
2519
  }
2355
2520
  }, [canvasMapRef, isWorkerMode, clipId, channelIndex, length, visibleChunkIndices]);
2356
- (0, import_react18.useEffect)(() => {
2521
+ (0, import_react19.useEffect)(() => {
2357
2522
  return () => {
2358
2523
  const api = workerApiRef.current;
2359
2524
  if (!api) return;
@@ -2367,7 +2532,7 @@ var SpectrogramChannel = ({
2367
2532
  registeredIdsRef.current = [];
2368
2533
  };
2369
2534
  }, []);
2370
- (0, import_react18.useLayoutEffect)(() => {
2535
+ (0, import_react19.useEffect)(() => {
2371
2536
  if (isWorkerMode || !data) return;
2372
2537
  const {
2373
2538
  frequencyBinCount,
@@ -2380,7 +2545,7 @@ var SpectrogramChannel = ({
2380
2545
  const rangeDb = rawRangeDb === 0 ? 1 : rawRangeDb;
2381
2546
  const binToFreq = (bin) => bin / frequencyBinCount * (sampleRate / 2);
2382
2547
  for (const [canvasIdx, canvas] of canvasMapRef.current.entries()) {
2383
- const globalPixelOffset = canvasIdx * import_core2.MAX_CANVAS_WIDTH;
2548
+ const globalPixelOffset = canvasIdx * import_core3.MAX_CANVAS_WIDTH;
2384
2549
  const ctx = canvas.getContext("2d");
2385
2550
  if (!ctx) continue;
2386
2551
  const canvasWidth = canvas.width / devicePixelRatio;
@@ -2456,9 +2621,9 @@ var SpectrogramChannel = ({
2456
2621
  visibleChunkIndices
2457
2622
  ]);
2458
2623
  const canvases = visibleChunkIndices.map((i) => {
2459
- const chunkLeft = i * import_core2.MAX_CANVAS_WIDTH;
2460
- const currentWidth = Math.min(length - chunkLeft, import_core2.MAX_CANVAS_WIDTH);
2461
- return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
2624
+ const chunkLeft = i * import_core3.MAX_CANVAS_WIDTH;
2625
+ const currentWidth = Math.min(length - chunkLeft, import_core3.MAX_CANVAS_WIDTH);
2626
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
2462
2627
  SpectrogramCanvas,
2463
2628
  {
2464
2629
  $cssWidth: currentWidth,
@@ -2472,11 +2637,11 @@ var SpectrogramChannel = ({
2472
2637
  `${length}-${i}`
2473
2638
  );
2474
2639
  });
2475
- return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(Wrapper3, { $index: index, $cssWidth: length, $waveHeight: waveHeight, children: canvases });
2640
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(Wrapper4, { $index: index, $cssWidth: length, $waveHeight: waveHeight, children: canvases });
2476
2641
  };
2477
2642
 
2478
2643
  // src/components/SmartChannel.tsx
2479
- var import_jsx_runtime22 = require("react/jsx-runtime");
2644
+ var import_jsx_runtime23 = require("react/jsx-runtime");
2480
2645
  var SmartChannel = ({
2481
2646
  isSelected,
2482
2647
  transparentBackground,
@@ -2490,10 +2655,19 @@ var SmartChannel = ({
2490
2655
  spectrogramWorkerApi,
2491
2656
  spectrogramClipId,
2492
2657
  spectrogramOnCanvasesReady,
2658
+ midiNotes,
2659
+ sampleRate: sampleRateProp,
2660
+ clipOffsetSeconds,
2493
2661
  ...props
2494
2662
  }) => {
2495
2663
  const theme = useTheme2();
2496
- const { waveHeight, barWidth, barGap, samplesPerPixel: contextSpp } = usePlaylistInfo();
2664
+ const {
2665
+ waveHeight,
2666
+ barWidth,
2667
+ barGap,
2668
+ samplesPerPixel: contextSpp,
2669
+ sampleRate: contextSampleRate
2670
+ } = usePlaylistInfo();
2497
2671
  const devicePixelRatio = useDevicePixelRatio();
2498
2672
  const samplesPerPixel = sppProp ?? contextSpp;
2499
2673
  const waveOutlineColor = isSelected && theme ? theme.selectedWaveOutlineColor : theme?.waveOutlineColor;
@@ -2501,7 +2675,7 @@ var SmartChannel = ({
2501
2675
  const drawMode = theme?.waveformDrawMode || "inverted";
2502
2676
  const hasSpectrogram = spectrogramData || spectrogramWorkerApi;
2503
2677
  if (renderMode === "spectrogram" && hasSpectrogram) {
2504
- return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
2678
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
2505
2679
  SpectrogramChannel,
2506
2680
  {
2507
2681
  index: props.index,
@@ -2522,8 +2696,8 @@ var SmartChannel = ({
2522
2696
  }
2523
2697
  if (renderMode === "both" && hasSpectrogram) {
2524
2698
  const halfHeight = Math.floor(waveHeight / 2);
2525
- return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(import_jsx_runtime22.Fragment, { children: [
2526
- /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
2699
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)(import_jsx_runtime23.Fragment, { children: [
2700
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
2527
2701
  SpectrogramChannel,
2528
2702
  {
2529
2703
  index: props.index * 2,
@@ -2542,7 +2716,7 @@ var SmartChannel = ({
2542
2716
  onCanvasesReady: spectrogramOnCanvasesReady
2543
2717
  }
2544
2718
  ),
2545
- /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
2719
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
2546
2720
  "div",
2547
2721
  {
2548
2722
  style: {
@@ -2551,7 +2725,7 @@ var SmartChannel = ({
2551
2725
  width: props.length,
2552
2726
  height: halfHeight
2553
2727
  },
2554
- children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
2728
+ children: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
2555
2729
  Channel,
2556
2730
  {
2557
2731
  ...props,
@@ -2570,7 +2744,27 @@ var SmartChannel = ({
2570
2744
  )
2571
2745
  ] });
2572
2746
  }
2573
- return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
2747
+ if (renderMode === "piano-roll") {
2748
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
2749
+ PianoRollChannel,
2750
+ {
2751
+ index: props.index,
2752
+ midiNotes: midiNotes ?? [],
2753
+ length: props.length,
2754
+ waveHeight,
2755
+ devicePixelRatio,
2756
+ samplesPerPixel,
2757
+ sampleRate: sampleRateProp ?? contextSampleRate,
2758
+ clipOffsetSeconds: clipOffsetSeconds ?? 0,
2759
+ noteColor: theme?.pianoRollNoteColor,
2760
+ selectedNoteColor: theme?.pianoRollSelectedNoteColor,
2761
+ isSelected,
2762
+ transparentBackground,
2763
+ backgroundColor: theme?.pianoRollBackgroundColor
2764
+ }
2765
+ );
2766
+ }
2767
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
2574
2768
  Channel,
2575
2769
  {
2576
2770
  ...props,
@@ -2587,11 +2781,11 @@ var SmartChannel = ({
2587
2781
  };
2588
2782
 
2589
2783
  // src/components/SpectrogramLabels.tsx
2590
- var import_react19 = require("react");
2591
- var import_styled_components21 = __toESM(require("styled-components"));
2592
- var import_jsx_runtime23 = require("react/jsx-runtime");
2784
+ var import_react20 = require("react");
2785
+ var import_styled_components22 = __toESM(require("styled-components"));
2786
+ var import_jsx_runtime24 = require("react/jsx-runtime");
2593
2787
  var LABELS_WIDTH = 72;
2594
- var LabelsStickyWrapper = import_styled_components21.default.div`
2788
+ var LabelsStickyWrapper = import_styled_components22.default.div`
2595
2789
  position: sticky;
2596
2790
  left: 0;
2597
2791
  z-index: 101;
@@ -2639,12 +2833,12 @@ var SpectrogramLabels = ({
2639
2833
  renderMode = "spectrogram",
2640
2834
  hasClipHeaders = false
2641
2835
  }) => {
2642
- const canvasRef = (0, import_react19.useRef)(null);
2836
+ const canvasRef = (0, import_react20.useRef)(null);
2643
2837
  const devicePixelRatio = useDevicePixelRatio();
2644
2838
  const spectrogramHeight = renderMode === "both" ? Math.floor(waveHeight / 2) : waveHeight;
2645
2839
  const totalHeight = numChannels * waveHeight;
2646
2840
  const clipHeaderOffset = hasClipHeaders ? 22 : 0;
2647
- (0, import_react19.useLayoutEffect)(() => {
2841
+ (0, import_react20.useLayoutEffect)(() => {
2648
2842
  const canvas = canvasRef.current;
2649
2843
  if (!canvas) return;
2650
2844
  const ctx = canvas.getContext("2d");
@@ -2682,7 +2876,7 @@ var SpectrogramLabels = ({
2682
2876
  spectrogramHeight,
2683
2877
  clipHeaderOffset
2684
2878
  ]);
2685
- return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(LabelsStickyWrapper, { $height: totalHeight + clipHeaderOffset, children: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
2879
+ return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(LabelsStickyWrapper, { $height: totalHeight + clipHeaderOffset, children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
2686
2880
  "canvas",
2687
2881
  {
2688
2882
  ref: canvasRef,
@@ -2698,11 +2892,11 @@ var SpectrogramLabels = ({
2698
2892
  };
2699
2893
 
2700
2894
  // src/components/SmartScale.tsx
2701
- var import_react21 = require("react");
2895
+ var import_react22 = require("react");
2702
2896
 
2703
2897
  // src/components/TimeScale.tsx
2704
- var import_react20 = __toESM(require("react"));
2705
- var import_styled_components22 = __toESM(require("styled-components"));
2898
+ var import_react21 = __toESM(require("react"));
2899
+ var import_styled_components23 = __toESM(require("styled-components"));
2706
2900
 
2707
2901
  // src/utils/conversions.ts
2708
2902
  function samplesToSeconds(samples, sampleRate) {
@@ -2725,18 +2919,17 @@ function secondsToPixels(seconds, samplesPerPixel, sampleRate) {
2725
2919
  }
2726
2920
 
2727
2921
  // src/components/TimeScale.tsx
2728
- var import_core3 = require("@waveform-playlist/core");
2729
- var import_jsx_runtime24 = require("react/jsx-runtime");
2922
+ var import_core4 = require("@waveform-playlist/core");
2923
+ var import_jsx_runtime25 = require("react/jsx-runtime");
2730
2924
  function formatTime2(milliseconds) {
2731
2925
  const seconds = Math.floor(milliseconds / 1e3);
2732
2926
  const s = seconds % 60;
2733
2927
  const m = (seconds - s) / 60;
2734
2928
  return `${m}:${String(s).padStart(2, "0")}`;
2735
2929
  }
2736
- var PlaylistTimeScaleScroll = import_styled_components22.default.div.attrs((props) => ({
2930
+ var PlaylistTimeScaleScroll = import_styled_components23.default.div.attrs((props) => ({
2737
2931
  style: {
2738
2932
  width: `${props.$cssWidth}px`,
2739
- marginLeft: `${props.$controlWidth}px`,
2740
2933
  height: `${props.$timeScaleHeight}px`
2741
2934
  }
2742
2935
  }))`
@@ -2745,7 +2938,7 @@ var PlaylistTimeScaleScroll = import_styled_components22.default.div.attrs((prop
2745
2938
  border-bottom: 1px solid ${(props) => props.theme.timeColor};
2746
2939
  box-sizing: border-box;
2747
2940
  `;
2748
- var TimeTickChunk = import_styled_components22.default.canvas.attrs((props) => ({
2941
+ var TimeTickChunk = import_styled_components23.default.canvas.attrs((props) => ({
2749
2942
  style: {
2750
2943
  width: `${props.$cssWidth}px`,
2751
2944
  height: `${props.$timeScaleHeight}px`,
@@ -2754,10 +2947,8 @@ var TimeTickChunk = import_styled_components22.default.canvas.attrs((props) => (
2754
2947
  }))`
2755
2948
  position: absolute;
2756
2949
  bottom: 0;
2757
- /* Promote to own compositing layer for smoother scrolling */
2758
- will-change: transform;
2759
2950
  `;
2760
- var TimeStamp = import_styled_components22.default.div.attrs((props) => ({
2951
+ var TimeStamp = import_styled_components23.default.div.attrs((props) => ({
2761
2952
  style: {
2762
2953
  left: `${props.$left + 4}px`
2763
2954
  // Offset 4px to the right of the tick
@@ -2778,14 +2969,9 @@ var TimeScale = (props) => {
2778
2969
  renderTimestamp
2779
2970
  } = props;
2780
2971
  const { canvasRef, canvasMapRef } = useChunkedCanvasRefs();
2781
- const {
2782
- sampleRate,
2783
- samplesPerPixel,
2784
- timeScaleHeight,
2785
- controls: { show: showControls, width: controlWidth }
2786
- } = (0, import_react20.useContext)(PlaylistInfoContext);
2972
+ const { sampleRate, samplesPerPixel, timeScaleHeight } = (0, import_react21.useContext)(PlaylistInfoContext);
2787
2973
  const devicePixelRatio = useDevicePixelRatio();
2788
- const { widthX, canvasInfo, timeMarkersWithPositions } = (0, import_react20.useMemo)(() => {
2974
+ const { widthX, canvasInfo, timeMarkersWithPositions } = (0, import_react21.useMemo)(() => {
2789
2975
  const nextCanvasInfo = /* @__PURE__ */ new Map();
2790
2976
  const nextMarkers = [];
2791
2977
  const nextWidthX = secondsToPixels(duration / 1e3, samplesPerPixel, sampleRate);
@@ -2796,7 +2982,7 @@ var TimeScale = (props) => {
2796
2982
  if (counter % marker === 0) {
2797
2983
  const timeMs = counter;
2798
2984
  const timestamp = formatTime2(timeMs);
2799
- const element = renderTimestamp ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_react20.default.Fragment, { children: renderTimestamp(timeMs, pix) }, `timestamp-${counter}`) : /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(TimeStamp, { $left: pix, children: timestamp }, timestamp);
2985
+ const element = renderTimestamp ? /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(import_react21.default.Fragment, { children: renderTimestamp(timeMs, pix) }, `timestamp-${counter}`) : /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(TimeStamp, { $left: pix, children: timestamp }, timestamp);
2800
2986
  nextMarkers.push({ pix, element });
2801
2987
  nextCanvasInfo.set(pix, timeScaleHeight);
2802
2988
  } else if (counter % bigStep === 0) {
@@ -2821,11 +3007,11 @@ var TimeScale = (props) => {
2821
3007
  renderTimestamp,
2822
3008
  timeScaleHeight
2823
3009
  ]);
2824
- const visibleChunkIndices = useVisibleChunkIndices(widthX, import_core3.MAX_CANVAS_WIDTH);
3010
+ const visibleChunkIndices = useVisibleChunkIndices(widthX, import_core4.MAX_CANVAS_WIDTH);
2825
3011
  const visibleChunks = visibleChunkIndices.map((i) => {
2826
- const chunkLeft = i * import_core3.MAX_CANVAS_WIDTH;
2827
- const chunkWidth = Math.min(widthX - chunkLeft, import_core3.MAX_CANVAS_WIDTH);
2828
- return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
3012
+ const chunkLeft = i * import_core4.MAX_CANVAS_WIDTH;
3013
+ const chunkWidth = Math.min(widthX - chunkLeft, import_core4.MAX_CANVAS_WIDTH);
3014
+ return /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
2829
3015
  TimeTickChunk,
2830
3016
  {
2831
3017
  $cssWidth: chunkWidth,
@@ -2839,14 +3025,14 @@ var TimeScale = (props) => {
2839
3025
  `timescale-${i}`
2840
3026
  );
2841
3027
  });
2842
- const firstChunkLeft = visibleChunkIndices.length > 0 ? visibleChunkIndices[0] * import_core3.MAX_CANVAS_WIDTH : 0;
2843
- const lastChunkRight = visibleChunkIndices.length > 0 ? (visibleChunkIndices[visibleChunkIndices.length - 1] + 1) * import_core3.MAX_CANVAS_WIDTH : Infinity;
3028
+ const firstChunkLeft = visibleChunkIndices.length > 0 ? visibleChunkIndices[0] * import_core4.MAX_CANVAS_WIDTH : 0;
3029
+ const lastChunkRight = visibleChunkIndices.length > 0 ? (visibleChunkIndices[visibleChunkIndices.length - 1] + 1) * import_core4.MAX_CANVAS_WIDTH : Infinity;
2844
3030
  const visibleMarkers = visibleChunkIndices.length > 0 ? timeMarkersWithPositions.filter(({ pix }) => pix >= firstChunkLeft && pix < lastChunkRight).map(({ element }) => element) : timeMarkersWithPositions.map(({ element }) => element);
2845
- (0, import_react20.useLayoutEffect)(() => {
3031
+ (0, import_react21.useLayoutEffect)(() => {
2846
3032
  for (const [chunkIdx, canvas] of canvasMapRef.current.entries()) {
2847
3033
  const ctx = canvas.getContext("2d");
2848
3034
  if (!ctx) continue;
2849
- const chunkLeft = chunkIdx * import_core3.MAX_CANVAS_WIDTH;
3035
+ const chunkLeft = chunkIdx * import_core4.MAX_CANVAS_WIDTH;
2850
3036
  const chunkWidth = canvas.width / devicePixelRatio;
2851
3037
  ctx.resetTransform();
2852
3038
  ctx.clearRect(0, 0, canvas.width, canvas.height);
@@ -2869,23 +3055,15 @@ var TimeScale = (props) => {
2869
3055
  canvasInfo,
2870
3056
  visibleChunkIndices
2871
3057
  ]);
2872
- return /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(
2873
- PlaylistTimeScaleScroll,
2874
- {
2875
- $cssWidth: widthX,
2876
- $controlWidth: showControls ? controlWidth : 0,
2877
- $timeScaleHeight: timeScaleHeight,
2878
- children: [
2879
- visibleMarkers,
2880
- visibleChunks
2881
- ]
2882
- }
2883
- );
3058
+ return /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(PlaylistTimeScaleScroll, { $cssWidth: widthX, $timeScaleHeight: timeScaleHeight, children: [
3059
+ visibleMarkers,
3060
+ visibleChunks
3061
+ ] });
2884
3062
  };
2885
- var StyledTimeScale = (0, import_styled_components22.withTheme)(TimeScale);
3063
+ var StyledTimeScale = (0, import_styled_components23.withTheme)(TimeScale);
2886
3064
 
2887
3065
  // src/components/SmartScale.tsx
2888
- var import_jsx_runtime25 = require("react/jsx-runtime");
3066
+ var import_jsx_runtime26 = require("react/jsx-runtime");
2889
3067
  var timeinfo = /* @__PURE__ */ new Map([
2890
3068
  [
2891
3069
  700,
@@ -2959,9 +3137,9 @@ function getScaleInfo(samplesPerPixel) {
2959
3137
  return config;
2960
3138
  }
2961
3139
  var SmartScale = ({ renderTimestamp }) => {
2962
- const { samplesPerPixel, duration } = (0, import_react21.useContext)(PlaylistInfoContext);
3140
+ const { samplesPerPixel, duration } = (0, import_react22.useContext)(PlaylistInfoContext);
2963
3141
  let config = getScaleInfo(samplesPerPixel);
2964
- return /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
3142
+ return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
2965
3143
  StyledTimeScale,
2966
3144
  {
2967
3145
  marker: config.marker,
@@ -2974,9 +3152,9 @@ var SmartScale = ({ renderTimestamp }) => {
2974
3152
  };
2975
3153
 
2976
3154
  // src/components/TimeFormatSelect.tsx
2977
- var import_styled_components23 = __toESM(require("styled-components"));
2978
- var import_jsx_runtime26 = require("react/jsx-runtime");
2979
- var SelectWrapper = import_styled_components23.default.div`
3155
+ var import_styled_components24 = __toESM(require("styled-components"));
3156
+ var import_jsx_runtime27 = require("react/jsx-runtime");
3157
+ var SelectWrapper = import_styled_components24.default.div`
2980
3158
  display: inline-flex;
2981
3159
  align-items: center;
2982
3160
  gap: 0.5rem;
@@ -2998,7 +3176,7 @@ var TimeFormatSelect = ({
2998
3176
  const handleChange = (e) => {
2999
3177
  onChange(e.target.value);
3000
3178
  };
3001
- return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(SelectWrapper, { className, children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
3179
+ return /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(SelectWrapper, { className, children: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
3002
3180
  BaseSelect,
3003
3181
  {
3004
3182
  className: "time-format",
@@ -3006,50 +3184,30 @@ var TimeFormatSelect = ({
3006
3184
  onChange: handleChange,
3007
3185
  disabled,
3008
3186
  "aria-label": "Time format selection",
3009
- children: TIME_FORMAT_OPTIONS.map((option) => /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("option", { value: option.value, children: option.label }, option.value))
3187
+ children: TIME_FORMAT_OPTIONS.map((option) => /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("option", { value: option.value, children: option.label }, option.value))
3010
3188
  }
3011
3189
  ) });
3012
3190
  };
3013
3191
 
3014
3192
  // src/components/Track.tsx
3015
- var import_styled_components24 = __toESM(require("styled-components"));
3016
- var import_jsx_runtime27 = require("react/jsx-runtime");
3017
- var Container = import_styled_components24.default.div.attrs((props) => ({
3193
+ var import_styled_components25 = __toESM(require("styled-components"));
3194
+ var import_jsx_runtime28 = require("react/jsx-runtime");
3195
+ var Container = import_styled_components25.default.div.attrs((props) => ({
3018
3196
  style: {
3019
3197
  height: `${props.$waveHeight * props.$numChannels + (props.$hasClipHeaders ? CLIP_HEADER_HEIGHT : 0)}px`
3020
3198
  }
3021
3199
  }))`
3022
3200
  position: relative;
3023
- display: flex;
3024
3201
  ${(props) => props.$width !== void 0 && `width: ${props.$width}px;`}
3025
3202
  `;
3026
- var ChannelContainer = import_styled_components24.default.div.attrs((props) => ({
3203
+ var ChannelContainer = import_styled_components25.default.div.attrs((props) => ({
3027
3204
  style: {
3028
3205
  paddingLeft: `${props.$offset || 0}px`
3029
3206
  }
3030
3207
  }))`
3031
3208
  position: relative;
3032
3209
  background: ${(props) => props.$backgroundColor || "transparent"};
3033
- flex: 1;
3034
- `;
3035
- var ControlsWrapper = import_styled_components24.default.div.attrs((props) => ({
3036
- style: {
3037
- width: `${props.$controlWidth}px`
3038
- }
3039
- }))`
3040
- position: sticky;
3041
- z-index: 102; /* Above waveform content and spectrogram labels (101), below Docusaurus navbar (200) */
3042
- left: 0;
3043
3210
  height: 100%;
3044
- flex-shrink: 0;
3045
- pointer-events: auto;
3046
- background: ${(props) => props.theme.surfaceColor};
3047
- transition: background 0.15s ease-in-out;
3048
-
3049
- /* Selected track: highlighted background */
3050
- ${(props) => props.$isSelected && `
3051
- background: ${props.theme.selectedTrackControlsBackground};
3052
- `}
3053
3211
  `;
3054
3212
  var Track = ({
3055
3213
  numChannels,
@@ -3061,44 +3219,34 @@ var Track = ({
3061
3219
  hasClipHeaders = false,
3062
3220
  onClick,
3063
3221
  trackId,
3064
- isSelected = false
3222
+ isSelected: _isSelected = false
3065
3223
  }) => {
3066
- const {
3067
- waveHeight,
3068
- controls: { show, width: controlWidth }
3069
- } = usePlaylistInfo();
3070
- const controls = useTrackControls();
3071
- return /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(
3224
+ const { waveHeight } = usePlaylistInfo();
3225
+ return /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
3072
3226
  Container,
3073
3227
  {
3074
3228
  $numChannels: numChannels,
3075
3229
  className,
3076
3230
  $waveHeight: waveHeight,
3077
- $controlWidth: show ? controlWidth : 0,
3078
3231
  $width: width,
3079
3232
  $hasClipHeaders: hasClipHeaders,
3080
- $isSelected: isSelected,
3081
- children: [
3082
- /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(ControlsWrapper, { $controlWidth: show ? controlWidth : 0, $isSelected: isSelected, children: controls }),
3083
- /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
3084
- ChannelContainer,
3085
- {
3086
- $controlWidth: show ? controlWidth : 0,
3087
- $backgroundColor: backgroundColor,
3088
- $offset: offset,
3089
- onClick,
3090
- "data-track-id": trackId,
3091
- children
3092
- }
3093
- )
3094
- ]
3233
+ children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
3234
+ ChannelContainer,
3235
+ {
3236
+ $backgroundColor: backgroundColor,
3237
+ $offset: offset,
3238
+ onClick,
3239
+ "data-track-id": trackId,
3240
+ children
3241
+ }
3242
+ )
3095
3243
  }
3096
3244
  );
3097
3245
  };
3098
3246
 
3099
3247
  // src/components/TrackControls/Button.tsx
3100
- var import_styled_components25 = __toESM(require("styled-components"));
3101
- var Button = import_styled_components25.default.button.attrs({
3248
+ var import_styled_components26 = __toESM(require("styled-components"));
3249
+ var Button = import_styled_components26.default.button.attrs({
3102
3250
  type: "button"
3103
3251
  })`
3104
3252
  display: inline-block;
@@ -3173,8 +3321,8 @@ var Button = import_styled_components25.default.button.attrs({
3173
3321
  `;
3174
3322
 
3175
3323
  // src/components/TrackControls/ButtonGroup.tsx
3176
- var import_styled_components26 = __toESM(require("styled-components"));
3177
- var ButtonGroup = import_styled_components26.default.div`
3324
+ var import_styled_components27 = __toESM(require("styled-components"));
3325
+ var ButtonGroup = import_styled_components27.default.div`
3178
3326
  margin-bottom: 0.3rem;
3179
3327
 
3180
3328
  button:not(:first-child) {
@@ -3189,10 +3337,10 @@ var ButtonGroup = import_styled_components26.default.div`
3189
3337
  `;
3190
3338
 
3191
3339
  // src/components/TrackControls/CloseButton.tsx
3192
- var import_styled_components27 = __toESM(require("styled-components"));
3193
- var import_react22 = require("@phosphor-icons/react");
3194
- var import_jsx_runtime28 = require("react/jsx-runtime");
3195
- var StyledCloseButton = import_styled_components27.default.button`
3340
+ var import_styled_components28 = __toESM(require("styled-components"));
3341
+ var import_react23 = require("@phosphor-icons/react");
3342
+ var import_jsx_runtime29 = require("react/jsx-runtime");
3343
+ var StyledCloseButton = import_styled_components28.default.button`
3196
3344
  position: absolute;
3197
3345
  left: 0;
3198
3346
  top: 0;
@@ -3215,11 +3363,11 @@ var StyledCloseButton = import_styled_components27.default.button`
3215
3363
  color: #dc3545;
3216
3364
  }
3217
3365
  `;
3218
- var CloseButton = ({ onClick, title = "Remove track" }) => /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(StyledCloseButton, { onClick, title, children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_react22.X, { size: 12, weight: "bold" }) });
3366
+ var CloseButton = ({ onClick, title = "Remove track" }) => /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(StyledCloseButton, { onClick, title, children: /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_react23.X, { size: 12, weight: "bold" }) });
3219
3367
 
3220
3368
  // src/components/TrackControls/Controls.tsx
3221
- var import_styled_components28 = __toESM(require("styled-components"));
3222
- var Controls = import_styled_components28.default.div`
3369
+ var import_styled_components29 = __toESM(require("styled-components"));
3370
+ var Controls = import_styled_components29.default.div`
3223
3371
  background: transparent;
3224
3372
  width: 100%;
3225
3373
  height: 100%;
@@ -3235,8 +3383,8 @@ var Controls = import_styled_components28.default.div`
3235
3383
  `;
3236
3384
 
3237
3385
  // src/components/TrackControls/Header.tsx
3238
- var import_styled_components29 = __toESM(require("styled-components"));
3239
- var Header = import_styled_components29.default.header`
3386
+ var import_styled_components30 = __toESM(require("styled-components"));
3387
+ var Header = import_styled_components30.default.header`
3240
3388
  overflow: hidden;
3241
3389
  height: 26px;
3242
3390
  width: 100%;
@@ -3250,28 +3398,28 @@ var Header = import_styled_components29.default.header`
3250
3398
  `;
3251
3399
 
3252
3400
  // src/components/TrackControls/VolumeDownIcon.tsx
3253
- var import_react23 = require("@phosphor-icons/react");
3254
- var import_jsx_runtime29 = require("react/jsx-runtime");
3255
- var VolumeDownIcon = (props) => /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_react23.SpeakerLowIcon, { weight: "light", ...props });
3256
-
3257
- // src/components/TrackControls/VolumeUpIcon.tsx
3258
3401
  var import_react24 = require("@phosphor-icons/react");
3259
3402
  var import_jsx_runtime30 = require("react/jsx-runtime");
3260
- var VolumeUpIcon = (props) => /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_react24.SpeakerHighIcon, { weight: "light", ...props });
3403
+ var VolumeDownIcon = (props) => /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_react24.SpeakerLowIcon, { weight: "light", ...props });
3261
3404
 
3262
- // src/components/TrackControls/TrashIcon.tsx
3405
+ // src/components/TrackControls/VolumeUpIcon.tsx
3263
3406
  var import_react25 = require("@phosphor-icons/react");
3264
3407
  var import_jsx_runtime31 = require("react/jsx-runtime");
3265
- var TrashIcon = (props) => /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(import_react25.TrashIcon, { weight: "light", ...props });
3408
+ var VolumeUpIcon = (props) => /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(import_react25.SpeakerHighIcon, { weight: "light", ...props });
3266
3409
 
3267
- // src/components/TrackControls/DotsIcon.tsx
3410
+ // src/components/TrackControls/TrashIcon.tsx
3268
3411
  var import_react26 = require("@phosphor-icons/react");
3269
3412
  var import_jsx_runtime32 = require("react/jsx-runtime");
3270
- var DotsIcon = (props) => /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_react26.DotsThreeIcon, { weight: "bold", ...props });
3413
+ var TrashIcon = (props) => /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_react26.TrashIcon, { weight: "light", ...props });
3414
+
3415
+ // src/components/TrackControls/DotsIcon.tsx
3416
+ var import_react27 = require("@phosphor-icons/react");
3417
+ var import_jsx_runtime33 = require("react/jsx-runtime");
3418
+ var DotsIcon = (props) => /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(import_react27.DotsThreeIcon, { weight: "bold", ...props });
3271
3419
 
3272
3420
  // src/components/TrackControls/Slider.tsx
3273
- var import_styled_components30 = __toESM(require("styled-components"));
3274
- var Slider = (0, import_styled_components30.default)(BaseSlider)`
3421
+ var import_styled_components31 = __toESM(require("styled-components"));
3422
+ var Slider = (0, import_styled_components31.default)(BaseSlider)`
3275
3423
  width: 75%;
3276
3424
  height: 5px;
3277
3425
  background: ${(props) => props.theme.sliderTrackColor};
@@ -3323,8 +3471,8 @@ var Slider = (0, import_styled_components30.default)(BaseSlider)`
3323
3471
  `;
3324
3472
 
3325
3473
  // src/components/TrackControls/SliderWrapper.tsx
3326
- var import_styled_components31 = __toESM(require("styled-components"));
3327
- var SliderWrapper = import_styled_components31.default.label`
3474
+ var import_styled_components32 = __toESM(require("styled-components"));
3475
+ var SliderWrapper = import_styled_components32.default.label`
3328
3476
  width: 100%;
3329
3477
  display: flex;
3330
3478
  justify-content: space-between;
@@ -3335,15 +3483,15 @@ var SliderWrapper = import_styled_components31.default.label`
3335
3483
  `;
3336
3484
 
3337
3485
  // src/components/TrackMenu.tsx
3338
- var import_react27 = __toESM(require("react"));
3486
+ var import_react28 = __toESM(require("react"));
3339
3487
  var import_react_dom = require("react-dom");
3340
- var import_styled_components32 = __toESM(require("styled-components"));
3341
- var import_jsx_runtime33 = require("react/jsx-runtime");
3342
- var MenuContainer = import_styled_components32.default.div`
3488
+ var import_styled_components33 = __toESM(require("styled-components"));
3489
+ var import_jsx_runtime34 = require("react/jsx-runtime");
3490
+ var MenuContainer = import_styled_components33.default.div`
3343
3491
  position: relative;
3344
3492
  display: inline-block;
3345
3493
  `;
3346
- var MenuButton = import_styled_components32.default.button`
3494
+ var MenuButton = import_styled_components33.default.button`
3347
3495
  background: none;
3348
3496
  border: none;
3349
3497
  cursor: pointer;
@@ -3358,7 +3506,8 @@ var MenuButton = import_styled_components32.default.button`
3358
3506
  opacity: 1;
3359
3507
  }
3360
3508
  `;
3361
- var Dropdown = import_styled_components32.default.div`
3509
+ var DROPDOWN_MIN_WIDTH = 180;
3510
+ var Dropdown = import_styled_components33.default.div`
3362
3511
  position: fixed;
3363
3512
  top: ${(p) => p.$top}px;
3364
3513
  left: ${(p) => p.$left}px;
@@ -3368,31 +3517,53 @@ var Dropdown = import_styled_components32.default.div`
3368
3517
  border: 1px solid rgba(128, 128, 128, 0.4);
3369
3518
  border-radius: 6px;
3370
3519
  padding: 0.5rem 0;
3371
- min-width: 180px;
3520
+ min-width: ${DROPDOWN_MIN_WIDTH}px;
3372
3521
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
3373
3522
  `;
3374
- var Divider = import_styled_components32.default.hr`
3523
+ var Divider = import_styled_components33.default.hr`
3375
3524
  border: none;
3376
3525
  border-top: 1px solid rgba(128, 128, 128, 0.3);
3377
3526
  margin: 0.35rem 0;
3378
3527
  `;
3379
3528
  var TrackMenu = ({ items: itemsProp }) => {
3380
- const [open, setOpen] = (0, import_react27.useState)(false);
3381
- const close = () => setOpen(false);
3529
+ const [open, setOpen] = (0, import_react28.useState)(false);
3530
+ const close = (0, import_react28.useCallback)(() => setOpen(false), []);
3382
3531
  const items = typeof itemsProp === "function" ? itemsProp(close) : itemsProp;
3383
- const [dropdownPos, setDropdownPos] = (0, import_react27.useState)({ top: 0, left: 0 });
3384
- const buttonRef = (0, import_react27.useRef)(null);
3385
- const dropdownRef = (0, import_react27.useRef)(null);
3386
- (0, import_react27.useEffect)(() => {
3387
- if (open && buttonRef.current) {
3388
- const rect = buttonRef.current.getBoundingClientRect();
3389
- setDropdownPos({
3390
- top: rect.bottom + 2,
3391
- left: Math.max(0, rect.right - 180)
3392
- });
3532
+ const [dropdownPos, setDropdownPos] = (0, import_react28.useState)({ top: 0, left: 0 });
3533
+ const buttonRef = (0, import_react28.useRef)(null);
3534
+ const dropdownRef = (0, import_react28.useRef)(null);
3535
+ const updatePosition = (0, import_react28.useCallback)(() => {
3536
+ if (!buttonRef.current) return;
3537
+ const rect = buttonRef.current.getBoundingClientRect();
3538
+ const vw = window.innerWidth;
3539
+ const vh = window.innerHeight;
3540
+ const dropHeight = dropdownRef.current?.offsetHeight ?? 160;
3541
+ let left = rect.right + 4;
3542
+ if (left + DROPDOWN_MIN_WIDTH > vw) {
3543
+ left = rect.left - DROPDOWN_MIN_WIDTH - 4;
3393
3544
  }
3394
- }, [open]);
3395
- (0, import_react27.useEffect)(() => {
3545
+ left = Math.max(4, Math.min(left, vw - DROPDOWN_MIN_WIDTH - 4));
3546
+ let top = rect.top;
3547
+ if (top + dropHeight > vh - 4) {
3548
+ top = Math.max(4, rect.bottom - dropHeight);
3549
+ }
3550
+ setDropdownPos({ top, left });
3551
+ }, []);
3552
+ (0, import_react28.useEffect)(() => {
3553
+ if (!open) return;
3554
+ updatePosition();
3555
+ const rafId = requestAnimationFrame(() => updatePosition());
3556
+ const onScroll = () => updatePosition();
3557
+ const onResize = () => updatePosition();
3558
+ window.addEventListener("scroll", onScroll, true);
3559
+ window.addEventListener("resize", onResize);
3560
+ return () => {
3561
+ cancelAnimationFrame(rafId);
3562
+ window.removeEventListener("scroll", onScroll, true);
3563
+ window.removeEventListener("resize", onResize);
3564
+ };
3565
+ }, [open, updatePosition]);
3566
+ (0, import_react28.useEffect)(() => {
3396
3567
  if (!open) return;
3397
3568
  const handleClick = (e) => {
3398
3569
  const target = e.target;
@@ -3400,11 +3571,20 @@ var TrackMenu = ({ items: itemsProp }) => {
3400
3571
  setOpen(false);
3401
3572
  }
3402
3573
  };
3574
+ const handleKeyDown = (e) => {
3575
+ if (e.key === "Escape") {
3576
+ setOpen(false);
3577
+ }
3578
+ };
3403
3579
  document.addEventListener("mousedown", handleClick);
3404
- return () => document.removeEventListener("mousedown", handleClick);
3580
+ document.addEventListener("keydown", handleKeyDown);
3581
+ return () => {
3582
+ document.removeEventListener("mousedown", handleClick);
3583
+ document.removeEventListener("keydown", handleKeyDown);
3584
+ };
3405
3585
  }, [open]);
3406
- return /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(MenuContainer, { children: [
3407
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
3586
+ return /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)(MenuContainer, { children: [
3587
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
3408
3588
  MenuButton,
3409
3589
  {
3410
3590
  ref: buttonRef,
@@ -3415,19 +3595,19 @@ var TrackMenu = ({ items: itemsProp }) => {
3415
3595
  onMouseDown: (e) => e.stopPropagation(),
3416
3596
  title: "Track menu",
3417
3597
  "aria-label": "Track menu",
3418
- children: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(DotsIcon, { size: 16 })
3598
+ children: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(DotsIcon, { size: 16 })
3419
3599
  }
3420
3600
  ),
3421
3601
  open && typeof document !== "undefined" && (0, import_react_dom.createPortal)(
3422
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
3602
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
3423
3603
  Dropdown,
3424
3604
  {
3425
3605
  ref: dropdownRef,
3426
3606
  $top: dropdownPos.top,
3427
3607
  $left: dropdownPos.left,
3428
3608
  onMouseDown: (e) => e.stopPropagation(),
3429
- children: items.map((item, index) => /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(import_react27.default.Fragment, { children: [
3430
- index > 0 && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(Divider, {}),
3609
+ children: items.map((item, index) => /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)(import_react28.default.Fragment, { children: [
3610
+ index > 0 && /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(Divider, {}),
3431
3611
  item.content
3432
3612
  ] }, item.id))
3433
3613
  }
@@ -3470,6 +3650,7 @@ var TrackMenu = ({ items: itemsProp }) => {
3470
3650
  LoopRegion,
3471
3651
  LoopRegionMarkers,
3472
3652
  MasterVolumeControl,
3653
+ PianoRollChannel,
3473
3654
  Playhead,
3474
3655
  PlayheadWithMarker,
3475
3656
  Playlist,