@waveform-playlist/ui-components 7.0.0 → 7.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
@@ -327,7 +327,7 @@ var AutomaticScrollCheckbox = ({
327
327
  };
328
328
 
329
329
  // src/components/Channel.tsx
330
- import { useLayoutEffect, useCallback, useRef } from "react";
330
+ import { useLayoutEffect, useEffect as useEffect2, useCallback as useCallback2, useRef as useRef2 } from "react";
331
331
  import styled9 from "styled-components";
332
332
 
333
333
  // src/wfpl-theme.ts
@@ -487,9 +487,112 @@ var darkTheme = {
487
487
  fontSizeSmall: "12px"
488
488
  };
489
489
 
490
- // src/components/Channel.tsx
490
+ // src/contexts/ScrollViewport.tsx
491
+ import {
492
+ createContext,
493
+ useContext,
494
+ useEffect,
495
+ useCallback,
496
+ useRef,
497
+ useSyncExternalStore
498
+ } from "react";
491
499
  import { jsx as jsx3 } from "react/jsx-runtime";
500
+ var ViewportStore = class {
501
+ constructor() {
502
+ this._state = null;
503
+ this._listeners = /* @__PURE__ */ new Set();
504
+ this.subscribe = (callback) => {
505
+ this._listeners.add(callback);
506
+ return () => this._listeners.delete(callback);
507
+ };
508
+ this.getSnapshot = () => this._state;
509
+ }
510
+ /**
511
+ * Update viewport state. Applies a 100px scroll threshold to skip updates
512
+ * that don't affect chunk visibility (1000px chunks with 1.5× overscan buffer).
513
+ * Only notifies listeners when the state actually changes.
514
+ */
515
+ update(scrollLeft, containerWidth) {
516
+ const buffer = containerWidth * 1.5;
517
+ const visibleStart = Math.max(0, scrollLeft - buffer);
518
+ const visibleEnd = scrollLeft + containerWidth + buffer;
519
+ if (this._state && this._state.containerWidth === containerWidth && Math.abs(this._state.scrollLeft - scrollLeft) < 100) {
520
+ return;
521
+ }
522
+ this._state = { scrollLeft, containerWidth, visibleStart, visibleEnd };
523
+ for (const listener of this._listeners) {
524
+ listener();
525
+ }
526
+ }
527
+ };
528
+ var ViewportStoreContext = createContext(null);
529
+ var EMPTY_SUBSCRIBE = () => () => {
530
+ };
531
+ var NULL_SNAPSHOT = () => null;
532
+ var ScrollViewportProvider = ({
533
+ containerRef,
534
+ children
535
+ }) => {
536
+ const storeRef = useRef(null);
537
+ if (storeRef.current === null) {
538
+ storeRef.current = new ViewportStore();
539
+ }
540
+ const store = storeRef.current;
541
+ const rafIdRef = useRef(null);
542
+ const measure = useCallback(() => {
543
+ const el = containerRef.current;
544
+ if (!el) return;
545
+ store.update(el.scrollLeft, el.clientWidth);
546
+ }, [containerRef, store]);
547
+ const scheduleUpdate = useCallback(() => {
548
+ if (rafIdRef.current !== null) return;
549
+ rafIdRef.current = requestAnimationFrame(() => {
550
+ rafIdRef.current = null;
551
+ measure();
552
+ });
553
+ }, [measure]);
554
+ useEffect(() => {
555
+ const el = containerRef.current;
556
+ if (!el) return;
557
+ measure();
558
+ el.addEventListener("scroll", scheduleUpdate, { passive: true });
559
+ const resizeObserver = new ResizeObserver(() => {
560
+ scheduleUpdate();
561
+ });
562
+ resizeObserver.observe(el);
563
+ return () => {
564
+ el.removeEventListener("scroll", scheduleUpdate);
565
+ resizeObserver.disconnect();
566
+ if (rafIdRef.current !== null) {
567
+ cancelAnimationFrame(rafIdRef.current);
568
+ rafIdRef.current = null;
569
+ }
570
+ };
571
+ }, [containerRef, measure, scheduleUpdate]);
572
+ return /* @__PURE__ */ jsx3(ViewportStoreContext.Provider, { value: store, children });
573
+ };
574
+ var useScrollViewport = () => {
575
+ const store = useContext(ViewportStoreContext);
576
+ return useSyncExternalStore(
577
+ store ? store.subscribe : EMPTY_SUBSCRIBE,
578
+ store ? store.getSnapshot : NULL_SNAPSHOT,
579
+ NULL_SNAPSHOT
580
+ );
581
+ };
582
+ function useScrollViewportSelector(selector) {
583
+ const store = useContext(ViewportStoreContext);
584
+ return useSyncExternalStore(
585
+ store ? store.subscribe : EMPTY_SUBSCRIBE,
586
+ () => selector(store ? store.getSnapshot() : null),
587
+ () => selector(null)
588
+ );
589
+ }
590
+
591
+ // src/constants.ts
492
592
  var MAX_CANVAS_WIDTH = 1e3;
593
+
594
+ // src/components/Channel.tsx
595
+ import { jsx as jsx4 } from "react/jsx-runtime";
493
596
  function createCanvasFillStyle(ctx, color, width, height) {
494
597
  if (!isWaveformGradient(color)) {
495
598
  return color;
@@ -508,11 +611,12 @@ function createCanvasFillStyle(ctx, color, width, height) {
508
611
  var Waveform = styled9.canvas.attrs((props) => ({
509
612
  style: {
510
613
  width: `${props.$cssWidth}px`,
511
- height: `${props.$waveHeight}px`
614
+ height: `${props.$waveHeight}px`,
615
+ left: `${props.$left}px`
512
616
  }
513
617
  }))`
514
- float: left;
515
- position: relative;
618
+ position: absolute;
619
+ top: 0;
516
620
  /* Promote to own compositing layer for smoother scrolling */
517
621
  will-change: transform;
518
622
  /* Disable image rendering interpolation */
@@ -548,8 +652,25 @@ var Channel = (props) => {
548
652
  transparentBackground = false,
549
653
  drawMode = "inverted"
550
654
  } = props;
551
- const canvasesRef = useRef([]);
552
- const canvasRef = useCallback(
655
+ const canvasesRef = useRef2([]);
656
+ const visibleChunkKey = useScrollViewportSelector((viewport) => {
657
+ const totalChunks = Math.ceil(length / MAX_CANVAS_WIDTH);
658
+ const indices = [];
659
+ for (let i = 0; i < totalChunks; i++) {
660
+ const chunkLeft = i * MAX_CANVAS_WIDTH;
661
+ const chunkWidth = Math.min(length - chunkLeft, MAX_CANVAS_WIDTH);
662
+ if (viewport) {
663
+ const chunkEnd = chunkLeft + chunkWidth;
664
+ if (chunkEnd <= viewport.visibleStart || chunkLeft >= viewport.visibleEnd) {
665
+ continue;
666
+ }
667
+ }
668
+ indices.push(i);
669
+ }
670
+ return indices.join(",");
671
+ });
672
+ const visibleChunkIndices = visibleChunkKey ? visibleChunkKey.split(",").map(Number) : [];
673
+ const canvasRef = useCallback2(
553
674
  (canvas) => {
554
675
  if (canvas !== null) {
555
676
  const index2 = parseInt(canvas.dataset.index, 10);
@@ -558,12 +679,22 @@ var Channel = (props) => {
558
679
  },
559
680
  []
560
681
  );
682
+ useEffect2(() => {
683
+ const canvases = canvasesRef.current;
684
+ for (let i = canvases.length - 1; i >= 0; i--) {
685
+ if (canvases[i] && !canvases[i].isConnected) {
686
+ delete canvases[i];
687
+ }
688
+ }
689
+ });
561
690
  useLayoutEffect(() => {
562
691
  const canvases = canvasesRef.current;
563
692
  const step = barWidth + barGap;
564
- let globalPixelOffset = 0;
565
693
  for (let i = 0; i < canvases.length; i++) {
566
694
  const canvas = canvases[i];
695
+ if (!canvas) continue;
696
+ const canvasIdx = parseInt(canvas.dataset.index, 10);
697
+ const globalPixelOffset = canvasIdx * MAX_CANVAS_WIDTH;
567
698
  const ctx = canvas.getContext("2d");
568
699
  const h2 = Math.floor(waveHeight / 2);
569
700
  const maxValue = 2 ** (bits - 1);
@@ -606,7 +737,6 @@ var Channel = (props) => {
606
737
  }
607
738
  }
608
739
  }
609
- globalPixelOffset += canvas.width / devicePixelRatio;
610
740
  }
611
741
  }, [
612
742
  data,
@@ -618,32 +748,29 @@ var Channel = (props) => {
618
748
  length,
619
749
  barWidth,
620
750
  barGap,
621
- drawMode
751
+ drawMode,
752
+ visibleChunkKey
622
753
  ]);
623
- let totalWidth = length;
624
- let waveformCount = 0;
625
- const waveforms = [];
626
- while (totalWidth > 0) {
627
- const currentWidth = Math.min(totalWidth, MAX_CANVAS_WIDTH);
628
- const waveform = /* @__PURE__ */ jsx3(
754
+ const waveforms = visibleChunkIndices.map((i) => {
755
+ const chunkLeft = i * MAX_CANVAS_WIDTH;
756
+ const currentWidth = Math.min(length - chunkLeft, MAX_CANVAS_WIDTH);
757
+ return /* @__PURE__ */ jsx4(
629
758
  Waveform,
630
759
  {
631
760
  $cssWidth: currentWidth,
761
+ $left: chunkLeft,
632
762
  width: currentWidth * devicePixelRatio,
633
763
  height: waveHeight * devicePixelRatio,
634
764
  $waveHeight: waveHeight,
635
- "data-index": waveformCount,
765
+ "data-index": i,
636
766
  ref: canvasRef
637
767
  },
638
- `${length}-${waveformCount}`
768
+ `${length}-${i}`
639
769
  );
640
- waveforms.push(waveform);
641
- totalWidth -= currentWidth;
642
- waveformCount += 1;
643
- }
770
+ });
644
771
  const bgColor = waveFillColor;
645
772
  const backgroundCss = transparentBackground ? "transparent" : waveformColorToCss(bgColor);
646
- return /* @__PURE__ */ jsx3(
773
+ return /* @__PURE__ */ jsx4(
647
774
  Wrapper,
648
775
  {
649
776
  $index: index,
@@ -657,8 +784,8 @@ var Channel = (props) => {
657
784
  };
658
785
 
659
786
  // src/components/ErrorBoundary.tsx
660
- import React2 from "react";
661
- import { jsx as jsx4 } from "react/jsx-runtime";
787
+ import React3 from "react";
788
+ import { jsx as jsx5 } from "react/jsx-runtime";
662
789
  var errorContainerStyle = {
663
790
  padding: "16px",
664
791
  background: "#1a1a2e",
@@ -672,7 +799,7 @@ var errorContainerStyle = {
672
799
  alignItems: "center",
673
800
  justifyContent: "center"
674
801
  };
675
- var PlaylistErrorBoundary = class extends React2.Component {
802
+ var PlaylistErrorBoundary = class extends React3.Component {
676
803
  constructor(props) {
677
804
  super(props);
678
805
  this.state = { hasError: false, error: null };
@@ -688,7 +815,7 @@ var PlaylistErrorBoundary = class extends React2.Component {
688
815
  if (this.props.fallback) {
689
816
  return this.props.fallback;
690
817
  }
691
- return /* @__PURE__ */ jsx4("div", { style: errorContainerStyle, children: "Waveform playlist encountered an error. Check console for details." });
818
+ return /* @__PURE__ */ jsx5("div", { style: errorContainerStyle, children: "Waveform playlist encountered an error. Check console for details." });
692
819
  }
693
820
  return this.props.children;
694
821
  }
@@ -701,7 +828,7 @@ import { CSS } from "@dnd-kit/utilities";
701
828
 
702
829
  // src/components/ClipHeader.tsx
703
830
  import styled10 from "styled-components";
704
- import { jsx as jsx5 } from "react/jsx-runtime";
831
+ import { jsx as jsx6 } from "react/jsx-runtime";
705
832
  var CLIP_HEADER_HEIGHT = 22;
706
833
  var HeaderContainer = styled10.div`
707
834
  position: relative;
@@ -741,27 +868,27 @@ var ClipHeaderPresentational = ({
741
868
  trackName,
742
869
  isSelected = false
743
870
  }) => {
744
- return /* @__PURE__ */ jsx5(
871
+ return /* @__PURE__ */ jsx6(
745
872
  HeaderContainer,
746
873
  {
747
874
  $isDragging: false,
748
875
  $interactive: false,
749
876
  $isSelected: isSelected,
750
- children: /* @__PURE__ */ jsx5(TrackName, { children: trackName })
877
+ children: /* @__PURE__ */ jsx6(TrackName, { children: trackName })
751
878
  }
752
879
  );
753
880
  };
754
881
  var ClipHeader = ({
755
882
  clipId,
756
- trackIndex,
757
- clipIndex,
883
+ trackIndex: _trackIndex,
884
+ clipIndex: _clipIndex,
758
885
  trackName,
759
886
  isSelected = false,
760
887
  disableDrag = false,
761
888
  dragHandleProps
762
889
  }) => {
763
890
  if (disableDrag || !dragHandleProps) {
764
- return /* @__PURE__ */ jsx5(
891
+ return /* @__PURE__ */ jsx6(
765
892
  ClipHeaderPresentational,
766
893
  {
767
894
  trackName,
@@ -770,7 +897,7 @@ var ClipHeader = ({
770
897
  );
771
898
  }
772
899
  const { attributes, listeners, setActivatorNodeRef } = dragHandleProps;
773
- return /* @__PURE__ */ jsx5(
900
+ return /* @__PURE__ */ jsx6(
774
901
  HeaderContainer,
775
902
  {
776
903
  ref: setActivatorNodeRef,
@@ -779,15 +906,15 @@ var ClipHeader = ({
779
906
  $isSelected: isSelected,
780
907
  ...listeners,
781
908
  ...attributes,
782
- children: /* @__PURE__ */ jsx5(TrackName, { children: trackName })
909
+ children: /* @__PURE__ */ jsx6(TrackName, { children: trackName })
783
910
  }
784
911
  );
785
912
  };
786
913
 
787
914
  // src/components/ClipBoundary.tsx
788
- import React3 from "react";
915
+ import React4 from "react";
789
916
  import styled11 from "styled-components";
790
- import { jsx as jsx6 } from "react/jsx-runtime";
917
+ import { jsx as jsx7 } from "react/jsx-runtime";
791
918
  var CLIP_BOUNDARY_WIDTH = 8;
792
919
  var CLIP_BOUNDARY_WIDTH_TOUCH = 24;
793
920
  var BoundaryContainer = styled11.div`
@@ -821,18 +948,18 @@ var BoundaryContainer = styled11.div`
821
948
  `;
822
949
  var ClipBoundary = ({
823
950
  clipId,
824
- trackIndex,
825
- clipIndex,
951
+ trackIndex: _trackIndex,
952
+ clipIndex: _clipIndex,
826
953
  edge,
827
954
  dragHandleProps,
828
955
  touchOptimized = false
829
956
  }) => {
830
- const [isHovered, setIsHovered] = React3.useState(false);
957
+ const [isHovered, setIsHovered] = React4.useState(false);
831
958
  if (!dragHandleProps) {
832
959
  return null;
833
960
  }
834
961
  const { attributes, listeners, setActivatorNodeRef, isDragging } = dragHandleProps;
835
- return /* @__PURE__ */ jsx6(
962
+ return /* @__PURE__ */ jsx7(
836
963
  BoundaryContainer,
837
964
  {
838
965
  ref: setActivatorNodeRef,
@@ -852,7 +979,7 @@ var ClipBoundary = ({
852
979
 
853
980
  // src/components/FadeOverlay.tsx
854
981
  import styled12, { useTheme } from "styled-components";
855
- import { jsx as jsx7 } from "react/jsx-runtime";
982
+ import { jsx as jsx8 } from "react/jsx-runtime";
856
983
  var FadeContainer = styled12.div.attrs((props) => ({
857
984
  style: {
858
985
  left: `${props.$left}px`,
@@ -909,7 +1036,7 @@ var FadeOverlay = ({
909
1036
  const theme = useTheme();
910
1037
  if (width < 1) return null;
911
1038
  const fillColor = color || theme?.fadeOverlayColor || "rgba(0, 0, 0, 0.4)";
912
- return /* @__PURE__ */ jsx7(FadeContainer, { $left: left, $width: width, $type: type, children: /* @__PURE__ */ jsx7(FadeSvg, { $type: type, viewBox: `0 0 ${width} 100`, preserveAspectRatio: "none", children: /* @__PURE__ */ jsx7(
1039
+ return /* @__PURE__ */ jsx8(FadeContainer, { $left: left, $width: width, $type: type, children: /* @__PURE__ */ jsx8(FadeSvg, { $type: type, viewBox: `0 0 ${width} 100`, preserveAspectRatio: "none", children: /* @__PURE__ */ jsx8(
913
1040
  "path",
914
1041
  {
915
1042
  d: generateFadePath(width, 100, curveType),
@@ -919,7 +1046,7 @@ var FadeOverlay = ({
919
1046
  };
920
1047
 
921
1048
  // src/components/Clip.tsx
922
- import { Fragment, jsx as jsx8, jsxs as jsxs2 } from "react/jsx-runtime";
1049
+ import { Fragment, jsx as jsx9, jsxs as jsxs2 } from "react/jsx-runtime";
923
1050
  var ClipContainer = styled13.div.attrs((props) => ({
924
1051
  style: props.$isOverlay ? {} : {
925
1052
  left: `${props.$left}px`,
@@ -1017,7 +1144,7 @@ var Clip = ({
1017
1144
  "data-track-id": trackId,
1018
1145
  onMouseDown,
1019
1146
  children: [
1020
- showHeader && /* @__PURE__ */ jsx8(
1147
+ showHeader && /* @__PURE__ */ jsx9(
1021
1148
  ClipHeader,
1022
1149
  {
1023
1150
  clipId,
@@ -1031,7 +1158,7 @@ var Clip = ({
1031
1158
  ),
1032
1159
  /* @__PURE__ */ jsxs2(ChannelsWrapper, { $isOverlay: isOverlay, children: [
1033
1160
  children,
1034
- showFades && fadeIn && fadeIn.duration > 0 && /* @__PURE__ */ jsx8(
1161
+ showFades && fadeIn && fadeIn.duration > 0 && /* @__PURE__ */ jsx9(
1035
1162
  FadeOverlay,
1036
1163
  {
1037
1164
  left: 0,
@@ -1040,7 +1167,7 @@ var Clip = ({
1040
1167
  curveType: fadeIn.type
1041
1168
  }
1042
1169
  ),
1043
- showFades && fadeOut && fadeOut.duration > 0 && /* @__PURE__ */ jsx8(
1170
+ showFades && fadeOut && fadeOut.duration > 0 && /* @__PURE__ */ jsx9(
1044
1171
  FadeOverlay,
1045
1172
  {
1046
1173
  left: width - Math.floor(fadeOut.duration * sampleRate / samplesPerPixel),
@@ -1051,7 +1178,7 @@ var Clip = ({
1051
1178
  )
1052
1179
  ] }),
1053
1180
  showHeader && !disableHeaderDrag && !isOverlay && /* @__PURE__ */ jsxs2(Fragment, { children: [
1054
- /* @__PURE__ */ jsx8(
1181
+ /* @__PURE__ */ jsx9(
1055
1182
  ClipBoundary,
1056
1183
  {
1057
1184
  clipId,
@@ -1067,7 +1194,7 @@ var Clip = ({
1067
1194
  }
1068
1195
  }
1069
1196
  ),
1070
- /* @__PURE__ */ jsx8(
1197
+ /* @__PURE__ */ jsx9(
1071
1198
  ClipBoundary,
1072
1199
  {
1073
1200
  clipId,
@@ -1091,7 +1218,7 @@ var Clip = ({
1091
1218
 
1092
1219
  // src/components/MasterVolumeControl.tsx
1093
1220
  import styled14 from "styled-components";
1094
- import { jsx as jsx9, jsxs as jsxs3 } from "react/jsx-runtime";
1221
+ import { jsx as jsx10, jsxs as jsxs3 } from "react/jsx-runtime";
1095
1222
  var VolumeContainer = styled14.div`
1096
1223
  display: inline-flex;
1097
1224
  align-items: center;
@@ -1114,8 +1241,8 @@ var MasterVolumeControl = ({
1114
1241
  onChange(parseFloat(e.target.value) / 100);
1115
1242
  };
1116
1243
  return /* @__PURE__ */ jsxs3(VolumeContainer, { className, children: [
1117
- /* @__PURE__ */ jsx9(VolumeLabel, { htmlFor: "master-gain", children: "Master Volume" }),
1118
- /* @__PURE__ */ jsx9(
1244
+ /* @__PURE__ */ jsx10(VolumeLabel, { htmlFor: "master-gain", children: "Master Volume" }),
1245
+ /* @__PURE__ */ jsx10(
1119
1246
  VolumeSlider,
1120
1247
  {
1121
1248
  min: "0",
@@ -1130,9 +1257,9 @@ var MasterVolumeControl = ({
1130
1257
  };
1131
1258
 
1132
1259
  // src/components/Playhead.tsx
1133
- import { useRef as useRef2, useEffect } from "react";
1260
+ import { useRef as useRef3, useEffect as useEffect3 } from "react";
1134
1261
  import styled15 from "styled-components";
1135
- import { jsx as jsx10, jsxs as jsxs4 } from "react/jsx-runtime";
1262
+ import { jsx as jsx11, jsxs as jsxs4 } from "react/jsx-runtime";
1136
1263
  var PlayheadLine = styled15.div.attrs((props) => ({
1137
1264
  style: {
1138
1265
  transform: `translate3d(${props.$position}px, 0, 0)`
@@ -1149,7 +1276,7 @@ var PlayheadLine = styled15.div.attrs((props) => ({
1149
1276
  will-change: transform;
1150
1277
  `;
1151
1278
  var Playhead = ({ position, color = "#ff0000" }) => {
1152
- return /* @__PURE__ */ jsx10(PlayheadLine, { $position: position, $color: color });
1279
+ return /* @__PURE__ */ jsx11(PlayheadLine, { $position: position, $color: color });
1153
1280
  };
1154
1281
  var PlayheadWithMarkerContainer = styled15.div`
1155
1282
  position: absolute;
@@ -1189,9 +1316,9 @@ var PlayheadWithMarker = ({
1189
1316
  controlsOffset,
1190
1317
  getAudioContextTime
1191
1318
  }) => {
1192
- const containerRef = useRef2(null);
1193
- const animationFrameRef = useRef2(null);
1194
- useEffect(() => {
1319
+ const containerRef = useRef3(null);
1320
+ const animationFrameRef = useRef3(null);
1321
+ useEffect3(() => {
1195
1322
  const updatePosition = () => {
1196
1323
  if (containerRef.current) {
1197
1324
  let time;
@@ -1220,7 +1347,7 @@ var PlayheadWithMarker = ({
1220
1347
  }
1221
1348
  };
1222
1349
  }, [isPlaying, sampleRate, samplesPerPixel, controlsOffset, currentTimeRef, playbackStartTimeRef, audioStartPositionRef, getAudioContextTime]);
1223
- useEffect(() => {
1350
+ useEffect3(() => {
1224
1351
  if (!isPlaying && containerRef.current) {
1225
1352
  const time = currentTimeRef.current ?? 0;
1226
1353
  const pos = time * sampleRate / samplesPerPixel + controlsOffset;
@@ -1228,14 +1355,15 @@ var PlayheadWithMarker = ({
1228
1355
  }
1229
1356
  });
1230
1357
  return /* @__PURE__ */ jsxs4(PlayheadWithMarkerContainer, { ref: containerRef, $color: color, children: [
1231
- /* @__PURE__ */ jsx10(MarkerTriangle, { $color: color }),
1232
- /* @__PURE__ */ jsx10(MarkerLine, { $color: color })
1358
+ /* @__PURE__ */ jsx11(MarkerTriangle, { $color: color }),
1359
+ /* @__PURE__ */ jsx11(MarkerLine, { $color: color })
1233
1360
  ] });
1234
1361
  };
1235
1362
 
1236
1363
  // src/components/Playlist.tsx
1237
1364
  import styled16, { withTheme } from "styled-components";
1238
- import { jsx as jsx11, jsxs as jsxs5 } from "react/jsx-runtime";
1365
+ import { useRef as useRef4, useCallback as useCallback3 } from "react";
1366
+ import { jsx as jsx12, jsxs as jsxs5 } from "react/jsx-runtime";
1239
1367
  var Wrapper2 = styled16.div`
1240
1368
  overflow-y: hidden;
1241
1369
  overflow-x: auto;
@@ -1289,16 +1417,21 @@ var Playlist = ({
1289
1417
  isSelecting,
1290
1418
  "data-playlist-state": playlistState
1291
1419
  }) => {
1292
- return /* @__PURE__ */ jsx11(Wrapper2, { "data-scroll-container": "true", "data-playlist-state": playlistState, ref: scrollContainerRef, children: /* @__PURE__ */ jsxs5(
1420
+ const wrapperRef = useRef4(null);
1421
+ const handleRef = useCallback3((el) => {
1422
+ wrapperRef.current = el;
1423
+ scrollContainerRef?.(el);
1424
+ }, [scrollContainerRef]);
1425
+ return /* @__PURE__ */ jsx12(Wrapper2, { "data-scroll-container": "true", "data-playlist-state": playlistState, ref: handleRef, children: /* @__PURE__ */ jsx12(ScrollViewportProvider, { containerRef: wrapperRef, children: /* @__PURE__ */ jsxs5(
1293
1426
  ScrollContainer,
1294
1427
  {
1295
1428
  $backgroundColor: backgroundColor,
1296
1429
  $width: scrollContainerWidth,
1297
1430
  children: [
1298
- timescale && /* @__PURE__ */ jsx11(TimescaleWrapper, { $width: timescaleWidth, $backgroundColor: timescaleBackgroundColor, children: timescale }),
1431
+ timescale && /* @__PURE__ */ jsx12(TimescaleWrapper, { $width: timescaleWidth, $backgroundColor: timescaleBackgroundColor, children: timescale }),
1299
1432
  /* @__PURE__ */ jsxs5(TracksContainer, { $width: tracksWidth, $backgroundColor: backgroundColor, children: [
1300
1433
  children,
1301
- (onTracksClick || onTracksMouseDown) && /* @__PURE__ */ jsx11(
1434
+ (onTracksClick || onTracksMouseDown) && /* @__PURE__ */ jsx12(
1302
1435
  ClickOverlay,
1303
1436
  {
1304
1437
  $controlsWidth: controlsWidth,
@@ -1312,13 +1445,13 @@ var Playlist = ({
1312
1445
  ] })
1313
1446
  ]
1314
1447
  }
1315
- ) });
1448
+ ) }) });
1316
1449
  };
1317
1450
  var StyledPlaylist = withTheme(Playlist);
1318
1451
 
1319
1452
  // src/components/Selection.tsx
1320
1453
  import styled17 from "styled-components";
1321
- import { jsx as jsx12 } from "react/jsx-runtime";
1454
+ import { jsx as jsx13 } from "react/jsx-runtime";
1322
1455
  var SelectionOverlay = styled17.div.attrs((props) => ({
1323
1456
  style: {
1324
1457
  left: `${props.$left}px`,
@@ -1342,13 +1475,13 @@ var Selection = ({
1342
1475
  if (width <= 0) {
1343
1476
  return null;
1344
1477
  }
1345
- return /* @__PURE__ */ jsx12(SelectionOverlay, { $left: startPosition, $width: width, $color: color, "data-selection": true });
1478
+ return /* @__PURE__ */ jsx13(SelectionOverlay, { $left: startPosition, $width: width, $color: color, "data-selection": true });
1346
1479
  };
1347
1480
 
1348
1481
  // src/components/LoopRegion.tsx
1349
- import { useCallback as useCallback2, useRef as useRef3, useState } from "react";
1482
+ import { useCallback as useCallback4, useRef as useRef5, useState } from "react";
1350
1483
  import styled18 from "styled-components";
1351
- import { Fragment as Fragment2, jsx as jsx13, jsxs as jsxs6 } from "react/jsx-runtime";
1484
+ import { Fragment as Fragment2, jsx as jsx14, jsxs as jsxs6 } from "react/jsx-runtime";
1352
1485
  var LoopRegionOverlayDiv = styled18.div.attrs((props) => ({
1353
1486
  style: {
1354
1487
  left: `${props.$left}px`,
@@ -1398,7 +1531,7 @@ var LoopRegion = ({
1398
1531
  return null;
1399
1532
  }
1400
1533
  return /* @__PURE__ */ jsxs6(Fragment2, { children: [
1401
- /* @__PURE__ */ jsx13(
1534
+ /* @__PURE__ */ jsx14(
1402
1535
  LoopRegionOverlayDiv,
1403
1536
  {
1404
1537
  $left: startPosition,
@@ -1407,7 +1540,7 @@ var LoopRegion = ({
1407
1540
  "data-loop-region": true
1408
1541
  }
1409
1542
  ),
1410
- /* @__PURE__ */ jsx13(
1543
+ /* @__PURE__ */ jsx14(
1411
1544
  LoopMarker,
1412
1545
  {
1413
1546
  $left: startPosition,
@@ -1416,7 +1549,7 @@ var LoopRegion = ({
1416
1549
  "data-loop-marker": "start"
1417
1550
  }
1418
1551
  ),
1419
- /* @__PURE__ */ jsx13(
1552
+ /* @__PURE__ */ jsx14(
1420
1553
  LoopMarker,
1421
1554
  {
1422
1555
  $left: endPosition - 2,
@@ -1498,11 +1631,11 @@ var LoopRegionMarkers = ({
1498
1631
  maxPosition = Infinity
1499
1632
  }) => {
1500
1633
  const [draggingMarker, setDraggingMarker] = useState(null);
1501
- const dragStartX = useRef3(0);
1502
- const dragStartPosition = useRef3(0);
1503
- const dragStartEnd = useRef3(0);
1634
+ const dragStartX = useRef5(0);
1635
+ const dragStartPosition = useRef5(0);
1636
+ const dragStartEnd = useRef5(0);
1504
1637
  const width = Math.max(0, endPosition - startPosition);
1505
- const handleMarkerMouseDown = useCallback2((e, marker) => {
1638
+ const handleMarkerMouseDown = useCallback4((e, marker) => {
1506
1639
  e.preventDefault();
1507
1640
  e.stopPropagation();
1508
1641
  setDraggingMarker(marker);
@@ -1527,7 +1660,7 @@ var LoopRegionMarkers = ({
1527
1660
  document.addEventListener("mousemove", handleMouseMove);
1528
1661
  document.addEventListener("mouseup", handleMouseUp);
1529
1662
  }, [startPosition, endPosition, minPosition, maxPosition, onLoopStartChange, onLoopEndChange]);
1530
- const handleRegionMouseDown = useCallback2((e) => {
1663
+ const handleRegionMouseDown = useCallback4((e) => {
1531
1664
  e.preventDefault();
1532
1665
  e.stopPropagation();
1533
1666
  setDraggingMarker("region");
@@ -1561,7 +1694,7 @@ var LoopRegionMarkers = ({
1561
1694
  return null;
1562
1695
  }
1563
1696
  return /* @__PURE__ */ jsxs6(Fragment2, { children: [
1564
- /* @__PURE__ */ jsx13(
1697
+ /* @__PURE__ */ jsx14(
1565
1698
  TimescaleLoopShade,
1566
1699
  {
1567
1700
  $left: startPosition,
@@ -1572,7 +1705,7 @@ var LoopRegionMarkers = ({
1572
1705
  "data-loop-region-timescale": true
1573
1706
  }
1574
1707
  ),
1575
- /* @__PURE__ */ jsx13(
1708
+ /* @__PURE__ */ jsx14(
1576
1709
  DraggableMarkerHandle,
1577
1710
  {
1578
1711
  $left: startPosition,
@@ -1583,7 +1716,7 @@ var LoopRegionMarkers = ({
1583
1716
  "data-loop-marker-handle": "start"
1584
1717
  }
1585
1718
  ),
1586
- /* @__PURE__ */ jsx13(
1719
+ /* @__PURE__ */ jsx14(
1587
1720
  DraggableMarkerHandle,
1588
1721
  {
1589
1722
  $left: endPosition,
@@ -1618,11 +1751,11 @@ var TimescaleLoopRegion = ({
1618
1751
  maxPosition = Infinity,
1619
1752
  controlsOffset = 0
1620
1753
  }) => {
1621
- const [isCreating, setIsCreating] = useState(false);
1622
- const createStartX = useRef3(0);
1623
- const containerRef = useRef3(null);
1754
+ const [, setIsCreating] = useState(false);
1755
+ const createStartX = useRef5(0);
1756
+ const containerRef = useRef5(null);
1624
1757
  const hasLoopRegion = endPosition > startPosition;
1625
- const handleBackgroundMouseDown = useCallback2((e) => {
1758
+ const handleBackgroundMouseDown = useCallback4((e) => {
1626
1759
  const target = e.target;
1627
1760
  if (target.closest("[data-loop-marker-handle]") || target.closest("[data-loop-region-timescale]")) {
1628
1761
  return;
@@ -1650,14 +1783,14 @@ var TimescaleLoopRegion = ({
1650
1783
  document.addEventListener("mousemove", handleMouseMove);
1651
1784
  document.addEventListener("mouseup", handleMouseUp);
1652
1785
  }, [minPosition, maxPosition, onLoopRegionChange]);
1653
- return /* @__PURE__ */ jsx13(
1786
+ return /* @__PURE__ */ jsx14(
1654
1787
  TimescaleLoopCreator,
1655
1788
  {
1656
1789
  ref: containerRef,
1657
1790
  $leftOffset: controlsOffset,
1658
1791
  onMouseDown: handleBackgroundMouseDown,
1659
1792
  "data-timescale-loop-creator": true,
1660
- children: hasLoopRegion && /* @__PURE__ */ jsx13(
1793
+ children: hasLoopRegion && /* @__PURE__ */ jsx14(
1661
1794
  LoopRegionMarkers,
1662
1795
  {
1663
1796
  startPosition,
@@ -1676,10 +1809,10 @@ var TimescaleLoopRegion = ({
1676
1809
  };
1677
1810
 
1678
1811
  // src/components/SelectionTimeInputs.tsx
1679
- import { useEffect as useEffect3, useState as useState3 } from "react";
1812
+ import { useEffect as useEffect5, useState as useState3 } from "react";
1680
1813
 
1681
1814
  // src/components/TimeInput.tsx
1682
- import { useEffect as useEffect2, useState as useState2 } from "react";
1815
+ import { useEffect as useEffect4, useState as useState2 } from "react";
1683
1816
 
1684
1817
  // src/utils/timeFormat.ts
1685
1818
  function clockFormat(seconds, decimals) {
@@ -1729,7 +1862,7 @@ function parseTime(timeStr, format) {
1729
1862
  }
1730
1863
 
1731
1864
  // src/components/TimeInput.tsx
1732
- import { Fragment as Fragment3, jsx as jsx14, jsxs as jsxs7 } from "react/jsx-runtime";
1865
+ import { Fragment as Fragment3, jsx as jsx15, jsxs as jsxs7 } from "react/jsx-runtime";
1733
1866
  var TimeInput = ({
1734
1867
  id,
1735
1868
  label,
@@ -1740,7 +1873,7 @@ var TimeInput = ({
1740
1873
  readOnly = false
1741
1874
  }) => {
1742
1875
  const [displayValue, setDisplayValue] = useState2("");
1743
- useEffect2(() => {
1876
+ useEffect4(() => {
1744
1877
  const formatted = formatTime(value, format);
1745
1878
  setDisplayValue(formatted);
1746
1879
  }, [value, format, id]);
@@ -1761,8 +1894,8 @@ var TimeInput = ({
1761
1894
  }
1762
1895
  };
1763
1896
  return /* @__PURE__ */ jsxs7(Fragment3, { children: [
1764
- /* @__PURE__ */ jsx14(ScreenReaderOnly, { as: "label", htmlFor: id, children: label }),
1765
- /* @__PURE__ */ jsx14(
1897
+ /* @__PURE__ */ jsx15(ScreenReaderOnly, { as: "label", htmlFor: id, children: label }),
1898
+ /* @__PURE__ */ jsx15(
1766
1899
  BaseInput,
1767
1900
  {
1768
1901
  type: "text",
@@ -1779,7 +1912,7 @@ var TimeInput = ({
1779
1912
  };
1780
1913
 
1781
1914
  // src/components/SelectionTimeInputs.tsx
1782
- import { Fragment as Fragment4, jsx as jsx15, jsxs as jsxs8 } from "react/jsx-runtime";
1915
+ import { jsx as jsx16, jsxs as jsxs8 } from "react/jsx-runtime";
1783
1916
  var SelectionTimeInputs = ({
1784
1917
  selectionStart,
1785
1918
  selectionEnd,
@@ -1787,7 +1920,7 @@ var SelectionTimeInputs = ({
1787
1920
  className
1788
1921
  }) => {
1789
1922
  const [timeFormat, setTimeFormat] = useState3("hh:mm:ss.uuu");
1790
- useEffect3(() => {
1923
+ useEffect5(() => {
1791
1924
  const timeFormatSelect = document.querySelector(".time-format");
1792
1925
  const handleFormatChange = () => {
1793
1926
  if (timeFormatSelect) {
@@ -1812,8 +1945,8 @@ var SelectionTimeInputs = ({
1812
1945
  onSelectionChange(selectionStart, value);
1813
1946
  }
1814
1947
  };
1815
- return /* @__PURE__ */ jsxs8(Fragment4, { children: [
1816
- /* @__PURE__ */ jsx15(
1948
+ return /* @__PURE__ */ jsxs8("div", { className, children: [
1949
+ /* @__PURE__ */ jsx16(
1817
1950
  TimeInput,
1818
1951
  {
1819
1952
  id: "audio_start",
@@ -1824,7 +1957,7 @@ var SelectionTimeInputs = ({
1824
1957
  onChange: handleStartChange
1825
1958
  }
1826
1959
  ),
1827
- /* @__PURE__ */ jsx15(
1960
+ /* @__PURE__ */ jsx16(
1828
1961
  TimeInput,
1829
1962
  {
1830
1963
  id: "audio_end",
@@ -1839,12 +1972,12 @@ var SelectionTimeInputs = ({
1839
1972
  };
1840
1973
 
1841
1974
  // src/contexts/DevicePixelRatio.tsx
1842
- import { useState as useState4, createContext, useContext } from "react";
1843
- import { jsx as jsx16 } from "react/jsx-runtime";
1975
+ import { useState as useState4, createContext as createContext2, useContext as useContext2 } from "react";
1976
+ import { jsx as jsx17 } from "react/jsx-runtime";
1844
1977
  function getScale() {
1845
1978
  return window.devicePixelRatio;
1846
1979
  }
1847
- var DevicePixelRatioContext = createContext(getScale());
1980
+ var DevicePixelRatioContext = createContext2(getScale());
1848
1981
  var DevicePixelRatioProvider = ({ children }) => {
1849
1982
  const [scale, setScale] = useState4(getScale());
1850
1983
  matchMedia(`(resolution: ${getScale()}dppx)`).addEventListener(
@@ -1854,13 +1987,13 @@ var DevicePixelRatioProvider = ({ children }) => {
1854
1987
  },
1855
1988
  { once: true }
1856
1989
  );
1857
- return /* @__PURE__ */ jsx16(DevicePixelRatioContext.Provider, { value: Math.ceil(scale), children });
1990
+ return /* @__PURE__ */ jsx17(DevicePixelRatioContext.Provider, { value: Math.ceil(scale), children });
1858
1991
  };
1859
- var useDevicePixelRatio = () => useContext(DevicePixelRatioContext);
1992
+ var useDevicePixelRatio = () => useContext2(DevicePixelRatioContext);
1860
1993
 
1861
1994
  // src/contexts/PlaylistInfo.tsx
1862
- import { createContext as createContext2, useContext as useContext2 } from "react";
1863
- var PlaylistInfoContext = createContext2({
1995
+ import { createContext as createContext3, useContext as useContext3 } from "react";
1996
+ var PlaylistInfoContext = createContext3({
1864
1997
  sampleRate: 48e3,
1865
1998
  samplesPerPixel: 1e3,
1866
1999
  zoomLevels: [1e3, 1500, 2e3, 2500],
@@ -1874,26 +2007,26 @@ var PlaylistInfoContext = createContext2({
1874
2007
  barWidth: 1,
1875
2008
  barGap: 0
1876
2009
  });
1877
- var usePlaylistInfo = () => useContext2(PlaylistInfoContext);
2010
+ var usePlaylistInfo = () => useContext3(PlaylistInfoContext);
1878
2011
 
1879
2012
  // src/contexts/Theme.tsx
1880
- import { useContext as useContext3 } from "react";
2013
+ import { useContext as useContext4 } from "react";
1881
2014
  import { ThemeContext } from "styled-components";
1882
- var useTheme2 = () => useContext3(ThemeContext);
2015
+ var useTheme2 = () => useContext4(ThemeContext);
1883
2016
 
1884
2017
  // src/contexts/TrackControls.tsx
1885
- import { createContext as createContext3, useContext as useContext4, Fragment as Fragment5 } from "react";
1886
- import { jsx as jsx17 } from "react/jsx-runtime";
1887
- var TrackControlsContext = createContext3(/* @__PURE__ */ jsx17(Fragment5, {}));
1888
- var useTrackControls = () => useContext4(TrackControlsContext);
2018
+ import { createContext as createContext4, useContext as useContext5, Fragment as Fragment4 } from "react";
2019
+ import { jsx as jsx18 } from "react/jsx-runtime";
2020
+ var TrackControlsContext = createContext4(/* @__PURE__ */ jsx18(Fragment4, {}));
2021
+ var useTrackControls = () => useContext5(TrackControlsContext);
1889
2022
 
1890
2023
  // src/contexts/Playout.tsx
1891
2024
  import {
1892
2025
  useState as useState5,
1893
- createContext as createContext4,
1894
- useContext as useContext5
2026
+ createContext as createContext5,
2027
+ useContext as useContext6
1895
2028
  } from "react";
1896
- import { jsx as jsx18 } from "react/jsx-runtime";
2029
+ import { jsx as jsx19 } from "react/jsx-runtime";
1897
2030
  var defaultProgress = 0;
1898
2031
  var defaultIsPlaying = false;
1899
2032
  var defaultSelectionStart = 0;
@@ -1904,8 +2037,8 @@ var defaultPlayout = {
1904
2037
  selectionStart: defaultSelectionStart,
1905
2038
  selectionEnd: defaultSelectionEnd
1906
2039
  };
1907
- var PlayoutStatusContext = createContext4(defaultPlayout);
1908
- var PlayoutStatusUpdateContext = createContext4({
2040
+ var PlayoutStatusContext = createContext5(defaultPlayout);
2041
+ var PlayoutStatusUpdateContext = createContext5({
1909
2042
  setIsPlaying: () => {
1910
2043
  },
1911
2044
  setProgress: () => {
@@ -1922,16 +2055,16 @@ var PlayoutProvider = ({ children }) => {
1922
2055
  setSelectionStart(start);
1923
2056
  setSelectionEnd(end);
1924
2057
  };
1925
- return /* @__PURE__ */ jsx18(PlayoutStatusUpdateContext.Provider, { value: { setIsPlaying, setProgress, setSelection }, children: /* @__PURE__ */ jsx18(PlayoutStatusContext.Provider, { value: { isPlaying, progress, selectionStart, selectionEnd }, children }) });
2058
+ return /* @__PURE__ */ jsx19(PlayoutStatusUpdateContext.Provider, { value: { setIsPlaying, setProgress, setSelection }, children: /* @__PURE__ */ jsx19(PlayoutStatusContext.Provider, { value: { isPlaying, progress, selectionStart, selectionEnd }, children }) });
1926
2059
  };
1927
- var usePlayoutStatus = () => useContext5(PlayoutStatusContext);
1928
- var usePlayoutStatusUpdate = () => useContext5(PlayoutStatusUpdateContext);
2060
+ var usePlayoutStatus = () => useContext6(PlayoutStatusContext);
2061
+ var usePlayoutStatusUpdate = () => useContext6(PlayoutStatusUpdateContext);
1929
2062
 
1930
2063
  // src/components/SpectrogramChannel.tsx
1931
- import { useLayoutEffect as useLayoutEffect2, useCallback as useCallback3, useRef as useRef4, useEffect as useEffect4 } from "react";
2064
+ import { useLayoutEffect as useLayoutEffect2, useCallback as useCallback5, useRef as useRef6, useEffect as useEffect6 } from "react";
1932
2065
  import styled19 from "styled-components";
1933
- import { jsx as jsx19 } from "react/jsx-runtime";
1934
- var MAX_CANVAS_WIDTH2 = 1e3;
2066
+ import { jsx as jsx20 } from "react/jsx-runtime";
2067
+ var LINEAR_FREQUENCY_SCALE = (f, minF, maxF) => (f - minF) / (maxF - minF);
1935
2068
  var Wrapper3 = styled19.div.attrs((props) => ({
1936
2069
  style: {
1937
2070
  top: `${props.$waveHeight * props.$index}px`,
@@ -1947,11 +2080,13 @@ var Wrapper3 = styled19.div.attrs((props) => ({
1947
2080
  var SpectrogramCanvas = styled19.canvas.attrs((props) => ({
1948
2081
  style: {
1949
2082
  width: `${props.$cssWidth}px`,
1950
- height: `${props.$waveHeight}px`
2083
+ height: `${props.$waveHeight}px`,
2084
+ left: `${props.$left}px`
1951
2085
  }
1952
2086
  }))`
1953
- float: left;
1954
- position: relative;
2087
+ position: absolute;
2088
+ top: 0;
2089
+ /* Promote to own compositing layer for smoother scrolling */
1955
2090
  will-change: transform;
1956
2091
  image-rendering: pixelated;
1957
2092
  image-rendering: crisp-edges;
@@ -1963,6 +2098,7 @@ function defaultGetColorMap() {
1963
2098
  }
1964
2099
  return lut;
1965
2100
  }
2101
+ var DEFAULT_COLOR_LUT = defaultGetColorMap();
1966
2102
  var SpectrogramChannel = ({
1967
2103
  index,
1968
2104
  channelIndex: channelIndexProp,
@@ -1980,11 +2116,30 @@ var SpectrogramChannel = ({
1980
2116
  onCanvasesReady
1981
2117
  }) => {
1982
2118
  const channelIndex = channelIndexProp ?? index;
1983
- const canvasesRef = useRef4([]);
1984
- const registeredIdsRef = useRef4([]);
1985
- const transferredCanvasesRef = useRef4(/* @__PURE__ */ new WeakSet());
2119
+ const canvasesRef = useRef6([]);
2120
+ const registeredIdsRef = useRef6([]);
2121
+ const transferredCanvasesRef = useRef6(/* @__PURE__ */ new WeakSet());
2122
+ const workerApiRef = useRef6(workerApi);
2123
+ const onCanvasesReadyRef = useRef6(onCanvasesReady);
1986
2124
  const isWorkerMode = !!(workerApi && clipId);
1987
- const canvasRef = useCallback3(
2125
+ const visibleChunkKey = useScrollViewportSelector((viewport) => {
2126
+ const totalChunks = Math.ceil(length / MAX_CANVAS_WIDTH);
2127
+ const indices = [];
2128
+ for (let i = 0; i < totalChunks; i++) {
2129
+ const chunkLeft = i * MAX_CANVAS_WIDTH;
2130
+ const chunkWidth = Math.min(length - chunkLeft, MAX_CANVAS_WIDTH);
2131
+ if (viewport) {
2132
+ const chunkEnd = chunkLeft + chunkWidth;
2133
+ if (chunkEnd <= viewport.visibleStart || chunkLeft >= viewport.visibleEnd) {
2134
+ continue;
2135
+ }
2136
+ }
2137
+ indices.push(i);
2138
+ }
2139
+ return indices.join(",");
2140
+ });
2141
+ const visibleChunkIndices = visibleChunkKey ? visibleChunkKey.split(",").map(Number) : [];
2142
+ const canvasRef = useCallback5(
1988
2143
  (canvas) => {
1989
2144
  if (canvas !== null) {
1990
2145
  const idx = parseInt(canvas.dataset.index, 10);
@@ -1993,53 +2148,101 @@ var SpectrogramChannel = ({
1993
2148
  },
1994
2149
  []
1995
2150
  );
1996
- useEffect4(() => {
2151
+ const lut = colorLUT ?? DEFAULT_COLOR_LUT;
2152
+ const maxF = maxFrequency ?? (data ? data.sampleRate / 2 : 22050);
2153
+ const scaleFn = frequencyScaleFn ?? LINEAR_FREQUENCY_SCALE;
2154
+ const hasCustomFrequencyScale = Boolean(frequencyScaleFn);
2155
+ useEffect6(() => {
2156
+ workerApiRef.current = workerApi;
2157
+ }, [workerApi]);
2158
+ useEffect6(() => {
2159
+ onCanvasesReadyRef.current = onCanvasesReady;
2160
+ }, [onCanvasesReady]);
2161
+ useEffect6(() => {
1997
2162
  if (!isWorkerMode) return;
1998
- const canvasCount2 = Math.ceil(length / MAX_CANVAS_WIDTH2);
1999
- canvasesRef.current.length = canvasCount2;
2163
+ const currentWorkerApi = workerApiRef.current;
2164
+ if (!currentWorkerApi || !clipId) return;
2000
2165
  const canvases2 = canvasesRef.current;
2001
- const ids = [];
2002
- const widths = [];
2166
+ const newIds = [];
2167
+ const newWidths = [];
2003
2168
  for (let i = 0; i < canvases2.length; i++) {
2004
2169
  const canvas = canvases2[i];
2005
2170
  if (!canvas) continue;
2006
2171
  if (transferredCanvasesRef.current.has(canvas)) continue;
2007
- const canvasId = `${clipId}-ch${channelIndex}-chunk${i}`;
2172
+ const canvasIdx = parseInt(canvas.dataset.index, 10);
2173
+ const canvasId = `${clipId}-ch${channelIndex}-chunk${canvasIdx}`;
2174
+ let offscreen;
2008
2175
  try {
2009
- const offscreen = canvas.transferControlToOffscreen();
2010
- workerApi.registerCanvas(canvasId, offscreen);
2011
- transferredCanvasesRef.current.add(canvas);
2012
- ids.push(canvasId);
2013
- widths.push(Math.min(length - i * MAX_CANVAS_WIDTH2, MAX_CANVAS_WIDTH2));
2176
+ offscreen = canvas.transferControlToOffscreen();
2014
2177
  } catch (err) {
2015
2178
  console.warn(`[spectrogram] transferControlToOffscreen failed for ${canvasId}:`, err);
2016
2179
  continue;
2017
2180
  }
2181
+ transferredCanvasesRef.current.add(canvas);
2182
+ try {
2183
+ currentWorkerApi.registerCanvas(canvasId, offscreen);
2184
+ newIds.push(canvasId);
2185
+ newWidths.push(Math.min(length - canvasIdx * MAX_CANVAS_WIDTH, MAX_CANVAS_WIDTH));
2186
+ } catch (err) {
2187
+ console.warn(`[spectrogram] registerCanvas failed for ${canvasId}:`, err);
2188
+ continue;
2189
+ }
2018
2190
  }
2019
- registeredIdsRef.current = ids;
2020
- if (ids.length > 0 && onCanvasesReady) {
2021
- onCanvasesReady(ids, widths);
2191
+ if (newIds.length > 0) {
2192
+ registeredIdsRef.current = [...registeredIdsRef.current, ...newIds];
2193
+ onCanvasesReadyRef.current?.(newIds, newWidths);
2194
+ }
2195
+ }, [isWorkerMode, clipId, channelIndex, length, visibleChunkKey]);
2196
+ useEffect6(() => {
2197
+ if (!isWorkerMode) return;
2198
+ const currentWorkerApi = workerApiRef.current;
2199
+ if (!currentWorkerApi) return;
2200
+ const remaining = [];
2201
+ for (const id of registeredIdsRef.current) {
2202
+ const match = id.match(/chunk(\d+)$/);
2203
+ if (!match) {
2204
+ remaining.push(id);
2205
+ continue;
2206
+ }
2207
+ const chunkIdx = parseInt(match[1], 10);
2208
+ const canvas = canvasesRef.current[chunkIdx];
2209
+ if (canvas && canvas.isConnected) {
2210
+ remaining.push(id);
2211
+ } else {
2212
+ try {
2213
+ currentWorkerApi.unregisterCanvas(id);
2214
+ } catch (err) {
2215
+ console.warn(`[spectrogram] unregisterCanvas failed for ${id}:`, err);
2216
+ }
2217
+ }
2022
2218
  }
2219
+ registeredIdsRef.current = remaining;
2220
+ });
2221
+ useEffect6(() => {
2023
2222
  return () => {
2223
+ const api = workerApiRef.current;
2224
+ if (!api) return;
2024
2225
  for (const id of registeredIdsRef.current) {
2025
- workerApi.unregisterCanvas(id);
2226
+ try {
2227
+ api.unregisterCanvas(id);
2228
+ } catch (err) {
2229
+ console.warn(`[spectrogram] unregisterCanvas failed for ${id}:`, err);
2230
+ }
2026
2231
  }
2027
2232
  registeredIdsRef.current = [];
2028
2233
  };
2029
- }, [isWorkerMode, clipId, channelIndex, length]);
2030
- const lut = colorLUT ?? defaultGetColorMap();
2031
- const maxF = maxFrequency ?? (data ? data.sampleRate / 2 : 22050);
2032
- const scaleFn = frequencyScaleFn ?? ((f, minF, maxF2) => (f - minF) / (maxF2 - minF));
2234
+ }, []);
2033
2235
  useLayoutEffect2(() => {
2034
2236
  if (isWorkerMode || !data) return;
2035
2237
  const canvases2 = canvasesRef.current;
2036
2238
  const { frequencyBinCount, frameCount, hopSize, sampleRate, gainDb, rangeDb: rawRangeDb } = data;
2037
2239
  const rangeDb = rawRangeDb === 0 ? 1 : rawRangeDb;
2038
- let globalPixelOffset = 0;
2039
2240
  const binToFreq = (bin) => bin / frequencyBinCount * (sampleRate / 2);
2040
- for (let canvasIdx = 0; canvasIdx < canvases2.length; canvasIdx++) {
2041
- const canvas = canvases2[canvasIdx];
2241
+ for (let i = 0; i < canvases2.length; i++) {
2242
+ const canvas = canvases2[i];
2042
2243
  if (!canvas) continue;
2244
+ const canvasIdx = parseInt(canvas.dataset.index, 10);
2245
+ const globalPixelOffset = canvasIdx * MAX_CANVAS_WIDTH;
2043
2246
  const ctx = canvas.getContext("2d");
2044
2247
  if (!ctx) continue;
2045
2248
  const canvasWidth = canvas.width / devicePixelRatio;
@@ -2059,7 +2262,7 @@ var SpectrogramChannel = ({
2059
2262
  for (let y = 0; y < canvasHeight; y++) {
2060
2263
  const normalizedY = 1 - y / canvasHeight;
2061
2264
  let bin = Math.floor(normalizedY * frequencyBinCount);
2062
- if (frequencyScaleFn) {
2265
+ if (hasCustomFrequencyScale) {
2063
2266
  let lo = 0;
2064
2267
  let hi = frequencyBinCount - 1;
2065
2268
  while (lo < hi) {
@@ -2098,36 +2301,30 @@ var SpectrogramChannel = ({
2098
2301
  ctx.imageSmoothingEnabled = false;
2099
2302
  ctx.drawImage(tmpCanvas, 0, 0, canvas.width, canvas.height);
2100
2303
  }
2101
- globalPixelOffset += canvasWidth;
2102
2304
  }
2103
- }, [isWorkerMode, data, length, waveHeight, devicePixelRatio, samplesPerPixel, lut, frequencyScaleFn, minFrequency, maxF, scaleFn]);
2104
- let totalWidth = length;
2105
- let canvasCount = 0;
2106
- const canvases = [];
2107
- while (totalWidth > 0) {
2108
- const currentWidth = Math.min(totalWidth, MAX_CANVAS_WIDTH2);
2109
- canvases.push(
2110
- /* @__PURE__ */ jsx19(
2111
- SpectrogramCanvas,
2112
- {
2113
- $cssWidth: currentWidth,
2114
- width: currentWidth * devicePixelRatio,
2115
- height: waveHeight * devicePixelRatio,
2116
- $waveHeight: waveHeight,
2117
- "data-index": canvasCount,
2118
- ref: canvasRef
2119
- },
2120
- `${length}-${canvasCount}`
2121
- )
2305
+ }, [isWorkerMode, data, length, waveHeight, devicePixelRatio, samplesPerPixel, lut, minFrequency, maxF, scaleFn, hasCustomFrequencyScale, visibleChunkKey]);
2306
+ const canvases = visibleChunkIndices.map((i) => {
2307
+ const chunkLeft = i * MAX_CANVAS_WIDTH;
2308
+ const currentWidth = Math.min(length - chunkLeft, MAX_CANVAS_WIDTH);
2309
+ return /* @__PURE__ */ jsx20(
2310
+ SpectrogramCanvas,
2311
+ {
2312
+ $cssWidth: currentWidth,
2313
+ $left: chunkLeft,
2314
+ width: currentWidth * devicePixelRatio,
2315
+ height: waveHeight * devicePixelRatio,
2316
+ $waveHeight: waveHeight,
2317
+ "data-index": i,
2318
+ ref: canvasRef
2319
+ },
2320
+ `${length}-${i}`
2122
2321
  );
2123
- totalWidth -= currentWidth;
2124
- canvasCount++;
2125
- }
2126
- return /* @__PURE__ */ jsx19(Wrapper3, { $index: index, $cssWidth: length, $waveHeight: waveHeight, children: canvases });
2322
+ });
2323
+ return /* @__PURE__ */ jsx20(Wrapper3, { $index: index, $cssWidth: length, $waveHeight: waveHeight, children: canvases });
2127
2324
  };
2128
2325
 
2129
2326
  // src/components/SmartChannel.tsx
2130
- import { Fragment as Fragment6, jsx as jsx20, jsxs as jsxs9 } from "react/jsx-runtime";
2327
+ import { Fragment as Fragment5, jsx as jsx21, jsxs as jsxs9 } from "react/jsx-runtime";
2131
2328
  var SmartChannel = ({
2132
2329
  isSelected,
2133
2330
  transparentBackground,
@@ -2152,7 +2349,7 @@ var SmartChannel = ({
2152
2349
  const drawMode = theme?.waveformDrawMode || "inverted";
2153
2350
  const hasSpectrogram = spectrogramData || spectrogramWorkerApi;
2154
2351
  if (renderMode === "spectrogram" && hasSpectrogram) {
2155
- return /* @__PURE__ */ jsx20(
2352
+ return /* @__PURE__ */ jsx21(
2156
2353
  SpectrogramChannel,
2157
2354
  {
2158
2355
  index: props.index,
@@ -2173,8 +2370,8 @@ var SmartChannel = ({
2173
2370
  }
2174
2371
  if (renderMode === "both" && hasSpectrogram) {
2175
2372
  const halfHeight = Math.floor(waveHeight / 2);
2176
- return /* @__PURE__ */ jsxs9(Fragment6, { children: [
2177
- /* @__PURE__ */ jsx20(
2373
+ return /* @__PURE__ */ jsxs9(Fragment5, { children: [
2374
+ /* @__PURE__ */ jsx21(
2178
2375
  SpectrogramChannel,
2179
2376
  {
2180
2377
  index: props.index * 2,
@@ -2193,7 +2390,7 @@ var SmartChannel = ({
2193
2390
  onCanvasesReady: spectrogramOnCanvasesReady
2194
2391
  }
2195
2392
  ),
2196
- /* @__PURE__ */ jsx20("div", { style: { position: "absolute", top: (props.index * 2 + 1) * halfHeight, width: props.length, height: halfHeight }, children: /* @__PURE__ */ jsx20(
2393
+ /* @__PURE__ */ jsx21("div", { style: { position: "absolute", top: (props.index * 2 + 1) * halfHeight, width: props.length, height: halfHeight }, children: /* @__PURE__ */ jsx21(
2197
2394
  Channel,
2198
2395
  {
2199
2396
  ...props,
@@ -2210,7 +2407,7 @@ var SmartChannel = ({
2210
2407
  ) })
2211
2408
  ] });
2212
2409
  }
2213
- return /* @__PURE__ */ jsx20(
2410
+ return /* @__PURE__ */ jsx21(
2214
2411
  Channel,
2215
2412
  {
2216
2413
  ...props,
@@ -2227,9 +2424,9 @@ var SmartChannel = ({
2227
2424
  };
2228
2425
 
2229
2426
  // src/components/SpectrogramLabels.tsx
2230
- import { useRef as useRef5, useLayoutEffect as useLayoutEffect3 } from "react";
2427
+ import { useRef as useRef7, useLayoutEffect as useLayoutEffect3 } from "react";
2231
2428
  import styled20 from "styled-components";
2232
- import { jsx as jsx21 } from "react/jsx-runtime";
2429
+ import { jsx as jsx22 } from "react/jsx-runtime";
2233
2430
  var LABELS_WIDTH = 72;
2234
2431
  var LabelsStickyWrapper = styled20.div`
2235
2432
  position: sticky;
@@ -2279,7 +2476,7 @@ var SpectrogramLabels = ({
2279
2476
  renderMode = "spectrogram",
2280
2477
  hasClipHeaders = false
2281
2478
  }) => {
2282
- const canvasRef = useRef5(null);
2479
+ const canvasRef = useRef7(null);
2283
2480
  const devicePixelRatio = useDevicePixelRatio();
2284
2481
  const spectrogramHeight = renderMode === "both" ? Math.floor(waveHeight / 2) : waveHeight;
2285
2482
  const totalHeight = numChannels * waveHeight;
@@ -2311,7 +2508,7 @@ var SpectrogramLabels = ({
2311
2508
  }
2312
2509
  }
2313
2510
  }, [waveHeight, numChannels, frequencyScaleFn, minFrequency, maxFrequency, labelsColor, labelsBackground, devicePixelRatio, spectrogramHeight, clipHeaderOffset]);
2314
- return /* @__PURE__ */ jsx21(LabelsStickyWrapper, { $height: totalHeight + clipHeaderOffset, children: /* @__PURE__ */ jsx21(
2511
+ return /* @__PURE__ */ jsx22(LabelsStickyWrapper, { $height: totalHeight + clipHeaderOffset, children: /* @__PURE__ */ jsx22(
2315
2512
  "canvas",
2316
2513
  {
2317
2514
  ref: canvasRef,
@@ -2327,10 +2524,10 @@ var SpectrogramLabels = ({
2327
2524
  };
2328
2525
 
2329
2526
  // src/components/SmartScale.tsx
2330
- import { useContext as useContext7 } from "react";
2527
+ import { useContext as useContext8 } from "react";
2331
2528
 
2332
2529
  // src/components/TimeScale.tsx
2333
- import React13, { useRef as useRef6, useEffect as useEffect5, useContext as useContext6 } from "react";
2530
+ import React15, { useRef as useRef8, useEffect as useEffect7, useLayoutEffect as useLayoutEffect4, useContext as useContext7, useMemo, useCallback as useCallback6 } from "react";
2334
2531
  import styled21, { withTheme as withTheme2 } from "styled-components";
2335
2532
 
2336
2533
  // src/utils/conversions.ts
@@ -2354,7 +2551,7 @@ function secondsToPixels(seconds, samplesPerPixel, sampleRate) {
2354
2551
  }
2355
2552
 
2356
2553
  // src/components/TimeScale.tsx
2357
- import { jsx as jsx22, jsxs as jsxs10 } from "react/jsx-runtime";
2554
+ import { jsx as jsx23, jsxs as jsxs10 } from "react/jsx-runtime";
2358
2555
  function formatTime2(milliseconds) {
2359
2556
  const seconds = Math.floor(milliseconds / 1e3);
2360
2557
  const s = seconds % 60;
@@ -2373,16 +2570,17 @@ var PlaylistTimeScaleScroll = styled21.div.attrs((props) => ({
2373
2570
  border-bottom: 1px solid ${(props) => props.theme.timeColor};
2374
2571
  box-sizing: border-box;
2375
2572
  `;
2376
- var TimeTicks = styled21.canvas.attrs((props) => ({
2573
+ var TimeTickChunk = styled21.canvas.attrs((props) => ({
2377
2574
  style: {
2378
2575
  width: `${props.$cssWidth}px`,
2379
- height: `${props.$timeScaleHeight}px`
2576
+ height: `${props.$timeScaleHeight}px`,
2577
+ left: `${props.$left}px`
2380
2578
  }
2381
2579
  }))`
2382
2580
  position: absolute;
2383
- left: 0;
2384
- right: 0;
2385
2581
  bottom: 0;
2582
+ /* Promote to own compositing layer for smoother scrolling */
2583
+ will-change: transform;
2386
2584
  `;
2387
2585
  var TimeStamp = styled21.div.attrs((props) => ({
2388
2586
  style: {
@@ -2404,60 +2602,111 @@ var TimeScale = (props) => {
2404
2602
  secondStep,
2405
2603
  renderTimestamp
2406
2604
  } = props;
2407
- const canvasInfo = /* @__PURE__ */ new Map();
2408
- const timeMarkers = [];
2409
- const canvasRef = useRef6(null);
2605
+ const canvasRefsMap = useRef8(/* @__PURE__ */ new Map());
2410
2606
  const {
2411
2607
  sampleRate,
2412
2608
  samplesPerPixel,
2413
2609
  timeScaleHeight,
2414
2610
  controls: { show: showControls, width: controlWidth }
2415
- } = useContext6(PlaylistInfoContext);
2611
+ } = useContext7(PlaylistInfoContext);
2416
2612
  const devicePixelRatio = useDevicePixelRatio();
2417
- useEffect5(() => {
2418
- if (canvasRef.current !== null) {
2419
- const canvas = canvasRef.current;
2420
- const ctx = canvas.getContext("2d");
2421
- if (ctx) {
2422
- ctx.resetTransform();
2423
- ctx.clearRect(0, 0, canvas.width, canvas.height);
2424
- ctx.imageSmoothingEnabled = false;
2425
- ctx.fillStyle = timeColor;
2426
- ctx.scale(devicePixelRatio, devicePixelRatio);
2427
- for (const [pixLeft, scaleHeight] of canvasInfo.entries()) {
2428
- const scaleY = timeScaleHeight - scaleHeight;
2429
- ctx.fillRect(pixLeft, scaleY, 1, scaleHeight);
2613
+ const canvasRefCallback = useCallback6((canvas) => {
2614
+ if (canvas !== null) {
2615
+ const idx = parseInt(canvas.dataset.index, 10);
2616
+ canvasRefsMap.current.set(idx, canvas);
2617
+ }
2618
+ }, []);
2619
+ const { widthX, canvasInfo, timeMarkersWithPositions } = useMemo(() => {
2620
+ const nextCanvasInfo = /* @__PURE__ */ new Map();
2621
+ const nextMarkers = [];
2622
+ const nextWidthX = secondsToPixels(duration / 1e3, samplesPerPixel, sampleRate);
2623
+ const pixPerSec = sampleRate / samplesPerPixel;
2624
+ let counter = 0;
2625
+ for (let i = 0; i < nextWidthX; i += pixPerSec * secondStep / 1e3) {
2626
+ const pix = Math.floor(i);
2627
+ if (counter % marker === 0) {
2628
+ const timeMs = counter;
2629
+ const timestamp = formatTime2(timeMs);
2630
+ const element = renderTimestamp ? /* @__PURE__ */ jsx23(React15.Fragment, { children: renderTimestamp(timeMs, pix) }, `timestamp-${counter}`) : /* @__PURE__ */ jsx23(TimeStamp, { $left: pix, children: timestamp }, timestamp);
2631
+ nextMarkers.push({ pix, element });
2632
+ nextCanvasInfo.set(pix, timeScaleHeight);
2633
+ } else if (counter % bigStep === 0) {
2634
+ nextCanvasInfo.set(pix, Math.floor(timeScaleHeight / 2));
2635
+ } else if (counter % secondStep === 0) {
2636
+ nextCanvasInfo.set(pix, Math.floor(timeScaleHeight / 5));
2637
+ }
2638
+ counter += secondStep;
2639
+ }
2640
+ return {
2641
+ widthX: nextWidthX,
2642
+ canvasInfo: nextCanvasInfo,
2643
+ timeMarkersWithPositions: nextMarkers
2644
+ };
2645
+ }, [duration, samplesPerPixel, sampleRate, marker, bigStep, secondStep, renderTimestamp, timeScaleHeight]);
2646
+ const visibleChunkKey = useScrollViewportSelector((viewport) => {
2647
+ const totalChunks = Math.ceil(widthX / MAX_CANVAS_WIDTH);
2648
+ const indices = [];
2649
+ for (let i = 0; i < totalChunks; i++) {
2650
+ const chunkLeft = i * MAX_CANVAS_WIDTH;
2651
+ const chunkWidth = Math.min(widthX - chunkLeft, MAX_CANVAS_WIDTH);
2652
+ if (viewport) {
2653
+ const chunkEnd = chunkLeft + chunkWidth;
2654
+ if (chunkEnd <= viewport.visibleStart || chunkLeft >= viewport.visibleEnd) {
2655
+ continue;
2430
2656
  }
2431
2657
  }
2658
+ indices.push(i);
2432
2659
  }
2433
- }, [
2434
- duration,
2435
- devicePixelRatio,
2436
- timeColor,
2437
- timeScaleHeight,
2438
- bigStep,
2439
- secondStep,
2440
- marker,
2441
- canvasInfo
2442
- ]);
2443
- const widthX = secondsToPixels(duration / 1e3, samplesPerPixel, sampleRate);
2444
- const pixPerSec = sampleRate / samplesPerPixel;
2445
- let counter = 0;
2446
- for (let i = 0; i < widthX; i += pixPerSec * secondStep / 1e3) {
2447
- const pix = Math.floor(i);
2448
- if (counter % marker === 0) {
2449
- const timeMs = counter;
2450
- const timestamp = formatTime2(timeMs);
2451
- const timestampContent = renderTimestamp ? /* @__PURE__ */ jsx22(React13.Fragment, { children: renderTimestamp(timeMs, pix) }, `timestamp-${counter}`) : /* @__PURE__ */ jsx22(TimeStamp, { $left: pix, children: timestamp }, timestamp);
2452
- timeMarkers.push(timestampContent);
2453
- canvasInfo.set(pix, timeScaleHeight);
2454
- } else if (counter % bigStep === 0) {
2455
- canvasInfo.set(pix, Math.floor(timeScaleHeight / 2));
2456
- } else if (counter % secondStep === 0) {
2457
- canvasInfo.set(pix, Math.floor(timeScaleHeight / 5));
2660
+ return indices.join(",");
2661
+ });
2662
+ const visibleChunkIndices = visibleChunkKey ? visibleChunkKey.split(",").map(Number) : [];
2663
+ const visibleChunks = visibleChunkIndices.map((i) => {
2664
+ const chunkLeft = i * MAX_CANVAS_WIDTH;
2665
+ const chunkWidth = Math.min(widthX - chunkLeft, MAX_CANVAS_WIDTH);
2666
+ return /* @__PURE__ */ jsx23(
2667
+ TimeTickChunk,
2668
+ {
2669
+ $cssWidth: chunkWidth,
2670
+ $left: chunkLeft,
2671
+ $timeScaleHeight: timeScaleHeight,
2672
+ width: chunkWidth * devicePixelRatio,
2673
+ height: timeScaleHeight * devicePixelRatio,
2674
+ "data-index": i,
2675
+ ref: canvasRefCallback
2676
+ },
2677
+ `timescale-${i}`
2678
+ );
2679
+ });
2680
+ const firstChunkLeft = visibleChunkIndices.length > 0 ? visibleChunkIndices[0] * MAX_CANVAS_WIDTH : 0;
2681
+ const lastChunkRight = visibleChunkIndices.length > 0 ? (visibleChunkIndices[visibleChunkIndices.length - 1] + 1) * MAX_CANVAS_WIDTH : Infinity;
2682
+ const visibleMarkers = visibleChunkIndices.length > 0 ? timeMarkersWithPositions.filter(({ pix }) => pix >= firstChunkLeft && pix < lastChunkRight).map(({ element }) => element) : timeMarkersWithPositions.map(({ element }) => element);
2683
+ useEffect7(() => {
2684
+ const currentMap = canvasRefsMap.current;
2685
+ for (const [idx, canvas] of currentMap.entries()) {
2686
+ if (!canvas.isConnected) {
2687
+ currentMap.delete(idx);
2688
+ }
2458
2689
  }
2459
- counter += secondStep;
2460
- }
2690
+ });
2691
+ useLayoutEffect4(() => {
2692
+ for (const [chunkIdx, canvas] of canvasRefsMap.current.entries()) {
2693
+ const ctx = canvas.getContext("2d");
2694
+ if (!ctx) continue;
2695
+ const chunkLeft = chunkIdx * MAX_CANVAS_WIDTH;
2696
+ const chunkWidth = canvas.width / devicePixelRatio;
2697
+ ctx.resetTransform();
2698
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
2699
+ ctx.imageSmoothingEnabled = false;
2700
+ ctx.fillStyle = timeColor;
2701
+ ctx.scale(devicePixelRatio, devicePixelRatio);
2702
+ for (const [pixLeft, scaleHeight] of canvasInfo.entries()) {
2703
+ if (pixLeft < chunkLeft || pixLeft >= chunkLeft + chunkWidth) continue;
2704
+ const localX = pixLeft - chunkLeft;
2705
+ const scaleY = timeScaleHeight - scaleHeight;
2706
+ ctx.fillRect(localX, scaleY, 1, scaleHeight);
2707
+ }
2708
+ }
2709
+ }, [duration, devicePixelRatio, timeColor, timeScaleHeight, canvasInfo, visibleChunkKey]);
2461
2710
  return /* @__PURE__ */ jsxs10(
2462
2711
  PlaylistTimeScaleScroll,
2463
2712
  {
@@ -2465,17 +2714,8 @@ var TimeScale = (props) => {
2465
2714
  $controlWidth: showControls ? controlWidth : 0,
2466
2715
  $timeScaleHeight: timeScaleHeight,
2467
2716
  children: [
2468
- timeMarkers,
2469
- /* @__PURE__ */ jsx22(
2470
- TimeTicks,
2471
- {
2472
- $cssWidth: widthX,
2473
- $timeScaleHeight: timeScaleHeight,
2474
- width: widthX * devicePixelRatio,
2475
- height: timeScaleHeight * devicePixelRatio,
2476
- ref: canvasRef
2477
- }
2478
- )
2717
+ visibleMarkers,
2718
+ visibleChunks
2479
2719
  ]
2480
2720
  }
2481
2721
  );
@@ -2483,7 +2723,7 @@ var TimeScale = (props) => {
2483
2723
  var StyledTimeScale = withTheme2(TimeScale);
2484
2724
 
2485
2725
  // src/components/SmartScale.tsx
2486
- import { jsx as jsx23 } from "react/jsx-runtime";
2726
+ import { jsx as jsx24 } from "react/jsx-runtime";
2487
2727
  var timeinfo = /* @__PURE__ */ new Map([
2488
2728
  [
2489
2729
  700,
@@ -2557,9 +2797,9 @@ function getScaleInfo(samplesPerPixel) {
2557
2797
  return config;
2558
2798
  }
2559
2799
  var SmartScale = ({ renderTimestamp }) => {
2560
- const { samplesPerPixel, duration } = useContext7(PlaylistInfoContext);
2800
+ const { samplesPerPixel, duration } = useContext8(PlaylistInfoContext);
2561
2801
  let config = getScaleInfo(samplesPerPixel);
2562
- return /* @__PURE__ */ jsx23(
2802
+ return /* @__PURE__ */ jsx24(
2563
2803
  StyledTimeScale,
2564
2804
  {
2565
2805
  marker: config.marker,
@@ -2573,7 +2813,7 @@ var SmartScale = ({ renderTimestamp }) => {
2573
2813
 
2574
2814
  // src/components/TimeFormatSelect.tsx
2575
2815
  import styled22 from "styled-components";
2576
- import { jsx as jsx24 } from "react/jsx-runtime";
2816
+ import { jsx as jsx25 } from "react/jsx-runtime";
2577
2817
  var SelectWrapper = styled22.div`
2578
2818
  display: inline-flex;
2579
2819
  align-items: center;
@@ -2596,7 +2836,7 @@ var TimeFormatSelect = ({
2596
2836
  const handleChange = (e) => {
2597
2837
  onChange(e.target.value);
2598
2838
  };
2599
- return /* @__PURE__ */ jsx24(SelectWrapper, { className, children: /* @__PURE__ */ jsx24(
2839
+ return /* @__PURE__ */ jsx25(SelectWrapper, { className, children: /* @__PURE__ */ jsx25(
2600
2840
  BaseSelect,
2601
2841
  {
2602
2842
  className: "time-format",
@@ -2604,14 +2844,14 @@ var TimeFormatSelect = ({
2604
2844
  onChange: handleChange,
2605
2845
  disabled,
2606
2846
  "aria-label": "Time format selection",
2607
- children: TIME_FORMAT_OPTIONS.map((option) => /* @__PURE__ */ jsx24("option", { value: option.value, children: option.label }, option.value))
2847
+ children: TIME_FORMAT_OPTIONS.map((option) => /* @__PURE__ */ jsx25("option", { value: option.value, children: option.label }, option.value))
2608
2848
  }
2609
2849
  ) });
2610
2850
  };
2611
2851
 
2612
2852
  // src/components/Track.tsx
2613
2853
  import styled23 from "styled-components";
2614
- import { jsx as jsx25, jsxs as jsxs11 } from "react/jsx-runtime";
2854
+ import { jsx as jsx26, jsxs as jsxs11 } from "react/jsx-runtime";
2615
2855
  var Container = styled23.div.attrs((props) => ({
2616
2856
  style: {
2617
2857
  height: `${props.$waveHeight * props.$numChannels + (props.$hasClipHeaders ? CLIP_HEADER_HEIGHT : 0)}px`
@@ -2677,7 +2917,7 @@ var Track = ({
2677
2917
  $hasClipHeaders: hasClipHeaders,
2678
2918
  $isSelected: isSelected,
2679
2919
  children: [
2680
- /* @__PURE__ */ jsx25(
2920
+ /* @__PURE__ */ jsx26(
2681
2921
  ControlsWrapper,
2682
2922
  {
2683
2923
  $controlWidth: show ? controlWidth : 0,
@@ -2685,7 +2925,7 @@ var Track = ({
2685
2925
  children: controls
2686
2926
  }
2687
2927
  ),
2688
- /* @__PURE__ */ jsx25(
2928
+ /* @__PURE__ */ jsx26(
2689
2929
  ChannelContainer,
2690
2930
  {
2691
2931
  $controlWidth: show ? controlWidth : 0,
@@ -2793,7 +3033,7 @@ var ButtonGroup = styled25.div`
2793
3033
  // src/components/TrackControls/CloseButton.tsx
2794
3034
  import styled26 from "styled-components";
2795
3035
  import { X as XIcon } from "@phosphor-icons/react";
2796
- import { jsx as jsx26 } from "react/jsx-runtime";
3036
+ import { jsx as jsx27 } from "react/jsx-runtime";
2797
3037
  var StyledCloseButton = styled26.button`
2798
3038
  position: absolute;
2799
3039
  left: 0;
@@ -2818,7 +3058,7 @@ var StyledCloseButton = styled26.button`
2818
3058
  var CloseButton = ({
2819
3059
  onClick,
2820
3060
  title = "Remove track"
2821
- }) => /* @__PURE__ */ jsx26(StyledCloseButton, { onClick, title, children: /* @__PURE__ */ jsx26(XIcon, { size: 12, weight: "bold" }) });
3061
+ }) => /* @__PURE__ */ jsx27(StyledCloseButton, { onClick, title, children: /* @__PURE__ */ jsx27(XIcon, { size: 12, weight: "bold" }) });
2822
3062
 
2823
3063
  // src/components/TrackControls/Controls.tsx
2824
3064
  import styled27 from "styled-components";
@@ -2854,23 +3094,23 @@ var Header = styled28.header`
2854
3094
 
2855
3095
  // src/components/TrackControls/VolumeDownIcon.tsx
2856
3096
  import { SpeakerLowIcon } from "@phosphor-icons/react";
2857
- import { jsx as jsx27 } from "react/jsx-runtime";
2858
- var VolumeDownIcon = (props) => /* @__PURE__ */ jsx27(SpeakerLowIcon, { weight: "light", ...props });
3097
+ import { jsx as jsx28 } from "react/jsx-runtime";
3098
+ var VolumeDownIcon = (props) => /* @__PURE__ */ jsx28(SpeakerLowIcon, { weight: "light", ...props });
2859
3099
 
2860
3100
  // src/components/TrackControls/VolumeUpIcon.tsx
2861
3101
  import { SpeakerHighIcon } from "@phosphor-icons/react";
2862
- import { jsx as jsx28 } from "react/jsx-runtime";
2863
- var VolumeUpIcon = (props) => /* @__PURE__ */ jsx28(SpeakerHighIcon, { weight: "light", ...props });
3102
+ import { jsx as jsx29 } from "react/jsx-runtime";
3103
+ var VolumeUpIcon = (props) => /* @__PURE__ */ jsx29(SpeakerHighIcon, { weight: "light", ...props });
2864
3104
 
2865
3105
  // src/components/TrackControls/TrashIcon.tsx
2866
3106
  import { TrashIcon as PhosphorTrashIcon } from "@phosphor-icons/react";
2867
- import { jsx as jsx29 } from "react/jsx-runtime";
2868
- var TrashIcon = (props) => /* @__PURE__ */ jsx29(PhosphorTrashIcon, { weight: "light", ...props });
3107
+ import { jsx as jsx30 } from "react/jsx-runtime";
3108
+ var TrashIcon = (props) => /* @__PURE__ */ jsx30(PhosphorTrashIcon, { weight: "light", ...props });
2869
3109
 
2870
3110
  // src/components/TrackControls/DotsIcon.tsx
2871
3111
  import { DotsThreeIcon } from "@phosphor-icons/react";
2872
- import { jsx as jsx30 } from "react/jsx-runtime";
2873
- var DotsIcon = (props) => /* @__PURE__ */ jsx30(DotsThreeIcon, { weight: "bold", ...props });
3112
+ import { jsx as jsx31 } from "react/jsx-runtime";
3113
+ var DotsIcon = (props) => /* @__PURE__ */ jsx31(DotsThreeIcon, { weight: "bold", ...props });
2874
3114
 
2875
3115
  // src/components/TrackControls/Slider.tsx
2876
3116
  import styled29 from "styled-components";
@@ -2938,10 +3178,10 @@ var SliderWrapper = styled30.label`
2938
3178
  `;
2939
3179
 
2940
3180
  // src/components/TrackMenu.tsx
2941
- import React15, { useState as useState6, useEffect as useEffect6, useRef as useRef7 } from "react";
3181
+ import React17, { useState as useState6, useEffect as useEffect8, useRef as useRef9 } from "react";
2942
3182
  import { createPortal } from "react-dom";
2943
3183
  import styled31 from "styled-components";
2944
- import { jsx as jsx31, jsxs as jsxs12 } from "react/jsx-runtime";
3184
+ import { jsx as jsx32, jsxs as jsxs12 } from "react/jsx-runtime";
2945
3185
  var MenuContainer = styled31.div`
2946
3186
  position: relative;
2947
3187
  display: inline-block;
@@ -2986,9 +3226,9 @@ var TrackMenu = ({
2986
3226
  const close = () => setOpen(false);
2987
3227
  const items = typeof itemsProp === "function" ? itemsProp(close) : itemsProp;
2988
3228
  const [dropdownPos, setDropdownPos] = useState6({ top: 0, left: 0 });
2989
- const buttonRef = useRef7(null);
2990
- const dropdownRef = useRef7(null);
2991
- useEffect6(() => {
3229
+ const buttonRef = useRef9(null);
3230
+ const dropdownRef = useRef9(null);
3231
+ useEffect8(() => {
2992
3232
  if (open && buttonRef.current) {
2993
3233
  const rect = buttonRef.current.getBoundingClientRect();
2994
3234
  setDropdownPos({
@@ -2997,7 +3237,7 @@ var TrackMenu = ({
2997
3237
  });
2998
3238
  }
2999
3239
  }, [open]);
3000
- useEffect6(() => {
3240
+ useEffect8(() => {
3001
3241
  if (!open) return;
3002
3242
  const handleClick = (e) => {
3003
3243
  const target = e.target;
@@ -3009,7 +3249,7 @@ var TrackMenu = ({
3009
3249
  return () => document.removeEventListener("mousedown", handleClick);
3010
3250
  }, [open]);
3011
3251
  return /* @__PURE__ */ jsxs12(MenuContainer, { children: [
3012
- /* @__PURE__ */ jsx31(
3252
+ /* @__PURE__ */ jsx32(
3013
3253
  MenuButton,
3014
3254
  {
3015
3255
  ref: buttonRef,
@@ -3020,19 +3260,19 @@ var TrackMenu = ({
3020
3260
  onMouseDown: (e) => e.stopPropagation(),
3021
3261
  title: "Track menu",
3022
3262
  "aria-label": "Track menu",
3023
- children: /* @__PURE__ */ jsx31(DotsIcon, { size: 16 })
3263
+ children: /* @__PURE__ */ jsx32(DotsIcon, { size: 16 })
3024
3264
  }
3025
3265
  ),
3026
3266
  open && typeof document !== "undefined" && createPortal(
3027
- /* @__PURE__ */ jsx31(
3267
+ /* @__PURE__ */ jsx32(
3028
3268
  Dropdown,
3029
3269
  {
3030
3270
  ref: dropdownRef,
3031
3271
  $top: dropdownPos.top,
3032
3272
  $left: dropdownPos.left,
3033
3273
  onMouseDown: (e) => e.stopPropagation(),
3034
- children: items.map((item, index) => /* @__PURE__ */ jsxs12(React15.Fragment, { children: [
3035
- index > 0 && /* @__PURE__ */ jsx31(Divider, {}),
3274
+ children: items.map((item, index) => /* @__PURE__ */ jsxs12(React17.Fragment, { children: [
3275
+ index > 0 && /* @__PURE__ */ jsx32(Divider, {}),
3036
3276
  item.content
3037
3277
  ] }, item.id))
3038
3278
  }
@@ -3072,6 +3312,7 @@ export {
3072
3312
  InlineLabel,
3073
3313
  LoopRegion,
3074
3314
  LoopRegionMarkers,
3315
+ MAX_CANVAS_WIDTH,
3075
3316
  MasterVolumeControl,
3076
3317
  Playhead,
3077
3318
  PlayheadWithMarker,
@@ -3080,6 +3321,7 @@ export {
3080
3321
  PlaylistInfoContext,
3081
3322
  PlayoutProvider,
3082
3323
  ScreenReaderOnly,
3324
+ ScrollViewportProvider,
3083
3325
  Selection,
3084
3326
  SelectionTimeInputs,
3085
3327
  Slider,
@@ -3115,6 +3357,8 @@ export {
3115
3357
  usePlaylistInfo,
3116
3358
  usePlayoutStatus,
3117
3359
  usePlayoutStatusUpdate,
3360
+ useScrollViewport,
3361
+ useScrollViewportSelector,
3118
3362
  useTheme2 as useTheme,
3119
3363
  useTrackControls,
3120
3364
  waveformColorToCss