@waveform-playlist/ui-components 9.0.3 → 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,10 +698,12 @@ 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
708
  const resizeObserver = new ResizeObserver(() => {
674
709
  scheduleUpdate();
@@ -681,8 +716,9 @@ var ScrollViewportProvider = ({ containerRef, children }) => {
681
716
  cancelAnimationFrame(rafIdRef.current);
682
717
  rafIdRef.current = null;
683
718
  }
719
+ store.cancelPendingNotification();
684
720
  };
685
- }, [containerRef, measure, scheduleUpdate]);
721
+ }, [containerRef, scheduleUpdate, store]);
686
722
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ViewportStoreContext.Provider, { value: store, children });
687
723
  };
688
724
  var useScrollViewport = () => {
@@ -817,8 +853,6 @@ var Waveform = import_styled_components9.default.canvas.attrs((props) => ({
817
853
  }))`
818
854
  position: absolute;
819
855
  top: 0;
820
- /* Promote to own compositing layer for smoother scrolling */
821
- will-change: transform;
822
856
  /* Disable image rendering interpolation */
823
857
  image-rendering: pixelated;
824
858
  image-rendering: crisp-edges;
@@ -855,7 +889,8 @@ var Channel = (props) => {
855
889
  const { canvasRef, canvasMapRef } = useChunkedCanvasRefs();
856
890
  const clipOriginX = useClipViewportOrigin();
857
891
  const visibleChunkIndices = useVisibleChunkIndices(length, import_core.MAX_CANVAS_WIDTH, clipOriginX);
858
- (0, import_react4.useLayoutEffect)(() => {
892
+ (0, import_react4.useEffect)(() => {
893
+ const tDraw = performance.now();
859
894
  const step = barWidth + barGap;
860
895
  for (const [canvasIdx, canvas] of canvasMapRef.current.entries()) {
861
896
  const globalPixelOffset = canvasIdx * import_core.MAX_CANVAS_WIDTH;
@@ -891,6 +926,9 @@ var Channel = (props) => {
891
926
  }
892
927
  }
893
928
  }
929
+ console.log(
930
+ `[waveform] draw ch${index}: ${canvasMapRef.current.size} chunks, ${(performance.now() - tDraw).toFixed(1)}ms`
931
+ );
894
932
  }, [
895
933
  canvasMapRef,
896
934
  data,
@@ -903,7 +941,8 @@ var Channel = (props) => {
903
941
  barWidth,
904
942
  barGap,
905
943
  drawMode,
906
- visibleChunkIndices
944
+ visibleChunkIndices,
945
+ index
907
946
  ]);
908
947
  const waveforms = visibleChunkIndices.map((i) => {
909
948
  const chunkLeft = i * import_core.MAX_CANVAS_WIDTH;
@@ -1021,7 +1060,7 @@ var ClipHeaderPresentational = ({
1021
1060
  trackName,
1022
1061
  isSelected = false
1023
1062
  }) => {
1024
- 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 }) });
1025
1064
  };
1026
1065
  var ClipHeader = ({
1027
1066
  clipId,
@@ -1043,7 +1082,7 @@ var ClipHeader = ({
1043
1082
  "data-clip-id": clipId,
1044
1083
  $interactive: true,
1045
1084
  $isSelected: isSelected,
1046
- children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(TrackName, { children: trackName })
1085
+ children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(TrackName, { title: trackName, children: trackName })
1047
1086
  }
1048
1087
  );
1049
1088
  };
@@ -1264,6 +1303,7 @@ var Clip = ({
1264
1303
  "data-clip-container": "true",
1265
1304
  "data-track-id": trackId,
1266
1305
  onMouseDown,
1306
+ tabIndex: -1,
1267
1307
  children: [
1268
1308
  showHeader && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1269
1309
  ClipHeader,
@@ -1373,11 +1413,140 @@ var MasterVolumeControl = ({
1373
1413
  ] });
1374
1414
  };
1375
1415
 
1376
- // src/components/Playhead.tsx
1416
+ // src/components/PianoRollChannel.tsx
1377
1417
  var import_react8 = require("react");
1378
1418
  var import_styled_components15 = __toESM(require("styled-components"));
1419
+ var import_core2 = require("@waveform-playlist/core");
1379
1420
  var import_jsx_runtime12 = require("react/jsx-runtime");
1380
- 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) => ({
1381
1550
  style: {
1382
1551
  transform: `translate3d(${props.$position}px, 0, 0)`
1383
1552
  }
@@ -1393,9 +1562,9 @@ var PlayheadLine = import_styled_components15.default.div.attrs((props) => ({
1393
1562
  will-change: transform;
1394
1563
  `;
1395
1564
  var Playhead = ({ position, color = "#ff0000" }) => {
1396
- 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 });
1397
1566
  };
1398
- var PlayheadWithMarkerContainer = import_styled_components15.default.div`
1567
+ var PlayheadWithMarkerContainer = import_styled_components16.default.div`
1399
1568
  position: absolute;
1400
1569
  top: 0;
1401
1570
  left: 0;
@@ -1404,7 +1573,7 @@ var PlayheadWithMarkerContainer = import_styled_components15.default.div`
1404
1573
  pointer-events: none;
1405
1574
  will-change: transform;
1406
1575
  `;
1407
- var MarkerTriangle = import_styled_components15.default.div`
1576
+ var MarkerTriangle = import_styled_components16.default.div`
1408
1577
  position: absolute;
1409
1578
  top: -10px;
1410
1579
  left: -6px;
@@ -1414,7 +1583,7 @@ var MarkerTriangle = import_styled_components15.default.div`
1414
1583
  border-right: 7px solid transparent;
1415
1584
  border-top: 10px solid ${(props) => props.$color};
1416
1585
  `;
1417
- var MarkerLine = import_styled_components15.default.div`
1586
+ var MarkerLine = import_styled_components16.default.div`
1418
1587
  position: absolute;
1419
1588
  top: 0;
1420
1589
  left: 0;
@@ -1430,13 +1599,13 @@ var PlayheadWithMarker = ({
1430
1599
  audioStartPositionRef,
1431
1600
  samplesPerPixel,
1432
1601
  sampleRate,
1433
- controlsOffset,
1602
+ controlsOffset = 0,
1434
1603
  getAudioContextTime,
1435
1604
  getPlaybackTime
1436
1605
  }) => {
1437
- const containerRef = (0, import_react8.useRef)(null);
1438
- const animationFrameRef = (0, import_react8.useRef)(null);
1439
- (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)(() => {
1440
1609
  const updatePosition = () => {
1441
1610
  if (containerRef.current) {
1442
1611
  let time;
@@ -1481,35 +1650,51 @@ var PlayheadWithMarker = ({
1481
1650
  getAudioContextTime,
1482
1651
  getPlaybackTime
1483
1652
  ]);
1484
- (0, import_react8.useEffect)(() => {
1653
+ (0, import_react9.useEffect)(() => {
1485
1654
  if (!isPlaying && containerRef.current) {
1486
1655
  const time = currentTimeRef.current ?? 0;
1487
1656
  const pos = time * sampleRate / samplesPerPixel + controlsOffset;
1488
1657
  containerRef.current.style.transform = `translate3d(${pos}px, 0, 0)`;
1489
1658
  }
1490
1659
  });
1491
- return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(PlayheadWithMarkerContainer, { ref: containerRef, $color: color, children: [
1492
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(MarkerTriangle, { $color: color }),
1493
- /* @__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 })
1494
1663
  ] });
1495
1664
  };
1496
1665
 
1497
1666
  // src/components/Playlist.tsx
1498
- var import_styled_components16 = __toESM(require("styled-components"));
1499
- var import_react9 = require("react");
1500
- var import_jsx_runtime13 = require("react/jsx-runtime");
1501
- 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;
1502
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`
1503
1685
  overflow-x: auto;
1686
+ overflow-y: hidden;
1687
+ overflow-anchor: none;
1688
+ flex: 1;
1504
1689
  position: relative;
1505
1690
  `;
1506
- var ScrollContainer = import_styled_components16.default.div.attrs((props) => ({
1691
+ var ScrollContainerInner = import_styled_components17.default.div.attrs((props) => ({
1507
1692
  style: props.$width !== void 0 ? { width: `${props.$width}px` } : {}
1508
1693
  }))`
1509
1694
  position: relative;
1510
1695
  background: ${(props) => props.$backgroundColor || "transparent"};
1511
1696
  `;
1512
- var TimescaleWrapper = import_styled_components16.default.div.attrs((props) => ({
1697
+ var TimescaleWrapper = import_styled_components17.default.div.attrs((props) => ({
1513
1698
  style: props.$width ? { minWidth: `${props.$width}px` } : {}
1514
1699
  }))`
1515
1700
  background: ${(props) => props.$backgroundColor || "white"};
@@ -1517,14 +1702,14 @@ var TimescaleWrapper = import_styled_components16.default.div.attrs((props) => (
1517
1702
  position: relative;
1518
1703
  overflow: hidden; /* Constrain loop region to timescale area */
1519
1704
  `;
1520
- var TracksContainer = import_styled_components16.default.div.attrs((props) => ({
1705
+ var TracksContainer = import_styled_components17.default.div.attrs((props) => ({
1521
1706
  style: props.$width !== void 0 ? { minWidth: `${props.$width}px` } : {}
1522
1707
  }))`
1523
1708
  position: relative;
1524
1709
  background: ${(props) => props.$backgroundColor || "transparent"};
1525
1710
  width: 100%;
1526
1711
  `;
1527
- var ClickOverlay = import_styled_components16.default.div`
1712
+ var ClickOverlay = import_styled_components17.default.div`
1528
1713
  position: absolute;
1529
1714
  top: 0;
1530
1715
  left: 0;
@@ -1541,7 +1726,6 @@ var Playlist = ({
1541
1726
  timescale,
1542
1727
  timescaleWidth,
1543
1728
  tracksWidth,
1544
- scrollContainerWidth,
1545
1729
  controlsWidth,
1546
1730
  onTracksClick,
1547
1731
  onTracksMouseDown,
@@ -1549,40 +1733,48 @@ var Playlist = ({
1549
1733
  onTracksMouseUp,
1550
1734
  scrollContainerRef,
1551
1735
  isSelecting,
1552
- "data-playlist-state": playlistState
1736
+ "data-playlist-state": playlistState,
1737
+ trackControlsSlots,
1738
+ timescaleGapHeight = 0
1553
1739
  }) => {
1554
- const wrapperRef = (0, import_react9.useRef)(null);
1555
- const handleRef = (0, import_react9.useCallback)(
1740
+ const scrollAreaRef = (0, import_react10.useRef)(null);
1741
+ const handleRef = (0, import_react10.useCallback)(
1556
1742
  (el) => {
1557
- wrapperRef.current = el;
1743
+ scrollAreaRef.current = el;
1558
1744
  scrollContainerRef?.(el);
1559
1745
  },
1560
1746
  [scrollContainerRef]
1561
1747
  );
1562
- 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: [
1563
- timescale && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(TimescaleWrapper, { $width: timescaleWidth, $backgroundColor: timescaleBackgroundColor, children: timescale }),
1564
- /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(TracksContainer, { $width: tracksWidth, $backgroundColor: backgroundColor, children: [
1565
- children,
1566
- (onTracksClick || onTracksMouseDown) && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1567
- ClickOverlay,
1568
- {
1569
- $controlsWidth: controlsWidth,
1570
- $isSelecting: isSelecting,
1571
- onClick: onTracksClick,
1572
- onMouseDown: onTracksMouseDown,
1573
- onMouseMove: onTracksMouseMove,
1574
- onMouseUp: onTracksMouseUp
1575
- }
1576
- )
1577
- ] })
1578
- ] }) }) });
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
+ ] });
1579
1771
  };
1580
- var StyledPlaylist = (0, import_styled_components16.withTheme)(Playlist);
1772
+ var StyledPlaylist = (0, import_styled_components17.withTheme)(Playlist);
1581
1773
 
1582
1774
  // src/components/Selection.tsx
1583
- var import_styled_components17 = __toESM(require("styled-components"));
1584
- var import_jsx_runtime14 = require("react/jsx-runtime");
1585
- 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) => ({
1586
1778
  style: {
1587
1779
  left: `${props.$left}px`,
1588
1780
  width: `${props.$width}px`
@@ -1605,14 +1797,14 @@ var Selection = ({
1605
1797
  if (width <= 0) {
1606
1798
  return null;
1607
1799
  }
1608
- 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 });
1609
1801
  };
1610
1802
 
1611
1803
  // src/components/LoopRegion.tsx
1612
- var import_react10 = require("react");
1613
- var import_styled_components18 = __toESM(require("styled-components"));
1614
- var import_jsx_runtime15 = require("react/jsx-runtime");
1615
- 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) => ({
1616
1808
  style: {
1617
1809
  left: `${props.$left}px`,
1618
1810
  width: `${props.$width}px`
@@ -1625,7 +1817,7 @@ var LoopRegionOverlayDiv = import_styled_components18.default.div.attrs((props)
1625
1817
  z-index: 55; /* Between clips (z-index: 50) and selection (z-index: 60) */
1626
1818
  pointer-events: none;
1627
1819
  `;
1628
- var LoopMarker = import_styled_components18.default.div.attrs((props) => ({
1820
+ var LoopMarker = import_styled_components19.default.div.attrs((props) => ({
1629
1821
  style: {
1630
1822
  left: `${props.$left}px`
1631
1823
  }
@@ -1660,8 +1852,8 @@ var LoopRegion = ({
1660
1852
  if (width <= 0) {
1661
1853
  return null;
1662
1854
  }
1663
- return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_jsx_runtime15.Fragment, { children: [
1664
- /* @__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)(
1665
1857
  LoopRegionOverlayDiv,
1666
1858
  {
1667
1859
  $left: startPosition,
@@ -1670,7 +1862,7 @@ var LoopRegion = ({
1670
1862
  "data-loop-region": true
1671
1863
  }
1672
1864
  ),
1673
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
1865
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1674
1866
  LoopMarker,
1675
1867
  {
1676
1868
  $left: startPosition,
@@ -1679,7 +1871,7 @@ var LoopRegion = ({
1679
1871
  "data-loop-marker": "start"
1680
1872
  }
1681
1873
  ),
1682
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
1874
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1683
1875
  LoopMarker,
1684
1876
  {
1685
1877
  $left: endPosition - 2,
@@ -1690,7 +1882,7 @@ var LoopRegion = ({
1690
1882
  )
1691
1883
  ] });
1692
1884
  };
1693
- var DraggableMarkerHandle = import_styled_components18.default.div.attrs((props) => ({
1885
+ var DraggableMarkerHandle = import_styled_components19.default.div.attrs((props) => ({
1694
1886
  style: {
1695
1887
  left: `${props.$left}px`
1696
1888
  }
@@ -1732,7 +1924,7 @@ var DraggableMarkerHandle = import_styled_components18.default.div.attrs((props)
1732
1924
  opacity: 1;
1733
1925
  }
1734
1926
  `;
1735
- var TimescaleLoopShade = import_styled_components18.default.div.attrs((props) => ({
1927
+ var TimescaleLoopShade = import_styled_components19.default.div.attrs((props) => ({
1736
1928
  style: {
1737
1929
  left: `${props.$left}px`,
1738
1930
  width: `${props.$width}px`
@@ -1760,12 +1952,12 @@ var LoopRegionMarkers = ({
1760
1952
  minPosition = 0,
1761
1953
  maxPosition = Infinity
1762
1954
  }) => {
1763
- const [draggingMarker, setDraggingMarker] = (0, import_react10.useState)(null);
1764
- const dragStartX = (0, import_react10.useRef)(0);
1765
- const dragStartPosition = (0, import_react10.useRef)(0);
1766
- 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);
1767
1959
  const width = Math.max(0, endPosition - startPosition);
1768
- const handleMarkerMouseDown = (0, import_react10.useCallback)(
1960
+ const handleMarkerMouseDown = (0, import_react11.useCallback)(
1769
1961
  (e, marker) => {
1770
1962
  e.preventDefault();
1771
1963
  e.stopPropagation();
@@ -1793,7 +1985,7 @@ var LoopRegionMarkers = ({
1793
1985
  },
1794
1986
  [startPosition, endPosition, minPosition, maxPosition, onLoopStartChange, onLoopEndChange]
1795
1987
  );
1796
- const handleRegionMouseDown = (0, import_react10.useCallback)(
1988
+ const handleRegionMouseDown = (0, import_react11.useCallback)(
1797
1989
  (e) => {
1798
1990
  e.preventDefault();
1799
1991
  e.stopPropagation();
@@ -1829,8 +2021,8 @@ var LoopRegionMarkers = ({
1829
2021
  if (width <= 0) {
1830
2022
  return null;
1831
2023
  }
1832
- return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_jsx_runtime15.Fragment, { children: [
1833
- /* @__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)(
1834
2026
  TimescaleLoopShade,
1835
2027
  {
1836
2028
  $left: startPosition,
@@ -1841,7 +2033,7 @@ var LoopRegionMarkers = ({
1841
2033
  "data-loop-region-timescale": true
1842
2034
  }
1843
2035
  ),
1844
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
2036
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1845
2037
  DraggableMarkerHandle,
1846
2038
  {
1847
2039
  $left: startPosition,
@@ -1852,7 +2044,7 @@ var LoopRegionMarkers = ({
1852
2044
  "data-loop-marker-handle": "start"
1853
2045
  }
1854
2046
  ),
1855
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
2047
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1856
2048
  DraggableMarkerHandle,
1857
2049
  {
1858
2050
  $left: endPosition,
@@ -1865,13 +2057,10 @@ var LoopRegionMarkers = ({
1865
2057
  )
1866
2058
  ] });
1867
2059
  };
1868
- var TimescaleLoopCreator = import_styled_components18.default.div.attrs((props) => ({
1869
- style: {
1870
- left: `${props.$leftOffset || 0}px`
1871
- }
1872
- }))`
2060
+ var TimescaleLoopCreator = import_styled_components19.default.div`
1873
2061
  position: absolute;
1874
2062
  top: 0;
2063
+ left: 0;
1875
2064
  right: 0;
1876
2065
  height: 100%; /* Stay within timescale bounds, don't extend into tracks */
1877
2066
  cursor: crosshair;
@@ -1884,14 +2073,13 @@ var TimescaleLoopRegion = ({
1884
2073
  regionColor = "rgba(59, 130, 246, 0.3)",
1885
2074
  onLoopRegionChange,
1886
2075
  minPosition = 0,
1887
- maxPosition = Infinity,
1888
- controlsOffset = 0
2076
+ maxPosition = Infinity
1889
2077
  }) => {
1890
- const [, setIsCreating] = (0, import_react10.useState)(false);
1891
- const createStartX = (0, import_react10.useRef)(0);
1892
- 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);
1893
2081
  const hasLoopRegion = endPosition > startPosition;
1894
- const handleBackgroundMouseDown = (0, import_react10.useCallback)(
2082
+ const handleBackgroundMouseDown = (0, import_react11.useCallback)(
1895
2083
  (e) => {
1896
2084
  const target = e.target;
1897
2085
  if (target.closest("[data-loop-marker-handle]") || target.closest("[data-loop-region-timescale]")) {
@@ -1922,14 +2110,13 @@ var TimescaleLoopRegion = ({
1922
2110
  },
1923
2111
  [minPosition, maxPosition, onLoopRegionChange]
1924
2112
  );
1925
- return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
2113
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1926
2114
  TimescaleLoopCreator,
1927
2115
  {
1928
2116
  ref: containerRef,
1929
- $leftOffset: controlsOffset,
1930
2117
  onMouseDown: handleBackgroundMouseDown,
1931
2118
  "data-timescale-loop-creator": true,
1932
- children: hasLoopRegion && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
2119
+ children: hasLoopRegion && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1933
2120
  LoopRegionMarkers,
1934
2121
  {
1935
2122
  startPosition,
@@ -1948,10 +2135,10 @@ var TimescaleLoopRegion = ({
1948
2135
  };
1949
2136
 
1950
2137
  // src/components/SelectionTimeInputs.tsx
1951
- var import_react12 = require("react");
2138
+ var import_react13 = require("react");
1952
2139
 
1953
2140
  // src/components/TimeInput.tsx
1954
- var import_react11 = require("react");
2141
+ var import_react12 = require("react");
1955
2142
 
1956
2143
  // src/utils/timeFormat.ts
1957
2144
  function clockFormat(seconds, decimals) {
@@ -2001,7 +2188,7 @@ function parseTime(timeStr, format) {
2001
2188
  }
2002
2189
 
2003
2190
  // src/components/TimeInput.tsx
2004
- var import_jsx_runtime16 = require("react/jsx-runtime");
2191
+ var import_jsx_runtime17 = require("react/jsx-runtime");
2005
2192
  var TimeInput = ({
2006
2193
  id,
2007
2194
  label,
@@ -2011,8 +2198,8 @@ var TimeInput = ({
2011
2198
  onChange,
2012
2199
  readOnly = false
2013
2200
  }) => {
2014
- const [displayValue, setDisplayValue] = (0, import_react11.useState)("");
2015
- (0, import_react11.useEffect)(() => {
2201
+ const [displayValue, setDisplayValue] = (0, import_react12.useState)("");
2202
+ (0, import_react12.useEffect)(() => {
2016
2203
  const formatted = formatTime(value, format);
2017
2204
  setDisplayValue(formatted);
2018
2205
  }, [value, format, id]);
@@ -2032,9 +2219,9 @@ var TimeInput = ({
2032
2219
  e.currentTarget.blur();
2033
2220
  }
2034
2221
  };
2035
- return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_jsx_runtime16.Fragment, { children: [
2036
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(ScreenReaderOnly, { as: "label", htmlFor: id, children: label }),
2037
- /* @__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)(
2038
2225
  BaseInput,
2039
2226
  {
2040
2227
  type: "text",
@@ -2051,15 +2238,15 @@ var TimeInput = ({
2051
2238
  };
2052
2239
 
2053
2240
  // src/components/SelectionTimeInputs.tsx
2054
- var import_jsx_runtime17 = require("react/jsx-runtime");
2241
+ var import_jsx_runtime18 = require("react/jsx-runtime");
2055
2242
  var SelectionTimeInputs = ({
2056
2243
  selectionStart,
2057
2244
  selectionEnd,
2058
2245
  onSelectionChange,
2059
2246
  className
2060
2247
  }) => {
2061
- const [timeFormat, setTimeFormat] = (0, import_react12.useState)("hh:mm:ss.uuu");
2062
- (0, import_react12.useEffect)(() => {
2248
+ const [timeFormat, setTimeFormat] = (0, import_react13.useState)("hh:mm:ss.uuu");
2249
+ (0, import_react13.useEffect)(() => {
2063
2250
  const timeFormatSelect = document.querySelector(".time-format");
2064
2251
  const handleFormatChange = () => {
2065
2252
  if (timeFormatSelect) {
@@ -2084,8 +2271,8 @@ var SelectionTimeInputs = ({
2084
2271
  onSelectionChange(selectionStart, value);
2085
2272
  }
2086
2273
  };
2087
- return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className, children: [
2088
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
2274
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className, children: [
2275
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
2089
2276
  TimeInput,
2090
2277
  {
2091
2278
  id: "audio_start",
@@ -2096,7 +2283,7 @@ var SelectionTimeInputs = ({
2096
2283
  onChange: handleStartChange
2097
2284
  }
2098
2285
  ),
2099
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
2286
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
2100
2287
  TimeInput,
2101
2288
  {
2102
2289
  id: "audio_end",
@@ -2111,14 +2298,14 @@ var SelectionTimeInputs = ({
2111
2298
  };
2112
2299
 
2113
2300
  // src/contexts/DevicePixelRatio.tsx
2114
- var import_react13 = require("react");
2115
- var import_jsx_runtime18 = require("react/jsx-runtime");
2301
+ var import_react14 = require("react");
2302
+ var import_jsx_runtime19 = require("react/jsx-runtime");
2116
2303
  function getScale() {
2117
2304
  return window.devicePixelRatio;
2118
2305
  }
2119
- var DevicePixelRatioContext = (0, import_react13.createContext)(getScale());
2306
+ var DevicePixelRatioContext = (0, import_react14.createContext)(getScale());
2120
2307
  var DevicePixelRatioProvider = ({ children }) => {
2121
- const [scale, setScale] = (0, import_react13.useState)(getScale());
2308
+ const [scale, setScale] = (0, import_react14.useState)(getScale());
2122
2309
  matchMedia(`(resolution: ${getScale()}dppx)`).addEventListener(
2123
2310
  "change",
2124
2311
  () => {
@@ -2126,13 +2313,13 @@ var DevicePixelRatioProvider = ({ children }) => {
2126
2313
  },
2127
2314
  { once: true }
2128
2315
  );
2129
- 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 });
2130
2317
  };
2131
- var useDevicePixelRatio = () => (0, import_react13.useContext)(DevicePixelRatioContext);
2318
+ var useDevicePixelRatio = () => (0, import_react14.useContext)(DevicePixelRatioContext);
2132
2319
 
2133
2320
  // src/contexts/PlaylistInfo.tsx
2134
- var import_react14 = require("react");
2135
- var PlaylistInfoContext = (0, import_react14.createContext)({
2321
+ var import_react15 = require("react");
2322
+ var PlaylistInfoContext = (0, import_react15.createContext)({
2136
2323
  sampleRate: 48e3,
2137
2324
  samplesPerPixel: 1e3,
2138
2325
  zoomLevels: [1e3, 1500, 2e3, 2500],
@@ -2146,22 +2333,22 @@ var PlaylistInfoContext = (0, import_react14.createContext)({
2146
2333
  barWidth: 1,
2147
2334
  barGap: 0
2148
2335
  });
2149
- var usePlaylistInfo = () => (0, import_react14.useContext)(PlaylistInfoContext);
2336
+ var usePlaylistInfo = () => (0, import_react15.useContext)(PlaylistInfoContext);
2150
2337
 
2151
2338
  // src/contexts/Theme.tsx
2152
- var import_react15 = require("react");
2153
- var import_styled_components19 = require("styled-components");
2154
- var useTheme2 = () => (0, import_react15.useContext)(import_styled_components19.ThemeContext);
2155
-
2156
- // src/contexts/TrackControls.tsx
2157
2339
  var import_react16 = require("react");
2158
- var import_jsx_runtime19 = require("react/jsx-runtime");
2159
- var TrackControlsContext = (0, import_react16.createContext)(/* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_react16.Fragment, {}));
2160
- 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);
2161
2342
 
2162
- // src/contexts/Playout.tsx
2343
+ // src/contexts/TrackControls.tsx
2163
2344
  var import_react17 = require("react");
2164
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");
2165
2352
  var defaultProgress = 0;
2166
2353
  var defaultIsPlaying = false;
2167
2354
  var defaultSelectionStart = 0;
@@ -2172,8 +2359,8 @@ var defaultPlayout = {
2172
2359
  selectionStart: defaultSelectionStart,
2173
2360
  selectionEnd: defaultSelectionEnd
2174
2361
  };
2175
- var PlayoutStatusContext = (0, import_react17.createContext)(defaultPlayout);
2176
- var PlayoutStatusUpdateContext = (0, import_react17.createContext)({
2362
+ var PlayoutStatusContext = (0, import_react18.createContext)(defaultPlayout);
2363
+ var PlayoutStatusUpdateContext = (0, import_react18.createContext)({
2177
2364
  setIsPlaying: () => {
2178
2365
  },
2179
2366
  setProgress: () => {
@@ -2182,26 +2369,26 @@ var PlayoutStatusUpdateContext = (0, import_react17.createContext)({
2182
2369
  }
2183
2370
  });
2184
2371
  var PlayoutProvider = ({ children }) => {
2185
- const [isPlaying, setIsPlaying] = (0, import_react17.useState)(defaultIsPlaying);
2186
- const [progress, setProgress] = (0, import_react17.useState)(defaultProgress);
2187
- const [selectionStart, setSelectionStart] = (0, import_react17.useState)(defaultSelectionStart);
2188
- 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);
2189
2376
  const setSelection = (start, end) => {
2190
2377
  setSelectionStart(start);
2191
2378
  setSelectionEnd(end);
2192
2379
  };
2193
- 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 }) });
2194
2381
  };
2195
- var usePlayoutStatus = () => (0, import_react17.useContext)(PlayoutStatusContext);
2196
- var usePlayoutStatusUpdate = () => (0, import_react17.useContext)(PlayoutStatusUpdateContext);
2382
+ var usePlayoutStatus = () => (0, import_react18.useContext)(PlayoutStatusContext);
2383
+ var usePlayoutStatusUpdate = () => (0, import_react18.useContext)(PlayoutStatusUpdateContext);
2197
2384
 
2198
2385
  // src/components/SpectrogramChannel.tsx
2199
- var import_react18 = require("react");
2200
- var import_styled_components20 = __toESM(require("styled-components"));
2201
- var import_core2 = require("@waveform-playlist/core");
2202
- 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");
2203
2390
  var LINEAR_FREQUENCY_SCALE = (f, minF, maxF) => (f - minF) / (maxF - minF);
2204
- var Wrapper3 = import_styled_components20.default.div.attrs((props) => ({
2391
+ var Wrapper4 = import_styled_components21.default.div.attrs((props) => ({
2205
2392
  style: {
2206
2393
  top: `${props.$waveHeight * props.$index}px`,
2207
2394
  width: `${props.$cssWidth}px`,
@@ -2213,7 +2400,7 @@ var Wrapper3 = import_styled_components20.default.div.attrs((props) => ({
2213
2400
  transform: translateZ(0);
2214
2401
  backface-visibility: hidden;
2215
2402
  `;
2216
- var SpectrogramCanvas = import_styled_components20.default.canvas.attrs((props) => ({
2403
+ var SpectrogramCanvas = import_styled_components21.default.canvas.attrs((props) => ({
2217
2404
  style: {
2218
2405
  width: `${props.$cssWidth}px`,
2219
2406
  height: `${props.$waveHeight}px`,
@@ -2222,8 +2409,6 @@ var SpectrogramCanvas = import_styled_components20.default.canvas.attrs((props)
2222
2409
  }))`
2223
2410
  position: absolute;
2224
2411
  top: 0;
2225
- /* Promote to own compositing layer for smoother scrolling */
2226
- will-change: transform;
2227
2412
  image-rendering: pixelated;
2228
2413
  image-rendering: crisp-edges;
2229
2414
  `;
@@ -2253,24 +2438,24 @@ var SpectrogramChannel = ({
2253
2438
  }) => {
2254
2439
  const channelIndex = channelIndexProp ?? index;
2255
2440
  const { canvasRef, canvasMapRef } = useChunkedCanvasRefs();
2256
- const registeredIdsRef = (0, import_react18.useRef)([]);
2257
- const transferredCanvasesRef = (0, import_react18.useRef)(/* @__PURE__ */ new WeakSet());
2258
- const workerApiRef = (0, import_react18.useRef)(workerApi);
2259
- 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);
2260
2445
  const isWorkerMode = !!(workerApi && clipId);
2261
2446
  const clipOriginX = useClipViewportOrigin();
2262
- const visibleChunkIndices = useVisibleChunkIndices(length, import_core2.MAX_CANVAS_WIDTH, clipOriginX);
2447
+ const visibleChunkIndices = useVisibleChunkIndices(length, import_core3.MAX_CANVAS_WIDTH, clipOriginX);
2263
2448
  const lut = colorLUT ?? DEFAULT_COLOR_LUT;
2264
2449
  const maxF = maxFrequency ?? (data ? data.sampleRate / 2 : 22050);
2265
2450
  const scaleFn = frequencyScaleFn ?? LINEAR_FREQUENCY_SCALE;
2266
2451
  const hasCustomFrequencyScale = Boolean(frequencyScaleFn);
2267
- (0, import_react18.useEffect)(() => {
2452
+ (0, import_react19.useEffect)(() => {
2268
2453
  workerApiRef.current = workerApi;
2269
2454
  }, [workerApi]);
2270
- (0, import_react18.useEffect)(() => {
2455
+ (0, import_react19.useEffect)(() => {
2271
2456
  onCanvasesReadyRef.current = onCanvasesReady;
2272
2457
  }, [onCanvasesReady]);
2273
- (0, import_react18.useEffect)(() => {
2458
+ (0, import_react19.useEffect)(() => {
2274
2459
  if (!isWorkerMode) return;
2275
2460
  const currentWorkerApi = workerApiRef.current;
2276
2461
  if (!currentWorkerApi || !clipId) return;
@@ -2325,15 +2510,15 @@ var SpectrogramChannel = ({
2325
2510
  const match = id.match(/chunk(\d+)$/);
2326
2511
  if (!match) {
2327
2512
  console.warn(`[spectrogram] Unexpected canvas ID format: ${id}`);
2328
- return import_core2.MAX_CANVAS_WIDTH;
2513
+ return import_core3.MAX_CANVAS_WIDTH;
2329
2514
  }
2330
2515
  const chunkIdx = parseInt(match[1], 10);
2331
- 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);
2332
2517
  });
2333
2518
  onCanvasesReadyRef.current?.(allIds, allWidths);
2334
2519
  }
2335
2520
  }, [canvasMapRef, isWorkerMode, clipId, channelIndex, length, visibleChunkIndices]);
2336
- (0, import_react18.useEffect)(() => {
2521
+ (0, import_react19.useEffect)(() => {
2337
2522
  return () => {
2338
2523
  const api = workerApiRef.current;
2339
2524
  if (!api) return;
@@ -2347,7 +2532,7 @@ var SpectrogramChannel = ({
2347
2532
  registeredIdsRef.current = [];
2348
2533
  };
2349
2534
  }, []);
2350
- (0, import_react18.useLayoutEffect)(() => {
2535
+ (0, import_react19.useEffect)(() => {
2351
2536
  if (isWorkerMode || !data) return;
2352
2537
  const {
2353
2538
  frequencyBinCount,
@@ -2360,7 +2545,7 @@ var SpectrogramChannel = ({
2360
2545
  const rangeDb = rawRangeDb === 0 ? 1 : rawRangeDb;
2361
2546
  const binToFreq = (bin) => bin / frequencyBinCount * (sampleRate / 2);
2362
2547
  for (const [canvasIdx, canvas] of canvasMapRef.current.entries()) {
2363
- const globalPixelOffset = canvasIdx * import_core2.MAX_CANVAS_WIDTH;
2548
+ const globalPixelOffset = canvasIdx * import_core3.MAX_CANVAS_WIDTH;
2364
2549
  const ctx = canvas.getContext("2d");
2365
2550
  if (!ctx) continue;
2366
2551
  const canvasWidth = canvas.width / devicePixelRatio;
@@ -2436,9 +2621,9 @@ var SpectrogramChannel = ({
2436
2621
  visibleChunkIndices
2437
2622
  ]);
2438
2623
  const canvases = visibleChunkIndices.map((i) => {
2439
- const chunkLeft = i * import_core2.MAX_CANVAS_WIDTH;
2440
- const currentWidth = Math.min(length - chunkLeft, import_core2.MAX_CANVAS_WIDTH);
2441
- 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)(
2442
2627
  SpectrogramCanvas,
2443
2628
  {
2444
2629
  $cssWidth: currentWidth,
@@ -2452,11 +2637,11 @@ var SpectrogramChannel = ({
2452
2637
  `${length}-${i}`
2453
2638
  );
2454
2639
  });
2455
- 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 });
2456
2641
  };
2457
2642
 
2458
2643
  // src/components/SmartChannel.tsx
2459
- var import_jsx_runtime22 = require("react/jsx-runtime");
2644
+ var import_jsx_runtime23 = require("react/jsx-runtime");
2460
2645
  var SmartChannel = ({
2461
2646
  isSelected,
2462
2647
  transparentBackground,
@@ -2470,10 +2655,19 @@ var SmartChannel = ({
2470
2655
  spectrogramWorkerApi,
2471
2656
  spectrogramClipId,
2472
2657
  spectrogramOnCanvasesReady,
2658
+ midiNotes,
2659
+ sampleRate: sampleRateProp,
2660
+ clipOffsetSeconds,
2473
2661
  ...props
2474
2662
  }) => {
2475
2663
  const theme = useTheme2();
2476
- const { waveHeight, barWidth, barGap, samplesPerPixel: contextSpp } = usePlaylistInfo();
2664
+ const {
2665
+ waveHeight,
2666
+ barWidth,
2667
+ barGap,
2668
+ samplesPerPixel: contextSpp,
2669
+ sampleRate: contextSampleRate
2670
+ } = usePlaylistInfo();
2477
2671
  const devicePixelRatio = useDevicePixelRatio();
2478
2672
  const samplesPerPixel = sppProp ?? contextSpp;
2479
2673
  const waveOutlineColor = isSelected && theme ? theme.selectedWaveOutlineColor : theme?.waveOutlineColor;
@@ -2481,7 +2675,7 @@ var SmartChannel = ({
2481
2675
  const drawMode = theme?.waveformDrawMode || "inverted";
2482
2676
  const hasSpectrogram = spectrogramData || spectrogramWorkerApi;
2483
2677
  if (renderMode === "spectrogram" && hasSpectrogram) {
2484
- return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
2678
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
2485
2679
  SpectrogramChannel,
2486
2680
  {
2487
2681
  index: props.index,
@@ -2502,8 +2696,8 @@ var SmartChannel = ({
2502
2696
  }
2503
2697
  if (renderMode === "both" && hasSpectrogram) {
2504
2698
  const halfHeight = Math.floor(waveHeight / 2);
2505
- return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(import_jsx_runtime22.Fragment, { children: [
2506
- /* @__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)(
2507
2701
  SpectrogramChannel,
2508
2702
  {
2509
2703
  index: props.index * 2,
@@ -2522,7 +2716,7 @@ var SmartChannel = ({
2522
2716
  onCanvasesReady: spectrogramOnCanvasesReady
2523
2717
  }
2524
2718
  ),
2525
- /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
2719
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
2526
2720
  "div",
2527
2721
  {
2528
2722
  style: {
@@ -2531,7 +2725,7 @@ var SmartChannel = ({
2531
2725
  width: props.length,
2532
2726
  height: halfHeight
2533
2727
  },
2534
- children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
2728
+ children: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
2535
2729
  Channel,
2536
2730
  {
2537
2731
  ...props,
@@ -2550,7 +2744,27 @@ var SmartChannel = ({
2550
2744
  )
2551
2745
  ] });
2552
2746
  }
2553
- 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)(
2554
2768
  Channel,
2555
2769
  {
2556
2770
  ...props,
@@ -2567,11 +2781,11 @@ var SmartChannel = ({
2567
2781
  };
2568
2782
 
2569
2783
  // src/components/SpectrogramLabels.tsx
2570
- var import_react19 = require("react");
2571
- var import_styled_components21 = __toESM(require("styled-components"));
2572
- 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");
2573
2787
  var LABELS_WIDTH = 72;
2574
- var LabelsStickyWrapper = import_styled_components21.default.div`
2788
+ var LabelsStickyWrapper = import_styled_components22.default.div`
2575
2789
  position: sticky;
2576
2790
  left: 0;
2577
2791
  z-index: 101;
@@ -2619,12 +2833,12 @@ var SpectrogramLabels = ({
2619
2833
  renderMode = "spectrogram",
2620
2834
  hasClipHeaders = false
2621
2835
  }) => {
2622
- const canvasRef = (0, import_react19.useRef)(null);
2836
+ const canvasRef = (0, import_react20.useRef)(null);
2623
2837
  const devicePixelRatio = useDevicePixelRatio();
2624
2838
  const spectrogramHeight = renderMode === "both" ? Math.floor(waveHeight / 2) : waveHeight;
2625
2839
  const totalHeight = numChannels * waveHeight;
2626
2840
  const clipHeaderOffset = hasClipHeaders ? 22 : 0;
2627
- (0, import_react19.useLayoutEffect)(() => {
2841
+ (0, import_react20.useLayoutEffect)(() => {
2628
2842
  const canvas = canvasRef.current;
2629
2843
  if (!canvas) return;
2630
2844
  const ctx = canvas.getContext("2d");
@@ -2662,7 +2876,7 @@ var SpectrogramLabels = ({
2662
2876
  spectrogramHeight,
2663
2877
  clipHeaderOffset
2664
2878
  ]);
2665
- 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)(
2666
2880
  "canvas",
2667
2881
  {
2668
2882
  ref: canvasRef,
@@ -2678,11 +2892,11 @@ var SpectrogramLabels = ({
2678
2892
  };
2679
2893
 
2680
2894
  // src/components/SmartScale.tsx
2681
- var import_react21 = require("react");
2895
+ var import_react22 = require("react");
2682
2896
 
2683
2897
  // src/components/TimeScale.tsx
2684
- var import_react20 = __toESM(require("react"));
2685
- var import_styled_components22 = __toESM(require("styled-components"));
2898
+ var import_react21 = __toESM(require("react"));
2899
+ var import_styled_components23 = __toESM(require("styled-components"));
2686
2900
 
2687
2901
  // src/utils/conversions.ts
2688
2902
  function samplesToSeconds(samples, sampleRate) {
@@ -2705,18 +2919,17 @@ function secondsToPixels(seconds, samplesPerPixel, sampleRate) {
2705
2919
  }
2706
2920
 
2707
2921
  // src/components/TimeScale.tsx
2708
- var import_core3 = require("@waveform-playlist/core");
2709
- 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");
2710
2924
  function formatTime2(milliseconds) {
2711
2925
  const seconds = Math.floor(milliseconds / 1e3);
2712
2926
  const s = seconds % 60;
2713
2927
  const m = (seconds - s) / 60;
2714
2928
  return `${m}:${String(s).padStart(2, "0")}`;
2715
2929
  }
2716
- var PlaylistTimeScaleScroll = import_styled_components22.default.div.attrs((props) => ({
2930
+ var PlaylistTimeScaleScroll = import_styled_components23.default.div.attrs((props) => ({
2717
2931
  style: {
2718
2932
  width: `${props.$cssWidth}px`,
2719
- marginLeft: `${props.$controlWidth}px`,
2720
2933
  height: `${props.$timeScaleHeight}px`
2721
2934
  }
2722
2935
  }))`
@@ -2725,7 +2938,7 @@ var PlaylistTimeScaleScroll = import_styled_components22.default.div.attrs((prop
2725
2938
  border-bottom: 1px solid ${(props) => props.theme.timeColor};
2726
2939
  box-sizing: border-box;
2727
2940
  `;
2728
- var TimeTickChunk = import_styled_components22.default.canvas.attrs((props) => ({
2941
+ var TimeTickChunk = import_styled_components23.default.canvas.attrs((props) => ({
2729
2942
  style: {
2730
2943
  width: `${props.$cssWidth}px`,
2731
2944
  height: `${props.$timeScaleHeight}px`,
@@ -2734,10 +2947,8 @@ var TimeTickChunk = import_styled_components22.default.canvas.attrs((props) => (
2734
2947
  }))`
2735
2948
  position: absolute;
2736
2949
  bottom: 0;
2737
- /* Promote to own compositing layer for smoother scrolling */
2738
- will-change: transform;
2739
2950
  `;
2740
- var TimeStamp = import_styled_components22.default.div.attrs((props) => ({
2951
+ var TimeStamp = import_styled_components23.default.div.attrs((props) => ({
2741
2952
  style: {
2742
2953
  left: `${props.$left + 4}px`
2743
2954
  // Offset 4px to the right of the tick
@@ -2758,14 +2969,9 @@ var TimeScale = (props) => {
2758
2969
  renderTimestamp
2759
2970
  } = props;
2760
2971
  const { canvasRef, canvasMapRef } = useChunkedCanvasRefs();
2761
- const {
2762
- sampleRate,
2763
- samplesPerPixel,
2764
- timeScaleHeight,
2765
- controls: { show: showControls, width: controlWidth }
2766
- } = (0, import_react20.useContext)(PlaylistInfoContext);
2972
+ const { sampleRate, samplesPerPixel, timeScaleHeight } = (0, import_react21.useContext)(PlaylistInfoContext);
2767
2973
  const devicePixelRatio = useDevicePixelRatio();
2768
- const { widthX, canvasInfo, timeMarkersWithPositions } = (0, import_react20.useMemo)(() => {
2974
+ const { widthX, canvasInfo, timeMarkersWithPositions } = (0, import_react21.useMemo)(() => {
2769
2975
  const nextCanvasInfo = /* @__PURE__ */ new Map();
2770
2976
  const nextMarkers = [];
2771
2977
  const nextWidthX = secondsToPixels(duration / 1e3, samplesPerPixel, sampleRate);
@@ -2776,7 +2982,7 @@ var TimeScale = (props) => {
2776
2982
  if (counter % marker === 0) {
2777
2983
  const timeMs = counter;
2778
2984
  const timestamp = formatTime2(timeMs);
2779
- 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);
2780
2986
  nextMarkers.push({ pix, element });
2781
2987
  nextCanvasInfo.set(pix, timeScaleHeight);
2782
2988
  } else if (counter % bigStep === 0) {
@@ -2801,11 +3007,11 @@ var TimeScale = (props) => {
2801
3007
  renderTimestamp,
2802
3008
  timeScaleHeight
2803
3009
  ]);
2804
- const visibleChunkIndices = useVisibleChunkIndices(widthX, import_core3.MAX_CANVAS_WIDTH);
3010
+ const visibleChunkIndices = useVisibleChunkIndices(widthX, import_core4.MAX_CANVAS_WIDTH);
2805
3011
  const visibleChunks = visibleChunkIndices.map((i) => {
2806
- const chunkLeft = i * import_core3.MAX_CANVAS_WIDTH;
2807
- const chunkWidth = Math.min(widthX - chunkLeft, import_core3.MAX_CANVAS_WIDTH);
2808
- 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)(
2809
3015
  TimeTickChunk,
2810
3016
  {
2811
3017
  $cssWidth: chunkWidth,
@@ -2819,14 +3025,14 @@ var TimeScale = (props) => {
2819
3025
  `timescale-${i}`
2820
3026
  );
2821
3027
  });
2822
- const firstChunkLeft = visibleChunkIndices.length > 0 ? visibleChunkIndices[0] * import_core3.MAX_CANVAS_WIDTH : 0;
2823
- 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;
2824
3030
  const visibleMarkers = visibleChunkIndices.length > 0 ? timeMarkersWithPositions.filter(({ pix }) => pix >= firstChunkLeft && pix < lastChunkRight).map(({ element }) => element) : timeMarkersWithPositions.map(({ element }) => element);
2825
- (0, import_react20.useLayoutEffect)(() => {
3031
+ (0, import_react21.useLayoutEffect)(() => {
2826
3032
  for (const [chunkIdx, canvas] of canvasMapRef.current.entries()) {
2827
3033
  const ctx = canvas.getContext("2d");
2828
3034
  if (!ctx) continue;
2829
- const chunkLeft = chunkIdx * import_core3.MAX_CANVAS_WIDTH;
3035
+ const chunkLeft = chunkIdx * import_core4.MAX_CANVAS_WIDTH;
2830
3036
  const chunkWidth = canvas.width / devicePixelRatio;
2831
3037
  ctx.resetTransform();
2832
3038
  ctx.clearRect(0, 0, canvas.width, canvas.height);
@@ -2849,23 +3055,15 @@ var TimeScale = (props) => {
2849
3055
  canvasInfo,
2850
3056
  visibleChunkIndices
2851
3057
  ]);
2852
- return /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(
2853
- PlaylistTimeScaleScroll,
2854
- {
2855
- $cssWidth: widthX,
2856
- $controlWidth: showControls ? controlWidth : 0,
2857
- $timeScaleHeight: timeScaleHeight,
2858
- children: [
2859
- visibleMarkers,
2860
- visibleChunks
2861
- ]
2862
- }
2863
- );
3058
+ return /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(PlaylistTimeScaleScroll, { $cssWidth: widthX, $timeScaleHeight: timeScaleHeight, children: [
3059
+ visibleMarkers,
3060
+ visibleChunks
3061
+ ] });
2864
3062
  };
2865
- var StyledTimeScale = (0, import_styled_components22.withTheme)(TimeScale);
3063
+ var StyledTimeScale = (0, import_styled_components23.withTheme)(TimeScale);
2866
3064
 
2867
3065
  // src/components/SmartScale.tsx
2868
- var import_jsx_runtime25 = require("react/jsx-runtime");
3066
+ var import_jsx_runtime26 = require("react/jsx-runtime");
2869
3067
  var timeinfo = /* @__PURE__ */ new Map([
2870
3068
  [
2871
3069
  700,
@@ -2939,9 +3137,9 @@ function getScaleInfo(samplesPerPixel) {
2939
3137
  return config;
2940
3138
  }
2941
3139
  var SmartScale = ({ renderTimestamp }) => {
2942
- const { samplesPerPixel, duration } = (0, import_react21.useContext)(PlaylistInfoContext);
3140
+ const { samplesPerPixel, duration } = (0, import_react22.useContext)(PlaylistInfoContext);
2943
3141
  let config = getScaleInfo(samplesPerPixel);
2944
- return /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
3142
+ return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
2945
3143
  StyledTimeScale,
2946
3144
  {
2947
3145
  marker: config.marker,
@@ -2954,9 +3152,9 @@ var SmartScale = ({ renderTimestamp }) => {
2954
3152
  };
2955
3153
 
2956
3154
  // src/components/TimeFormatSelect.tsx
2957
- var import_styled_components23 = __toESM(require("styled-components"));
2958
- var import_jsx_runtime26 = require("react/jsx-runtime");
2959
- 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`
2960
3158
  display: inline-flex;
2961
3159
  align-items: center;
2962
3160
  gap: 0.5rem;
@@ -2978,7 +3176,7 @@ var TimeFormatSelect = ({
2978
3176
  const handleChange = (e) => {
2979
3177
  onChange(e.target.value);
2980
3178
  };
2981
- 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)(
2982
3180
  BaseSelect,
2983
3181
  {
2984
3182
  className: "time-format",
@@ -2986,50 +3184,30 @@ var TimeFormatSelect = ({
2986
3184
  onChange: handleChange,
2987
3185
  disabled,
2988
3186
  "aria-label": "Time format selection",
2989
- 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))
2990
3188
  }
2991
3189
  ) });
2992
3190
  };
2993
3191
 
2994
3192
  // src/components/Track.tsx
2995
- var import_styled_components24 = __toESM(require("styled-components"));
2996
- var import_jsx_runtime27 = require("react/jsx-runtime");
2997
- 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) => ({
2998
3196
  style: {
2999
3197
  height: `${props.$waveHeight * props.$numChannels + (props.$hasClipHeaders ? CLIP_HEADER_HEIGHT : 0)}px`
3000
3198
  }
3001
3199
  }))`
3002
3200
  position: relative;
3003
- display: flex;
3004
3201
  ${(props) => props.$width !== void 0 && `width: ${props.$width}px;`}
3005
3202
  `;
3006
- var ChannelContainer = import_styled_components24.default.div.attrs((props) => ({
3203
+ var ChannelContainer = import_styled_components25.default.div.attrs((props) => ({
3007
3204
  style: {
3008
3205
  paddingLeft: `${props.$offset || 0}px`
3009
3206
  }
3010
3207
  }))`
3011
3208
  position: relative;
3012
3209
  background: ${(props) => props.$backgroundColor || "transparent"};
3013
- flex: 1;
3014
- `;
3015
- var ControlsWrapper = import_styled_components24.default.div.attrs((props) => ({
3016
- style: {
3017
- width: `${props.$controlWidth}px`
3018
- }
3019
- }))`
3020
- position: sticky;
3021
- z-index: 102; /* Above waveform content and spectrogram labels (101), below Docusaurus navbar (200) */
3022
- left: 0;
3023
3210
  height: 100%;
3024
- flex-shrink: 0;
3025
- pointer-events: auto;
3026
- background: ${(props) => props.theme.surfaceColor};
3027
- transition: background 0.15s ease-in-out;
3028
-
3029
- /* Selected track: highlighted background */
3030
- ${(props) => props.$isSelected && `
3031
- background: ${props.theme.selectedTrackControlsBackground};
3032
- `}
3033
3211
  `;
3034
3212
  var Track = ({
3035
3213
  numChannels,
@@ -3041,44 +3219,34 @@ var Track = ({
3041
3219
  hasClipHeaders = false,
3042
3220
  onClick,
3043
3221
  trackId,
3044
- isSelected = false
3222
+ isSelected: _isSelected = false
3045
3223
  }) => {
3046
- const {
3047
- waveHeight,
3048
- controls: { show, width: controlWidth }
3049
- } = usePlaylistInfo();
3050
- const controls = useTrackControls();
3051
- return /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(
3224
+ const { waveHeight } = usePlaylistInfo();
3225
+ return /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
3052
3226
  Container,
3053
3227
  {
3054
3228
  $numChannels: numChannels,
3055
3229
  className,
3056
3230
  $waveHeight: waveHeight,
3057
- $controlWidth: show ? controlWidth : 0,
3058
3231
  $width: width,
3059
3232
  $hasClipHeaders: hasClipHeaders,
3060
- $isSelected: isSelected,
3061
- children: [
3062
- /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(ControlsWrapper, { $controlWidth: show ? controlWidth : 0, $isSelected: isSelected, children: controls }),
3063
- /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
3064
- ChannelContainer,
3065
- {
3066
- $controlWidth: show ? controlWidth : 0,
3067
- $backgroundColor: backgroundColor,
3068
- $offset: offset,
3069
- onClick,
3070
- "data-track-id": trackId,
3071
- children
3072
- }
3073
- )
3074
- ]
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
+ )
3075
3243
  }
3076
3244
  );
3077
3245
  };
3078
3246
 
3079
3247
  // src/components/TrackControls/Button.tsx
3080
- var import_styled_components25 = __toESM(require("styled-components"));
3081
- 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({
3082
3250
  type: "button"
3083
3251
  })`
3084
3252
  display: inline-block;
@@ -3153,8 +3321,8 @@ var Button = import_styled_components25.default.button.attrs({
3153
3321
  `;
3154
3322
 
3155
3323
  // src/components/TrackControls/ButtonGroup.tsx
3156
- var import_styled_components26 = __toESM(require("styled-components"));
3157
- var ButtonGroup = import_styled_components26.default.div`
3324
+ var import_styled_components27 = __toESM(require("styled-components"));
3325
+ var ButtonGroup = import_styled_components27.default.div`
3158
3326
  margin-bottom: 0.3rem;
3159
3327
 
3160
3328
  button:not(:first-child) {
@@ -3169,10 +3337,10 @@ var ButtonGroup = import_styled_components26.default.div`
3169
3337
  `;
3170
3338
 
3171
3339
  // src/components/TrackControls/CloseButton.tsx
3172
- var import_styled_components27 = __toESM(require("styled-components"));
3173
- var import_react22 = require("@phosphor-icons/react");
3174
- var import_jsx_runtime28 = require("react/jsx-runtime");
3175
- 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`
3176
3344
  position: absolute;
3177
3345
  left: 0;
3178
3346
  top: 0;
@@ -3195,11 +3363,11 @@ var StyledCloseButton = import_styled_components27.default.button`
3195
3363
  color: #dc3545;
3196
3364
  }
3197
3365
  `;
3198
- 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" }) });
3199
3367
 
3200
3368
  // src/components/TrackControls/Controls.tsx
3201
- var import_styled_components28 = __toESM(require("styled-components"));
3202
- var Controls = import_styled_components28.default.div`
3369
+ var import_styled_components29 = __toESM(require("styled-components"));
3370
+ var Controls = import_styled_components29.default.div`
3203
3371
  background: transparent;
3204
3372
  width: 100%;
3205
3373
  height: 100%;
@@ -3215,8 +3383,8 @@ var Controls = import_styled_components28.default.div`
3215
3383
  `;
3216
3384
 
3217
3385
  // src/components/TrackControls/Header.tsx
3218
- var import_styled_components29 = __toESM(require("styled-components"));
3219
- var Header = import_styled_components29.default.header`
3386
+ var import_styled_components30 = __toESM(require("styled-components"));
3387
+ var Header = import_styled_components30.default.header`
3220
3388
  overflow: hidden;
3221
3389
  height: 26px;
3222
3390
  width: 100%;
@@ -3230,28 +3398,28 @@ var Header = import_styled_components29.default.header`
3230
3398
  `;
3231
3399
 
3232
3400
  // src/components/TrackControls/VolumeDownIcon.tsx
3233
- var import_react23 = require("@phosphor-icons/react");
3234
- var import_jsx_runtime29 = require("react/jsx-runtime");
3235
- var VolumeDownIcon = (props) => /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_react23.SpeakerLowIcon, { weight: "light", ...props });
3236
-
3237
- // src/components/TrackControls/VolumeUpIcon.tsx
3238
3401
  var import_react24 = require("@phosphor-icons/react");
3239
3402
  var import_jsx_runtime30 = require("react/jsx-runtime");
3240
- 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 });
3241
3404
 
3242
- // src/components/TrackControls/TrashIcon.tsx
3405
+ // src/components/TrackControls/VolumeUpIcon.tsx
3243
3406
  var import_react25 = require("@phosphor-icons/react");
3244
3407
  var import_jsx_runtime31 = require("react/jsx-runtime");
3245
- 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 });
3246
3409
 
3247
- // src/components/TrackControls/DotsIcon.tsx
3410
+ // src/components/TrackControls/TrashIcon.tsx
3248
3411
  var import_react26 = require("@phosphor-icons/react");
3249
3412
  var import_jsx_runtime32 = require("react/jsx-runtime");
3250
- 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 });
3251
3419
 
3252
3420
  // src/components/TrackControls/Slider.tsx
3253
- var import_styled_components30 = __toESM(require("styled-components"));
3254
- 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)`
3255
3423
  width: 75%;
3256
3424
  height: 5px;
3257
3425
  background: ${(props) => props.theme.sliderTrackColor};
@@ -3303,8 +3471,8 @@ var Slider = (0, import_styled_components30.default)(BaseSlider)`
3303
3471
  `;
3304
3472
 
3305
3473
  // src/components/TrackControls/SliderWrapper.tsx
3306
- var import_styled_components31 = __toESM(require("styled-components"));
3307
- var SliderWrapper = import_styled_components31.default.label`
3474
+ var import_styled_components32 = __toESM(require("styled-components"));
3475
+ var SliderWrapper = import_styled_components32.default.label`
3308
3476
  width: 100%;
3309
3477
  display: flex;
3310
3478
  justify-content: space-between;
@@ -3315,15 +3483,15 @@ var SliderWrapper = import_styled_components31.default.label`
3315
3483
  `;
3316
3484
 
3317
3485
  // src/components/TrackMenu.tsx
3318
- var import_react27 = __toESM(require("react"));
3486
+ var import_react28 = __toESM(require("react"));
3319
3487
  var import_react_dom = require("react-dom");
3320
- var import_styled_components32 = __toESM(require("styled-components"));
3321
- var import_jsx_runtime33 = require("react/jsx-runtime");
3322
- 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`
3323
3491
  position: relative;
3324
3492
  display: inline-block;
3325
3493
  `;
3326
- var MenuButton = import_styled_components32.default.button`
3494
+ var MenuButton = import_styled_components33.default.button`
3327
3495
  background: none;
3328
3496
  border: none;
3329
3497
  cursor: pointer;
@@ -3338,7 +3506,8 @@ var MenuButton = import_styled_components32.default.button`
3338
3506
  opacity: 1;
3339
3507
  }
3340
3508
  `;
3341
- var Dropdown = import_styled_components32.default.div`
3509
+ var DROPDOWN_MIN_WIDTH = 180;
3510
+ var Dropdown = import_styled_components33.default.div`
3342
3511
  position: fixed;
3343
3512
  top: ${(p) => p.$top}px;
3344
3513
  left: ${(p) => p.$left}px;
@@ -3348,31 +3517,53 @@ var Dropdown = import_styled_components32.default.div`
3348
3517
  border: 1px solid rgba(128, 128, 128, 0.4);
3349
3518
  border-radius: 6px;
3350
3519
  padding: 0.5rem 0;
3351
- min-width: 180px;
3520
+ min-width: ${DROPDOWN_MIN_WIDTH}px;
3352
3521
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
3353
3522
  `;
3354
- var Divider = import_styled_components32.default.hr`
3523
+ var Divider = import_styled_components33.default.hr`
3355
3524
  border: none;
3356
3525
  border-top: 1px solid rgba(128, 128, 128, 0.3);
3357
3526
  margin: 0.35rem 0;
3358
3527
  `;
3359
3528
  var TrackMenu = ({ items: itemsProp }) => {
3360
- const [open, setOpen] = (0, import_react27.useState)(false);
3361
- const close = () => setOpen(false);
3529
+ const [open, setOpen] = (0, import_react28.useState)(false);
3530
+ const close = (0, import_react28.useCallback)(() => setOpen(false), []);
3362
3531
  const items = typeof itemsProp === "function" ? itemsProp(close) : itemsProp;
3363
- const [dropdownPos, setDropdownPos] = (0, import_react27.useState)({ top: 0, left: 0 });
3364
- const buttonRef = (0, import_react27.useRef)(null);
3365
- const dropdownRef = (0, import_react27.useRef)(null);
3366
- (0, import_react27.useEffect)(() => {
3367
- if (open && buttonRef.current) {
3368
- const rect = buttonRef.current.getBoundingClientRect();
3369
- setDropdownPos({
3370
- top: rect.bottom + 2,
3371
- left: Math.max(0, rect.right - 180)
3372
- });
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;
3373
3544
  }
3374
- }, [open]);
3375
- (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)(() => {
3376
3567
  if (!open) return;
3377
3568
  const handleClick = (e) => {
3378
3569
  const target = e.target;
@@ -3380,11 +3571,20 @@ var TrackMenu = ({ items: itemsProp }) => {
3380
3571
  setOpen(false);
3381
3572
  }
3382
3573
  };
3574
+ const handleKeyDown = (e) => {
3575
+ if (e.key === "Escape") {
3576
+ setOpen(false);
3577
+ }
3578
+ };
3383
3579
  document.addEventListener("mousedown", handleClick);
3384
- return () => document.removeEventListener("mousedown", handleClick);
3580
+ document.addEventListener("keydown", handleKeyDown);
3581
+ return () => {
3582
+ document.removeEventListener("mousedown", handleClick);
3583
+ document.removeEventListener("keydown", handleKeyDown);
3584
+ };
3385
3585
  }, [open]);
3386
- return /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(MenuContainer, { children: [
3387
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
3586
+ return /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)(MenuContainer, { children: [
3587
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
3388
3588
  MenuButton,
3389
3589
  {
3390
3590
  ref: buttonRef,
@@ -3395,19 +3595,19 @@ var TrackMenu = ({ items: itemsProp }) => {
3395
3595
  onMouseDown: (e) => e.stopPropagation(),
3396
3596
  title: "Track menu",
3397
3597
  "aria-label": "Track menu",
3398
- children: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(DotsIcon, { size: 16 })
3598
+ children: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(DotsIcon, { size: 16 })
3399
3599
  }
3400
3600
  ),
3401
3601
  open && typeof document !== "undefined" && (0, import_react_dom.createPortal)(
3402
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
3602
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
3403
3603
  Dropdown,
3404
3604
  {
3405
3605
  ref: dropdownRef,
3406
3606
  $top: dropdownPos.top,
3407
3607
  $left: dropdownPos.left,
3408
3608
  onMouseDown: (e) => e.stopPropagation(),
3409
- children: items.map((item, index) => /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(import_react27.default.Fragment, { children: [
3410
- 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, {}),
3411
3611
  item.content
3412
3612
  ] }, item.id))
3413
3613
  }
@@ -3450,6 +3650,7 @@ var TrackMenu = ({ items: itemsProp }) => {
3450
3650
  LoopRegion,
3451
3651
  LoopRegionMarkers,
3452
3652
  MasterVolumeControl,
3653
+ PianoRollChannel,
3453
3654
  Playhead,
3454
3655
  PlayheadWithMarker,
3455
3656
  Playlist,