@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.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,10 +589,12 @@ 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
599
  const resizeObserver = new ResizeObserver(() => {
565
600
  scheduleUpdate();
@@ -572,8 +607,9 @@ var ScrollViewportProvider = ({ containerRef, children }) => {
572
607
  cancelAnimationFrame(rafIdRef.current);
573
608
  rafIdRef.current = null;
574
609
  }
610
+ store.cancelPendingNotification();
575
611
  };
576
- }, [containerRef, measure, scheduleUpdate]);
612
+ }, [containerRef, scheduleUpdate, store]);
577
613
  return /* @__PURE__ */ jsx3(ViewportStoreContext.Provider, { value: store, children });
578
614
  };
579
615
  var useScrollViewport = () => {
@@ -708,8 +744,6 @@ var Waveform = styled9.canvas.attrs((props) => ({
708
744
  }))`
709
745
  position: absolute;
710
746
  top: 0;
711
- /* Promote to own compositing layer for smoother scrolling */
712
- will-change: transform;
713
747
  /* Disable image rendering interpolation */
714
748
  image-rendering: pixelated;
715
749
  image-rendering: crisp-edges;
@@ -746,7 +780,8 @@ var Channel = (props) => {
746
780
  const { canvasRef, canvasMapRef } = useChunkedCanvasRefs();
747
781
  const clipOriginX = useClipViewportOrigin();
748
782
  const visibleChunkIndices = useVisibleChunkIndices(length, MAX_CANVAS_WIDTH, clipOriginX);
749
- useLayoutEffect(() => {
783
+ useEffect3(() => {
784
+ const tDraw = performance.now();
750
785
  const step = barWidth + barGap;
751
786
  for (const [canvasIdx, canvas] of canvasMapRef.current.entries()) {
752
787
  const globalPixelOffset = canvasIdx * MAX_CANVAS_WIDTH;
@@ -782,6 +817,9 @@ var Channel = (props) => {
782
817
  }
783
818
  }
784
819
  }
820
+ console.log(
821
+ `[waveform] draw ch${index}: ${canvasMapRef.current.size} chunks, ${(performance.now() - tDraw).toFixed(1)}ms`
822
+ );
785
823
  }, [
786
824
  canvasMapRef,
787
825
  data,
@@ -794,7 +832,8 @@ var Channel = (props) => {
794
832
  barWidth,
795
833
  barGap,
796
834
  drawMode,
797
- visibleChunkIndices
835
+ visibleChunkIndices,
836
+ index
798
837
  ]);
799
838
  const waveforms = visibleChunkIndices.map((i) => {
800
839
  const chunkLeft = i * MAX_CANVAS_WIDTH;
@@ -912,7 +951,7 @@ var ClipHeaderPresentational = ({
912
951
  trackName,
913
952
  isSelected = false
914
953
  }) => {
915
- 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 }) });
916
955
  };
917
956
  var ClipHeader = ({
918
957
  clipId,
@@ -934,7 +973,7 @@ var ClipHeader = ({
934
973
  "data-clip-id": clipId,
935
974
  $interactive: true,
936
975
  $isSelected: isSelected,
937
- children: /* @__PURE__ */ jsx7(TrackName, { children: trackName })
976
+ children: /* @__PURE__ */ jsx7(TrackName, { title: trackName, children: trackName })
938
977
  }
939
978
  );
940
979
  };
@@ -1155,6 +1194,7 @@ var Clip = ({
1155
1194
  "data-clip-container": "true",
1156
1195
  "data-track-id": trackId,
1157
1196
  onMouseDown,
1197
+ tabIndex: -1,
1158
1198
  children: [
1159
1199
  showHeader && /* @__PURE__ */ jsx10(
1160
1200
  ClipHeader,
@@ -1264,11 +1304,140 @@ var MasterVolumeControl = ({
1264
1304
  ] });
1265
1305
  };
1266
1306
 
1267
- // src/components/Playhead.tsx
1268
- import { useRef as useRef3, useEffect as useEffect3 } from "react";
1307
+ // src/components/PianoRollChannel.tsx
1308
+ import { useEffect as useEffect4, useMemo as useMemo2 } from "react";
1269
1309
  import styled15 from "styled-components";
1270
- import { jsx as jsx12, jsxs as jsxs4 } from "react/jsx-runtime";
1271
- 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) => ({
1272
1441
  style: {
1273
1442
  transform: `translate3d(${props.$position}px, 0, 0)`
1274
1443
  }
@@ -1284,9 +1453,9 @@ var PlayheadLine = styled15.div.attrs((props) => ({
1284
1453
  will-change: transform;
1285
1454
  `;
1286
1455
  var Playhead = ({ position, color = "#ff0000" }) => {
1287
- return /* @__PURE__ */ jsx12(PlayheadLine, { $position: position, $color: color });
1456
+ return /* @__PURE__ */ jsx13(PlayheadLine, { $position: position, $color: color });
1288
1457
  };
1289
- var PlayheadWithMarkerContainer = styled15.div`
1458
+ var PlayheadWithMarkerContainer = styled16.div`
1290
1459
  position: absolute;
1291
1460
  top: 0;
1292
1461
  left: 0;
@@ -1295,7 +1464,7 @@ var PlayheadWithMarkerContainer = styled15.div`
1295
1464
  pointer-events: none;
1296
1465
  will-change: transform;
1297
1466
  `;
1298
- var MarkerTriangle = styled15.div`
1467
+ var MarkerTriangle = styled16.div`
1299
1468
  position: absolute;
1300
1469
  top: -10px;
1301
1470
  left: -6px;
@@ -1305,7 +1474,7 @@ var MarkerTriangle = styled15.div`
1305
1474
  border-right: 7px solid transparent;
1306
1475
  border-top: 10px solid ${(props) => props.$color};
1307
1476
  `;
1308
- var MarkerLine = styled15.div`
1477
+ var MarkerLine = styled16.div`
1309
1478
  position: absolute;
1310
1479
  top: 0;
1311
1480
  left: 0;
@@ -1321,13 +1490,13 @@ var PlayheadWithMarker = ({
1321
1490
  audioStartPositionRef,
1322
1491
  samplesPerPixel,
1323
1492
  sampleRate,
1324
- controlsOffset,
1493
+ controlsOffset = 0,
1325
1494
  getAudioContextTime,
1326
1495
  getPlaybackTime
1327
1496
  }) => {
1328
1497
  const containerRef = useRef3(null);
1329
1498
  const animationFrameRef = useRef3(null);
1330
- useEffect3(() => {
1499
+ useEffect5(() => {
1331
1500
  const updatePosition = () => {
1332
1501
  if (containerRef.current) {
1333
1502
  let time;
@@ -1372,7 +1541,7 @@ var PlayheadWithMarker = ({
1372
1541
  getAudioContextTime,
1373
1542
  getPlaybackTime
1374
1543
  ]);
1375
- useEffect3(() => {
1544
+ useEffect5(() => {
1376
1545
  if (!isPlaying && containerRef.current) {
1377
1546
  const time = currentTimeRef.current ?? 0;
1378
1547
  const pos = time * sampleRate / samplesPerPixel + controlsOffset;
@@ -1380,27 +1549,43 @@ var PlayheadWithMarker = ({
1380
1549
  }
1381
1550
  });
1382
1551
  return /* @__PURE__ */ jsxs4(PlayheadWithMarkerContainer, { ref: containerRef, $color: color, children: [
1383
- /* @__PURE__ */ jsx12(MarkerTriangle, { $color: color }),
1384
- /* @__PURE__ */ jsx12(MarkerLine, { $color: color })
1552
+ /* @__PURE__ */ jsx13(MarkerTriangle, { $color: color }),
1553
+ /* @__PURE__ */ jsx13(MarkerLine, { $color: color })
1385
1554
  ] });
1386
1555
  };
1387
1556
 
1388
1557
  // src/components/Playlist.tsx
1389
- import styled16, { withTheme } from "styled-components";
1558
+ import styled17, { withTheme } from "styled-components";
1390
1559
  import { useRef as useRef4, useCallback as useCallback3 } from "react";
1391
- import { jsx as jsx13, jsxs as jsxs5 } from "react/jsx-runtime";
1392
- var Wrapper2 = styled16.div`
1560
+ import { jsx as jsx14, jsxs as jsxs5 } from "react/jsx-runtime";
1561
+ var Wrapper3 = styled17.div`
1562
+ display: flex;
1393
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`
1394
1576
  overflow-x: auto;
1577
+ overflow-y: hidden;
1578
+ overflow-anchor: none;
1579
+ flex: 1;
1395
1580
  position: relative;
1396
1581
  `;
1397
- var ScrollContainer = styled16.div.attrs((props) => ({
1582
+ var ScrollContainerInner = styled17.div.attrs((props) => ({
1398
1583
  style: props.$width !== void 0 ? { width: `${props.$width}px` } : {}
1399
1584
  }))`
1400
1585
  position: relative;
1401
1586
  background: ${(props) => props.$backgroundColor || "transparent"};
1402
1587
  `;
1403
- var TimescaleWrapper = styled16.div.attrs((props) => ({
1588
+ var TimescaleWrapper = styled17.div.attrs((props) => ({
1404
1589
  style: props.$width ? { minWidth: `${props.$width}px` } : {}
1405
1590
  }))`
1406
1591
  background: ${(props) => props.$backgroundColor || "white"};
@@ -1408,14 +1593,14 @@ var TimescaleWrapper = styled16.div.attrs((props) => ({
1408
1593
  position: relative;
1409
1594
  overflow: hidden; /* Constrain loop region to timescale area */
1410
1595
  `;
1411
- var TracksContainer = styled16.div.attrs((props) => ({
1596
+ var TracksContainer = styled17.div.attrs((props) => ({
1412
1597
  style: props.$width !== void 0 ? { minWidth: `${props.$width}px` } : {}
1413
1598
  }))`
1414
1599
  position: relative;
1415
1600
  background: ${(props) => props.$backgroundColor || "transparent"};
1416
1601
  width: 100%;
1417
1602
  `;
1418
- var ClickOverlay = styled16.div`
1603
+ var ClickOverlay = styled17.div`
1419
1604
  position: absolute;
1420
1605
  top: 0;
1421
1606
  left: 0;
@@ -1432,7 +1617,6 @@ var Playlist = ({
1432
1617
  timescale,
1433
1618
  timescaleWidth,
1434
1619
  tracksWidth,
1435
- scrollContainerWidth,
1436
1620
  controlsWidth,
1437
1621
  onTracksClick,
1438
1622
  onTracksMouseDown,
@@ -1440,40 +1624,48 @@ var Playlist = ({
1440
1624
  onTracksMouseUp,
1441
1625
  scrollContainerRef,
1442
1626
  isSelecting,
1443
- "data-playlist-state": playlistState
1627
+ "data-playlist-state": playlistState,
1628
+ trackControlsSlots,
1629
+ timescaleGapHeight = 0
1444
1630
  }) => {
1445
- const wrapperRef = useRef4(null);
1631
+ const scrollAreaRef = useRef4(null);
1446
1632
  const handleRef = useCallback3(
1447
1633
  (el) => {
1448
- wrapperRef.current = el;
1634
+ scrollAreaRef.current = el;
1449
1635
  scrollContainerRef?.(el);
1450
1636
  },
1451
1637
  [scrollContainerRef]
1452
1638
  );
1453
- 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: [
1454
- timescale && /* @__PURE__ */ jsx13(TimescaleWrapper, { $width: timescaleWidth, $backgroundColor: timescaleBackgroundColor, children: timescale }),
1455
- /* @__PURE__ */ jsxs5(TracksContainer, { $width: tracksWidth, $backgroundColor: backgroundColor, children: [
1456
- children,
1457
- (onTracksClick || onTracksMouseDown) && /* @__PURE__ */ jsx13(
1458
- ClickOverlay,
1459
- {
1460
- $controlsWidth: controlsWidth,
1461
- $isSelecting: isSelecting,
1462
- onClick: onTracksClick,
1463
- onMouseDown: onTracksMouseDown,
1464
- onMouseMove: onTracksMouseMove,
1465
- onMouseUp: onTracksMouseUp
1466
- }
1467
- )
1468
- ] })
1469
- ] }) }) });
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
+ ] });
1470
1662
  };
1471
1663
  var StyledPlaylist = withTheme(Playlist);
1472
1664
 
1473
1665
  // src/components/Selection.tsx
1474
- import styled17 from "styled-components";
1475
- import { jsx as jsx14 } from "react/jsx-runtime";
1476
- 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) => ({
1477
1669
  style: {
1478
1670
  left: `${props.$left}px`,
1479
1671
  width: `${props.$width}px`
@@ -1496,14 +1688,14 @@ var Selection = ({
1496
1688
  if (width <= 0) {
1497
1689
  return null;
1498
1690
  }
1499
- 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 });
1500
1692
  };
1501
1693
 
1502
1694
  // src/components/LoopRegion.tsx
1503
1695
  import { useCallback as useCallback4, useRef as useRef5, useState } from "react";
1504
- import styled18 from "styled-components";
1505
- import { Fragment as Fragment2, jsx as jsx15, jsxs as jsxs6 } from "react/jsx-runtime";
1506
- 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) => ({
1507
1699
  style: {
1508
1700
  left: `${props.$left}px`,
1509
1701
  width: `${props.$width}px`
@@ -1516,7 +1708,7 @@ var LoopRegionOverlayDiv = styled18.div.attrs((props) => ({
1516
1708
  z-index: 55; /* Between clips (z-index: 50) and selection (z-index: 60) */
1517
1709
  pointer-events: none;
1518
1710
  `;
1519
- var LoopMarker = styled18.div.attrs((props) => ({
1711
+ var LoopMarker = styled19.div.attrs((props) => ({
1520
1712
  style: {
1521
1713
  left: `${props.$left}px`
1522
1714
  }
@@ -1552,7 +1744,7 @@ var LoopRegion = ({
1552
1744
  return null;
1553
1745
  }
1554
1746
  return /* @__PURE__ */ jsxs6(Fragment2, { children: [
1555
- /* @__PURE__ */ jsx15(
1747
+ /* @__PURE__ */ jsx16(
1556
1748
  LoopRegionOverlayDiv,
1557
1749
  {
1558
1750
  $left: startPosition,
@@ -1561,7 +1753,7 @@ var LoopRegion = ({
1561
1753
  "data-loop-region": true
1562
1754
  }
1563
1755
  ),
1564
- /* @__PURE__ */ jsx15(
1756
+ /* @__PURE__ */ jsx16(
1565
1757
  LoopMarker,
1566
1758
  {
1567
1759
  $left: startPosition,
@@ -1570,7 +1762,7 @@ var LoopRegion = ({
1570
1762
  "data-loop-marker": "start"
1571
1763
  }
1572
1764
  ),
1573
- /* @__PURE__ */ jsx15(
1765
+ /* @__PURE__ */ jsx16(
1574
1766
  LoopMarker,
1575
1767
  {
1576
1768
  $left: endPosition - 2,
@@ -1581,7 +1773,7 @@ var LoopRegion = ({
1581
1773
  )
1582
1774
  ] });
1583
1775
  };
1584
- var DraggableMarkerHandle = styled18.div.attrs((props) => ({
1776
+ var DraggableMarkerHandle = styled19.div.attrs((props) => ({
1585
1777
  style: {
1586
1778
  left: `${props.$left}px`
1587
1779
  }
@@ -1623,7 +1815,7 @@ var DraggableMarkerHandle = styled18.div.attrs((props) => ({
1623
1815
  opacity: 1;
1624
1816
  }
1625
1817
  `;
1626
- var TimescaleLoopShade = styled18.div.attrs((props) => ({
1818
+ var TimescaleLoopShade = styled19.div.attrs((props) => ({
1627
1819
  style: {
1628
1820
  left: `${props.$left}px`,
1629
1821
  width: `${props.$width}px`
@@ -1721,7 +1913,7 @@ var LoopRegionMarkers = ({
1721
1913
  return null;
1722
1914
  }
1723
1915
  return /* @__PURE__ */ jsxs6(Fragment2, { children: [
1724
- /* @__PURE__ */ jsx15(
1916
+ /* @__PURE__ */ jsx16(
1725
1917
  TimescaleLoopShade,
1726
1918
  {
1727
1919
  $left: startPosition,
@@ -1732,7 +1924,7 @@ var LoopRegionMarkers = ({
1732
1924
  "data-loop-region-timescale": true
1733
1925
  }
1734
1926
  ),
1735
- /* @__PURE__ */ jsx15(
1927
+ /* @__PURE__ */ jsx16(
1736
1928
  DraggableMarkerHandle,
1737
1929
  {
1738
1930
  $left: startPosition,
@@ -1743,7 +1935,7 @@ var LoopRegionMarkers = ({
1743
1935
  "data-loop-marker-handle": "start"
1744
1936
  }
1745
1937
  ),
1746
- /* @__PURE__ */ jsx15(
1938
+ /* @__PURE__ */ jsx16(
1747
1939
  DraggableMarkerHandle,
1748
1940
  {
1749
1941
  $left: endPosition,
@@ -1756,13 +1948,10 @@ var LoopRegionMarkers = ({
1756
1948
  )
1757
1949
  ] });
1758
1950
  };
1759
- var TimescaleLoopCreator = styled18.div.attrs((props) => ({
1760
- style: {
1761
- left: `${props.$leftOffset || 0}px`
1762
- }
1763
- }))`
1951
+ var TimescaleLoopCreator = styled19.div`
1764
1952
  position: absolute;
1765
1953
  top: 0;
1954
+ left: 0;
1766
1955
  right: 0;
1767
1956
  height: 100%; /* Stay within timescale bounds, don't extend into tracks */
1768
1957
  cursor: crosshair;
@@ -1775,8 +1964,7 @@ var TimescaleLoopRegion = ({
1775
1964
  regionColor = "rgba(59, 130, 246, 0.3)",
1776
1965
  onLoopRegionChange,
1777
1966
  minPosition = 0,
1778
- maxPosition = Infinity,
1779
- controlsOffset = 0
1967
+ maxPosition = Infinity
1780
1968
  }) => {
1781
1969
  const [, setIsCreating] = useState(false);
1782
1970
  const createStartX = useRef5(0);
@@ -1813,14 +2001,13 @@ var TimescaleLoopRegion = ({
1813
2001
  },
1814
2002
  [minPosition, maxPosition, onLoopRegionChange]
1815
2003
  );
1816
- return /* @__PURE__ */ jsx15(
2004
+ return /* @__PURE__ */ jsx16(
1817
2005
  TimescaleLoopCreator,
1818
2006
  {
1819
2007
  ref: containerRef,
1820
- $leftOffset: controlsOffset,
1821
2008
  onMouseDown: handleBackgroundMouseDown,
1822
2009
  "data-timescale-loop-creator": true,
1823
- children: hasLoopRegion && /* @__PURE__ */ jsx15(
2010
+ children: hasLoopRegion && /* @__PURE__ */ jsx16(
1824
2011
  LoopRegionMarkers,
1825
2012
  {
1826
2013
  startPosition,
@@ -1839,10 +2026,10 @@ var TimescaleLoopRegion = ({
1839
2026
  };
1840
2027
 
1841
2028
  // src/components/SelectionTimeInputs.tsx
1842
- import { useEffect as useEffect5, useState as useState3 } from "react";
2029
+ import { useEffect as useEffect7, useState as useState3 } from "react";
1843
2030
 
1844
2031
  // src/components/TimeInput.tsx
1845
- import { useEffect as useEffect4, useState as useState2 } from "react";
2032
+ import { useEffect as useEffect6, useState as useState2 } from "react";
1846
2033
 
1847
2034
  // src/utils/timeFormat.ts
1848
2035
  function clockFormat(seconds, decimals) {
@@ -1892,7 +2079,7 @@ function parseTime(timeStr, format) {
1892
2079
  }
1893
2080
 
1894
2081
  // src/components/TimeInput.tsx
1895
- 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";
1896
2083
  var TimeInput = ({
1897
2084
  id,
1898
2085
  label,
@@ -1903,7 +2090,7 @@ var TimeInput = ({
1903
2090
  readOnly = false
1904
2091
  }) => {
1905
2092
  const [displayValue, setDisplayValue] = useState2("");
1906
- useEffect4(() => {
2093
+ useEffect6(() => {
1907
2094
  const formatted = formatTime(value, format);
1908
2095
  setDisplayValue(formatted);
1909
2096
  }, [value, format, id]);
@@ -1924,8 +2111,8 @@ var TimeInput = ({
1924
2111
  }
1925
2112
  };
1926
2113
  return /* @__PURE__ */ jsxs7(Fragment3, { children: [
1927
- /* @__PURE__ */ jsx16(ScreenReaderOnly, { as: "label", htmlFor: id, children: label }),
1928
- /* @__PURE__ */ jsx16(
2114
+ /* @__PURE__ */ jsx17(ScreenReaderOnly, { as: "label", htmlFor: id, children: label }),
2115
+ /* @__PURE__ */ jsx17(
1929
2116
  BaseInput,
1930
2117
  {
1931
2118
  type: "text",
@@ -1942,7 +2129,7 @@ var TimeInput = ({
1942
2129
  };
1943
2130
 
1944
2131
  // src/components/SelectionTimeInputs.tsx
1945
- import { jsx as jsx17, jsxs as jsxs8 } from "react/jsx-runtime";
2132
+ import { jsx as jsx18, jsxs as jsxs8 } from "react/jsx-runtime";
1946
2133
  var SelectionTimeInputs = ({
1947
2134
  selectionStart,
1948
2135
  selectionEnd,
@@ -1950,7 +2137,7 @@ var SelectionTimeInputs = ({
1950
2137
  className
1951
2138
  }) => {
1952
2139
  const [timeFormat, setTimeFormat] = useState3("hh:mm:ss.uuu");
1953
- useEffect5(() => {
2140
+ useEffect7(() => {
1954
2141
  const timeFormatSelect = document.querySelector(".time-format");
1955
2142
  const handleFormatChange = () => {
1956
2143
  if (timeFormatSelect) {
@@ -1976,7 +2163,7 @@ var SelectionTimeInputs = ({
1976
2163
  }
1977
2164
  };
1978
2165
  return /* @__PURE__ */ jsxs8("div", { className, children: [
1979
- /* @__PURE__ */ jsx17(
2166
+ /* @__PURE__ */ jsx18(
1980
2167
  TimeInput,
1981
2168
  {
1982
2169
  id: "audio_start",
@@ -1987,7 +2174,7 @@ var SelectionTimeInputs = ({
1987
2174
  onChange: handleStartChange
1988
2175
  }
1989
2176
  ),
1990
- /* @__PURE__ */ jsx17(
2177
+ /* @__PURE__ */ jsx18(
1991
2178
  TimeInput,
1992
2179
  {
1993
2180
  id: "audio_end",
@@ -2003,7 +2190,7 @@ var SelectionTimeInputs = ({
2003
2190
 
2004
2191
  // src/contexts/DevicePixelRatio.tsx
2005
2192
  import { useState as useState4, createContext as createContext3, useContext as useContext3 } from "react";
2006
- import { jsx as jsx18 } from "react/jsx-runtime";
2193
+ import { jsx as jsx19 } from "react/jsx-runtime";
2007
2194
  function getScale() {
2008
2195
  return window.devicePixelRatio;
2009
2196
  }
@@ -2017,7 +2204,7 @@ var DevicePixelRatioProvider = ({ children }) => {
2017
2204
  },
2018
2205
  { once: true }
2019
2206
  );
2020
- return /* @__PURE__ */ jsx18(DevicePixelRatioContext.Provider, { value: Math.ceil(scale), children });
2207
+ return /* @__PURE__ */ jsx19(DevicePixelRatioContext.Provider, { value: Math.ceil(scale), children });
2021
2208
  };
2022
2209
  var useDevicePixelRatio = () => useContext3(DevicePixelRatioContext);
2023
2210
 
@@ -2046,8 +2233,8 @@ var useTheme2 = () => useContext5(ThemeContext);
2046
2233
 
2047
2234
  // src/contexts/TrackControls.tsx
2048
2235
  import { createContext as createContext5, useContext as useContext6, Fragment as Fragment4 } from "react";
2049
- import { jsx as jsx19 } from "react/jsx-runtime";
2050
- var TrackControlsContext = createContext5(/* @__PURE__ */ jsx19(Fragment4, {}));
2236
+ import { jsx as jsx20 } from "react/jsx-runtime";
2237
+ var TrackControlsContext = createContext5(/* @__PURE__ */ jsx20(Fragment4, {}));
2051
2238
  var useTrackControls = () => useContext6(TrackControlsContext);
2052
2239
 
2053
2240
  // src/contexts/Playout.tsx
@@ -2056,7 +2243,7 @@ import {
2056
2243
  createContext as createContext6,
2057
2244
  useContext as useContext7
2058
2245
  } from "react";
2059
- import { jsx as jsx20 } from "react/jsx-runtime";
2246
+ import { jsx as jsx21 } from "react/jsx-runtime";
2060
2247
  var defaultProgress = 0;
2061
2248
  var defaultIsPlaying = false;
2062
2249
  var defaultSelectionStart = 0;
@@ -2085,18 +2272,18 @@ var PlayoutProvider = ({ children }) => {
2085
2272
  setSelectionStart(start);
2086
2273
  setSelectionEnd(end);
2087
2274
  };
2088
- 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 }) });
2089
2276
  };
2090
2277
  var usePlayoutStatus = () => useContext7(PlayoutStatusContext);
2091
2278
  var usePlayoutStatusUpdate = () => useContext7(PlayoutStatusUpdateContext);
2092
2279
 
2093
2280
  // src/components/SpectrogramChannel.tsx
2094
- import { useLayoutEffect as useLayoutEffect2, useRef as useRef6, useEffect as useEffect6 } from "react";
2095
- import styled19 from "styled-components";
2096
- import { MAX_CANVAS_WIDTH as MAX_CANVAS_WIDTH2 } from "@waveform-playlist/core";
2097
- 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";
2098
2285
  var LINEAR_FREQUENCY_SCALE = (f, minF, maxF) => (f - minF) / (maxF - minF);
2099
- var Wrapper3 = styled19.div.attrs((props) => ({
2286
+ var Wrapper4 = styled20.div.attrs((props) => ({
2100
2287
  style: {
2101
2288
  top: `${props.$waveHeight * props.$index}px`,
2102
2289
  width: `${props.$cssWidth}px`,
@@ -2108,7 +2295,7 @@ var Wrapper3 = styled19.div.attrs((props) => ({
2108
2295
  transform: translateZ(0);
2109
2296
  backface-visibility: hidden;
2110
2297
  `;
2111
- var SpectrogramCanvas = styled19.canvas.attrs((props) => ({
2298
+ var SpectrogramCanvas = styled20.canvas.attrs((props) => ({
2112
2299
  style: {
2113
2300
  width: `${props.$cssWidth}px`,
2114
2301
  height: `${props.$waveHeight}px`,
@@ -2117,8 +2304,6 @@ var SpectrogramCanvas = styled19.canvas.attrs((props) => ({
2117
2304
  }))`
2118
2305
  position: absolute;
2119
2306
  top: 0;
2120
- /* Promote to own compositing layer for smoother scrolling */
2121
- will-change: transform;
2122
2307
  image-rendering: pixelated;
2123
2308
  image-rendering: crisp-edges;
2124
2309
  `;
@@ -2154,18 +2339,18 @@ var SpectrogramChannel = ({
2154
2339
  const onCanvasesReadyRef = useRef6(onCanvasesReady);
2155
2340
  const isWorkerMode = !!(workerApi && clipId);
2156
2341
  const clipOriginX = useClipViewportOrigin();
2157
- const visibleChunkIndices = useVisibleChunkIndices(length, MAX_CANVAS_WIDTH2, clipOriginX);
2342
+ const visibleChunkIndices = useVisibleChunkIndices(length, MAX_CANVAS_WIDTH3, clipOriginX);
2158
2343
  const lut = colorLUT ?? DEFAULT_COLOR_LUT;
2159
2344
  const maxF = maxFrequency ?? (data ? data.sampleRate / 2 : 22050);
2160
2345
  const scaleFn = frequencyScaleFn ?? LINEAR_FREQUENCY_SCALE;
2161
2346
  const hasCustomFrequencyScale = Boolean(frequencyScaleFn);
2162
- useEffect6(() => {
2347
+ useEffect8(() => {
2163
2348
  workerApiRef.current = workerApi;
2164
2349
  }, [workerApi]);
2165
- useEffect6(() => {
2350
+ useEffect8(() => {
2166
2351
  onCanvasesReadyRef.current = onCanvasesReady;
2167
2352
  }, [onCanvasesReady]);
2168
- useEffect6(() => {
2353
+ useEffect8(() => {
2169
2354
  if (!isWorkerMode) return;
2170
2355
  const currentWorkerApi = workerApiRef.current;
2171
2356
  if (!currentWorkerApi || !clipId) return;
@@ -2220,15 +2405,15 @@ var SpectrogramChannel = ({
2220
2405
  const match = id.match(/chunk(\d+)$/);
2221
2406
  if (!match) {
2222
2407
  console.warn(`[spectrogram] Unexpected canvas ID format: ${id}`);
2223
- return MAX_CANVAS_WIDTH2;
2408
+ return MAX_CANVAS_WIDTH3;
2224
2409
  }
2225
2410
  const chunkIdx = parseInt(match[1], 10);
2226
- return Math.min(length - chunkIdx * MAX_CANVAS_WIDTH2, MAX_CANVAS_WIDTH2);
2411
+ return Math.min(length - chunkIdx * MAX_CANVAS_WIDTH3, MAX_CANVAS_WIDTH3);
2227
2412
  });
2228
2413
  onCanvasesReadyRef.current?.(allIds, allWidths);
2229
2414
  }
2230
2415
  }, [canvasMapRef, isWorkerMode, clipId, channelIndex, length, visibleChunkIndices]);
2231
- useEffect6(() => {
2416
+ useEffect8(() => {
2232
2417
  return () => {
2233
2418
  const api = workerApiRef.current;
2234
2419
  if (!api) return;
@@ -2242,7 +2427,7 @@ var SpectrogramChannel = ({
2242
2427
  registeredIdsRef.current = [];
2243
2428
  };
2244
2429
  }, []);
2245
- useLayoutEffect2(() => {
2430
+ useEffect8(() => {
2246
2431
  if (isWorkerMode || !data) return;
2247
2432
  const {
2248
2433
  frequencyBinCount,
@@ -2255,7 +2440,7 @@ var SpectrogramChannel = ({
2255
2440
  const rangeDb = rawRangeDb === 0 ? 1 : rawRangeDb;
2256
2441
  const binToFreq = (bin) => bin / frequencyBinCount * (sampleRate / 2);
2257
2442
  for (const [canvasIdx, canvas] of canvasMapRef.current.entries()) {
2258
- const globalPixelOffset = canvasIdx * MAX_CANVAS_WIDTH2;
2443
+ const globalPixelOffset = canvasIdx * MAX_CANVAS_WIDTH3;
2259
2444
  const ctx = canvas.getContext("2d");
2260
2445
  if (!ctx) continue;
2261
2446
  const canvasWidth = canvas.width / devicePixelRatio;
@@ -2331,9 +2516,9 @@ var SpectrogramChannel = ({
2331
2516
  visibleChunkIndices
2332
2517
  ]);
2333
2518
  const canvases = visibleChunkIndices.map((i) => {
2334
- const chunkLeft = i * MAX_CANVAS_WIDTH2;
2335
- const currentWidth = Math.min(length - chunkLeft, MAX_CANVAS_WIDTH2);
2336
- return /* @__PURE__ */ jsx21(
2519
+ const chunkLeft = i * MAX_CANVAS_WIDTH3;
2520
+ const currentWidth = Math.min(length - chunkLeft, MAX_CANVAS_WIDTH3);
2521
+ return /* @__PURE__ */ jsx22(
2337
2522
  SpectrogramCanvas,
2338
2523
  {
2339
2524
  $cssWidth: currentWidth,
@@ -2347,11 +2532,11 @@ var SpectrogramChannel = ({
2347
2532
  `${length}-${i}`
2348
2533
  );
2349
2534
  });
2350
- 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 });
2351
2536
  };
2352
2537
 
2353
2538
  // src/components/SmartChannel.tsx
2354
- 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";
2355
2540
  var SmartChannel = ({
2356
2541
  isSelected,
2357
2542
  transparentBackground,
@@ -2365,10 +2550,19 @@ var SmartChannel = ({
2365
2550
  spectrogramWorkerApi,
2366
2551
  spectrogramClipId,
2367
2552
  spectrogramOnCanvasesReady,
2553
+ midiNotes,
2554
+ sampleRate: sampleRateProp,
2555
+ clipOffsetSeconds,
2368
2556
  ...props
2369
2557
  }) => {
2370
2558
  const theme = useTheme2();
2371
- const { waveHeight, barWidth, barGap, samplesPerPixel: contextSpp } = usePlaylistInfo();
2559
+ const {
2560
+ waveHeight,
2561
+ barWidth,
2562
+ barGap,
2563
+ samplesPerPixel: contextSpp,
2564
+ sampleRate: contextSampleRate
2565
+ } = usePlaylistInfo();
2372
2566
  const devicePixelRatio = useDevicePixelRatio();
2373
2567
  const samplesPerPixel = sppProp ?? contextSpp;
2374
2568
  const waveOutlineColor = isSelected && theme ? theme.selectedWaveOutlineColor : theme?.waveOutlineColor;
@@ -2376,7 +2570,7 @@ var SmartChannel = ({
2376
2570
  const drawMode = theme?.waveformDrawMode || "inverted";
2377
2571
  const hasSpectrogram = spectrogramData || spectrogramWorkerApi;
2378
2572
  if (renderMode === "spectrogram" && hasSpectrogram) {
2379
- return /* @__PURE__ */ jsx22(
2573
+ return /* @__PURE__ */ jsx23(
2380
2574
  SpectrogramChannel,
2381
2575
  {
2382
2576
  index: props.index,
@@ -2398,7 +2592,7 @@ var SmartChannel = ({
2398
2592
  if (renderMode === "both" && hasSpectrogram) {
2399
2593
  const halfHeight = Math.floor(waveHeight / 2);
2400
2594
  return /* @__PURE__ */ jsxs9(Fragment5, { children: [
2401
- /* @__PURE__ */ jsx22(
2595
+ /* @__PURE__ */ jsx23(
2402
2596
  SpectrogramChannel,
2403
2597
  {
2404
2598
  index: props.index * 2,
@@ -2417,7 +2611,7 @@ var SmartChannel = ({
2417
2611
  onCanvasesReady: spectrogramOnCanvasesReady
2418
2612
  }
2419
2613
  ),
2420
- /* @__PURE__ */ jsx22(
2614
+ /* @__PURE__ */ jsx23(
2421
2615
  "div",
2422
2616
  {
2423
2617
  style: {
@@ -2426,7 +2620,7 @@ var SmartChannel = ({
2426
2620
  width: props.length,
2427
2621
  height: halfHeight
2428
2622
  },
2429
- children: /* @__PURE__ */ jsx22(
2623
+ children: /* @__PURE__ */ jsx23(
2430
2624
  Channel,
2431
2625
  {
2432
2626
  ...props,
@@ -2445,7 +2639,27 @@ var SmartChannel = ({
2445
2639
  )
2446
2640
  ] });
2447
2641
  }
2448
- 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(
2449
2663
  Channel,
2450
2664
  {
2451
2665
  ...props,
@@ -2462,11 +2676,11 @@ var SmartChannel = ({
2462
2676
  };
2463
2677
 
2464
2678
  // src/components/SpectrogramLabels.tsx
2465
- import { useRef as useRef7, useLayoutEffect as useLayoutEffect3 } from "react";
2466
- import styled20 from "styled-components";
2467
- 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";
2468
2682
  var LABELS_WIDTH = 72;
2469
- var LabelsStickyWrapper = styled20.div`
2683
+ var LabelsStickyWrapper = styled21.div`
2470
2684
  position: sticky;
2471
2685
  left: 0;
2472
2686
  z-index: 101;
@@ -2519,7 +2733,7 @@ var SpectrogramLabels = ({
2519
2733
  const spectrogramHeight = renderMode === "both" ? Math.floor(waveHeight / 2) : waveHeight;
2520
2734
  const totalHeight = numChannels * waveHeight;
2521
2735
  const clipHeaderOffset = hasClipHeaders ? 22 : 0;
2522
- useLayoutEffect3(() => {
2736
+ useLayoutEffect2(() => {
2523
2737
  const canvas = canvasRef.current;
2524
2738
  if (!canvas) return;
2525
2739
  const ctx = canvas.getContext("2d");
@@ -2557,7 +2771,7 @@ var SpectrogramLabels = ({
2557
2771
  spectrogramHeight,
2558
2772
  clipHeaderOffset
2559
2773
  ]);
2560
- return /* @__PURE__ */ jsx23(LabelsStickyWrapper, { $height: totalHeight + clipHeaderOffset, children: /* @__PURE__ */ jsx23(
2774
+ return /* @__PURE__ */ jsx24(LabelsStickyWrapper, { $height: totalHeight + clipHeaderOffset, children: /* @__PURE__ */ jsx24(
2561
2775
  "canvas",
2562
2776
  {
2563
2777
  ref: canvasRef,
@@ -2576,8 +2790,8 @@ var SpectrogramLabels = ({
2576
2790
  import { useContext as useContext9 } from "react";
2577
2791
 
2578
2792
  // src/components/TimeScale.tsx
2579
- import React16, { useLayoutEffect as useLayoutEffect4, useContext as useContext8, useMemo as useMemo2 } from "react";
2580
- 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";
2581
2795
 
2582
2796
  // src/utils/conversions.ts
2583
2797
  function samplesToSeconds(samples, sampleRate) {
@@ -2600,18 +2814,17 @@ function secondsToPixels(seconds, samplesPerPixel, sampleRate) {
2600
2814
  }
2601
2815
 
2602
2816
  // src/components/TimeScale.tsx
2603
- import { MAX_CANVAS_WIDTH as MAX_CANVAS_WIDTH3 } from "@waveform-playlist/core";
2604
- 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";
2605
2819
  function formatTime2(milliseconds) {
2606
2820
  const seconds = Math.floor(milliseconds / 1e3);
2607
2821
  const s = seconds % 60;
2608
2822
  const m = (seconds - s) / 60;
2609
2823
  return `${m}:${String(s).padStart(2, "0")}`;
2610
2824
  }
2611
- var PlaylistTimeScaleScroll = styled21.div.attrs((props) => ({
2825
+ var PlaylistTimeScaleScroll = styled22.div.attrs((props) => ({
2612
2826
  style: {
2613
2827
  width: `${props.$cssWidth}px`,
2614
- marginLeft: `${props.$controlWidth}px`,
2615
2828
  height: `${props.$timeScaleHeight}px`
2616
2829
  }
2617
2830
  }))`
@@ -2620,7 +2833,7 @@ var PlaylistTimeScaleScroll = styled21.div.attrs((props) => ({
2620
2833
  border-bottom: 1px solid ${(props) => props.theme.timeColor};
2621
2834
  box-sizing: border-box;
2622
2835
  `;
2623
- var TimeTickChunk = styled21.canvas.attrs((props) => ({
2836
+ var TimeTickChunk = styled22.canvas.attrs((props) => ({
2624
2837
  style: {
2625
2838
  width: `${props.$cssWidth}px`,
2626
2839
  height: `${props.$timeScaleHeight}px`,
@@ -2629,10 +2842,8 @@ var TimeTickChunk = styled21.canvas.attrs((props) => ({
2629
2842
  }))`
2630
2843
  position: absolute;
2631
2844
  bottom: 0;
2632
- /* Promote to own compositing layer for smoother scrolling */
2633
- will-change: transform;
2634
2845
  `;
2635
- var TimeStamp = styled21.div.attrs((props) => ({
2846
+ var TimeStamp = styled22.div.attrs((props) => ({
2636
2847
  style: {
2637
2848
  left: `${props.$left + 4}px`
2638
2849
  // Offset 4px to the right of the tick
@@ -2653,14 +2864,9 @@ var TimeScale = (props) => {
2653
2864
  renderTimestamp
2654
2865
  } = props;
2655
2866
  const { canvasRef, canvasMapRef } = useChunkedCanvasRefs();
2656
- const {
2657
- sampleRate,
2658
- samplesPerPixel,
2659
- timeScaleHeight,
2660
- controls: { show: showControls, width: controlWidth }
2661
- } = useContext8(PlaylistInfoContext);
2867
+ const { sampleRate, samplesPerPixel, timeScaleHeight } = useContext8(PlaylistInfoContext);
2662
2868
  const devicePixelRatio = useDevicePixelRatio();
2663
- const { widthX, canvasInfo, timeMarkersWithPositions } = useMemo2(() => {
2869
+ const { widthX, canvasInfo, timeMarkersWithPositions } = useMemo3(() => {
2664
2870
  const nextCanvasInfo = /* @__PURE__ */ new Map();
2665
2871
  const nextMarkers = [];
2666
2872
  const nextWidthX = secondsToPixels(duration / 1e3, samplesPerPixel, sampleRate);
@@ -2671,7 +2877,7 @@ var TimeScale = (props) => {
2671
2877
  if (counter % marker === 0) {
2672
2878
  const timeMs = counter;
2673
2879
  const timestamp = formatTime2(timeMs);
2674
- 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);
2675
2881
  nextMarkers.push({ pix, element });
2676
2882
  nextCanvasInfo.set(pix, timeScaleHeight);
2677
2883
  } else if (counter % bigStep === 0) {
@@ -2696,11 +2902,11 @@ var TimeScale = (props) => {
2696
2902
  renderTimestamp,
2697
2903
  timeScaleHeight
2698
2904
  ]);
2699
- const visibleChunkIndices = useVisibleChunkIndices(widthX, MAX_CANVAS_WIDTH3);
2905
+ const visibleChunkIndices = useVisibleChunkIndices(widthX, MAX_CANVAS_WIDTH4);
2700
2906
  const visibleChunks = visibleChunkIndices.map((i) => {
2701
- const chunkLeft = i * MAX_CANVAS_WIDTH3;
2702
- const chunkWidth = Math.min(widthX - chunkLeft, MAX_CANVAS_WIDTH3);
2703
- return /* @__PURE__ */ jsx24(
2907
+ const chunkLeft = i * MAX_CANVAS_WIDTH4;
2908
+ const chunkWidth = Math.min(widthX - chunkLeft, MAX_CANVAS_WIDTH4);
2909
+ return /* @__PURE__ */ jsx25(
2704
2910
  TimeTickChunk,
2705
2911
  {
2706
2912
  $cssWidth: chunkWidth,
@@ -2714,14 +2920,14 @@ var TimeScale = (props) => {
2714
2920
  `timescale-${i}`
2715
2921
  );
2716
2922
  });
2717
- const firstChunkLeft = visibleChunkIndices.length > 0 ? visibleChunkIndices[0] * MAX_CANVAS_WIDTH3 : 0;
2718
- 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;
2719
2925
  const visibleMarkers = visibleChunkIndices.length > 0 ? timeMarkersWithPositions.filter(({ pix }) => pix >= firstChunkLeft && pix < lastChunkRight).map(({ element }) => element) : timeMarkersWithPositions.map(({ element }) => element);
2720
- useLayoutEffect4(() => {
2926
+ useLayoutEffect3(() => {
2721
2927
  for (const [chunkIdx, canvas] of canvasMapRef.current.entries()) {
2722
2928
  const ctx = canvas.getContext("2d");
2723
2929
  if (!ctx) continue;
2724
- const chunkLeft = chunkIdx * MAX_CANVAS_WIDTH3;
2930
+ const chunkLeft = chunkIdx * MAX_CANVAS_WIDTH4;
2725
2931
  const chunkWidth = canvas.width / devicePixelRatio;
2726
2932
  ctx.resetTransform();
2727
2933
  ctx.clearRect(0, 0, canvas.width, canvas.height);
@@ -2744,23 +2950,15 @@ var TimeScale = (props) => {
2744
2950
  canvasInfo,
2745
2951
  visibleChunkIndices
2746
2952
  ]);
2747
- return /* @__PURE__ */ jsxs10(
2748
- PlaylistTimeScaleScroll,
2749
- {
2750
- $cssWidth: widthX,
2751
- $controlWidth: showControls ? controlWidth : 0,
2752
- $timeScaleHeight: timeScaleHeight,
2753
- children: [
2754
- visibleMarkers,
2755
- visibleChunks
2756
- ]
2757
- }
2758
- );
2953
+ return /* @__PURE__ */ jsxs10(PlaylistTimeScaleScroll, { $cssWidth: widthX, $timeScaleHeight: timeScaleHeight, children: [
2954
+ visibleMarkers,
2955
+ visibleChunks
2956
+ ] });
2759
2957
  };
2760
2958
  var StyledTimeScale = withTheme2(TimeScale);
2761
2959
 
2762
2960
  // src/components/SmartScale.tsx
2763
- import { jsx as jsx25 } from "react/jsx-runtime";
2961
+ import { jsx as jsx26 } from "react/jsx-runtime";
2764
2962
  var timeinfo = /* @__PURE__ */ new Map([
2765
2963
  [
2766
2964
  700,
@@ -2836,7 +3034,7 @@ function getScaleInfo(samplesPerPixel) {
2836
3034
  var SmartScale = ({ renderTimestamp }) => {
2837
3035
  const { samplesPerPixel, duration } = useContext9(PlaylistInfoContext);
2838
3036
  let config = getScaleInfo(samplesPerPixel);
2839
- return /* @__PURE__ */ jsx25(
3037
+ return /* @__PURE__ */ jsx26(
2840
3038
  StyledTimeScale,
2841
3039
  {
2842
3040
  marker: config.marker,
@@ -2849,9 +3047,9 @@ var SmartScale = ({ renderTimestamp }) => {
2849
3047
  };
2850
3048
 
2851
3049
  // src/components/TimeFormatSelect.tsx
2852
- import styled22 from "styled-components";
2853
- import { jsx as jsx26 } from "react/jsx-runtime";
2854
- var SelectWrapper = styled22.div`
3050
+ import styled23 from "styled-components";
3051
+ import { jsx as jsx27 } from "react/jsx-runtime";
3052
+ var SelectWrapper = styled23.div`
2855
3053
  display: inline-flex;
2856
3054
  align-items: center;
2857
3055
  gap: 0.5rem;
@@ -2873,7 +3071,7 @@ var TimeFormatSelect = ({
2873
3071
  const handleChange = (e) => {
2874
3072
  onChange(e.target.value);
2875
3073
  };
2876
- return /* @__PURE__ */ jsx26(SelectWrapper, { className, children: /* @__PURE__ */ jsx26(
3074
+ return /* @__PURE__ */ jsx27(SelectWrapper, { className, children: /* @__PURE__ */ jsx27(
2877
3075
  BaseSelect,
2878
3076
  {
2879
3077
  className: "time-format",
@@ -2881,50 +3079,30 @@ var TimeFormatSelect = ({
2881
3079
  onChange: handleChange,
2882
3080
  disabled,
2883
3081
  "aria-label": "Time format selection",
2884
- 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))
2885
3083
  }
2886
3084
  ) });
2887
3085
  };
2888
3086
 
2889
3087
  // src/components/Track.tsx
2890
- import styled23 from "styled-components";
2891
- import { jsx as jsx27, jsxs as jsxs11 } from "react/jsx-runtime";
2892
- 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) => ({
2893
3091
  style: {
2894
3092
  height: `${props.$waveHeight * props.$numChannels + (props.$hasClipHeaders ? CLIP_HEADER_HEIGHT : 0)}px`
2895
3093
  }
2896
3094
  }))`
2897
3095
  position: relative;
2898
- display: flex;
2899
3096
  ${(props) => props.$width !== void 0 && `width: ${props.$width}px;`}
2900
3097
  `;
2901
- var ChannelContainer = styled23.div.attrs((props) => ({
3098
+ var ChannelContainer = styled24.div.attrs((props) => ({
2902
3099
  style: {
2903
3100
  paddingLeft: `${props.$offset || 0}px`
2904
3101
  }
2905
3102
  }))`
2906
3103
  position: relative;
2907
3104
  background: ${(props) => props.$backgroundColor || "transparent"};
2908
- flex: 1;
2909
- `;
2910
- var ControlsWrapper = styled23.div.attrs((props) => ({
2911
- style: {
2912
- width: `${props.$controlWidth}px`
2913
- }
2914
- }))`
2915
- position: sticky;
2916
- z-index: 102; /* Above waveform content and spectrogram labels (101), below Docusaurus navbar (200) */
2917
- left: 0;
2918
3105
  height: 100%;
2919
- flex-shrink: 0;
2920
- pointer-events: auto;
2921
- background: ${(props) => props.theme.surfaceColor};
2922
- transition: background 0.15s ease-in-out;
2923
-
2924
- /* Selected track: highlighted background */
2925
- ${(props) => props.$isSelected && `
2926
- background: ${props.theme.selectedTrackControlsBackground};
2927
- `}
2928
3106
  `;
2929
3107
  var Track = ({
2930
3108
  numChannels,
@@ -2936,44 +3114,34 @@ var Track = ({
2936
3114
  hasClipHeaders = false,
2937
3115
  onClick,
2938
3116
  trackId,
2939
- isSelected = false
3117
+ isSelected: _isSelected = false
2940
3118
  }) => {
2941
- const {
2942
- waveHeight,
2943
- controls: { show, width: controlWidth }
2944
- } = usePlaylistInfo();
2945
- const controls = useTrackControls();
2946
- return /* @__PURE__ */ jsxs11(
3119
+ const { waveHeight } = usePlaylistInfo();
3120
+ return /* @__PURE__ */ jsx28(
2947
3121
  Container,
2948
3122
  {
2949
3123
  $numChannels: numChannels,
2950
3124
  className,
2951
3125
  $waveHeight: waveHeight,
2952
- $controlWidth: show ? controlWidth : 0,
2953
3126
  $width: width,
2954
3127
  $hasClipHeaders: hasClipHeaders,
2955
- $isSelected: isSelected,
2956
- children: [
2957
- /* @__PURE__ */ jsx27(ControlsWrapper, { $controlWidth: show ? controlWidth : 0, $isSelected: isSelected, children: controls }),
2958
- /* @__PURE__ */ jsx27(
2959
- ChannelContainer,
2960
- {
2961
- $controlWidth: show ? controlWidth : 0,
2962
- $backgroundColor: backgroundColor,
2963
- $offset: offset,
2964
- onClick,
2965
- "data-track-id": trackId,
2966
- children
2967
- }
2968
- )
2969
- ]
3128
+ children: /* @__PURE__ */ jsx28(
3129
+ ChannelContainer,
3130
+ {
3131
+ $backgroundColor: backgroundColor,
3132
+ $offset: offset,
3133
+ onClick,
3134
+ "data-track-id": trackId,
3135
+ children
3136
+ }
3137
+ )
2970
3138
  }
2971
3139
  );
2972
3140
  };
2973
3141
 
2974
3142
  // src/components/TrackControls/Button.tsx
2975
- import styled24 from "styled-components";
2976
- var Button = styled24.button.attrs({
3143
+ import styled25 from "styled-components";
3144
+ var Button = styled25.button.attrs({
2977
3145
  type: "button"
2978
3146
  })`
2979
3147
  display: inline-block;
@@ -3048,8 +3216,8 @@ var Button = styled24.button.attrs({
3048
3216
  `;
3049
3217
 
3050
3218
  // src/components/TrackControls/ButtonGroup.tsx
3051
- import styled25 from "styled-components";
3052
- var ButtonGroup = styled25.div`
3219
+ import styled26 from "styled-components";
3220
+ var ButtonGroup = styled26.div`
3053
3221
  margin-bottom: 0.3rem;
3054
3222
 
3055
3223
  button:not(:first-child) {
@@ -3064,10 +3232,10 @@ var ButtonGroup = styled25.div`
3064
3232
  `;
3065
3233
 
3066
3234
  // src/components/TrackControls/CloseButton.tsx
3067
- import styled26 from "styled-components";
3235
+ import styled27 from "styled-components";
3068
3236
  import { X as XIcon } from "@phosphor-icons/react";
3069
- import { jsx as jsx28 } from "react/jsx-runtime";
3070
- var StyledCloseButton = styled26.button`
3237
+ import { jsx as jsx29 } from "react/jsx-runtime";
3238
+ var StyledCloseButton = styled27.button`
3071
3239
  position: absolute;
3072
3240
  left: 0;
3073
3241
  top: 0;
@@ -3090,11 +3258,11 @@ var StyledCloseButton = styled26.button`
3090
3258
  color: #dc3545;
3091
3259
  }
3092
3260
  `;
3093
- 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" }) });
3094
3262
 
3095
3263
  // src/components/TrackControls/Controls.tsx
3096
- import styled27 from "styled-components";
3097
- var Controls = styled27.div`
3264
+ import styled28 from "styled-components";
3265
+ var Controls = styled28.div`
3098
3266
  background: transparent;
3099
3267
  width: 100%;
3100
3268
  height: 100%;
@@ -3110,8 +3278,8 @@ var Controls = styled27.div`
3110
3278
  `;
3111
3279
 
3112
3280
  // src/components/TrackControls/Header.tsx
3113
- import styled28 from "styled-components";
3114
- var Header = styled28.header`
3281
+ import styled29 from "styled-components";
3282
+ var Header = styled29.header`
3115
3283
  overflow: hidden;
3116
3284
  height: 26px;
3117
3285
  width: 100%;
@@ -3126,27 +3294,27 @@ var Header = styled28.header`
3126
3294
 
3127
3295
  // src/components/TrackControls/VolumeDownIcon.tsx
3128
3296
  import { SpeakerLowIcon } from "@phosphor-icons/react";
3129
- import { jsx as jsx29 } from "react/jsx-runtime";
3130
- 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 });
3131
3299
 
3132
3300
  // src/components/TrackControls/VolumeUpIcon.tsx
3133
3301
  import { SpeakerHighIcon } from "@phosphor-icons/react";
3134
- import { jsx as jsx30 } from "react/jsx-runtime";
3135
- 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 });
3136
3304
 
3137
3305
  // src/components/TrackControls/TrashIcon.tsx
3138
3306
  import { TrashIcon as PhosphorTrashIcon } from "@phosphor-icons/react";
3139
- import { jsx as jsx31 } from "react/jsx-runtime";
3140
- 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 });
3141
3309
 
3142
3310
  // src/components/TrackControls/DotsIcon.tsx
3143
3311
  import { DotsThreeIcon } from "@phosphor-icons/react";
3144
- import { jsx as jsx32 } from "react/jsx-runtime";
3145
- 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 });
3146
3314
 
3147
3315
  // src/components/TrackControls/Slider.tsx
3148
- import styled29 from "styled-components";
3149
- var Slider = styled29(BaseSlider)`
3316
+ import styled30 from "styled-components";
3317
+ var Slider = styled30(BaseSlider)`
3150
3318
  width: 75%;
3151
3319
  height: 5px;
3152
3320
  background: ${(props) => props.theme.sliderTrackColor};
@@ -3198,8 +3366,8 @@ var Slider = styled29(BaseSlider)`
3198
3366
  `;
3199
3367
 
3200
3368
  // src/components/TrackControls/SliderWrapper.tsx
3201
- import styled30 from "styled-components";
3202
- var SliderWrapper = styled30.label`
3369
+ import styled31 from "styled-components";
3370
+ var SliderWrapper = styled31.label`
3203
3371
  width: 100%;
3204
3372
  display: flex;
3205
3373
  justify-content: space-between;
@@ -3210,15 +3378,15 @@ var SliderWrapper = styled30.label`
3210
3378
  `;
3211
3379
 
3212
3380
  // src/components/TrackMenu.tsx
3213
- 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";
3214
3382
  import { createPortal } from "react-dom";
3215
- import styled31 from "styled-components";
3216
- import { jsx as jsx33, jsxs as jsxs12 } from "react/jsx-runtime";
3217
- 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`
3218
3386
  position: relative;
3219
3387
  display: inline-block;
3220
3388
  `;
3221
- var MenuButton = styled31.button`
3389
+ var MenuButton = styled32.button`
3222
3390
  background: none;
3223
3391
  border: none;
3224
3392
  cursor: pointer;
@@ -3233,7 +3401,8 @@ var MenuButton = styled31.button`
3233
3401
  opacity: 1;
3234
3402
  }
3235
3403
  `;
3236
- var Dropdown = styled31.div`
3404
+ var DROPDOWN_MIN_WIDTH = 180;
3405
+ var Dropdown = styled32.div`
3237
3406
  position: fixed;
3238
3407
  top: ${(p) => p.$top}px;
3239
3408
  left: ${(p) => p.$left}px;
@@ -3243,31 +3412,53 @@ var Dropdown = styled31.div`
3243
3412
  border: 1px solid rgba(128, 128, 128, 0.4);
3244
3413
  border-radius: 6px;
3245
3414
  padding: 0.5rem 0;
3246
- min-width: 180px;
3415
+ min-width: ${DROPDOWN_MIN_WIDTH}px;
3247
3416
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
3248
3417
  `;
3249
- var Divider = styled31.hr`
3418
+ var Divider = styled32.hr`
3250
3419
  border: none;
3251
3420
  border-top: 1px solid rgba(128, 128, 128, 0.3);
3252
3421
  margin: 0.35rem 0;
3253
3422
  `;
3254
3423
  var TrackMenu = ({ items: itemsProp }) => {
3255
3424
  const [open, setOpen] = useState6(false);
3256
- const close = () => setOpen(false);
3425
+ const close = useCallback5(() => setOpen(false), []);
3257
3426
  const items = typeof itemsProp === "function" ? itemsProp(close) : itemsProp;
3258
3427
  const [dropdownPos, setDropdownPos] = useState6({ top: 0, left: 0 });
3259
3428
  const buttonRef = useRef8(null);
3260
3429
  const dropdownRef = useRef8(null);
3261
- useEffect7(() => {
3262
- if (open && buttonRef.current) {
3263
- const rect = buttonRef.current.getBoundingClientRect();
3264
- setDropdownPos({
3265
- top: rect.bottom + 2,
3266
- left: Math.max(0, rect.right - 180)
3267
- });
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;
3268
3439
  }
3269
- }, [open]);
3270
- 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(() => {
3271
3462
  if (!open) return;
3272
3463
  const handleClick = (e) => {
3273
3464
  const target = e.target;
@@ -3275,11 +3466,20 @@ var TrackMenu = ({ items: itemsProp }) => {
3275
3466
  setOpen(false);
3276
3467
  }
3277
3468
  };
3469
+ const handleKeyDown = (e) => {
3470
+ if (e.key === "Escape") {
3471
+ setOpen(false);
3472
+ }
3473
+ };
3278
3474
  document.addEventListener("mousedown", handleClick);
3279
- return () => document.removeEventListener("mousedown", handleClick);
3475
+ document.addEventListener("keydown", handleKeyDown);
3476
+ return () => {
3477
+ document.removeEventListener("mousedown", handleClick);
3478
+ document.removeEventListener("keydown", handleKeyDown);
3479
+ };
3280
3480
  }, [open]);
3281
- return /* @__PURE__ */ jsxs12(MenuContainer, { children: [
3282
- /* @__PURE__ */ jsx33(
3481
+ return /* @__PURE__ */ jsxs11(MenuContainer, { children: [
3482
+ /* @__PURE__ */ jsx34(
3283
3483
  MenuButton,
3284
3484
  {
3285
3485
  ref: buttonRef,
@@ -3290,19 +3490,19 @@ var TrackMenu = ({ items: itemsProp }) => {
3290
3490
  onMouseDown: (e) => e.stopPropagation(),
3291
3491
  title: "Track menu",
3292
3492
  "aria-label": "Track menu",
3293
- children: /* @__PURE__ */ jsx33(DotsIcon, { size: 16 })
3493
+ children: /* @__PURE__ */ jsx34(DotsIcon, { size: 16 })
3294
3494
  }
3295
3495
  ),
3296
3496
  open && typeof document !== "undefined" && createPortal(
3297
- /* @__PURE__ */ jsx33(
3497
+ /* @__PURE__ */ jsx34(
3298
3498
  Dropdown,
3299
3499
  {
3300
3500
  ref: dropdownRef,
3301
3501
  $top: dropdownPos.top,
3302
3502
  $left: dropdownPos.left,
3303
3503
  onMouseDown: (e) => e.stopPropagation(),
3304
- children: items.map((item, index) => /* @__PURE__ */ jsxs12(React18.Fragment, { children: [
3305
- index > 0 && /* @__PURE__ */ jsx33(Divider, {}),
3504
+ children: items.map((item, index) => /* @__PURE__ */ jsxs11(React19.Fragment, { children: [
3505
+ index > 0 && /* @__PURE__ */ jsx34(Divider, {}),
3306
3506
  item.content
3307
3507
  ] }, item.id))
3308
3508
  }
@@ -3344,6 +3544,7 @@ export {
3344
3544
  LoopRegion,
3345
3545
  LoopRegionMarkers,
3346
3546
  MasterVolumeControl,
3547
+ PianoRollChannel,
3347
3548
  Playhead,
3348
3549
  PlayheadWithMarker,
3349
3550
  Playlist,