@waveform-playlist/ui-components 9.0.4 → 9.1.1

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.mjs CHANGED
@@ -334,7 +334,7 @@ var AutomaticScrollCheckbox = ({
334
334
  };
335
335
 
336
336
  // src/components/Channel.tsx
337
- import { useLayoutEffect } from "react";
337
+ import { useEffect as useEffect3 } from "react";
338
338
  import styled9 from "styled-components";
339
339
 
340
340
  // src/wfpl-theme.ts
@@ -410,6 +410,10 @@ var defaultTheme = {
410
410
  annotationResizeHandleColor: "rgba(0, 0, 0, 0.4)",
411
411
  annotationResizeHandleActiveColor: "rgba(0, 0, 0, 0.8)",
412
412
  annotationTextItemHoverBackground: "rgba(0, 0, 0, 0.03)",
413
+ // Piano roll colors
414
+ pianoRollNoteColor: "#2a7070",
415
+ pianoRollSelectedNoteColor: "#3d9e9e",
416
+ pianoRollBackgroundColor: "#1a1a2e",
413
417
  // Spacing and sizing
414
418
  borderRadius: "4px",
415
419
  fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, sans-serif',
@@ -487,6 +491,10 @@ var darkTheme = {
487
491
  annotationResizeHandleColor: "rgba(200, 160, 120, 0.5)",
488
492
  annotationResizeHandleActiveColor: "rgba(220, 180, 140, 0.8)",
489
493
  annotationTextItemHoverBackground: "rgba(200, 160, 120, 0.08)",
494
+ // Piano roll colors
495
+ pianoRollNoteColor: "#c49a6c",
496
+ pianoRollSelectedNoteColor: "#e8c090",
497
+ pianoRollBackgroundColor: "#0d0d14",
490
498
  // Spacing and sizing
491
499
  borderRadius: "4px",
492
500
  fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, sans-serif',
@@ -499,6 +507,7 @@ import {
499
507
  createContext,
500
508
  useContext,
501
509
  useEffect,
510
+ useLayoutEffect,
502
511
  useCallback,
503
512
  useMemo,
504
513
  useRef,
@@ -506,19 +515,32 @@ import {
506
515
  } from "react";
507
516
  import { jsx as jsx3 } from "react/jsx-runtime";
508
517
  var ViewportStore = class {
509
- constructor() {
510
- this._state = null;
518
+ constructor(containerEl) {
511
519
  this._listeners = /* @__PURE__ */ new Set();
520
+ this._notifyRafId = null;
512
521
  this.subscribe = (callback) => {
513
522
  this._listeners.add(callback);
514
523
  return () => this._listeners.delete(callback);
515
524
  };
516
525
  this.getSnapshot = () => this._state;
526
+ const width = containerEl?.clientWidth ?? (typeof window !== "undefined" ? window.innerWidth : 1024);
527
+ const buffer = width * 1.5;
528
+ this._state = {
529
+ scrollLeft: 0,
530
+ containerWidth: width,
531
+ visibleStart: 0,
532
+ visibleEnd: width + buffer
533
+ };
517
534
  }
518
535
  /**
519
536
  * Update viewport state. Applies a 100px scroll threshold to skip updates
520
537
  * that don't affect chunk visibility (1000px chunks with 1.5× overscan buffer).
521
538
  * Only notifies listeners when the state actually changes.
539
+ *
540
+ * Listener notification is deferred by one frame via requestAnimationFrame
541
+ * to avoid conflicting with React 19's concurrent rendering. When React
542
+ * time-slices a render across frames, synchronous useSyncExternalStore
543
+ * notifications can trigger "Should not already be working" errors.
522
544
  */
523
545
  update(scrollLeft, containerWidth) {
524
546
  const buffer = containerWidth * 1.5;
@@ -528,8 +550,19 @@ var ViewportStore = class {
528
550
  return;
529
551
  }
530
552
  this._state = { scrollLeft, containerWidth, visibleStart, visibleEnd };
531
- for (const listener of this._listeners) {
532
- listener();
553
+ if (this._notifyRafId === null) {
554
+ this._notifyRafId = requestAnimationFrame(() => {
555
+ this._notifyRafId = null;
556
+ for (const listener of this._listeners) {
557
+ listener();
558
+ }
559
+ });
560
+ }
561
+ }
562
+ cancelPendingNotification() {
563
+ if (this._notifyRafId !== null) {
564
+ cancelAnimationFrame(this._notifyRafId);
565
+ this._notifyRafId = null;
533
566
  }
534
567
  }
535
568
  };
@@ -540,7 +573,7 @@ var NULL_SNAPSHOT = () => null;
540
573
  var ScrollViewportProvider = ({ containerRef, children }) => {
541
574
  const storeRef = useRef(null);
542
575
  if (storeRef.current === null) {
543
- storeRef.current = new ViewportStore();
576
+ storeRef.current = new ViewportStore(containerRef.current);
544
577
  }
545
578
  const store = storeRef.current;
546
579
  const rafIdRef = useRef(null);
@@ -556,43 +589,27 @@ var ScrollViewportProvider = ({ containerRef, children }) => {
556
589
  measure();
557
590
  });
558
591
  }, [measure]);
592
+ useLayoutEffect(() => {
593
+ measure();
594
+ }, [measure]);
559
595
  useEffect(() => {
560
596
  const el = containerRef.current;
561
597
  if (!el) return;
562
- measure();
563
598
  el.addEventListener("scroll", scheduleUpdate, { passive: true });
564
- let userHasInteracted = false;
565
- const markInteracted = () => {
566
- userHasInteracted = true;
567
- };
568
- el.addEventListener("pointerdown", markInteracted, { once: true });
569
- el.addEventListener("keydown", markInteracted, { once: true });
570
- el.addEventListener("wheel", markInteracted, { once: true, passive: true });
571
- const resetHandler = () => {
572
- if (!userHasInteracted && el.scrollLeft !== 0) {
573
- el.scrollLeft = 0;
574
- measure();
575
- }
576
- el.removeEventListener("scroll", resetHandler);
577
- };
578
- el.addEventListener("scroll", resetHandler);
579
599
  const resizeObserver = new ResizeObserver(() => {
580
600
  scheduleUpdate();
581
601
  });
582
602
  resizeObserver.observe(el);
583
603
  return () => {
584
604
  el.removeEventListener("scroll", scheduleUpdate);
585
- el.removeEventListener("scroll", resetHandler);
586
- el.removeEventListener("pointerdown", markInteracted);
587
- el.removeEventListener("keydown", markInteracted);
588
- el.removeEventListener("wheel", markInteracted);
589
605
  resizeObserver.disconnect();
590
606
  if (rafIdRef.current !== null) {
591
607
  cancelAnimationFrame(rafIdRef.current);
592
608
  rafIdRef.current = null;
593
609
  }
610
+ store.cancelPendingNotification();
594
611
  };
595
- }, [containerRef, measure, scheduleUpdate]);
612
+ }, [containerRef, scheduleUpdate, store]);
596
613
  return /* @__PURE__ */ jsx3(ViewportStoreContext.Provider, { value: store, children });
597
614
  };
598
615
  var useScrollViewport = () => {
@@ -727,8 +744,6 @@ var Waveform = styled9.canvas.attrs((props) => ({
727
744
  }))`
728
745
  position: absolute;
729
746
  top: 0;
730
- /* Promote to own compositing layer for smoother scrolling */
731
- will-change: transform;
732
747
  /* Disable image rendering interpolation */
733
748
  image-rendering: pixelated;
734
749
  image-rendering: crisp-edges;
@@ -765,7 +780,8 @@ var Channel = (props) => {
765
780
  const { canvasRef, canvasMapRef } = useChunkedCanvasRefs();
766
781
  const clipOriginX = useClipViewportOrigin();
767
782
  const visibleChunkIndices = useVisibleChunkIndices(length, MAX_CANVAS_WIDTH, clipOriginX);
768
- useLayoutEffect(() => {
783
+ useEffect3(() => {
784
+ const tDraw = performance.now();
769
785
  const step = barWidth + barGap;
770
786
  for (const [canvasIdx, canvas] of canvasMapRef.current.entries()) {
771
787
  const globalPixelOffset = canvasIdx * MAX_CANVAS_WIDTH;
@@ -801,6 +817,9 @@ var Channel = (props) => {
801
817
  }
802
818
  }
803
819
  }
820
+ console.log(
821
+ `[waveform] draw ch${index}: ${canvasMapRef.current.size} chunks, ${(performance.now() - tDraw).toFixed(1)}ms`
822
+ );
804
823
  }, [
805
824
  canvasMapRef,
806
825
  data,
@@ -813,7 +832,8 @@ var Channel = (props) => {
813
832
  barWidth,
814
833
  barGap,
815
834
  drawMode,
816
- visibleChunkIndices
835
+ visibleChunkIndices,
836
+ index
817
837
  ]);
818
838
  const waveforms = visibleChunkIndices.map((i) => {
819
839
  const chunkLeft = i * MAX_CANVAS_WIDTH;
@@ -931,7 +951,7 @@ var ClipHeaderPresentational = ({
931
951
  trackName,
932
952
  isSelected = false
933
953
  }) => {
934
- return /* @__PURE__ */ jsx7(HeaderContainer, { $interactive: false, $isSelected: isSelected, children: /* @__PURE__ */ jsx7(TrackName, { children: trackName }) });
954
+ return /* @__PURE__ */ jsx7(HeaderContainer, { $interactive: false, $isSelected: isSelected, children: /* @__PURE__ */ jsx7(TrackName, { title: trackName, children: trackName }) });
935
955
  };
936
956
  var ClipHeader = ({
937
957
  clipId,
@@ -953,7 +973,7 @@ var ClipHeader = ({
953
973
  "data-clip-id": clipId,
954
974
  $interactive: true,
955
975
  $isSelected: isSelected,
956
- children: /* @__PURE__ */ jsx7(TrackName, { children: trackName })
976
+ children: /* @__PURE__ */ jsx7(TrackName, { title: trackName, children: trackName })
957
977
  }
958
978
  );
959
979
  };
@@ -1174,7 +1194,7 @@ var Clip = ({
1174
1194
  "data-clip-container": "true",
1175
1195
  "data-track-id": trackId,
1176
1196
  onMouseDown,
1177
- ...!enableDrag ? { tabIndex: -1 } : {},
1197
+ tabIndex: -1,
1178
1198
  children: [
1179
1199
  showHeader && /* @__PURE__ */ jsx10(
1180
1200
  ClipHeader,
@@ -1284,11 +1304,140 @@ var MasterVolumeControl = ({
1284
1304
  ] });
1285
1305
  };
1286
1306
 
1287
- // src/components/Playhead.tsx
1288
- import { useRef as useRef3, useEffect as useEffect3 } from "react";
1307
+ // src/components/PianoRollChannel.tsx
1308
+ import { useEffect as useEffect4, useMemo as useMemo2 } from "react";
1289
1309
  import styled15 from "styled-components";
1290
- import { jsx as jsx12, jsxs as jsxs4 } from "react/jsx-runtime";
1291
- var PlayheadLine = styled15.div.attrs((props) => ({
1310
+ import { MAX_CANVAS_WIDTH as MAX_CANVAS_WIDTH2 } from "@waveform-playlist/core";
1311
+ import { jsx as jsx12 } from "react/jsx-runtime";
1312
+ var NoteCanvas = styled15.canvas.attrs((props) => ({
1313
+ style: {
1314
+ width: `${props.$cssWidth}px`,
1315
+ height: `${props.$waveHeight}px`,
1316
+ left: `${props.$left}px`
1317
+ }
1318
+ }))`
1319
+ position: absolute;
1320
+ top: 0;
1321
+ image-rendering: pixelated;
1322
+ image-rendering: crisp-edges;
1323
+ `;
1324
+ var Wrapper2 = styled15.div.attrs((props) => ({
1325
+ style: {
1326
+ top: `${props.$waveHeight * props.$index}px`,
1327
+ width: `${props.$cssWidth}px`,
1328
+ height: `${props.$waveHeight}px`
1329
+ }
1330
+ }))`
1331
+ position: absolute;
1332
+ background: ${(props) => props.$backgroundColor};
1333
+ transform: translateZ(0);
1334
+ backface-visibility: hidden;
1335
+ `;
1336
+ var PianoRollChannel = ({
1337
+ index,
1338
+ midiNotes,
1339
+ length,
1340
+ waveHeight,
1341
+ devicePixelRatio,
1342
+ samplesPerPixel,
1343
+ sampleRate,
1344
+ clipOffsetSeconds,
1345
+ noteColor = "#2a7070",
1346
+ selectedNoteColor = "#3d9e9e",
1347
+ isSelected = false,
1348
+ transparentBackground = false,
1349
+ backgroundColor = "#1a1a2e"
1350
+ }) => {
1351
+ const { canvasRef, canvasMapRef } = useChunkedCanvasRefs();
1352
+ const clipOriginX = useClipViewportOrigin();
1353
+ const visibleChunkIndices = useVisibleChunkIndices(length, MAX_CANVAS_WIDTH2, clipOriginX);
1354
+ const { minMidi, maxMidi } = useMemo2(() => {
1355
+ if (midiNotes.length === 0) return { minMidi: 0, maxMidi: 127 };
1356
+ let min = 127, max = 0;
1357
+ for (const note of midiNotes) {
1358
+ if (note.midi < min) min = note.midi;
1359
+ if (note.midi > max) max = note.midi;
1360
+ }
1361
+ return { minMidi: Math.max(0, min - 1), maxMidi: Math.min(127, max + 1) };
1362
+ }, [midiNotes]);
1363
+ const color = isSelected ? selectedNoteColor : noteColor;
1364
+ useEffect4(() => {
1365
+ const tDraw = performance.now();
1366
+ const noteRange = maxMidi - minMidi + 1;
1367
+ const noteHeight = Math.max(2, waveHeight / noteRange);
1368
+ const pixelsPerSecond = sampleRate / samplesPerPixel;
1369
+ for (const [canvasIdx, canvas] of canvasMapRef.current.entries()) {
1370
+ const chunkPixelStart = canvasIdx * MAX_CANVAS_WIDTH2;
1371
+ const canvasWidth = canvas.width / devicePixelRatio;
1372
+ const ctx = canvas.getContext("2d");
1373
+ if (!ctx) continue;
1374
+ ctx.resetTransform();
1375
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
1376
+ ctx.imageSmoothingEnabled = false;
1377
+ ctx.scale(devicePixelRatio, devicePixelRatio);
1378
+ const chunkStartTime = chunkPixelStart * samplesPerPixel / sampleRate;
1379
+ const chunkEndTime = (chunkPixelStart + canvasWidth) * samplesPerPixel / sampleRate;
1380
+ for (const note of midiNotes) {
1381
+ const noteStart = note.time - clipOffsetSeconds;
1382
+ const noteEnd = noteStart + note.duration;
1383
+ if (noteEnd <= chunkStartTime || noteStart >= chunkEndTime) continue;
1384
+ const x = noteStart * pixelsPerSecond - chunkPixelStart;
1385
+ const w = Math.max(2, note.duration * pixelsPerSecond);
1386
+ const y = (maxMidi - note.midi) / noteRange * waveHeight;
1387
+ const alpha = 0.3 + note.velocity * 0.7;
1388
+ ctx.fillStyle = color;
1389
+ ctx.globalAlpha = alpha;
1390
+ const r = 1;
1391
+ ctx.beginPath();
1392
+ ctx.roundRect(x, y, w, noteHeight, r);
1393
+ ctx.fill();
1394
+ }
1395
+ ctx.globalAlpha = 1;
1396
+ }
1397
+ console.log(
1398
+ `[piano-roll] draw ch${index}: ${canvasMapRef.current.size} chunks, ${midiNotes.length} notes, ${(performance.now() - tDraw).toFixed(1)}ms`
1399
+ );
1400
+ }, [
1401
+ canvasMapRef,
1402
+ midiNotes,
1403
+ waveHeight,
1404
+ devicePixelRatio,
1405
+ samplesPerPixel,
1406
+ sampleRate,
1407
+ clipOffsetSeconds,
1408
+ color,
1409
+ minMidi,
1410
+ maxMidi,
1411
+ length,
1412
+ visibleChunkIndices,
1413
+ index
1414
+ ]);
1415
+ const canvases = visibleChunkIndices.map((i) => {
1416
+ const chunkLeft = i * MAX_CANVAS_WIDTH2;
1417
+ const currentWidth = Math.min(length - chunkLeft, MAX_CANVAS_WIDTH2);
1418
+ return /* @__PURE__ */ jsx12(
1419
+ NoteCanvas,
1420
+ {
1421
+ $cssWidth: currentWidth,
1422
+ $left: chunkLeft,
1423
+ width: currentWidth * devicePixelRatio,
1424
+ height: waveHeight * devicePixelRatio,
1425
+ $waveHeight: waveHeight,
1426
+ "data-index": i,
1427
+ ref: canvasRef
1428
+ },
1429
+ `${length}-${i}`
1430
+ );
1431
+ });
1432
+ const bgColor = transparentBackground ? "transparent" : backgroundColor;
1433
+ return /* @__PURE__ */ jsx12(Wrapper2, { $index: index, $cssWidth: length, $waveHeight: waveHeight, $backgroundColor: bgColor, children: canvases });
1434
+ };
1435
+
1436
+ // src/components/Playhead.tsx
1437
+ import { useRef as useRef3, useEffect as useEffect5 } from "react";
1438
+ import styled16 from "styled-components";
1439
+ import { jsx as jsx13, jsxs as jsxs4 } from "react/jsx-runtime";
1440
+ var PlayheadLine = styled16.div.attrs((props) => ({
1292
1441
  style: {
1293
1442
  transform: `translate3d(${props.$position}px, 0, 0)`
1294
1443
  }
@@ -1304,9 +1453,9 @@ var PlayheadLine = styled15.div.attrs((props) => ({
1304
1453
  will-change: transform;
1305
1454
  `;
1306
1455
  var Playhead = ({ position, color = "#ff0000" }) => {
1307
- return /* @__PURE__ */ jsx12(PlayheadLine, { $position: position, $color: color });
1456
+ return /* @__PURE__ */ jsx13(PlayheadLine, { $position: position, $color: color });
1308
1457
  };
1309
- var PlayheadWithMarkerContainer = styled15.div`
1458
+ var PlayheadWithMarkerContainer = styled16.div`
1310
1459
  position: absolute;
1311
1460
  top: 0;
1312
1461
  left: 0;
@@ -1315,7 +1464,7 @@ var PlayheadWithMarkerContainer = styled15.div`
1315
1464
  pointer-events: none;
1316
1465
  will-change: transform;
1317
1466
  `;
1318
- var MarkerTriangle = styled15.div`
1467
+ var MarkerTriangle = styled16.div`
1319
1468
  position: absolute;
1320
1469
  top: -10px;
1321
1470
  left: -6px;
@@ -1325,7 +1474,7 @@ var MarkerTriangle = styled15.div`
1325
1474
  border-right: 7px solid transparent;
1326
1475
  border-top: 10px solid ${(props) => props.$color};
1327
1476
  `;
1328
- var MarkerLine = styled15.div`
1477
+ var MarkerLine = styled16.div`
1329
1478
  position: absolute;
1330
1479
  top: 0;
1331
1480
  left: 0;
@@ -1341,13 +1490,13 @@ var PlayheadWithMarker = ({
1341
1490
  audioStartPositionRef,
1342
1491
  samplesPerPixel,
1343
1492
  sampleRate,
1344
- controlsOffset,
1493
+ controlsOffset = 0,
1345
1494
  getAudioContextTime,
1346
1495
  getPlaybackTime
1347
1496
  }) => {
1348
1497
  const containerRef = useRef3(null);
1349
1498
  const animationFrameRef = useRef3(null);
1350
- useEffect3(() => {
1499
+ useEffect5(() => {
1351
1500
  const updatePosition = () => {
1352
1501
  if (containerRef.current) {
1353
1502
  let time;
@@ -1392,7 +1541,7 @@ var PlayheadWithMarker = ({
1392
1541
  getAudioContextTime,
1393
1542
  getPlaybackTime
1394
1543
  ]);
1395
- useEffect3(() => {
1544
+ useEffect5(() => {
1396
1545
  if (!isPlaying && containerRef.current) {
1397
1546
  const time = currentTimeRef.current ?? 0;
1398
1547
  const pos = time * sampleRate / samplesPerPixel + controlsOffset;
@@ -1400,27 +1549,43 @@ var PlayheadWithMarker = ({
1400
1549
  }
1401
1550
  });
1402
1551
  return /* @__PURE__ */ jsxs4(PlayheadWithMarkerContainer, { ref: containerRef, $color: color, children: [
1403
- /* @__PURE__ */ jsx12(MarkerTriangle, { $color: color }),
1404
- /* @__PURE__ */ jsx12(MarkerLine, { $color: color })
1552
+ /* @__PURE__ */ jsx13(MarkerTriangle, { $color: color }),
1553
+ /* @__PURE__ */ jsx13(MarkerLine, { $color: color })
1405
1554
  ] });
1406
1555
  };
1407
1556
 
1408
1557
  // src/components/Playlist.tsx
1409
- import styled16, { withTheme } from "styled-components";
1558
+ import styled17, { withTheme } from "styled-components";
1410
1559
  import { useRef as useRef4, useCallback as useCallback3 } from "react";
1411
- import { jsx as jsx13, jsxs as jsxs5 } from "react/jsx-runtime";
1412
- var Wrapper2 = styled16.div`
1560
+ import { jsx as jsx14, jsxs as jsxs5 } from "react/jsx-runtime";
1561
+ var Wrapper3 = styled17.div`
1562
+ display: flex;
1413
1563
  overflow-y: hidden;
1564
+ position: relative;
1565
+ `;
1566
+ var ControlsColumn = styled17.div.attrs((props) => ({
1567
+ style: { width: `${props.$width}px` }
1568
+ }))`
1569
+ flex-shrink: 0;
1570
+ overflow: hidden;
1571
+ `;
1572
+ var TimescaleGap = styled17.div.attrs((props) => ({
1573
+ style: { height: `${props.$height}px` }
1574
+ }))``;
1575
+ var ScrollArea = styled17.div`
1414
1576
  overflow-x: auto;
1577
+ overflow-y: hidden;
1578
+ overflow-anchor: none;
1579
+ flex: 1;
1415
1580
  position: relative;
1416
1581
  `;
1417
- var ScrollContainer = styled16.div.attrs((props) => ({
1582
+ var ScrollContainerInner = styled17.div.attrs((props) => ({
1418
1583
  style: props.$width !== void 0 ? { width: `${props.$width}px` } : {}
1419
1584
  }))`
1420
1585
  position: relative;
1421
1586
  background: ${(props) => props.$backgroundColor || "transparent"};
1422
1587
  `;
1423
- var TimescaleWrapper = styled16.div.attrs((props) => ({
1588
+ var TimescaleWrapper = styled17.div.attrs((props) => ({
1424
1589
  style: props.$width ? { minWidth: `${props.$width}px` } : {}
1425
1590
  }))`
1426
1591
  background: ${(props) => props.$backgroundColor || "white"};
@@ -1428,14 +1593,14 @@ var TimescaleWrapper = styled16.div.attrs((props) => ({
1428
1593
  position: relative;
1429
1594
  overflow: hidden; /* Constrain loop region to timescale area */
1430
1595
  `;
1431
- var TracksContainer = styled16.div.attrs((props) => ({
1596
+ var TracksContainer = styled17.div.attrs((props) => ({
1432
1597
  style: props.$width !== void 0 ? { minWidth: `${props.$width}px` } : {}
1433
1598
  }))`
1434
1599
  position: relative;
1435
1600
  background: ${(props) => props.$backgroundColor || "transparent"};
1436
1601
  width: 100%;
1437
1602
  `;
1438
- var ClickOverlay = styled16.div`
1603
+ var ClickOverlay = styled17.div`
1439
1604
  position: absolute;
1440
1605
  top: 0;
1441
1606
  left: 0;
@@ -1452,7 +1617,6 @@ var Playlist = ({
1452
1617
  timescale,
1453
1618
  timescaleWidth,
1454
1619
  tracksWidth,
1455
- scrollContainerWidth,
1456
1620
  controlsWidth,
1457
1621
  onTracksClick,
1458
1622
  onTracksMouseDown,
@@ -1460,40 +1624,48 @@ var Playlist = ({
1460
1624
  onTracksMouseUp,
1461
1625
  scrollContainerRef,
1462
1626
  isSelecting,
1463
- "data-playlist-state": playlistState
1627
+ "data-playlist-state": playlistState,
1628
+ trackControlsSlots,
1629
+ timescaleGapHeight = 0
1464
1630
  }) => {
1465
- const wrapperRef = useRef4(null);
1631
+ const scrollAreaRef = useRef4(null);
1466
1632
  const handleRef = useCallback3(
1467
1633
  (el) => {
1468
- wrapperRef.current = el;
1634
+ scrollAreaRef.current = el;
1469
1635
  scrollContainerRef?.(el);
1470
1636
  },
1471
1637
  [scrollContainerRef]
1472
1638
  );
1473
- return /* @__PURE__ */ jsx13(Wrapper2, { "data-scroll-container": "true", "data-playlist-state": playlistState, ref: handleRef, children: /* @__PURE__ */ jsx13(ScrollViewportProvider, { containerRef: wrapperRef, children: /* @__PURE__ */ jsxs5(ScrollContainer, { $backgroundColor: backgroundColor, $width: scrollContainerWidth, children: [
1474
- timescale && /* @__PURE__ */ jsx13(TimescaleWrapper, { $width: timescaleWidth, $backgroundColor: timescaleBackgroundColor, children: timescale }),
1475
- /* @__PURE__ */ jsxs5(TracksContainer, { $width: tracksWidth, $backgroundColor: backgroundColor, children: [
1476
- children,
1477
- (onTracksClick || onTracksMouseDown) && /* @__PURE__ */ jsx13(
1478
- ClickOverlay,
1479
- {
1480
- $controlsWidth: controlsWidth,
1481
- $isSelecting: isSelecting,
1482
- onClick: onTracksClick,
1483
- onMouseDown: onTracksMouseDown,
1484
- onMouseMove: onTracksMouseMove,
1485
- onMouseUp: onTracksMouseUp
1486
- }
1487
- )
1488
- ] })
1489
- ] }) }) });
1639
+ const showControls = controlsWidth !== void 0 && controlsWidth > 0;
1640
+ return /* @__PURE__ */ jsxs5(Wrapper3, { "data-playlist-state": playlistState, children: [
1641
+ showControls && /* @__PURE__ */ jsxs5(ControlsColumn, { $width: controlsWidth, children: [
1642
+ timescaleGapHeight > 0 && /* @__PURE__ */ jsx14(TimescaleGap, { $height: timescaleGapHeight }),
1643
+ trackControlsSlots
1644
+ ] }),
1645
+ /* @__PURE__ */ jsx14(ScrollArea, { "data-scroll-container": "true", ref: handleRef, children: /* @__PURE__ */ jsx14(ScrollViewportProvider, { containerRef: scrollAreaRef, children: /* @__PURE__ */ jsxs5(ScrollContainerInner, { $backgroundColor: backgroundColor, $width: tracksWidth, children: [
1646
+ timescale && /* @__PURE__ */ jsx14(TimescaleWrapper, { $width: timescaleWidth, $backgroundColor: timescaleBackgroundColor, children: timescale }),
1647
+ /* @__PURE__ */ jsxs5(TracksContainer, { $width: tracksWidth, $backgroundColor: backgroundColor, children: [
1648
+ children,
1649
+ (onTracksClick || onTracksMouseDown) && /* @__PURE__ */ jsx14(
1650
+ ClickOverlay,
1651
+ {
1652
+ $isSelecting: isSelecting,
1653
+ onClick: onTracksClick,
1654
+ onMouseDown: onTracksMouseDown,
1655
+ onMouseMove: onTracksMouseMove,
1656
+ onMouseUp: onTracksMouseUp
1657
+ }
1658
+ )
1659
+ ] })
1660
+ ] }) }) })
1661
+ ] });
1490
1662
  };
1491
1663
  var StyledPlaylist = withTheme(Playlist);
1492
1664
 
1493
1665
  // src/components/Selection.tsx
1494
- import styled17 from "styled-components";
1495
- import { jsx as jsx14 } from "react/jsx-runtime";
1496
- var SelectionOverlay = styled17.div.attrs((props) => ({
1666
+ import styled18 from "styled-components";
1667
+ import { jsx as jsx15 } from "react/jsx-runtime";
1668
+ var SelectionOverlay = styled18.div.attrs((props) => ({
1497
1669
  style: {
1498
1670
  left: `${props.$left}px`,
1499
1671
  width: `${props.$width}px`
@@ -1516,14 +1688,14 @@ var Selection = ({
1516
1688
  if (width <= 0) {
1517
1689
  return null;
1518
1690
  }
1519
- return /* @__PURE__ */ jsx14(SelectionOverlay, { $left: startPosition, $width: width, $color: color, "data-selection": true });
1691
+ return /* @__PURE__ */ jsx15(SelectionOverlay, { $left: startPosition, $width: width, $color: color, "data-selection": true });
1520
1692
  };
1521
1693
 
1522
1694
  // src/components/LoopRegion.tsx
1523
1695
  import { useCallback as useCallback4, useRef as useRef5, useState } from "react";
1524
- import styled18 from "styled-components";
1525
- import { Fragment as Fragment2, jsx as jsx15, jsxs as jsxs6 } from "react/jsx-runtime";
1526
- var LoopRegionOverlayDiv = styled18.div.attrs((props) => ({
1696
+ import styled19 from "styled-components";
1697
+ import { Fragment as Fragment2, jsx as jsx16, jsxs as jsxs6 } from "react/jsx-runtime";
1698
+ var LoopRegionOverlayDiv = styled19.div.attrs((props) => ({
1527
1699
  style: {
1528
1700
  left: `${props.$left}px`,
1529
1701
  width: `${props.$width}px`
@@ -1536,7 +1708,7 @@ var LoopRegionOverlayDiv = styled18.div.attrs((props) => ({
1536
1708
  z-index: 55; /* Between clips (z-index: 50) and selection (z-index: 60) */
1537
1709
  pointer-events: none;
1538
1710
  `;
1539
- var LoopMarker = styled18.div.attrs((props) => ({
1711
+ var LoopMarker = styled19.div.attrs((props) => ({
1540
1712
  style: {
1541
1713
  left: `${props.$left}px`
1542
1714
  }
@@ -1572,7 +1744,7 @@ var LoopRegion = ({
1572
1744
  return null;
1573
1745
  }
1574
1746
  return /* @__PURE__ */ jsxs6(Fragment2, { children: [
1575
- /* @__PURE__ */ jsx15(
1747
+ /* @__PURE__ */ jsx16(
1576
1748
  LoopRegionOverlayDiv,
1577
1749
  {
1578
1750
  $left: startPosition,
@@ -1581,7 +1753,7 @@ var LoopRegion = ({
1581
1753
  "data-loop-region": true
1582
1754
  }
1583
1755
  ),
1584
- /* @__PURE__ */ jsx15(
1756
+ /* @__PURE__ */ jsx16(
1585
1757
  LoopMarker,
1586
1758
  {
1587
1759
  $left: startPosition,
@@ -1590,7 +1762,7 @@ var LoopRegion = ({
1590
1762
  "data-loop-marker": "start"
1591
1763
  }
1592
1764
  ),
1593
- /* @__PURE__ */ jsx15(
1765
+ /* @__PURE__ */ jsx16(
1594
1766
  LoopMarker,
1595
1767
  {
1596
1768
  $left: endPosition - 2,
@@ -1601,7 +1773,7 @@ var LoopRegion = ({
1601
1773
  )
1602
1774
  ] });
1603
1775
  };
1604
- var DraggableMarkerHandle = styled18.div.attrs((props) => ({
1776
+ var DraggableMarkerHandle = styled19.div.attrs((props) => ({
1605
1777
  style: {
1606
1778
  left: `${props.$left}px`
1607
1779
  }
@@ -1643,7 +1815,7 @@ var DraggableMarkerHandle = styled18.div.attrs((props) => ({
1643
1815
  opacity: 1;
1644
1816
  }
1645
1817
  `;
1646
- var TimescaleLoopShade = styled18.div.attrs((props) => ({
1818
+ var TimescaleLoopShade = styled19.div.attrs((props) => ({
1647
1819
  style: {
1648
1820
  left: `${props.$left}px`,
1649
1821
  width: `${props.$width}px`
@@ -1741,7 +1913,7 @@ var LoopRegionMarkers = ({
1741
1913
  return null;
1742
1914
  }
1743
1915
  return /* @__PURE__ */ jsxs6(Fragment2, { children: [
1744
- /* @__PURE__ */ jsx15(
1916
+ /* @__PURE__ */ jsx16(
1745
1917
  TimescaleLoopShade,
1746
1918
  {
1747
1919
  $left: startPosition,
@@ -1752,7 +1924,7 @@ var LoopRegionMarkers = ({
1752
1924
  "data-loop-region-timescale": true
1753
1925
  }
1754
1926
  ),
1755
- /* @__PURE__ */ jsx15(
1927
+ /* @__PURE__ */ jsx16(
1756
1928
  DraggableMarkerHandle,
1757
1929
  {
1758
1930
  $left: startPosition,
@@ -1763,7 +1935,7 @@ var LoopRegionMarkers = ({
1763
1935
  "data-loop-marker-handle": "start"
1764
1936
  }
1765
1937
  ),
1766
- /* @__PURE__ */ jsx15(
1938
+ /* @__PURE__ */ jsx16(
1767
1939
  DraggableMarkerHandle,
1768
1940
  {
1769
1941
  $left: endPosition,
@@ -1776,13 +1948,10 @@ var LoopRegionMarkers = ({
1776
1948
  )
1777
1949
  ] });
1778
1950
  };
1779
- var TimescaleLoopCreator = styled18.div.attrs((props) => ({
1780
- style: {
1781
- left: `${props.$leftOffset || 0}px`
1782
- }
1783
- }))`
1951
+ var TimescaleLoopCreator = styled19.div`
1784
1952
  position: absolute;
1785
1953
  top: 0;
1954
+ left: 0;
1786
1955
  right: 0;
1787
1956
  height: 100%; /* Stay within timescale bounds, don't extend into tracks */
1788
1957
  cursor: crosshair;
@@ -1795,8 +1964,7 @@ var TimescaleLoopRegion = ({
1795
1964
  regionColor = "rgba(59, 130, 246, 0.3)",
1796
1965
  onLoopRegionChange,
1797
1966
  minPosition = 0,
1798
- maxPosition = Infinity,
1799
- controlsOffset = 0
1967
+ maxPosition = Infinity
1800
1968
  }) => {
1801
1969
  const [, setIsCreating] = useState(false);
1802
1970
  const createStartX = useRef5(0);
@@ -1833,14 +2001,13 @@ var TimescaleLoopRegion = ({
1833
2001
  },
1834
2002
  [minPosition, maxPosition, onLoopRegionChange]
1835
2003
  );
1836
- return /* @__PURE__ */ jsx15(
2004
+ return /* @__PURE__ */ jsx16(
1837
2005
  TimescaleLoopCreator,
1838
2006
  {
1839
2007
  ref: containerRef,
1840
- $leftOffset: controlsOffset,
1841
2008
  onMouseDown: handleBackgroundMouseDown,
1842
2009
  "data-timescale-loop-creator": true,
1843
- children: hasLoopRegion && /* @__PURE__ */ jsx15(
2010
+ children: hasLoopRegion && /* @__PURE__ */ jsx16(
1844
2011
  LoopRegionMarkers,
1845
2012
  {
1846
2013
  startPosition,
@@ -1859,10 +2026,10 @@ var TimescaleLoopRegion = ({
1859
2026
  };
1860
2027
 
1861
2028
  // src/components/SelectionTimeInputs.tsx
1862
- import { useEffect as useEffect5, useState as useState3 } from "react";
2029
+ import { useEffect as useEffect7, useState as useState3 } from "react";
1863
2030
 
1864
2031
  // src/components/TimeInput.tsx
1865
- import { useEffect as useEffect4, useState as useState2 } from "react";
2032
+ import { useEffect as useEffect6, useState as useState2 } from "react";
1866
2033
 
1867
2034
  // src/utils/timeFormat.ts
1868
2035
  function clockFormat(seconds, decimals) {
@@ -1912,7 +2079,7 @@ function parseTime(timeStr, format) {
1912
2079
  }
1913
2080
 
1914
2081
  // src/components/TimeInput.tsx
1915
- import { Fragment as Fragment3, jsx as jsx16, jsxs as jsxs7 } from "react/jsx-runtime";
2082
+ import { Fragment as Fragment3, jsx as jsx17, jsxs as jsxs7 } from "react/jsx-runtime";
1916
2083
  var TimeInput = ({
1917
2084
  id,
1918
2085
  label,
@@ -1923,7 +2090,7 @@ var TimeInput = ({
1923
2090
  readOnly = false
1924
2091
  }) => {
1925
2092
  const [displayValue, setDisplayValue] = useState2("");
1926
- useEffect4(() => {
2093
+ useEffect6(() => {
1927
2094
  const formatted = formatTime(value, format);
1928
2095
  setDisplayValue(formatted);
1929
2096
  }, [value, format, id]);
@@ -1944,8 +2111,8 @@ var TimeInput = ({
1944
2111
  }
1945
2112
  };
1946
2113
  return /* @__PURE__ */ jsxs7(Fragment3, { children: [
1947
- /* @__PURE__ */ jsx16(ScreenReaderOnly, { as: "label", htmlFor: id, children: label }),
1948
- /* @__PURE__ */ jsx16(
2114
+ /* @__PURE__ */ jsx17(ScreenReaderOnly, { as: "label", htmlFor: id, children: label }),
2115
+ /* @__PURE__ */ jsx17(
1949
2116
  BaseInput,
1950
2117
  {
1951
2118
  type: "text",
@@ -1962,7 +2129,7 @@ var TimeInput = ({
1962
2129
  };
1963
2130
 
1964
2131
  // src/components/SelectionTimeInputs.tsx
1965
- import { jsx as jsx17, jsxs as jsxs8 } from "react/jsx-runtime";
2132
+ import { jsx as jsx18, jsxs as jsxs8 } from "react/jsx-runtime";
1966
2133
  var SelectionTimeInputs = ({
1967
2134
  selectionStart,
1968
2135
  selectionEnd,
@@ -1970,7 +2137,7 @@ var SelectionTimeInputs = ({
1970
2137
  className
1971
2138
  }) => {
1972
2139
  const [timeFormat, setTimeFormat] = useState3("hh:mm:ss.uuu");
1973
- useEffect5(() => {
2140
+ useEffect7(() => {
1974
2141
  const timeFormatSelect = document.querySelector(".time-format");
1975
2142
  const handleFormatChange = () => {
1976
2143
  if (timeFormatSelect) {
@@ -1996,7 +2163,7 @@ var SelectionTimeInputs = ({
1996
2163
  }
1997
2164
  };
1998
2165
  return /* @__PURE__ */ jsxs8("div", { className, children: [
1999
- /* @__PURE__ */ jsx17(
2166
+ /* @__PURE__ */ jsx18(
2000
2167
  TimeInput,
2001
2168
  {
2002
2169
  id: "audio_start",
@@ -2007,7 +2174,7 @@ var SelectionTimeInputs = ({
2007
2174
  onChange: handleStartChange
2008
2175
  }
2009
2176
  ),
2010
- /* @__PURE__ */ jsx17(
2177
+ /* @__PURE__ */ jsx18(
2011
2178
  TimeInput,
2012
2179
  {
2013
2180
  id: "audio_end",
@@ -2023,7 +2190,7 @@ var SelectionTimeInputs = ({
2023
2190
 
2024
2191
  // src/contexts/DevicePixelRatio.tsx
2025
2192
  import { useState as useState4, createContext as createContext3, useContext as useContext3 } from "react";
2026
- import { jsx as jsx18 } from "react/jsx-runtime";
2193
+ import { jsx as jsx19 } from "react/jsx-runtime";
2027
2194
  function getScale() {
2028
2195
  return window.devicePixelRatio;
2029
2196
  }
@@ -2037,7 +2204,7 @@ var DevicePixelRatioProvider = ({ children }) => {
2037
2204
  },
2038
2205
  { once: true }
2039
2206
  );
2040
- return /* @__PURE__ */ jsx18(DevicePixelRatioContext.Provider, { value: Math.ceil(scale), children });
2207
+ return /* @__PURE__ */ jsx19(DevicePixelRatioContext.Provider, { value: Math.ceil(scale), children });
2041
2208
  };
2042
2209
  var useDevicePixelRatio = () => useContext3(DevicePixelRatioContext);
2043
2210
 
@@ -2066,8 +2233,8 @@ var useTheme2 = () => useContext5(ThemeContext);
2066
2233
 
2067
2234
  // src/contexts/TrackControls.tsx
2068
2235
  import { createContext as createContext5, useContext as useContext6, Fragment as Fragment4 } from "react";
2069
- import { jsx as jsx19 } from "react/jsx-runtime";
2070
- var TrackControlsContext = createContext5(/* @__PURE__ */ jsx19(Fragment4, {}));
2236
+ import { jsx as jsx20 } from "react/jsx-runtime";
2237
+ var TrackControlsContext = createContext5(/* @__PURE__ */ jsx20(Fragment4, {}));
2071
2238
  var useTrackControls = () => useContext6(TrackControlsContext);
2072
2239
 
2073
2240
  // src/contexts/Playout.tsx
@@ -2076,7 +2243,7 @@ import {
2076
2243
  createContext as createContext6,
2077
2244
  useContext as useContext7
2078
2245
  } from "react";
2079
- import { jsx as jsx20 } from "react/jsx-runtime";
2246
+ import { jsx as jsx21 } from "react/jsx-runtime";
2080
2247
  var defaultProgress = 0;
2081
2248
  var defaultIsPlaying = false;
2082
2249
  var defaultSelectionStart = 0;
@@ -2105,18 +2272,18 @@ var PlayoutProvider = ({ children }) => {
2105
2272
  setSelectionStart(start);
2106
2273
  setSelectionEnd(end);
2107
2274
  };
2108
- return /* @__PURE__ */ jsx20(PlayoutStatusUpdateContext.Provider, { value: { setIsPlaying, setProgress, setSelection }, children: /* @__PURE__ */ jsx20(PlayoutStatusContext.Provider, { value: { isPlaying, progress, selectionStart, selectionEnd }, children }) });
2275
+ return /* @__PURE__ */ jsx21(PlayoutStatusUpdateContext.Provider, { value: { setIsPlaying, setProgress, setSelection }, children: /* @__PURE__ */ jsx21(PlayoutStatusContext.Provider, { value: { isPlaying, progress, selectionStart, selectionEnd }, children }) });
2109
2276
  };
2110
2277
  var usePlayoutStatus = () => useContext7(PlayoutStatusContext);
2111
2278
  var usePlayoutStatusUpdate = () => useContext7(PlayoutStatusUpdateContext);
2112
2279
 
2113
2280
  // src/components/SpectrogramChannel.tsx
2114
- import { useLayoutEffect as useLayoutEffect2, useRef as useRef6, useEffect as useEffect6 } from "react";
2115
- import styled19 from "styled-components";
2116
- import { MAX_CANVAS_WIDTH as MAX_CANVAS_WIDTH2 } from "@waveform-playlist/core";
2117
- import { jsx as jsx21 } from "react/jsx-runtime";
2281
+ import { useRef as useRef6, useEffect as useEffect8 } from "react";
2282
+ import styled20 from "styled-components";
2283
+ import { MAX_CANVAS_WIDTH as MAX_CANVAS_WIDTH3 } from "@waveform-playlist/core";
2284
+ import { jsx as jsx22 } from "react/jsx-runtime";
2118
2285
  var LINEAR_FREQUENCY_SCALE = (f, minF, maxF) => (f - minF) / (maxF - minF);
2119
- var Wrapper3 = styled19.div.attrs((props) => ({
2286
+ var Wrapper4 = styled20.div.attrs((props) => ({
2120
2287
  style: {
2121
2288
  top: `${props.$waveHeight * props.$index}px`,
2122
2289
  width: `${props.$cssWidth}px`,
@@ -2128,7 +2295,7 @@ var Wrapper3 = styled19.div.attrs((props) => ({
2128
2295
  transform: translateZ(0);
2129
2296
  backface-visibility: hidden;
2130
2297
  `;
2131
- var SpectrogramCanvas = styled19.canvas.attrs((props) => ({
2298
+ var SpectrogramCanvas = styled20.canvas.attrs((props) => ({
2132
2299
  style: {
2133
2300
  width: `${props.$cssWidth}px`,
2134
2301
  height: `${props.$waveHeight}px`,
@@ -2137,8 +2304,6 @@ var SpectrogramCanvas = styled19.canvas.attrs((props) => ({
2137
2304
  }))`
2138
2305
  position: absolute;
2139
2306
  top: 0;
2140
- /* Promote to own compositing layer for smoother scrolling */
2141
- will-change: transform;
2142
2307
  image-rendering: pixelated;
2143
2308
  image-rendering: crisp-edges;
2144
2309
  `;
@@ -2174,18 +2339,18 @@ var SpectrogramChannel = ({
2174
2339
  const onCanvasesReadyRef = useRef6(onCanvasesReady);
2175
2340
  const isWorkerMode = !!(workerApi && clipId);
2176
2341
  const clipOriginX = useClipViewportOrigin();
2177
- const visibleChunkIndices = useVisibleChunkIndices(length, MAX_CANVAS_WIDTH2, clipOriginX);
2342
+ const visibleChunkIndices = useVisibleChunkIndices(length, MAX_CANVAS_WIDTH3, clipOriginX);
2178
2343
  const lut = colorLUT ?? DEFAULT_COLOR_LUT;
2179
2344
  const maxF = maxFrequency ?? (data ? data.sampleRate / 2 : 22050);
2180
2345
  const scaleFn = frequencyScaleFn ?? LINEAR_FREQUENCY_SCALE;
2181
2346
  const hasCustomFrequencyScale = Boolean(frequencyScaleFn);
2182
- useEffect6(() => {
2347
+ useEffect8(() => {
2183
2348
  workerApiRef.current = workerApi;
2184
2349
  }, [workerApi]);
2185
- useEffect6(() => {
2350
+ useEffect8(() => {
2186
2351
  onCanvasesReadyRef.current = onCanvasesReady;
2187
2352
  }, [onCanvasesReady]);
2188
- useEffect6(() => {
2353
+ useEffect8(() => {
2189
2354
  if (!isWorkerMode) return;
2190
2355
  const currentWorkerApi = workerApiRef.current;
2191
2356
  if (!currentWorkerApi || !clipId) return;
@@ -2240,15 +2405,15 @@ var SpectrogramChannel = ({
2240
2405
  const match = id.match(/chunk(\d+)$/);
2241
2406
  if (!match) {
2242
2407
  console.warn(`[spectrogram] Unexpected canvas ID format: ${id}`);
2243
- return MAX_CANVAS_WIDTH2;
2408
+ return MAX_CANVAS_WIDTH3;
2244
2409
  }
2245
2410
  const chunkIdx = parseInt(match[1], 10);
2246
- return Math.min(length - chunkIdx * MAX_CANVAS_WIDTH2, MAX_CANVAS_WIDTH2);
2411
+ return Math.min(length - chunkIdx * MAX_CANVAS_WIDTH3, MAX_CANVAS_WIDTH3);
2247
2412
  });
2248
2413
  onCanvasesReadyRef.current?.(allIds, allWidths);
2249
2414
  }
2250
2415
  }, [canvasMapRef, isWorkerMode, clipId, channelIndex, length, visibleChunkIndices]);
2251
- useEffect6(() => {
2416
+ useEffect8(() => {
2252
2417
  return () => {
2253
2418
  const api = workerApiRef.current;
2254
2419
  if (!api) return;
@@ -2262,7 +2427,7 @@ var SpectrogramChannel = ({
2262
2427
  registeredIdsRef.current = [];
2263
2428
  };
2264
2429
  }, []);
2265
- useLayoutEffect2(() => {
2430
+ useEffect8(() => {
2266
2431
  if (isWorkerMode || !data) return;
2267
2432
  const {
2268
2433
  frequencyBinCount,
@@ -2275,7 +2440,7 @@ var SpectrogramChannel = ({
2275
2440
  const rangeDb = rawRangeDb === 0 ? 1 : rawRangeDb;
2276
2441
  const binToFreq = (bin) => bin / frequencyBinCount * (sampleRate / 2);
2277
2442
  for (const [canvasIdx, canvas] of canvasMapRef.current.entries()) {
2278
- const globalPixelOffset = canvasIdx * MAX_CANVAS_WIDTH2;
2443
+ const globalPixelOffset = canvasIdx * MAX_CANVAS_WIDTH3;
2279
2444
  const ctx = canvas.getContext("2d");
2280
2445
  if (!ctx) continue;
2281
2446
  const canvasWidth = canvas.width / devicePixelRatio;
@@ -2351,9 +2516,9 @@ var SpectrogramChannel = ({
2351
2516
  visibleChunkIndices
2352
2517
  ]);
2353
2518
  const canvases = visibleChunkIndices.map((i) => {
2354
- const chunkLeft = i * MAX_CANVAS_WIDTH2;
2355
- const currentWidth = Math.min(length - chunkLeft, MAX_CANVAS_WIDTH2);
2356
- return /* @__PURE__ */ jsx21(
2519
+ const chunkLeft = i * MAX_CANVAS_WIDTH3;
2520
+ const currentWidth = Math.min(length - chunkLeft, MAX_CANVAS_WIDTH3);
2521
+ return /* @__PURE__ */ jsx22(
2357
2522
  SpectrogramCanvas,
2358
2523
  {
2359
2524
  $cssWidth: currentWidth,
@@ -2367,11 +2532,11 @@ var SpectrogramChannel = ({
2367
2532
  `${length}-${i}`
2368
2533
  );
2369
2534
  });
2370
- return /* @__PURE__ */ jsx21(Wrapper3, { $index: index, $cssWidth: length, $waveHeight: waveHeight, children: canvases });
2535
+ return /* @__PURE__ */ jsx22(Wrapper4, { $index: index, $cssWidth: length, $waveHeight: waveHeight, children: canvases });
2371
2536
  };
2372
2537
 
2373
2538
  // src/components/SmartChannel.tsx
2374
- import { Fragment as Fragment5, jsx as jsx22, jsxs as jsxs9 } from "react/jsx-runtime";
2539
+ import { Fragment as Fragment5, jsx as jsx23, jsxs as jsxs9 } from "react/jsx-runtime";
2375
2540
  var SmartChannel = ({
2376
2541
  isSelected,
2377
2542
  transparentBackground,
@@ -2385,10 +2550,19 @@ var SmartChannel = ({
2385
2550
  spectrogramWorkerApi,
2386
2551
  spectrogramClipId,
2387
2552
  spectrogramOnCanvasesReady,
2553
+ midiNotes,
2554
+ sampleRate: sampleRateProp,
2555
+ clipOffsetSeconds,
2388
2556
  ...props
2389
2557
  }) => {
2390
2558
  const theme = useTheme2();
2391
- const { waveHeight, barWidth, barGap, samplesPerPixel: contextSpp } = usePlaylistInfo();
2559
+ const {
2560
+ waveHeight,
2561
+ barWidth,
2562
+ barGap,
2563
+ samplesPerPixel: contextSpp,
2564
+ sampleRate: contextSampleRate
2565
+ } = usePlaylistInfo();
2392
2566
  const devicePixelRatio = useDevicePixelRatio();
2393
2567
  const samplesPerPixel = sppProp ?? contextSpp;
2394
2568
  const waveOutlineColor = isSelected && theme ? theme.selectedWaveOutlineColor : theme?.waveOutlineColor;
@@ -2396,7 +2570,7 @@ var SmartChannel = ({
2396
2570
  const drawMode = theme?.waveformDrawMode || "inverted";
2397
2571
  const hasSpectrogram = spectrogramData || spectrogramWorkerApi;
2398
2572
  if (renderMode === "spectrogram" && hasSpectrogram) {
2399
- return /* @__PURE__ */ jsx22(
2573
+ return /* @__PURE__ */ jsx23(
2400
2574
  SpectrogramChannel,
2401
2575
  {
2402
2576
  index: props.index,
@@ -2418,7 +2592,7 @@ var SmartChannel = ({
2418
2592
  if (renderMode === "both" && hasSpectrogram) {
2419
2593
  const halfHeight = Math.floor(waveHeight / 2);
2420
2594
  return /* @__PURE__ */ jsxs9(Fragment5, { children: [
2421
- /* @__PURE__ */ jsx22(
2595
+ /* @__PURE__ */ jsx23(
2422
2596
  SpectrogramChannel,
2423
2597
  {
2424
2598
  index: props.index * 2,
@@ -2437,7 +2611,7 @@ var SmartChannel = ({
2437
2611
  onCanvasesReady: spectrogramOnCanvasesReady
2438
2612
  }
2439
2613
  ),
2440
- /* @__PURE__ */ jsx22(
2614
+ /* @__PURE__ */ jsx23(
2441
2615
  "div",
2442
2616
  {
2443
2617
  style: {
@@ -2446,7 +2620,7 @@ var SmartChannel = ({
2446
2620
  width: props.length,
2447
2621
  height: halfHeight
2448
2622
  },
2449
- children: /* @__PURE__ */ jsx22(
2623
+ children: /* @__PURE__ */ jsx23(
2450
2624
  Channel,
2451
2625
  {
2452
2626
  ...props,
@@ -2465,7 +2639,27 @@ var SmartChannel = ({
2465
2639
  )
2466
2640
  ] });
2467
2641
  }
2468
- return /* @__PURE__ */ jsx22(
2642
+ if (renderMode === "piano-roll") {
2643
+ return /* @__PURE__ */ jsx23(
2644
+ PianoRollChannel,
2645
+ {
2646
+ index: props.index,
2647
+ midiNotes: midiNotes ?? [],
2648
+ length: props.length,
2649
+ waveHeight,
2650
+ devicePixelRatio,
2651
+ samplesPerPixel,
2652
+ sampleRate: sampleRateProp ?? contextSampleRate,
2653
+ clipOffsetSeconds: clipOffsetSeconds ?? 0,
2654
+ noteColor: theme?.pianoRollNoteColor,
2655
+ selectedNoteColor: theme?.pianoRollSelectedNoteColor,
2656
+ isSelected,
2657
+ transparentBackground,
2658
+ backgroundColor: theme?.pianoRollBackgroundColor
2659
+ }
2660
+ );
2661
+ }
2662
+ return /* @__PURE__ */ jsx23(
2469
2663
  Channel,
2470
2664
  {
2471
2665
  ...props,
@@ -2482,11 +2676,11 @@ var SmartChannel = ({
2482
2676
  };
2483
2677
 
2484
2678
  // src/components/SpectrogramLabels.tsx
2485
- import { useRef as useRef7, useLayoutEffect as useLayoutEffect3 } from "react";
2486
- import styled20 from "styled-components";
2487
- import { jsx as jsx23 } from "react/jsx-runtime";
2679
+ import { useRef as useRef7, useLayoutEffect as useLayoutEffect2 } from "react";
2680
+ import styled21 from "styled-components";
2681
+ import { jsx as jsx24 } from "react/jsx-runtime";
2488
2682
  var LABELS_WIDTH = 72;
2489
- var LabelsStickyWrapper = styled20.div`
2683
+ var LabelsStickyWrapper = styled21.div`
2490
2684
  position: sticky;
2491
2685
  left: 0;
2492
2686
  z-index: 101;
@@ -2539,7 +2733,7 @@ var SpectrogramLabels = ({
2539
2733
  const spectrogramHeight = renderMode === "both" ? Math.floor(waveHeight / 2) : waveHeight;
2540
2734
  const totalHeight = numChannels * waveHeight;
2541
2735
  const clipHeaderOffset = hasClipHeaders ? 22 : 0;
2542
- useLayoutEffect3(() => {
2736
+ useLayoutEffect2(() => {
2543
2737
  const canvas = canvasRef.current;
2544
2738
  if (!canvas) return;
2545
2739
  const ctx = canvas.getContext("2d");
@@ -2577,7 +2771,7 @@ var SpectrogramLabels = ({
2577
2771
  spectrogramHeight,
2578
2772
  clipHeaderOffset
2579
2773
  ]);
2580
- return /* @__PURE__ */ jsx23(LabelsStickyWrapper, { $height: totalHeight + clipHeaderOffset, children: /* @__PURE__ */ jsx23(
2774
+ return /* @__PURE__ */ jsx24(LabelsStickyWrapper, { $height: totalHeight + clipHeaderOffset, children: /* @__PURE__ */ jsx24(
2581
2775
  "canvas",
2582
2776
  {
2583
2777
  ref: canvasRef,
@@ -2596,8 +2790,8 @@ var SpectrogramLabels = ({
2596
2790
  import { useContext as useContext9 } from "react";
2597
2791
 
2598
2792
  // src/components/TimeScale.tsx
2599
- import React16, { useLayoutEffect as useLayoutEffect4, useContext as useContext8, useMemo as useMemo2 } from "react";
2600
- import styled21, { withTheme as withTheme2 } from "styled-components";
2793
+ import React17, { useLayoutEffect as useLayoutEffect3, useContext as useContext8, useMemo as useMemo3 } from "react";
2794
+ import styled22, { withTheme as withTheme2 } from "styled-components";
2601
2795
 
2602
2796
  // src/utils/conversions.ts
2603
2797
  function samplesToSeconds(samples, sampleRate) {
@@ -2620,18 +2814,17 @@ function secondsToPixels(seconds, samplesPerPixel, sampleRate) {
2620
2814
  }
2621
2815
 
2622
2816
  // src/components/TimeScale.tsx
2623
- import { MAX_CANVAS_WIDTH as MAX_CANVAS_WIDTH3 } from "@waveform-playlist/core";
2624
- import { jsx as jsx24, jsxs as jsxs10 } from "react/jsx-runtime";
2817
+ import { MAX_CANVAS_WIDTH as MAX_CANVAS_WIDTH4 } from "@waveform-playlist/core";
2818
+ import { jsx as jsx25, jsxs as jsxs10 } from "react/jsx-runtime";
2625
2819
  function formatTime2(milliseconds) {
2626
2820
  const seconds = Math.floor(milliseconds / 1e3);
2627
2821
  const s = seconds % 60;
2628
2822
  const m = (seconds - s) / 60;
2629
2823
  return `${m}:${String(s).padStart(2, "0")}`;
2630
2824
  }
2631
- var PlaylistTimeScaleScroll = styled21.div.attrs((props) => ({
2825
+ var PlaylistTimeScaleScroll = styled22.div.attrs((props) => ({
2632
2826
  style: {
2633
2827
  width: `${props.$cssWidth}px`,
2634
- marginLeft: `${props.$controlWidth}px`,
2635
2828
  height: `${props.$timeScaleHeight}px`
2636
2829
  }
2637
2830
  }))`
@@ -2640,7 +2833,7 @@ var PlaylistTimeScaleScroll = styled21.div.attrs((props) => ({
2640
2833
  border-bottom: 1px solid ${(props) => props.theme.timeColor};
2641
2834
  box-sizing: border-box;
2642
2835
  `;
2643
- var TimeTickChunk = styled21.canvas.attrs((props) => ({
2836
+ var TimeTickChunk = styled22.canvas.attrs((props) => ({
2644
2837
  style: {
2645
2838
  width: `${props.$cssWidth}px`,
2646
2839
  height: `${props.$timeScaleHeight}px`,
@@ -2649,10 +2842,8 @@ var TimeTickChunk = styled21.canvas.attrs((props) => ({
2649
2842
  }))`
2650
2843
  position: absolute;
2651
2844
  bottom: 0;
2652
- /* Promote to own compositing layer for smoother scrolling */
2653
- will-change: transform;
2654
2845
  `;
2655
- var TimeStamp = styled21.div.attrs((props) => ({
2846
+ var TimeStamp = styled22.div.attrs((props) => ({
2656
2847
  style: {
2657
2848
  left: `${props.$left + 4}px`
2658
2849
  // Offset 4px to the right of the tick
@@ -2673,14 +2864,9 @@ var TimeScale = (props) => {
2673
2864
  renderTimestamp
2674
2865
  } = props;
2675
2866
  const { canvasRef, canvasMapRef } = useChunkedCanvasRefs();
2676
- const {
2677
- sampleRate,
2678
- samplesPerPixel,
2679
- timeScaleHeight,
2680
- controls: { show: showControls, width: controlWidth }
2681
- } = useContext8(PlaylistInfoContext);
2867
+ const { sampleRate, samplesPerPixel, timeScaleHeight } = useContext8(PlaylistInfoContext);
2682
2868
  const devicePixelRatio = useDevicePixelRatio();
2683
- const { widthX, canvasInfo, timeMarkersWithPositions } = useMemo2(() => {
2869
+ const { widthX, canvasInfo, timeMarkersWithPositions } = useMemo3(() => {
2684
2870
  const nextCanvasInfo = /* @__PURE__ */ new Map();
2685
2871
  const nextMarkers = [];
2686
2872
  const nextWidthX = secondsToPixels(duration / 1e3, samplesPerPixel, sampleRate);
@@ -2691,7 +2877,7 @@ var TimeScale = (props) => {
2691
2877
  if (counter % marker === 0) {
2692
2878
  const timeMs = counter;
2693
2879
  const timestamp = formatTime2(timeMs);
2694
- const element = renderTimestamp ? /* @__PURE__ */ jsx24(React16.Fragment, { children: renderTimestamp(timeMs, pix) }, `timestamp-${counter}`) : /* @__PURE__ */ jsx24(TimeStamp, { $left: pix, children: timestamp }, timestamp);
2880
+ const element = renderTimestamp ? /* @__PURE__ */ jsx25(React17.Fragment, { children: renderTimestamp(timeMs, pix) }, `timestamp-${counter}`) : /* @__PURE__ */ jsx25(TimeStamp, { $left: pix, children: timestamp }, timestamp);
2695
2881
  nextMarkers.push({ pix, element });
2696
2882
  nextCanvasInfo.set(pix, timeScaleHeight);
2697
2883
  } else if (counter % bigStep === 0) {
@@ -2716,11 +2902,11 @@ var TimeScale = (props) => {
2716
2902
  renderTimestamp,
2717
2903
  timeScaleHeight
2718
2904
  ]);
2719
- const visibleChunkIndices = useVisibleChunkIndices(widthX, MAX_CANVAS_WIDTH3);
2905
+ const visibleChunkIndices = useVisibleChunkIndices(widthX, MAX_CANVAS_WIDTH4);
2720
2906
  const visibleChunks = visibleChunkIndices.map((i) => {
2721
- const chunkLeft = i * MAX_CANVAS_WIDTH3;
2722
- const chunkWidth = Math.min(widthX - chunkLeft, MAX_CANVAS_WIDTH3);
2723
- return /* @__PURE__ */ jsx24(
2907
+ const chunkLeft = i * MAX_CANVAS_WIDTH4;
2908
+ const chunkWidth = Math.min(widthX - chunkLeft, MAX_CANVAS_WIDTH4);
2909
+ return /* @__PURE__ */ jsx25(
2724
2910
  TimeTickChunk,
2725
2911
  {
2726
2912
  $cssWidth: chunkWidth,
@@ -2734,14 +2920,14 @@ var TimeScale = (props) => {
2734
2920
  `timescale-${i}`
2735
2921
  );
2736
2922
  });
2737
- const firstChunkLeft = visibleChunkIndices.length > 0 ? visibleChunkIndices[0] * MAX_CANVAS_WIDTH3 : 0;
2738
- const lastChunkRight = visibleChunkIndices.length > 0 ? (visibleChunkIndices[visibleChunkIndices.length - 1] + 1) * MAX_CANVAS_WIDTH3 : Infinity;
2923
+ const firstChunkLeft = visibleChunkIndices.length > 0 ? visibleChunkIndices[0] * MAX_CANVAS_WIDTH4 : 0;
2924
+ const lastChunkRight = visibleChunkIndices.length > 0 ? (visibleChunkIndices[visibleChunkIndices.length - 1] + 1) * MAX_CANVAS_WIDTH4 : Infinity;
2739
2925
  const visibleMarkers = visibleChunkIndices.length > 0 ? timeMarkersWithPositions.filter(({ pix }) => pix >= firstChunkLeft && pix < lastChunkRight).map(({ element }) => element) : timeMarkersWithPositions.map(({ element }) => element);
2740
- useLayoutEffect4(() => {
2926
+ useLayoutEffect3(() => {
2741
2927
  for (const [chunkIdx, canvas] of canvasMapRef.current.entries()) {
2742
2928
  const ctx = canvas.getContext("2d");
2743
2929
  if (!ctx) continue;
2744
- const chunkLeft = chunkIdx * MAX_CANVAS_WIDTH3;
2930
+ const chunkLeft = chunkIdx * MAX_CANVAS_WIDTH4;
2745
2931
  const chunkWidth = canvas.width / devicePixelRatio;
2746
2932
  ctx.resetTransform();
2747
2933
  ctx.clearRect(0, 0, canvas.width, canvas.height);
@@ -2764,23 +2950,15 @@ var TimeScale = (props) => {
2764
2950
  canvasInfo,
2765
2951
  visibleChunkIndices
2766
2952
  ]);
2767
- return /* @__PURE__ */ jsxs10(
2768
- PlaylistTimeScaleScroll,
2769
- {
2770
- $cssWidth: widthX,
2771
- $controlWidth: showControls ? controlWidth : 0,
2772
- $timeScaleHeight: timeScaleHeight,
2773
- children: [
2774
- visibleMarkers,
2775
- visibleChunks
2776
- ]
2777
- }
2778
- );
2953
+ return /* @__PURE__ */ jsxs10(PlaylistTimeScaleScroll, { $cssWidth: widthX, $timeScaleHeight: timeScaleHeight, children: [
2954
+ visibleMarkers,
2955
+ visibleChunks
2956
+ ] });
2779
2957
  };
2780
2958
  var StyledTimeScale = withTheme2(TimeScale);
2781
2959
 
2782
2960
  // src/components/SmartScale.tsx
2783
- import { jsx as jsx25 } from "react/jsx-runtime";
2961
+ import { jsx as jsx26 } from "react/jsx-runtime";
2784
2962
  var timeinfo = /* @__PURE__ */ new Map([
2785
2963
  [
2786
2964
  700,
@@ -2856,7 +3034,7 @@ function getScaleInfo(samplesPerPixel) {
2856
3034
  var SmartScale = ({ renderTimestamp }) => {
2857
3035
  const { samplesPerPixel, duration } = useContext9(PlaylistInfoContext);
2858
3036
  let config = getScaleInfo(samplesPerPixel);
2859
- return /* @__PURE__ */ jsx25(
3037
+ return /* @__PURE__ */ jsx26(
2860
3038
  StyledTimeScale,
2861
3039
  {
2862
3040
  marker: config.marker,
@@ -2869,9 +3047,9 @@ var SmartScale = ({ renderTimestamp }) => {
2869
3047
  };
2870
3048
 
2871
3049
  // src/components/TimeFormatSelect.tsx
2872
- import styled22 from "styled-components";
2873
- import { jsx as jsx26 } from "react/jsx-runtime";
2874
- var SelectWrapper = styled22.div`
3050
+ import styled23 from "styled-components";
3051
+ import { jsx as jsx27 } from "react/jsx-runtime";
3052
+ var SelectWrapper = styled23.div`
2875
3053
  display: inline-flex;
2876
3054
  align-items: center;
2877
3055
  gap: 0.5rem;
@@ -2893,7 +3071,7 @@ var TimeFormatSelect = ({
2893
3071
  const handleChange = (e) => {
2894
3072
  onChange(e.target.value);
2895
3073
  };
2896
- return /* @__PURE__ */ jsx26(SelectWrapper, { className, children: /* @__PURE__ */ jsx26(
3074
+ return /* @__PURE__ */ jsx27(SelectWrapper, { className, children: /* @__PURE__ */ jsx27(
2897
3075
  BaseSelect,
2898
3076
  {
2899
3077
  className: "time-format",
@@ -2901,50 +3079,30 @@ var TimeFormatSelect = ({
2901
3079
  onChange: handleChange,
2902
3080
  disabled,
2903
3081
  "aria-label": "Time format selection",
2904
- children: TIME_FORMAT_OPTIONS.map((option) => /* @__PURE__ */ jsx26("option", { value: option.value, children: option.label }, option.value))
3082
+ children: TIME_FORMAT_OPTIONS.map((option) => /* @__PURE__ */ jsx27("option", { value: option.value, children: option.label }, option.value))
2905
3083
  }
2906
3084
  ) });
2907
3085
  };
2908
3086
 
2909
3087
  // src/components/Track.tsx
2910
- import styled23 from "styled-components";
2911
- import { jsx as jsx27, jsxs as jsxs11 } from "react/jsx-runtime";
2912
- var Container = styled23.div.attrs((props) => ({
3088
+ import styled24 from "styled-components";
3089
+ import { jsx as jsx28 } from "react/jsx-runtime";
3090
+ var Container = styled24.div.attrs((props) => ({
2913
3091
  style: {
2914
3092
  height: `${props.$waveHeight * props.$numChannels + (props.$hasClipHeaders ? CLIP_HEADER_HEIGHT : 0)}px`
2915
3093
  }
2916
3094
  }))`
2917
3095
  position: relative;
2918
- display: flex;
2919
3096
  ${(props) => props.$width !== void 0 && `width: ${props.$width}px;`}
2920
3097
  `;
2921
- var ChannelContainer = styled23.div.attrs((props) => ({
3098
+ var ChannelContainer = styled24.div.attrs((props) => ({
2922
3099
  style: {
2923
3100
  paddingLeft: `${props.$offset || 0}px`
2924
3101
  }
2925
3102
  }))`
2926
3103
  position: relative;
2927
3104
  background: ${(props) => props.$backgroundColor || "transparent"};
2928
- flex: 1;
2929
- `;
2930
- var ControlsWrapper = styled23.div.attrs((props) => ({
2931
- style: {
2932
- width: `${props.$controlWidth}px`
2933
- }
2934
- }))`
2935
- position: sticky;
2936
- z-index: 102; /* Above waveform content and spectrogram labels (101), below Docusaurus navbar (200) */
2937
- left: 0;
2938
3105
  height: 100%;
2939
- flex-shrink: 0;
2940
- pointer-events: auto;
2941
- background: ${(props) => props.theme.surfaceColor};
2942
- transition: background 0.15s ease-in-out;
2943
-
2944
- /* Selected track: highlighted background */
2945
- ${(props) => props.$isSelected && `
2946
- background: ${props.theme.selectedTrackControlsBackground};
2947
- `}
2948
3106
  `;
2949
3107
  var Track = ({
2950
3108
  numChannels,
@@ -2956,44 +3114,34 @@ var Track = ({
2956
3114
  hasClipHeaders = false,
2957
3115
  onClick,
2958
3116
  trackId,
2959
- isSelected = false
3117
+ isSelected: _isSelected = false
2960
3118
  }) => {
2961
- const {
2962
- waveHeight,
2963
- controls: { show, width: controlWidth }
2964
- } = usePlaylistInfo();
2965
- const controls = useTrackControls();
2966
- return /* @__PURE__ */ jsxs11(
3119
+ const { waveHeight } = usePlaylistInfo();
3120
+ return /* @__PURE__ */ jsx28(
2967
3121
  Container,
2968
3122
  {
2969
3123
  $numChannels: numChannels,
2970
3124
  className,
2971
3125
  $waveHeight: waveHeight,
2972
- $controlWidth: show ? controlWidth : 0,
2973
3126
  $width: width,
2974
3127
  $hasClipHeaders: hasClipHeaders,
2975
- $isSelected: isSelected,
2976
- children: [
2977
- /* @__PURE__ */ jsx27(ControlsWrapper, { $controlWidth: show ? controlWidth : 0, $isSelected: isSelected, children: controls }),
2978
- /* @__PURE__ */ jsx27(
2979
- ChannelContainer,
2980
- {
2981
- $controlWidth: show ? controlWidth : 0,
2982
- $backgroundColor: backgroundColor,
2983
- $offset: offset,
2984
- onClick,
2985
- "data-track-id": trackId,
2986
- children
2987
- }
2988
- )
2989
- ]
3128
+ children: /* @__PURE__ */ jsx28(
3129
+ ChannelContainer,
3130
+ {
3131
+ $backgroundColor: backgroundColor,
3132
+ $offset: offset,
3133
+ onClick,
3134
+ "data-track-id": trackId,
3135
+ children
3136
+ }
3137
+ )
2990
3138
  }
2991
3139
  );
2992
3140
  };
2993
3141
 
2994
3142
  // src/components/TrackControls/Button.tsx
2995
- import styled24 from "styled-components";
2996
- var Button = styled24.button.attrs({
3143
+ import styled25 from "styled-components";
3144
+ var Button = styled25.button.attrs({
2997
3145
  type: "button"
2998
3146
  })`
2999
3147
  display: inline-block;
@@ -3068,8 +3216,8 @@ var Button = styled24.button.attrs({
3068
3216
  `;
3069
3217
 
3070
3218
  // src/components/TrackControls/ButtonGroup.tsx
3071
- import styled25 from "styled-components";
3072
- var ButtonGroup = styled25.div`
3219
+ import styled26 from "styled-components";
3220
+ var ButtonGroup = styled26.div`
3073
3221
  margin-bottom: 0.3rem;
3074
3222
 
3075
3223
  button:not(:first-child) {
@@ -3084,10 +3232,10 @@ var ButtonGroup = styled25.div`
3084
3232
  `;
3085
3233
 
3086
3234
  // src/components/TrackControls/CloseButton.tsx
3087
- import styled26 from "styled-components";
3235
+ import styled27 from "styled-components";
3088
3236
  import { X as XIcon } from "@phosphor-icons/react";
3089
- import { jsx as jsx28 } from "react/jsx-runtime";
3090
- var StyledCloseButton = styled26.button`
3237
+ import { jsx as jsx29 } from "react/jsx-runtime";
3238
+ var StyledCloseButton = styled27.button`
3091
3239
  position: absolute;
3092
3240
  left: 0;
3093
3241
  top: 0;
@@ -3110,11 +3258,11 @@ var StyledCloseButton = styled26.button`
3110
3258
  color: #dc3545;
3111
3259
  }
3112
3260
  `;
3113
- var CloseButton = ({ onClick, title = "Remove track" }) => /* @__PURE__ */ jsx28(StyledCloseButton, { onClick, title, children: /* @__PURE__ */ jsx28(XIcon, { size: 12, weight: "bold" }) });
3261
+ var CloseButton = ({ onClick, title = "Remove track" }) => /* @__PURE__ */ jsx29(StyledCloseButton, { onClick, title, children: /* @__PURE__ */ jsx29(XIcon, { size: 12, weight: "bold" }) });
3114
3262
 
3115
3263
  // src/components/TrackControls/Controls.tsx
3116
- import styled27 from "styled-components";
3117
- var Controls = styled27.div`
3264
+ import styled28 from "styled-components";
3265
+ var Controls = styled28.div`
3118
3266
  background: transparent;
3119
3267
  width: 100%;
3120
3268
  height: 100%;
@@ -3130,8 +3278,8 @@ var Controls = styled27.div`
3130
3278
  `;
3131
3279
 
3132
3280
  // src/components/TrackControls/Header.tsx
3133
- import styled28 from "styled-components";
3134
- var Header = styled28.header`
3281
+ import styled29 from "styled-components";
3282
+ var Header = styled29.header`
3135
3283
  overflow: hidden;
3136
3284
  height: 26px;
3137
3285
  width: 100%;
@@ -3146,27 +3294,27 @@ var Header = styled28.header`
3146
3294
 
3147
3295
  // src/components/TrackControls/VolumeDownIcon.tsx
3148
3296
  import { SpeakerLowIcon } from "@phosphor-icons/react";
3149
- import { jsx as jsx29 } from "react/jsx-runtime";
3150
- var VolumeDownIcon = (props) => /* @__PURE__ */ jsx29(SpeakerLowIcon, { weight: "light", ...props });
3297
+ import { jsx as jsx30 } from "react/jsx-runtime";
3298
+ var VolumeDownIcon = (props) => /* @__PURE__ */ jsx30(SpeakerLowIcon, { weight: "light", ...props });
3151
3299
 
3152
3300
  // src/components/TrackControls/VolumeUpIcon.tsx
3153
3301
  import { SpeakerHighIcon } from "@phosphor-icons/react";
3154
- import { jsx as jsx30 } from "react/jsx-runtime";
3155
- var VolumeUpIcon = (props) => /* @__PURE__ */ jsx30(SpeakerHighIcon, { weight: "light", ...props });
3302
+ import { jsx as jsx31 } from "react/jsx-runtime";
3303
+ var VolumeUpIcon = (props) => /* @__PURE__ */ jsx31(SpeakerHighIcon, { weight: "light", ...props });
3156
3304
 
3157
3305
  // src/components/TrackControls/TrashIcon.tsx
3158
3306
  import { TrashIcon as PhosphorTrashIcon } from "@phosphor-icons/react";
3159
- import { jsx as jsx31 } from "react/jsx-runtime";
3160
- var TrashIcon = (props) => /* @__PURE__ */ jsx31(PhosphorTrashIcon, { weight: "light", ...props });
3307
+ import { jsx as jsx32 } from "react/jsx-runtime";
3308
+ var TrashIcon = (props) => /* @__PURE__ */ jsx32(PhosphorTrashIcon, { weight: "light", ...props });
3161
3309
 
3162
3310
  // src/components/TrackControls/DotsIcon.tsx
3163
3311
  import { DotsThreeIcon } from "@phosphor-icons/react";
3164
- import { jsx as jsx32 } from "react/jsx-runtime";
3165
- var DotsIcon = (props) => /* @__PURE__ */ jsx32(DotsThreeIcon, { weight: "bold", ...props });
3312
+ import { jsx as jsx33 } from "react/jsx-runtime";
3313
+ var DotsIcon = (props) => /* @__PURE__ */ jsx33(DotsThreeIcon, { weight: "bold", ...props });
3166
3314
 
3167
3315
  // src/components/TrackControls/Slider.tsx
3168
- import styled29 from "styled-components";
3169
- var Slider = styled29(BaseSlider)`
3316
+ import styled30 from "styled-components";
3317
+ var Slider = styled30(BaseSlider)`
3170
3318
  width: 75%;
3171
3319
  height: 5px;
3172
3320
  background: ${(props) => props.theme.sliderTrackColor};
@@ -3218,8 +3366,8 @@ var Slider = styled29(BaseSlider)`
3218
3366
  `;
3219
3367
 
3220
3368
  // src/components/TrackControls/SliderWrapper.tsx
3221
- import styled30 from "styled-components";
3222
- var SliderWrapper = styled30.label`
3369
+ import styled31 from "styled-components";
3370
+ var SliderWrapper = styled31.label`
3223
3371
  width: 100%;
3224
3372
  display: flex;
3225
3373
  justify-content: space-between;
@@ -3230,15 +3378,15 @@ var SliderWrapper = styled30.label`
3230
3378
  `;
3231
3379
 
3232
3380
  // src/components/TrackMenu.tsx
3233
- import React18, { useState as useState6, useEffect as useEffect7, useRef as useRef8 } from "react";
3381
+ import React19, { useState as useState6, useEffect as useEffect9, useRef as useRef8, useCallback as useCallback5 } from "react";
3234
3382
  import { createPortal } from "react-dom";
3235
- import styled31 from "styled-components";
3236
- import { jsx as jsx33, jsxs as jsxs12 } from "react/jsx-runtime";
3237
- var MenuContainer = styled31.div`
3383
+ import styled32 from "styled-components";
3384
+ import { jsx as jsx34, jsxs as jsxs11 } from "react/jsx-runtime";
3385
+ var MenuContainer = styled32.div`
3238
3386
  position: relative;
3239
3387
  display: inline-block;
3240
3388
  `;
3241
- var MenuButton = styled31.button`
3389
+ var MenuButton = styled32.button`
3242
3390
  background: none;
3243
3391
  border: none;
3244
3392
  cursor: pointer;
@@ -3253,7 +3401,8 @@ var MenuButton = styled31.button`
3253
3401
  opacity: 1;
3254
3402
  }
3255
3403
  `;
3256
- var Dropdown = styled31.div`
3404
+ var DROPDOWN_MIN_WIDTH = 180;
3405
+ var Dropdown = styled32.div`
3257
3406
  position: fixed;
3258
3407
  top: ${(p) => p.$top}px;
3259
3408
  left: ${(p) => p.$left}px;
@@ -3263,31 +3412,53 @@ var Dropdown = styled31.div`
3263
3412
  border: 1px solid rgba(128, 128, 128, 0.4);
3264
3413
  border-radius: 6px;
3265
3414
  padding: 0.5rem 0;
3266
- min-width: 180px;
3415
+ min-width: ${DROPDOWN_MIN_WIDTH}px;
3267
3416
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
3268
3417
  `;
3269
- var Divider = styled31.hr`
3418
+ var Divider = styled32.hr`
3270
3419
  border: none;
3271
3420
  border-top: 1px solid rgba(128, 128, 128, 0.3);
3272
3421
  margin: 0.35rem 0;
3273
3422
  `;
3274
3423
  var TrackMenu = ({ items: itemsProp }) => {
3275
3424
  const [open, setOpen] = useState6(false);
3276
- const close = () => setOpen(false);
3425
+ const close = useCallback5(() => setOpen(false), []);
3277
3426
  const items = typeof itemsProp === "function" ? itemsProp(close) : itemsProp;
3278
3427
  const [dropdownPos, setDropdownPos] = useState6({ top: 0, left: 0 });
3279
3428
  const buttonRef = useRef8(null);
3280
3429
  const dropdownRef = useRef8(null);
3281
- useEffect7(() => {
3282
- if (open && buttonRef.current) {
3283
- const rect = buttonRef.current.getBoundingClientRect();
3284
- setDropdownPos({
3285
- top: rect.bottom + 2,
3286
- left: Math.max(0, rect.right - 180)
3287
- });
3430
+ const updatePosition = useCallback5(() => {
3431
+ if (!buttonRef.current) return;
3432
+ const rect = buttonRef.current.getBoundingClientRect();
3433
+ const vw = window.innerWidth;
3434
+ const vh = window.innerHeight;
3435
+ const dropHeight = dropdownRef.current?.offsetHeight ?? 160;
3436
+ let left = rect.right + 4;
3437
+ if (left + DROPDOWN_MIN_WIDTH > vw) {
3438
+ left = rect.left - DROPDOWN_MIN_WIDTH - 4;
3288
3439
  }
3289
- }, [open]);
3290
- useEffect7(() => {
3440
+ left = Math.max(4, Math.min(left, vw - DROPDOWN_MIN_WIDTH - 4));
3441
+ let top = rect.top;
3442
+ if (top + dropHeight > vh - 4) {
3443
+ top = Math.max(4, rect.bottom - dropHeight);
3444
+ }
3445
+ setDropdownPos({ top, left });
3446
+ }, []);
3447
+ useEffect9(() => {
3448
+ if (!open) return;
3449
+ updatePosition();
3450
+ const rafId = requestAnimationFrame(() => updatePosition());
3451
+ const onScroll = () => updatePosition();
3452
+ const onResize = () => updatePosition();
3453
+ window.addEventListener("scroll", onScroll, true);
3454
+ window.addEventListener("resize", onResize);
3455
+ return () => {
3456
+ cancelAnimationFrame(rafId);
3457
+ window.removeEventListener("scroll", onScroll, true);
3458
+ window.removeEventListener("resize", onResize);
3459
+ };
3460
+ }, [open, updatePosition]);
3461
+ useEffect9(() => {
3291
3462
  if (!open) return;
3292
3463
  const handleClick = (e) => {
3293
3464
  const target = e.target;
@@ -3295,11 +3466,20 @@ var TrackMenu = ({ items: itemsProp }) => {
3295
3466
  setOpen(false);
3296
3467
  }
3297
3468
  };
3469
+ const handleKeyDown = (e) => {
3470
+ if (e.key === "Escape") {
3471
+ setOpen(false);
3472
+ }
3473
+ };
3298
3474
  document.addEventListener("mousedown", handleClick);
3299
- return () => document.removeEventListener("mousedown", handleClick);
3475
+ document.addEventListener("keydown", handleKeyDown);
3476
+ return () => {
3477
+ document.removeEventListener("mousedown", handleClick);
3478
+ document.removeEventListener("keydown", handleKeyDown);
3479
+ };
3300
3480
  }, [open]);
3301
- return /* @__PURE__ */ jsxs12(MenuContainer, { children: [
3302
- /* @__PURE__ */ jsx33(
3481
+ return /* @__PURE__ */ jsxs11(MenuContainer, { children: [
3482
+ /* @__PURE__ */ jsx34(
3303
3483
  MenuButton,
3304
3484
  {
3305
3485
  ref: buttonRef,
@@ -3310,19 +3490,19 @@ var TrackMenu = ({ items: itemsProp }) => {
3310
3490
  onMouseDown: (e) => e.stopPropagation(),
3311
3491
  title: "Track menu",
3312
3492
  "aria-label": "Track menu",
3313
- children: /* @__PURE__ */ jsx33(DotsIcon, { size: 16 })
3493
+ children: /* @__PURE__ */ jsx34(DotsIcon, { size: 16 })
3314
3494
  }
3315
3495
  ),
3316
3496
  open && typeof document !== "undefined" && createPortal(
3317
- /* @__PURE__ */ jsx33(
3497
+ /* @__PURE__ */ jsx34(
3318
3498
  Dropdown,
3319
3499
  {
3320
3500
  ref: dropdownRef,
3321
3501
  $top: dropdownPos.top,
3322
3502
  $left: dropdownPos.left,
3323
3503
  onMouseDown: (e) => e.stopPropagation(),
3324
- children: items.map((item, index) => /* @__PURE__ */ jsxs12(React18.Fragment, { children: [
3325
- index > 0 && /* @__PURE__ */ jsx33(Divider, {}),
3504
+ children: items.map((item, index) => /* @__PURE__ */ jsxs11(React19.Fragment, { children: [
3505
+ index > 0 && /* @__PURE__ */ jsx34(Divider, {}),
3326
3506
  item.content
3327
3507
  ] }, item.id))
3328
3508
  }
@@ -3364,6 +3544,7 @@ export {
3364
3544
  LoopRegion,
3365
3545
  LoopRegionMarkers,
3366
3546
  MasterVolumeControl,
3547
+ PianoRollChannel,
3367
3548
  Playhead,
3368
3549
  PlayheadWithMarker,
3369
3550
  Playlist,