box-content-preview 3.90.0 → 3.91.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/lib/index.js CHANGED
@@ -21,6 +21,7 @@
21
21
  * and limitations under the license.
22
22
  */
23
23
  import { decode as __WEBPACK_EXTERNAL_MODULE_box_ui_elements_es_utils_keys_d752524f_decode__ } from "box-ui-elements/es/utils/keys";
24
+ import { createPortal as __WEBPACK_EXTERNAL_MODULE_react_dom_7dac9eee_createPortal__ } from "react-dom";
24
25
  import * as __WEBPACK_EXTERNAL_MODULE_react__ from "react";
25
26
  import { default as __WEBPACK_EXTERNAL_MODULE_axios_default__ } from "axios";
26
27
  import { bdlBoxBlue as __WEBPACK_EXTERNAL_MODULE_box_ui_elements_es_styles_variables_c815ce80_bdlBoxBlue__, bdlGray65 as __WEBPACK_EXTERNAL_MODULE_box_ui_elements_es_styles_variables_c815ce80_bdlGray65__, bdlGreenLight as __WEBPACK_EXTERNAL_MODULE_box_ui_elements_es_styles_variables_c815ce80_bdlGreenLight__, bdlGrimace as __WEBPACK_EXTERNAL_MODULE_box_ui_elements_es_styles_variables_c815ce80_bdlGrimace__, bdlWatermelonRed as __WEBPACK_EXTERNAL_MODULE_box_ui_elements_es_styles_variables_c815ce80_bdlWatermelonRed__, bdlYellorange as __WEBPACK_EXTERNAL_MODULE_box_ui_elements_es_styles_variables_c815ce80_bdlYellorange__, bdlYellow as __WEBPACK_EXTERNAL_MODULE_box_ui_elements_es_styles_variables_c815ce80_bdlYellow__, white as __WEBPACK_EXTERNAL_MODULE_box_ui_elements_es_styles_variables_c815ce80_white__ } from "box-ui-elements/es/styles/variables";
@@ -1017,7 +1018,7 @@ function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e =
1017
1018
  const CLIENT_NAME = "box-content-preview"; // eslint-disable-line no-undef
1018
1019
  const CLIENT_NAME_KEY = 'box_client_name';
1019
1020
  const CLIENT_VERSION_KEY = 'box_client_version';
1020
- const CLIENT_VERSION = "3.90.0"; // eslint-disable-line no-undef
1021
+ const CLIENT_VERSION = "3.91.0"; // eslint-disable-line no-undef
1021
1022
  const HEADER_CLIENT_NAME = 'X-Box-Client-Name';
1022
1023
  const HEADER_CLIENT_VERSION = 'X-Box-Client-Version';
1023
1024
  const PROMISE_MAP = {};
@@ -4487,7 +4488,7 @@ function getPdfjsWorkerSrc() {
4487
4488
 
4488
4489
  /***/ },
4489
4490
 
4490
- /***/ 6180
4491
+ /***/ 2668
4491
4492
  (__unused_webpack_module, __webpack_exports__, __webpack_require__) {
4492
4493
 
4493
4494
 
@@ -4621,6 +4622,7 @@ function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e =
4621
4622
  /** Visible window: start/end times, pixels-per-second, and the current zoom. */
4622
4623
  function createWaveformViewport({
4623
4624
  durationSec,
4625
+ gutterPx = 0,
4624
4626
  heightPx,
4625
4627
  maxZoom,
4626
4628
  scrollLeftPx,
@@ -4630,11 +4632,16 @@ function createWaveformViewport({
4630
4632
  const zoom = Number.isFinite(zoomLevel) && zoomLevel > 0 ? zoomLevel : constants/* WAVEFORM_ZOOM_MIN */.LW;
4631
4633
  const viewDurationSec = durationSec > 0 ? durationSec / zoom : 0; // seconds visible at this zoom
4632
4634
  const pixelsPerSecond = viewDurationSec > 0 && widthPx > 0 ? widthPx / viewDurationSec : 0;
4633
- const maxStartSec = Math.max(0, durationSec - viewDurationSec); // last start that still fills the window
4634
- const startSec = pixelsPerSecond > 0 ? Math.min(maxStartSec, Math.max(0, scrollLeftPx / pixelsPerSecond)) : 0;
4635
+ const leadingGutterPx = Number.isFinite(gutterPx) && gutterPx > 0 ? gutterPx : 0;
4636
+ const unclampedStartSec = pixelsPerSecond > 0 ? (scrollLeftPx - leadingGutterPx) / pixelsPerSecond : 0;
4637
+ const gutterDurationSec = pixelsPerSecond > 0 ? leadingGutterPx / pixelsPerSecond : 0;
4638
+ const latestWindowStartSec = leadingGutterPx > 0 ? durationSec + gutterDurationSec - viewDurationSec : Math.max(0, durationSec - viewDurationSec);
4639
+ const earliestWindowStartSec = leadingGutterPx > 0 ? -gutterDurationSec : 0;
4640
+ const startSec = pixelsPerSecond > 0 ? Math.min(latestWindowStartSec, Math.max(earliestWindowStartSec, unclampedStartSec)) : 0;
4635
4641
  return {
4636
4642
  durationSec,
4637
4643
  endSec: startSec + viewDurationSec,
4644
+ gutterPx: leadingGutterPx,
4638
4645
  heightPx,
4639
4646
  maxZoom,
4640
4647
  pixelsPerSecond,
@@ -4654,7 +4661,7 @@ function getViewportAtScroll(viewport, scrollLeftPx) {
4654
4661
 
4655
4662
  /** True when the visible window has not moved — skip a React state update. */
4656
4663
  function viewportEquals(prev, next) {
4657
- return !!prev && prev.durationSec === next.durationSec && prev.endSec === next.endSec && prev.maxZoom === next.maxZoom && prev.scrollLeftPx === next.scrollLeftPx && prev.startSec === next.startSec && prev.widthPx === next.widthPx && prev.zoomLevel === next.zoomLevel;
4664
+ return !!prev && prev.durationSec === next.durationSec && prev.endSec === next.endSec && prev.gutterPx === next.gutterPx && prev.maxZoom === next.maxZoom && prev.scrollLeftPx === next.scrollLeftPx && prev.startSec === next.startSec && prev.widthPx === next.widthPx && prev.zoomLevel === next.zoomLevel;
4658
4665
  }
4659
4666
 
4660
4667
  /** How many CSS pixels from the left of the visible window this time sits. */
@@ -4701,17 +4708,34 @@ function clampWaveformZoom(zoomLevel, maxZoom = constants/* WAVEFORM_ZOOM_MIN */
4701
4708
  /** WaveSurfer zoom density. 0 = fit the whole file in the view. */
4702
4709
  function getZoomedPixelsPerSecond({
4703
4710
  durationSec,
4711
+ isTape = false,
4704
4712
  maxZoom = constants/* WAVEFORM_ZOOM_MIN */.LW,
4705
4713
  viewWidthPx,
4706
4714
  zoomLevel
4707
4715
  }) {
4708
4716
  const zoom = clampWaveformZoom(zoomLevel, maxZoom);
4709
- if (zoom <= constants/* WAVEFORM_ZOOM_MIN */.LW || !(durationSec > 0) || !(viewWidthPx > 0)) {
4717
+ if (!(durationSec > 0) || !(viewWidthPx > 0)) {
4718
+ return 0;
4719
+ }
4720
+ if (!isTape && zoom <= constants/* WAVEFORM_ZOOM_MIN */.LW) {
4710
4721
  return 0;
4711
4722
  }
4712
4723
  return viewWidthPx / durationSec * zoom;
4713
4724
  }
4714
4725
 
4726
+ /** Half-view empty space so t=0 and duration can sit under the center pin. */
4727
+ function getTapeGutterPx(widthPx) {
4728
+ return widthPx > 0 ? widthPx / 2 : 0;
4729
+ }
4730
+
4731
+ /** Tape first paint: 10s on screen, or 1× when the file is shorter than that. */
4732
+ function getTapeDefaultZoom(durationSec, maxZoom = constants/* WAVEFORM_ZOOM_MIN */.LW) {
4733
+ if (!(durationSec > 0)) {
4734
+ return constants/* WAVEFORM_ZOOM_MIN */.LW;
4735
+ }
4736
+ return clampWaveformZoom(durationSec / constants/* WAVEFORM_TAPE_DEFAULT_WINDOW_SEC */.Av, maxZoom);
4737
+ }
4738
+
4715
4739
  /** Map zoom (1…max) onto the 0–100 slider. */
4716
4740
  function sliderValueFromZoom(zoomLevel, maxZoom = constants/* WAVEFORM_ZOOM_MIN */.LW) {
4717
4741
  const max = Math.max(constants/* WAVEFORM_ZOOM_MIN */.LW, maxZoom);
@@ -4732,9 +4756,10 @@ function zoomFromSliderValue(value, maxZoom = constants/* WAVEFORM_ZOOM_MIN */.L
4732
4756
  return clampWaveformZoom(constants/* WAVEFORM_ZOOM_MIN */.LW + t * (max - constants/* WAVEFORM_ZOOM_MIN */.LW), max);
4733
4757
  }
4734
4758
 
4735
- /** Max scroll that still shows a full window (no overscroll). */
4759
+ /** Max scroll that still shows a full window (no overscroll). Gutters add empty lead/trail. */
4736
4760
  function maxScrollLeft(viewport) {
4737
- return Math.max(0, viewport.durationSec * viewport.pixelsPerSecond - viewport.widthPx);
4761
+ const gutterPx = viewport.gutterPx || 0;
4762
+ return Math.max(0, viewport.durationSec * viewport.pixelsPerSecond + 2 * gutterPx - viewport.widthPx);
4738
4763
  }
4739
4764
 
4740
4765
  /** Scroll offset that still shows a full window (no overscroll). */
@@ -4747,7 +4772,7 @@ function clampScrollLeft(scrollLeftPx, viewport) {
4747
4772
 
4748
4773
  /** Center this time in the view, clamped so the window still fills the canvas. */
4749
4774
  function getCenteredScrollLeft(timeSec, viewport) {
4750
- return clampScrollLeft(timeSec * viewport.pixelsPerSecond - viewport.widthPx / 2, viewport);
4775
+ return clampScrollLeft(timeSec * viewport.pixelsPerSecond + (viewport.gutterPx || 0) - viewport.widthPx / 2, viewport);
4751
4776
  }
4752
4777
 
4753
4778
  /**
@@ -4755,10 +4780,28 @@ function getCenteredScrollLeft(timeSec, viewport) {
4755
4780
  * No-op at fit-to-width or when that time is already visible.
4756
4781
  */
4757
4782
  function getSeekCameraAction({
4783
+ cameraMode = 'desktop',
4758
4784
  timeSec,
4759
4785
  viewport
4760
4786
  }) {
4761
- if (viewport.zoomLevel <= constants/* WAVEFORM_ZOOM_MIN */.LW || !(viewport.widthPx > 0) || !(viewport.pixelsPerSecond > 0)) {
4787
+ if (!(viewport.widthPx > 0) || !(viewport.pixelsPerSecond > 0)) {
4788
+ return {
4789
+ type: 'none'
4790
+ };
4791
+ }
4792
+ if (cameraMode === 'tape') {
4793
+ const scrollLeftPx = getCenteredScrollLeft(timeSec, viewport);
4794
+ if (Math.abs(scrollLeftPx - viewport.scrollLeftPx) < 1) {
4795
+ return {
4796
+ type: 'none'
4797
+ };
4798
+ }
4799
+ return {
4800
+ type: 'jump',
4801
+ scrollLeftPx
4802
+ };
4803
+ }
4804
+ if (viewport.zoomLevel <= constants/* WAVEFORM_ZOOM_MIN */.LW) {
4762
4805
  return {
4763
4806
  type: 'none'
4764
4807
  };
@@ -4774,6 +4817,23 @@ function getSeekCameraAction({
4774
4817
  };
4775
4818
  }
4776
4819
 
4820
+ /** Always keep this time under the center pin. Reuses followRight so the camera hook can pin. */
4821
+ function getTapeCameraAction({
4822
+ timeSec,
4823
+ viewport
4824
+ }) {
4825
+ if (!(viewport.widthPx > 0) || !(viewport.pixelsPerSecond > 0)) {
4826
+ return {
4827
+ type: 'none'
4828
+ };
4829
+ }
4830
+ return {
4831
+ isPlayheadPinned: true,
4832
+ scrollLeftPx: getCenteredScrollLeft(timeSec, viewport),
4833
+ type: 'followRight'
4834
+ };
4835
+ }
4836
+
4777
4837
  /** Follow inset in CSS px, capped at one third of the view so a narrow player still has room. */
4778
4838
  function getFollowInsetPx(widthPx, insetPx = constants/* WAVEFORM_FOLLOW_INSET_PX */.NM) {
4779
4839
  if (!(widthPx > 0)) {
@@ -4791,6 +4851,11 @@ function getPinnedPlayheadLeft(widthPx, insetPx = constants/* WAVEFORM_FOLLOW_IN
4791
4851
  return `${(widthPx - inset) / widthPx * 100}%`;
4792
4852
  }
4793
4853
 
4854
+ /** CSS left for a playhead locked to the center of the view. */
4855
+ function getTapePinnedPlayheadLeft() {
4856
+ return '50%';
4857
+ }
4858
+
4794
4859
  /** CSS left % of the playhead from the left of the visible window. */
4795
4860
  function timeLeftPercent(timeSec, durationSec, viewport) {
4796
4861
  if (viewport.widthPx > 0 && viewport.pixelsPerSecond > 0) {
@@ -4805,12 +4870,19 @@ function timeLeftPercent(timeSec, durationSec, viewport) {
4805
4870
  * Off-screen at play start jumps in; off-screen right while playing keeps following.
4806
4871
  */
4807
4872
  function getPlayheadCameraAction({
4873
+ cameraMode = 'desktop',
4808
4874
  insetPx = constants/* WAVEFORM_FOLLOW_INSET_PX */.NM,
4809
4875
  isPlaying,
4810
4876
  playJustStarted,
4811
4877
  timeSec,
4812
4878
  viewport
4813
4879
  }) {
4880
+ if (cameraMode === 'tape') {
4881
+ return getTapeCameraAction({
4882
+ timeSec,
4883
+ viewport
4884
+ });
4885
+ }
4814
4886
  if (!isPlaying || viewport.zoomLevel <= constants/* WAVEFORM_ZOOM_MIN */.LW || !(viewport.widthPx > 0) || !(viewport.pixelsPerSecond > 0)) {
4815
4887
  return {
4816
4888
  type: 'none'
@@ -4850,6 +4922,46 @@ function getPlayheadCameraAction({
4850
4922
  type: 'none'
4851
4923
  };
4852
4924
  }
4925
+ ;// ./src/lib/viewers/media/waveform/useTapeWaveform.ts
4926
+
4927
+
4928
+ function isIPadNavigator(tapeNavigator) {
4929
+ return /iPad/i.test(tapeNavigator.userAgent || '') || tapeNavigator.platform === 'MacIntel' && (tapeNavigator.maxTouchPoints || 0) > 1;
4930
+ }
4931
+ function isCoarsePrimaryPointer(tapeWindow) {
4932
+ return typeof tapeWindow.matchMedia === 'function' && tapeWindow.matchMedia(constants/* TAPE_POINTER_MEDIA_QUERY */.Le).matches;
4933
+ }
4934
+
4935
+ /**
4936
+ * Tape vs desktop camera: coarse primary pointer (finger) or iPad
4937
+ * (including iPadOS reporting itself as Mac). Not width, not hasTouch,
4938
+ * not Browser.isMobile().
4939
+ */
4940
+ function isTapeWaveformInput(tapeWindow = window) {
4941
+ return isCoarsePrimaryPointer(tapeWindow) || isIPadNavigator(tapeWindow.navigator);
4942
+ }
4943
+
4944
+ /** Subscribe to the pointer media query; re-read the iPad heuristic on change. */
4945
+ function useTapeWaveform() {
4946
+ const [isTape, setIsTape] = (0,external_react_.useState)(() => typeof window !== 'undefined' && isTapeWaveformInput(window));
4947
+ (0,external_react_.useEffect)(() => {
4948
+ if (typeof window.matchMedia !== 'function') {
4949
+ setIsTape(isTapeWaveformInput(window));
4950
+ return undefined;
4951
+ }
4952
+ const mediaQuery = window.matchMedia(constants/* TAPE_POINTER_MEDIA_QUERY */.Le);
4953
+ const sync = () => {
4954
+ setIsTape(isTapeWaveformInput(window));
4955
+ };
4956
+ sync();
4957
+ mediaQuery.addEventListener('change', sync);
4958
+ return () => mediaQuery.removeEventListener('change', sync);
4959
+ }, []);
4960
+ return isTape;
4961
+ }
4962
+ // EXTERNAL MODULE: ./node_modules/classnames/index.js
4963
+ var classnames = __webpack_require__(2485);
4964
+ var classnames_default = /*#__PURE__*/__webpack_require__.n(classnames);
4853
4965
  // EXTERNAL MODULE: ./src/lib/viewers/controls/media/markers/helpers/useDismissableMarkerSelection.ts
4854
4966
  var useDismissableMarkerSelection = __webpack_require__(2378);
4855
4967
  // EXTERNAL MODULE: ./src/lib/viewers/controls/media/markers/helpers/buildClusters.ts
@@ -4869,6 +4981,7 @@ var utils = __webpack_require__(5346);
4869
4981
 
4870
4982
 
4871
4983
 
4984
+
4872
4985
  const WAVEFORM_MARKER_SIZE_PX = 20;
4873
4986
  function hasMappedWindow(viewport) {
4874
4987
  return !!viewport && Number.isFinite(viewport.startSec) && viewport.endSec > viewport.startSec;
@@ -4907,11 +5020,12 @@ function clustersByExactTime(markers, durationSec) {
4907
5020
  function WaveformCommentMarkers({
4908
5021
  commentMarkers,
4909
5022
  durationSec,
5023
+ isTape = false,
4910
5024
  onCommentMarkerClick,
4911
5025
  selectedId: hostSelectedId = null,
4912
5026
  viewport = null
4913
5027
  }) {
4914
- const trackRef = (0,external_react_.useRef)(null);
5028
+ const trackRef = (0,external_react_.useRef)(null); // width source for clustering overlapping badges
4915
5029
  const {
4916
5030
  containerRef,
4917
5031
  selectMarker,
@@ -4920,7 +5034,7 @@ function WaveformCommentMarkers({
4920
5034
  const [trackWidth, setTrackWidth] = (0,external_react_.useState)(0);
4921
5035
  const canShowTrack = durationSec > 0 && commentMarkers.length > 0;
4922
5036
  const zoomLevel = viewport?.zoomLevel ?? constants/* WAVEFORM_ZOOM_MIN */.LW;
4923
- const isZoomed = hasMappedWindow(viewport) && viewport.zoomLevel > constants/* WAVEFORM_ZOOM_MIN */.LW;
5037
+ const alignsMarkersToVisibleWindow = hasMappedWindow(viewport) && (viewport.zoomLevel > constants/* WAVEFORM_ZOOM_MIN */.LW || isTape);
4924
5038
  (0,external_react_.useLayoutEffect)(() => {
4925
5039
  if (!canShowTrack) {
4926
5040
  setTrackWidth(0);
@@ -4956,7 +5070,10 @@ function WaveformCommentMarkers({
4956
5070
  }
4957
5071
  return /*#__PURE__*/external_react_["default"].createElement("div", {
4958
5072
  ref: containerRef,
4959
- className: `bp-WaveformCommentMarkers${isZoomed ? ' bp-WaveformCommentMarkers--zoomed' : ''}`,
5073
+ className: classnames_default()('bp-WaveformCommentMarkers', {
5074
+ 'bp-WaveformCommentMarkers--tape': isTape,
5075
+ 'bp-WaveformCommentMarkers--zoomed': alignsMarkersToVisibleWindow
5076
+ }),
4960
5077
  "data-testid": "bp-waveform-comment-markers"
4961
5078
  }, /*#__PURE__*/external_react_["default"].createElement("div", {
4962
5079
  ref: trackRef,
@@ -4965,7 +5082,10 @@ function WaveformCommentMarkers({
4965
5082
  const marker = cluster.markers[0];
4966
5083
  const isGroup = cluster.markers.length > 1;
4967
5084
  const isSelected = cluster.markers.some(entry => entry.id === selectedId);
4968
- const className = `bp-WaveformCommentMarkers-marker${isSelected ? ' bp-WaveformCommentMarkers-marker--selected' : ''}${isGroup ? ' bp-WaveformCommentMarkers-marker--group' : ''}`;
5085
+ const className = classnames_default()('bp-WaveformCommentMarkers-marker', {
5086
+ 'bp-WaveformCommentMarkers-marker--group': isGroup,
5087
+ 'bp-WaveformCommentMarkers-marker--selected': isSelected
5088
+ });
4969
5089
  const left = `${markerLeftPercent(marker.time, durationSec, viewport)}%`;
4970
5090
  if (isGroup) {
4971
5091
  return /*#__PURE__*/external_react_["default"].createElement("div", {
@@ -5003,6 +5123,8 @@ function WaveformCommentMarkers({
5003
5123
  }));
5004
5124
  })));
5005
5125
  }
5126
+ ;// external "react-dom"
5127
+
5006
5128
  ;// ./node_modules/wavesurfer.js/dist/wavesurfer.esm.js
5007
5129
  function t(t,e,i,n){return new(i||(i=Promise))((function(s,r){function o(t){try{l(n.next(t))}catch(t){r(t)}}function a(t){try{l(n.throw(t))}catch(t){r(t)}}function l(t){var e;t.done?s(t.value):(e=t.value,e instanceof i?e:new i((function(t){t(e)}))).then(o,a)}l((n=n.apply(t,e||[])).next())}))}"function"==typeof SuppressedError&&SuppressedError;class e{constructor(){this.listeners={}}on(t,e,i){if(this.listeners[t]||(this.listeners[t]=new Set),null==i?void 0:i.once){const i=(...n)=>{this.un(t,i),e(...n)};return this.listeners[t].add(i),()=>this.un(t,i)}return this.listeners[t].add(e),()=>this.un(t,e)}un(t,e){var i;null===(i=this.listeners[t])||void 0===i||i.delete(e)}once(t,e){return this.on(t,e,{once:!0})}unAll(){this.listeners={}}emit(t,...e){this.listeners[t]&&this.listeners[t].forEach((t=>t(...e)))}}const i={decode:function(e,i){return t(this,void 0,void 0,(function*(){const t=new AudioContext({sampleRate:i});try{return yield t.decodeAudioData(e)}finally{"closed"!==t.state&&(yield t.close().catch((()=>{})))}}))},createBuffer:function(t,e){if(!t||0===t.length)throw new Error("channelData must be a non-empty array");if(e<=0)throw new Error("duration must be greater than 0");if("number"==typeof t[0]&&(t=[t]),!t[0]||0===t[0].length)throw new Error("channelData must contain non-empty channel arrays");!function(t){const e=t[0];if(e.some((t=>t>1||t<-1))){const i=e.length;let n=0;for(let t=0;t<i;t++){const i=Math.abs(e[t]);i>n&&(n=i)}for(const e of t)for(let t=0;t<i;t++)e[t]/=n}}(t);const i=t.map((t=>t instanceof Float32Array?t:Float32Array.from(t)));return{duration:e,length:i[0].length,sampleRate:i[0].length/e,numberOfChannels:i.length,getChannelData:t=>{const e=i[t];if(!e)throw new Error(`Channel ${t} not found`);return e},copyFromChannel:AudioBuffer.prototype.copyFromChannel,copyToChannel:AudioBuffer.prototype.copyToChannel}}};function n(t,e){const i=e.xmlns?document.createElementNS(e.xmlns,t):document.createElement(t);for(const[t,s]of Object.entries(e))if("children"===t&&s)for(const[t,e]of Object.entries(s))e instanceof Node?i.appendChild(e):"string"==typeof e?i.appendChild(document.createTextNode(e)):i.appendChild(n(t,e));else"style"===t?Object.assign(i.style,s):"textContent"===t?i.textContent=s:i.setAttribute(t,s.toString());return i}function s(t,e,i){const s=n(t,e||{});return null==i||i.appendChild(s),s}function r(t){return t instanceof HTMLElement||"object"==typeof t&&null!==t&&t.nodeType===Node.ELEMENT_NODE&&"object"==typeof t.style}var o=Object.freeze({__proto__:null,createElement:s,default:s,isHTMLElement:r});const a={fetchBlob:function(e,i,n){return t(this,void 0,void 0,(function*(){var s;const r=yield fetch(e,n);if(r.status>=400)throw new Error(`Failed to fetch ${e}: ${r.status} (${r.statusText})`);return function(e,i,n){t(this,void 0,void 0,(function*(){var t;if(!e.body||!e.headers)return;const s=e.body.getReader(),r=Number(e.headers.get("Content-Length"))||0;let o=0;const a=()=>{s.cancel()};if(n){if(n.aborted)return void s.cancel();n.addEventListener("abort",a,{once:!0})}try{for(;;){const e=yield s.read();if(e.done)break;if(o+=(null===(t=e.value)||void 0===t?void 0:t.length)||0,r>0){const t=Math.round(o/r*100);i(t)}}}catch(t){if(t instanceof DOMException&&"AbortError"===t.name)return;console.warn("Progress tracking error:",t)}finally{n&&n.removeEventListener("abort",a)}}))}(r.clone(),i,null!==(s=null==n?void 0:n.signal)&&void 0!==s?s:void 0),r.blob()}))}};function l(t){let e=t;const i=new Set;return{get value(){return e},set(t){Object.is(e,t)||(e=t,i.forEach((t=>t(e))))},update(t){this.set(t(e))},subscribe:t=>(i.add(t),()=>i.delete(t))}}function h(t,e){const i=l(t());return e.forEach((e=>e.subscribe((()=>{const e=t();Object.is(i.value,e)||i.set(e)})))),{get value(){return i.value},subscribe:t=>i.subscribe(t)}}function c(t,e){let i;const n=()=>{i&&(i(),i=void 0),i=t()},s=e.map((t=>t.subscribe(n)));return n(),()=>{i&&(i(),i=void 0),s.forEach((t=>t()))}}class u extends e{get isPlayingSignal(){return this._isPlaying}get currentTimeSignal(){return this._currentTime}get durationSignal(){return this._duration}get volumeSignal(){return this._volume}get mutedSignal(){return this._muted}get playbackRateSignal(){return this._playbackRate}get seekingSignal(){return this._seeking}constructor(t){super(),this.isExternalMedia=!1,this._ownBlobUrl=null,this.reactiveMediaEventCleanups=[],t.media?(this.media=t.media,this.isExternalMedia=!0):this.media=document.createElement("audio"),this._isPlaying=l(!1),this._currentTime=l(0),this._duration=l(0),this._volume=l(this.media.volume),this._muted=l(this.media.muted),this._playbackRate=l(this.media.playbackRate||1),this._seeking=l(!1),this.setupReactiveMediaEvents(),t.mediaControls&&(this.media.controls=!0),t.autoplay&&(this.media.autoplay=!0),null!=t.playbackRate&&this.onMediaEvent("canplay",(()=>{null!=t.playbackRate&&(this.media.playbackRate=t.playbackRate)}),{once:!0})}setupReactiveMediaEvents(){this.reactiveMediaEventCleanups.push(this.onMediaEvent("play",(()=>{this._isPlaying.set(!0)}))),this.reactiveMediaEventCleanups.push(this.onMediaEvent("pause",(()=>{this._isPlaying.set(!1)}))),this.reactiveMediaEventCleanups.push(this.onMediaEvent("ended",(()=>{this._isPlaying.set(!1)}))),this.reactiveMediaEventCleanups.push(this.onMediaEvent("timeupdate",(()=>{this._currentTime.set(this.media.currentTime)}))),this.reactiveMediaEventCleanups.push(this.onMediaEvent("durationchange",(()=>{this._duration.set(this.media.duration||0)}))),this.reactiveMediaEventCleanups.push(this.onMediaEvent("loadedmetadata",(()=>{this._duration.set(this.media.duration||0)}))),this.reactiveMediaEventCleanups.push(this.onMediaEvent("seeking",(()=>{this._seeking.set(!0)}))),this.reactiveMediaEventCleanups.push(this.onMediaEvent("seeked",(()=>{this._seeking.set(!1)}))),this.reactiveMediaEventCleanups.push(this.onMediaEvent("volumechange",(()=>{this._volume.set(this.media.volume),this._muted.set(this.media.muted)}))),this.reactiveMediaEventCleanups.push(this.onMediaEvent("ratechange",(()=>{this._playbackRate.set(this.media.playbackRate)})))}onMediaEvent(t,e,i){return this.media.addEventListener(t,e,i),()=>this.media.removeEventListener(t,e,i)}getSrc(){return this.media.currentSrc||this.media.src||""}revokeSrc(){this._ownBlobUrl&&(URL.revokeObjectURL(this._ownBlobUrl),this._ownBlobUrl=null)}canPlayType(t){return""!==this.media.canPlayType(t)}setSrc(t,e){const i=this.getSrc();if(t&&i===t)return;this.revokeSrc();const n=e instanceof Blob&&(this.canPlayType(e.type)||!t)?URL.createObjectURL(e):t;if(n!==t&&(this._ownBlobUrl=n),i&&this.media.removeAttribute("src"),n||t)try{this.media.src=n}catch(e){this.media.src=t}}destroy(){this.reactiveMediaEventCleanups.forEach((t=>t())),this.reactiveMediaEventCleanups=[],this.revokeSrc(),this.unAll(),this.isExternalMedia||(this.media.pause(),this.media.removeAttribute("src"),this.media.load(),this.media.remove())}setMediaElement(t){this.reactiveMediaEventCleanups.forEach((t=>t())),this.reactiveMediaEventCleanups=[],this.media=t,this.setupReactiveMediaEvents()}play(){return t(this,void 0,void 0,(function*(){try{return yield this.media.play()}catch(t){if(t instanceof DOMException&&"AbortError"===t.name)return;throw t}}))}pause(){this.media.pause()}isPlaying(){return!this.media.paused&&!this.media.ended}setTime(t){this.media.currentTime=Math.max(0,Math.min(t,this.getDuration()))}getDuration(){return this.media.duration}getCurrentTime(){return this.media.currentTime}getVolume(){return this.media.volume}setVolume(t){this.media.volume=t}getMuted(){return this.media.muted}setMuted(t){this.media.muted=t}getPlaybackRate(){return this.media.playbackRate}isSeeking(){return this.media.seeking}setPlaybackRate(t,e){null!=e&&(this.media.preservesPitch=e),this.media.playbackRate=t}getMediaElement(){return this.media}setSinkId(t){return this.media.setSinkId(t)}}function d({maxTop:t,maxBottom:e,halfHeight:i,vScale:n,barMinHeight:s=0,barAlign:r}){let o=Math.round(t*i*n);let a=o+Math.round(e*i*n)||1;return a<s&&(a=s,r||(o=a/2)),{topHeight:o,totalHeight:a}}function p({barAlign:t,halfHeight:e,topHeight:i,totalHeight:n,canvasHeight:s}){return"top"===t?0:"bottom"===t?s-n:e-i}function m(t,e,i){const n=e-t.left,s=i-t.top;return[n/t.width,s/t.height]}function g(t){return Boolean(t.barWidth||t.barGap||t.barAlign)}function f(t,e){if(!g(e))return t;const i=e.barWidth||.5,n=i+(e.barGap||i/2);return 0===n?t:Math.floor(t/n)*n}function v({scrollLeft:t,totalWidth:e,numCanvases:i}){if(0===e)return[0];const n=t/e,s=Math.floor(n*i);return[s-1,s,s+1]}function b(t){const e=t._cleanup;"function"==typeof e&&e()}function y(t){const e=l({scrollLeft:t.scrollLeft,scrollWidth:t.scrollWidth,clientWidth:t.clientWidth}),i=h((()=>function(t){const{scrollLeft:e,scrollWidth:i,clientWidth:n}=t;if(0===i)return{startX:0,endX:1};const s=e/i,r=(e+n)/i;return{startX:Math.max(0,Math.min(1,s)),endX:Math.max(0,Math.min(1,r))}}(e.value)),[e]),n=h((()=>function(t){return{left:t.scrollLeft,right:t.scrollLeft+t.clientWidth}}(e.value)),[e]),s=()=>{e.set({scrollLeft:t.scrollLeft,scrollWidth:t.scrollWidth,clientWidth:t.clientWidth})};t.addEventListener("scroll",s,{passive:!0});return{scrollData:e,percentages:i,bounds:n,cleanup:()=>{t.removeEventListener("scroll",s),b(e)}}}class C extends e{constructor(t,e){super(),this.timeouts=[],this.isScrollable=!1,this.audioData=null,this.resizeObserver=null,this.lastContainerWidth=0,this.isDragging=!1,this.subscriptions=[],this.unsubscribeOnScroll=[],this.dragStream=null,this.scrollStream=null,this.containerInlinePadding=0,this.onClickWrapper=t=>{const e=this.wrapper.getBoundingClientRect(),[i,n]=m(e,t.clientX,t.clientY);this.emit("click",i,n)},this.onDblClickWrapper=t=>{const e=this.wrapper.getBoundingClientRect(),[i,n]=m(e,t.clientX,t.clientY);this.emit("dblclick",i,n)},this.subscriptions=[],this.options=t;const i=this.parentFromOptionsContainer(t.container);this.parent=i;const[n,s]=this.initHtml();i.appendChild(n),this.container=n,this.scrollContainer=s.querySelector(".scroll"),this.wrapper=s.querySelector(".wrapper"),this.canvasWrapper=s.querySelector(".canvases"),this.progressWrapper=s.querySelector(".progress"),this.cursor=s.querySelector(".cursor"),this.calculateInlinePadding(),e&&s.appendChild(e),this.initEvents()}parentFromOptionsContainer(t){let e;if("string"==typeof t?e=document.querySelector(t):r(t)&&(e=t),!e)throw new Error("Container not found");return e}initEvents(){this.wrapper.addEventListener("click",this.onClickWrapper),this.wrapper.addEventListener("dblclick",this.onDblClickWrapper),!0!==this.options.dragToSeek&&"object"!=typeof this.options.dragToSeek||this.initDrag(),this.scrollStream=y(this.scrollContainer);const t=c((()=>{const{startX:t,endX:e}=this.scrollStream.percentages.value,{left:i,right:n}=this.scrollStream.bounds.value;this.emit("scroll",t,e,i,n)}),[this.scrollStream.percentages,this.scrollStream.bounds]);if(this.subscriptions.push(t),"function"==typeof ResizeObserver){const t=this.createDelay(100);this.resizeObserver=new ResizeObserver((()=>{t().then((()=>this.onContainerResize())).catch((()=>{}))})),this.resizeObserver.observe(this.scrollContainer)}}onContainerResize(){const t=this.parent.clientWidth;this.calculateInlinePadding(),t===this.lastContainerWidth&&"auto"!==this.options.height||(this.lastContainerWidth=t,this.reRender(),this.emit("resize"))}initDrag(){if(this.dragStream)return;this.dragStream=function(t,e={}){const{threshold:i=3,mouseButton:n=0,touchDelay:s=100}=e,r=l(null),o=new Map,a=matchMedia("(pointer: coarse)").matches;let h=()=>{};const c=e=>{if(e.button!==n)return;if(o.has(e.pointerId))return;if(o.set(e.pointerId,e),o.size>1)return;const l=e.pointerId;let c=e.clientX,u=e.clientY,d=!1;const p=Date.now(),m=t.getBoundingClientRect(),{left:g,top:f}=m,v=t=>{if(t.pointerId!==l)return;if(t.defaultPrevented||o.size>1)return;if(a&&Date.now()-p<s)return;const e=t.clientX,n=t.clientY,h=e-c,m=n-u;(d||Math.abs(h)>i||Math.abs(m)>i)&&(t.preventDefault(),t.stopPropagation(),d||(r.set({type:"start",x:c-g,y:u-f}),d=!0),r.set({type:"move",x:e-g,y:n-f,deltaX:h,deltaY:m}),c=e,u=n)},b=t=>{if(o.delete(t.pointerId)){if(t.pointerId===l&&d){const e=t.clientX,i=t.clientY;r.set({type:"end",x:e-g,y:i-f})}0===o.size&&h()}},y=t=>{t.relatedTarget&&t.relatedTarget!==document.documentElement||b(t)},C=t=>{d&&(t.stopPropagation(),t.preventDefault())},S=t=>{t.defaultPrevented||o.size>1||d&&t.preventDefault()};document.addEventListener("pointermove",v),document.addEventListener("pointerup",b),document.addEventListener("pointerout",y),document.addEventListener("pointercancel",y),document.addEventListener("touchmove",S,{passive:!1}),document.addEventListener("click",C,{capture:!0}),h=()=>{document.removeEventListener("pointermove",v),document.removeEventListener("pointerup",b),document.removeEventListener("pointerout",y),document.removeEventListener("pointercancel",y),document.removeEventListener("touchmove",S),setTimeout((()=>{document.removeEventListener("click",C,{capture:!0})}),10)}};return t.addEventListener("pointerdown",c),{signal:r,cleanup:()=>{h(),t.removeEventListener("pointerdown",c),o.clear(),b(r)}}}(this.wrapper);const t=c((()=>{const t=this.dragStream.signal.value;if(!t)return;const e=this.wrapper.getBoundingClientRect().width,i=(n=t.x/e)<0?0:n>1?1:n;var n;"start"===t.type?(this.isDragging=!0,this.emit("dragstart",i)):"move"===t.type?this.emit("drag",i):"end"===t.type&&(this.isDragging=!1,this.emit("dragend",i))}),[this.dragStream.signal]);this.subscriptions.push(t)}calculateInlinePadding(){const{paddingLeft:t,paddingRight:e}=getComputedStyle(this.scrollContainer),i=parseFloat(t)+parseFloat(e);this.containerInlinePadding=Number.isNaN(i)?0:i}initHtml(){const t=document.createElement("div"),e=t.attachShadow({mode:"open"}),i=this.options.cspNonce&&"string"==typeof this.options.cspNonce?this.options.cspNonce.replace(/"/g,""):"";return e.innerHTML=`\n <style${i?` nonce="${i}"`:""}>\n :host {\n user-select: none;\n min-width: 1px;\n }\n :host audio {\n display: block;\n width: 100%;\n }\n :host .scroll {\n overflow-x: auto;\n overflow-y: hidden;\n width: 100%;\n position: relative;\n }\n :host .noScrollbar {\n scrollbar-color: transparent;\n scrollbar-width: none;\n }\n :host .noScrollbar::-webkit-scrollbar {\n display: none;\n -webkit-appearance: none;\n }\n :host .wrapper {\n position: relative;\n overflow: visible;\n z-index: 2;\n }\n :host .canvases {\n min-height: ${this.getHeight(this.options.height,this.options.splitChannels)}px;\n pointer-events: none;\n }\n :host .canvases > div {\n position: relative;\n }\n :host canvas {\n display: block;\n position: absolute;\n top: 0;\n image-rendering: pixelated;\n }\n :host .progress {\n pointer-events: none;\n position: absolute;\n z-index: 2;\n top: 0;\n left: 0;\n width: 0;\n height: 100%;\n overflow: hidden;\n }\n :host .progress > div {\n position: relative;\n }\n :host .cursor {\n pointer-events: none;\n position: absolute;\n z-index: 5;\n top: 0;\n left: 0;\n height: 100%;\n border-radius: 2px;\n }\n </style>\n\n <div class="scroll" part="scroll">\n <div class="wrapper" part="wrapper">\n <div class="canvases" part="canvases"></div>\n <div class="progress" part="progress"></div>\n <div class="cursor" part="cursor"></div>\n </div>\n </div>\n `,[t,e]}setOptions(t){var e;if(this.options.container!==t.container){const e=this.parentFromOptionsContainer(t.container);e.appendChild(this.container),this.parent=e}!0===t.dragToSeek||"object"==typeof this.options.dragToSeek?this.initDrag():(null===(e=this.dragStream)||void 0===e||e.cleanup(),this.dragStream=null),this.options=t,this.reRender()}getWrapper(){return this.wrapper}getWidth(){return this.scrollContainer.clientWidth-this.containerInlinePadding}getScroll(){return this.scrollContainer.scrollLeft}setScroll(t){this.scrollContainer.scrollLeft=t}setScrollPercentage(t){const{scrollWidth:e}=this.scrollContainer,i=e*t;this.setScroll(i)}destroy(){var t;this.wrapper.removeEventListener("click",this.onClickWrapper),this.wrapper.removeEventListener("dblclick",this.onDblClickWrapper),this.timeouts.forEach((t=>t())),this.timeouts=[],this.subscriptions.forEach((t=>t())),this.container.remove(),this.resizeObserver&&(this.resizeObserver.disconnect(),this.resizeObserver=null),null===(t=this.unsubscribeOnScroll)||void 0===t||t.forEach((t=>t())),this.unsubscribeOnScroll=[],this.dragStream&&(this.dragStream.cleanup(),this.dragStream=null),this.scrollStream&&(this.scrollStream.cleanup(),this.scrollStream=null)}createDelay(t=10){let e,i;const n=()=>{e&&(clearTimeout(e),e=void 0),i&&(i(),i=void 0)};return this.timeouts.push(n),()=>new Promise(((s,r)=>{n(),i=r,e=setTimeout((()=>{e=void 0,i=void 0,s()}),t)}))}getHeight(t,e){var i;const n=(null===(i=this.audioData)||void 0===i?void 0:i.numberOfChannels)||1;return function({optionsHeight:t,optionsSplitChannels:e,parentHeight:i,numberOfChannels:n,defaultHeight:s=128}){if(null==t)return s;const r=Number(t);if(!isNaN(r))return r;if("auto"===t){const t=i||s;return(null==e?void 0:e.every((t=>!t.overlay)))?t/n:t}return s}({optionsHeight:t,optionsSplitChannels:e,parentHeight:this.parent.clientHeight,numberOfChannels:n,defaultHeight:128})}convertColorValues(t,e){return function(t,e,i){if(!Array.isArray(t))return t||"";if(0===t.length)return"#999";if(t.length<2)return t[0]||"";const n=document.createElement("canvas"),s=n.getContext("2d");if(!s)return t[0]||"";const r=i||n.height*e,o=s.createLinearGradient(0,0,0,r),a=1/(t.length-1);return t.forEach(((t,e)=>{o.addColorStop(e*a,t)})),o}(t,this.getPixelRatio(),null==e?void 0:e.canvas.height)}getPixelRatio(){return t=window.devicePixelRatio,Math.max(1,t||1);// removed by dead control flow
5008
5130
  var t; }renderBarWaveform(t,e,i,n){const{width:s,height:r}=i.canvas,{halfHeight:o,barWidth:a,barRadius:l,barIndexScale:h,barSpacing:c,barMinHeight:u}=function({width:t,height:e,length:i,options:n,pixelRatio:s}){const r=e/2,o=n.barWidth?n.barWidth*s:1,a=n.barGap?n.barGap*s:n.barWidth?o/2:0,l=o+a||1;return{halfHeight:r,barWidth:o,barGap:a,barRadius:n.barRadius||0,barMinHeight:n.barMinHeight?n.barMinHeight*s:0,barIndexScale:i>0?t/l/i:0,barSpacing:l}}({width:s,height:r,length:(t[0]||[]).length,options:e,pixelRatio:this.getPixelRatio()}),m=function({channelData:t,barIndexScale:e,barSpacing:i,barWidth:n,halfHeight:s,vScale:r,canvasHeight:o,barAlign:a,barMinHeight:l}){const h=t[0]||[],c=t[1]||h,u=h.length,m=[];let g=0,f=0,v=0;for(let t=0;t<=u;t++){const u=Math.round(t*e);if(u>g){const{topHeight:t,totalHeight:e}=d({maxTop:f,maxBottom:v,halfHeight:s,vScale:r,barMinHeight:l,barAlign:a}),h=p({barAlign:a,halfHeight:s,topHeight:t,totalHeight:e,canvasHeight:o});m.push({x:g*i,y:h,width:n,height:e}),g=u,f=0,v=0}const b=Math.abs(h[t]||0),y=Math.abs(c[t]||0);b>f&&(f=b),y>v&&(v=y)}return m}({channelData:t,barIndexScale:h,barSpacing:c,barWidth:a,halfHeight:o,vScale:n,canvasHeight:r,barAlign:e.barAlign,barMinHeight:u});i.beginPath();for(const t of m)l&&"roundRect"in i?i.roundRect(t.x,t.y,t.width,t.height,l):i.rect(t.x,t.y,t.width,t.height);i.fill(),i.closePath()}renderLineWaveform(t,e,i,n){const{width:s,height:r}=i.canvas,o=function({channelData:t,width:e,height:i,vScale:n}){const s=i/2,r=t[0]||[];return[r,t[1]||r].map(((t,i)=>{const r=t.length,o=r?e/r:0,a=s,l=0===i?-1:1,h=[{x:0,y:a}];let c=0,u=0;for(let e=0;e<=r;e++){const i=Math.round(e*o);if(i>c){const t=a+(Math.round(u*s*n)||1)*l;h.push({x:c,y:t}),c=i,u=0}const r=Math.abs(t[e]||0);r>u&&(u=r)}return h.push({x:c,y:a}),h}))}({channelData:t,width:s,height:r,vScale:n});i.beginPath();for(const t of o)if(t.length){i.moveTo(t[0].x,t[0].y);for(let e=1;e<t.length;e++){const n=t[e];i.lineTo(n.x,n.y)}}i.fill(),i.closePath()}renderWaveform(t,e,i){if(i.fillStyle=this.convertColorValues(e.waveColor,i),e.renderFunction)return void e.renderFunction(t,i);const n=function({channelData:t,barHeight:e,normalize:i,maxPeak:n}){var s;const r=e||1;if(!i)return r;const o=t[0];if(!o||0===o.length)return r;let a=null!=n?n:0;if(!n)for(let t=0;t<o.length;t++){const e=null!==(s=o[t])&&void 0!==s?s:0,i=Math.abs(e);i>a&&(a=i)}return a?r/a:r}({channelData:t,barHeight:e.barHeight,normalize:e.normalize,maxPeak:e.maxPeak});g(e)?this.renderBarWaveform(t,e,i,n):this.renderLineWaveform(t,e,i,n)}renderSingleCanvas(t,e,i,n,s,r,o){const a=this.getPixelRatio(),l=document.createElement("canvas");l.width=Math.round(i*a),l.height=Math.round(n*a),l.style.width=`${i}px`,l.style.height=`${n}px`,l.style.left=`${Math.round(s)}px`,r.appendChild(l);const h=l.getContext("2d");if(e.renderFunction?(h.fillStyle=this.convertColorValues(e.waveColor,h),e.renderFunction(t,h)):this.renderWaveform(t,e,h),l.width>0&&l.height>0){const t=l.cloneNode(),i=t.getContext("2d");i.drawImage(l,0,0),i.globalCompositeOperation="source-in",i.fillStyle=this.convertColorValues(e.progressColor,i),i.fillRect(0,0,l.width,l.height),o.appendChild(t)}}renderMultiCanvas(t,e,i,n,s,r){const o=this.getPixelRatio(),{clientWidth:a}=this.scrollContainer,l=i/o,h=function({clientWidth:t,totalWidth:e,options:i}){return f(Math.min(8e3,t,e),i)}({clientWidth:a,totalWidth:l,options:e});let c={};if(0===h)return;const u=i=>{if(i<0||i>=d)return;if(c[i])return;c[i]=!0;const o=i*h;let a=Math.min(l-o,h);if(a=f(a,e),a<=0)return;const u=function({channelData:t,offset:e,clampedWidth:i,totalWidth:n}){return t.map((t=>{const s=Math.floor(e/n*t.length),r=Math.floor((e+i)/n*t.length);return t.slice(s,r)}))}({channelData:t,offset:o,clampedWidth:a,totalWidth:l});this.renderSingleCanvas(u,e,a,n,o,s,r)},d=Math.ceil(l/h);if(!this.isScrollable){for(let t=0;t<d;t++)u(t);return}if(v({scrollLeft:this.scrollContainer.scrollLeft,totalWidth:l,numCanvases:d}).forEach((t=>u(t))),d>1){const t=this.on("scroll",(()=>{const{scrollLeft:t}=this.scrollContainer;Object.keys(c).length>10&&(s.innerHTML="",r.innerHTML="",c={}),v({scrollLeft:t,totalWidth:l,numCanvases:d}).forEach((t=>u(t)))}));this.unsubscribeOnScroll.push(t)}}renderChannel(t,e,i,n){var{overlay:s}=e,r=function(t,e){var i={};for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&e.indexOf(n)<0&&(i[n]=t[n]);if(null!=t&&"function"==typeof Object.getOwnPropertySymbols){var s=0;for(n=Object.getOwnPropertySymbols(t);s<n.length;s++)e.indexOf(n[s])<0&&Object.prototype.propertyIsEnumerable.call(t,n[s])&&(i[n[s]]=t[n[s]])}return i}(e,["overlay"]);const o=document.createElement("div"),a=this.getHeight(r.height,r.splitChannels);o.style.height=`${a}px`,s&&n>0&&(o.style.marginTop=`-${a}px`),this.canvasWrapper.style.minHeight=`${a}px`,this.canvasWrapper.appendChild(o);const l=o.cloneNode();this.progressWrapper.appendChild(l),this.renderMultiCanvas(t,r,i,a,o,l)}render(e){return t(this,void 0,void 0,(function*(){var t;this.timeouts.forEach((t=>t())),this.timeouts=[],this.unsubscribeOnScroll.forEach((t=>t())),this.unsubscribeOnScroll=[],this.canvasWrapper.innerHTML="",this.progressWrapper.innerHTML="",null!=this.options.width&&(this.scrollContainer.style.width="number"==typeof this.options.width?`${this.options.width}px`:this.options.width);const i=this.getPixelRatio(),n=this.scrollContainer.clientWidth-this.containerInlinePadding,{scrollWidth:s,isScrollable:r,useParentWidth:o,width:a}=function({duration:t,minPxPerSec:e=0,parentWidth:i,fillParent:n,pixelRatio:s}){const r=Math.ceil(t*e),o=r>i,a=Boolean(n&&!o);return{scrollWidth:r,isScrollable:o,useParentWidth:a,width:(a?i:r)*s}}({duration:e.duration,minPxPerSec:this.options.minPxPerSec||0,parentWidth:n,fillParent:this.options.fillParent,pixelRatio:i});if(this.isScrollable=r,this.wrapper.style.width=o?"100%":`${s}px`,this.scrollContainer.style.overflowX=this.isScrollable?"auto":"hidden",this.scrollContainer.classList.toggle("noScrollbar",!!this.options.hideScrollbar),this.cursor.style.backgroundColor=`${this.options.cursorColor||this.options.progressColor}`,this.cursor.style.width=`${this.options.cursorWidth}px`,this.audioData=e,this.emit("render"),this.options.splitChannels)for(let i=0;i<e.numberOfChannels;i++){const n=Object.assign(Object.assign({},this.options),null===(t=this.options.splitChannels)||void 0===t?void 0:t[i]);this.renderChannel([e.getChannelData(i)],n,a,i)}else{const t=[e.getChannelData(0)];e.numberOfChannels>1&&t.push(e.getChannelData(1)),this.renderChannel(t,this.options,a,0)}Promise.resolve().then((()=>this.emit("rendered")))}))}reRender(){if(this.unsubscribeOnScroll.forEach((t=>t())),this.unsubscribeOnScroll=[],!this.audioData)return;const{scrollWidth:t}=this.scrollContainer,{right:e}=this.progressWrapper.getBoundingClientRect();if(this.render(this.audioData),!this.isScrollable&&this.scrollContainer.scrollLeft)this.scrollContainer.scrollLeft=0;else if(this.isScrollable&&t!==this.scrollContainer.scrollWidth){const{right:t}=this.progressWrapper.getBoundingClientRect(),i=function(t){const e=2*t;return(e<0?Math.floor(e):Math.ceil(e))/2}(t-e);this.scrollContainer.scrollLeft+=i}}zoom(t){this.options.minPxPerSec=t,this.reRender()}scrollIntoView(t,e=!1){var i;const{scrollLeft:n,scrollWidth:s,clientWidth:r}=this.scrollContainer,o=t*s,a=n,l=n+r,h=r/2;if(this.isDragging){const t=30;o+t>l?this.scrollContainer.scrollLeft+=t:o-t<a&&(this.scrollContainer.scrollLeft-=t)}else{(o<a||o>l)&&(this.scrollContainer.scrollLeft=o-(this.options.autoCenter?h:0));const t=o-n-h;if(e&&this.options.autoCenter&&t>0){const e=null===(i=this.audioData)||void 0===i?void 0:i.duration;if(void 0===e||e<=0)return void(this.scrollContainer.scrollLeft+=t);const n=s/e;this.scrollContainer.scrollLeft+=n<=600?Math.min(t,10):t}}}renderProgress(t,e){if(isNaN(t))return;const i=100*t;this.canvasWrapper.style.clipPath=`polygon(${i}% 0%, 100% 0%, 100% 100%, ${i}% 100%)`,this.progressWrapper.style.width=`${i}%`,this.cursor.style.left=`${i}%`,this.cursor.style.transform=this.options.cursorWidth?`translateX(-${t*this.options.cursorWidth}px)`:"",this.isScrollable&&this.options.autoScroll&&this.audioData&&this.audioData.duration>0&&this.scrollIntoView(t,e)}exportImage(e,i,n){return t(this,void 0,void 0,(function*(){const t=this.canvasWrapper.querySelectorAll("canvas");if(!t.length)throw new Error("No waveform data");if("dataURL"===n){const n=Array.from(t).map((t=>t.toDataURL(e,i)));return Promise.resolve(n)}return Promise.all(Array.from(t).map((t=>new Promise(((n,s)=>{t.toBlob((t=>{t?n(t):s(new Error("Could not export image"))}),e,i)})))))}))}}class S extends e{constructor(){super(...arguments),this.animationFrameId=null,this.isRunning=!1}start(){if(this.isRunning)return;this.isRunning=!0;const t=()=>{this.isRunning&&(this.emit("tick"),this.animationFrameId=requestAnimationFrame(t))};t()}stop(){this.isRunning=!1,null!==this.animationFrameId&&(cancelAnimationFrame(this.animationFrameId),this.animationFrameId=null)}destroy(){this.stop(),this.unAll()}}class E extends e{constructor(t){super(),this.bufferNode=null,this.playStartTime=0,this.playbackPosition=0,this._muted=!1,this._playbackRate=1,this._duration=void 0,this.buffer=null,this.currentSrc="",this.paused=!0,this.crossOrigin=null,this.seeking=!1,this.autoplay=!1,this.addEventListener=this.on,this.removeEventListener=this.un,this._destroyed=!1,function(){const t=globalThis.navigator;if(null==t?void 0:t.audioSession)try{t.audioSession.type="playback"}catch(t){console.warn("Setting navigator.audioSession.type failed:",t)}}(),this.audioContext=t||new AudioContext,this.gainNode=this.audioContext.createGain(),this.gainNode.connect(this.audioContext.destination)}load(){return t(this,void 0,void 0,(function*(){}))}remove(){this.destroy()}destroy(){if(!this._destroyed){if(this._destroyed=!0,this.currentSrc="",this.bufferNode){this.bufferNode.onended=null;try{this.bufferNode.stop()}catch(t){}this.bufferNode.disconnect(),this.bufferNode=null}this.gainNode.disconnect(),"function"==typeof this.audioContext.close&&Promise.resolve(this.audioContext.close.call(this.audioContext)).catch((()=>{})),this.buffer=null,this.unAll()}}get src(){return this.currentSrc}set src(t){if(this.currentSrc=t,this._duration=void 0,!t)return this.buffer=null,void this.emit("emptied");fetch(t).then((e=>{if(e.status>=400)throw new Error(`Failed to fetch ${t}: ${e.status} (${e.statusText})`);return e.arrayBuffer()})).then((e=>this.currentSrc!==t?null:this.audioContext.decodeAudioData(e))).then((e=>{this.currentSrc===t&&(this.buffer=e,this.emit("loadedmetadata"),this.emit("canplay"),this.autoplay&&this.play())})).catch((t=>{console.error("WebAudioPlayer load error:",t)}))}_play(){if(!this.paused)return;this.paused=!1,this.bufferNode&&(this.bufferNode.onended=null,this.bufferNode.disconnect()),this.bufferNode=this.audioContext.createBufferSource(),this.buffer&&(this.bufferNode.buffer=this.buffer),this.bufferNode.playbackRate.value=this._playbackRate,this.bufferNode.connect(this.gainNode);let t=this.playbackPosition;(t>=this.duration||t<0)&&(t=0,this.playbackPosition=0),this.bufferNode.start(this.audioContext.currentTime,t),this.playStartTime=this.audioContext.currentTime,this.bufferNode.onended=()=>{!this.paused&&this.duration-this.currentTime<.01&&(this.pause(),this.emit("ended"))}}_pause(){if(this.paused=!0,this.bufferNode){this.bufferNode.onended=null;try{this.bufferNode.stop()}catch(t){}}this.playbackPosition+=(this.audioContext.currentTime-this.playStartTime)*this._playbackRate}play(){return t(this,void 0,void 0,(function*(){this.paused&&(this._play(),this.emit("play"))}))}pause(){this.paused||(this._pause(),this.emit("pause"))}stopAt(t){const e=(t-this.currentTime)/this._playbackRate,i=this.bufferNode;null==i||i.stop(this.audioContext.currentTime+e),null==i||i.addEventListener("ended",(()=>{i===this.bufferNode&&(this.bufferNode=null,this.pause(),this.playbackPosition=Math.min(t,this.duration),this.emit("timeupdate"))}),{once:!0})}setSinkId(e){return t(this,void 0,void 0,(function*(){return this.audioContext.setSinkId(e)}))}get playbackRate(){return this._playbackRate}set playbackRate(t){const e=!this.paused;e&&this._pause(),this._playbackRate=t,e&&this._play(),this.bufferNode&&(this.bufferNode.playbackRate.value=t)}get currentTime(){return this.paused?this.playbackPosition:this.playbackPosition+(this.audioContext.currentTime-this.playStartTime)*this._playbackRate}set currentTime(t){const e=!this.paused;e&&this._pause(),this.playbackPosition=t,e&&this._play(),this.emit("seeking"),this.emit("timeupdate")}get duration(){var t,e;return null!==(t=this._duration)&&void 0!==t?t:(null===(e=this.buffer)||void 0===e?void 0:e.duration)||0}set duration(t){this._duration=t}get volume(){return this.gainNode.gain.value}set volume(t){this.gainNode.gain.value=t,this.emit("volumechange")}get muted(){return this._muted}set muted(t){this._muted!==t&&(this._muted=t,this._muted?this.gainNode.disconnect():this.gainNode.connect(this.audioContext.destination))}canPlayType(t){return/^(audio|video)\//.test(t)}getGainNode(){return this.gainNode}getChannelData(){const t=[];if(!this.buffer)return t;const e=this.buffer.numberOfChannels;for(let i=0;i<e;i++)t.push(this.buffer.getChannelData(i));return t}removeAttribute(t){switch(t){case"src":this.src="";break;case"playbackRate":this.playbackRate=0;break;case"currentTime":this.currentTime=0;break;case"duration":this.duration=0;break;case"volume":this.volume=0;break;case"muted":this.muted=!1}}}const P={waveColor:"#999",progressColor:"#555",cursorWidth:1,minPxPerSec:0,fillParent:!0,interact:!0,dragToSeek:!1,autoScroll:!0,autoCenter:!0,sampleRate:8e3};class w extends u{static create(t){return new w(t)}getState(){return this.wavesurferState}getRenderer(){return this.renderer}constructor(t){const e=t.media||("WebAudio"===t.backend?new E:void 0);super({media:e,mediaControls:t.mediaControls,autoplay:t.autoplay,playbackRate:t.audioRate}),this.plugins=[],this.decodedData=null,this.stopAtPosition=null,this.subscriptions=[],this.mediaSubscriptions=[],this.abortController=null,this._isDestroyed=!1,this._loadVersion=0,this.reactiveCleanups=[],this.options=Object.assign({},P,t);const{state:i,actions:n}=function(t){var e,i,n,s,r,o;const a=null!==(e=null==t?void 0:t.currentTime)&&void 0!==e?e:l(0),c=null!==(i=null==t?void 0:t.duration)&&void 0!==i?i:l(0),u=null!==(n=null==t?void 0:t.isPlaying)&&void 0!==n?n:l(!1),d=null!==(s=null==t?void 0:t.isSeeking)&&void 0!==s?s:l(!1),p=null!==(r=null==t?void 0:t.volume)&&void 0!==r?r:l(1),m=null!==(o=null==t?void 0:t.playbackRate)&&void 0!==o?o:l(1),g=l(null),f=l(null),v=l(""),b=l(0),y=l(0),C=h((()=>!u.value),[u]),S=h((()=>null!==g.value),[g]),E=h((()=>S.value&&c.value>0),[S,c]),P=h((()=>a.value),[a]),w=h((()=>c.value>0?a.value/c.value:0),[a,c]);return{state:{currentTime:a,duration:c,isPlaying:u,isPaused:C,isSeeking:d,volume:p,playbackRate:m,audioBuffer:g,peaks:f,url:v,zoom:b,scrollPosition:y,canPlay:S,isReady:E,progress:P,progressPercent:w},actions:{setCurrentTime:t=>{const e=Math.max(0,Math.min(c.value||1/0,t));a.set(e)},setDuration:t=>{c.set(Math.max(0,t))},setPlaying:t=>{u.set(t)},setSeeking:t=>{d.set(t)},setVolume:t=>{const e=Math.max(0,Math.min(1,t));p.set(e)},setPlaybackRate:t=>{const e=Math.max(.1,Math.min(16,t));m.set(e)},setAudioBuffer:t=>{g.set(t),t&&c.set(t.duration)},setPeaks:t=>{f.set(t)},setUrl:t=>{v.set(t)},setZoom:t=>{b.set(Math.max(0,t))},setScrollPosition:t=>{y.set(Math.max(0,t))}}}}({isPlaying:this.isPlayingSignal,currentTime:this.currentTimeSignal,duration:this.durationSignal,volume:this.volumeSignal,playbackRate:this.playbackRateSignal,isSeeking:this.seekingSignal});this.wavesurferState=i,this.wavesurferActions=n,this.timer=new S;const s=e?void 0:this.getMediaElement();this.renderer=new C(this.options,s),this.initPlayerEvents(),this.initRendererEvents(),this.initTimerEvents(),this.initReactiveState(),this.initPlugins();const r=this.options.url||this.getSrc()||"";Promise.resolve().then((()=>{this.emit("init");const{peaks:t,duration:e}=this.options;(r||t&&e)&&this.load(r,t,e).catch((()=>{}))}))}updateProgress(t=this.getCurrentTime()){return this.renderer.renderProgress(t/this.getDuration(),this.isPlaying()),t}initTimerEvents(){this.subscriptions.push(this.timer.on("tick",(()=>{if(!this.isSeeking()){const t=this.updateProgress();if(this.emit("timeupdate",t),this.emit("audioprocess",t),null!=this.stopAtPosition&&this.isPlaying()&&t>=this.stopAtPosition){const t=this.stopAtPosition;this.pause(),this.setTime(t)}}})))}initReactiveState(){this.reactiveCleanups.push(function(t,e){const i=[];i.push(c((()=>{const i=t.isPlaying.value;e.emit(i?"play":"pause")}),[t.isPlaying])),i.push(c((()=>{const i=t.currentTime.value;e.emit("timeupdate",i),t.isPlaying.value&&e.emit("audioprocess",i)}),[t.currentTime,t.isPlaying])),i.push(c((()=>{t.isSeeking.value&&e.emit("seeking",t.currentTime.value)}),[t.isSeeking,t.currentTime]));let n=!1;i.push(c((()=>{t.isReady.value&&!n&&(n=!0,e.emit("ready",t.duration.value))}),[t.isReady,t.duration])),i.push(c((()=>{null===t.audioBuffer.value&&(n=!1)}),[t.audioBuffer]));let s=!1;return i.push(c((()=>{const i=t.isPlaying.value,n=t.currentTime.value,r=t.duration.value,o=r>0&&n>=r;s&&!i&&o&&e.emit("finish"),s=i&&o}),[t.isPlaying,t.currentTime,t.duration])),i.push(c((()=>{const i=t.zoom.value;i>0&&e.emit("zoom",i)}),[t.zoom])),()=>{i.forEach((t=>t()))}}(this.wavesurferState,{emit:this.emit.bind(this)}))}initPlayerEvents(){this.isPlaying()&&(this.emit("play"),this.timer.start()),this.mediaSubscriptions.push(this.onMediaEvent("timeupdate",(()=>{const t=this.updateProgress();this.emit("timeupdate",t)})),this.onMediaEvent("play",(()=>{this.emit("play"),this.timer.start()})),this.onMediaEvent("pause",(()=>{this.emit("pause"),this.timer.stop(),this.stopAtPosition=null})),this.onMediaEvent("emptied",(()=>{this.timer.stop(),this.stopAtPosition=null})),this.onMediaEvent("ended",(()=>{this.emit("timeupdate",this.getDuration()),this.emit("finish"),this.stopAtPosition=null})),this.onMediaEvent("seeking",(()=>{this.emit("seeking",this.getCurrentTime())})),this.onMediaEvent("error",(()=>{var t;this.emit("error",null!==(t=this.getMediaElement().error)&&void 0!==t?t:new Error("Media error")),this.stopAtPosition=null})))}initRendererEvents(){this.subscriptions.push(this.renderer.on("click",((t,e)=>{this.options.interact&&(this.seekTo(t),this.emit("interaction",t*this.getDuration()),this.emit("click",t,e))})),this.renderer.on("dblclick",((t,e)=>{this.emit("dblclick",t,e)})),this.renderer.on("scroll",((t,e,i,n)=>{const s=this.getDuration();this.emit("scroll",t*s,e*s,i,n)})),this.renderer.on("render",(()=>{this.emit("redraw")})),this.renderer.on("rendered",(()=>{this.emit("redrawcomplete")})),this.renderer.on("dragstart",(t=>{this.emit("dragstart",t)})),this.renderer.on("dragend",(t=>{this.emit("dragend",t)})),this.renderer.on("resize",(()=>{this.emit("resize")})));{let t;const e=this.renderer.on("drag",(e=>{var i;if(!this.options.interact)return;this.renderer.renderProgress(e),clearTimeout(t);let n=0;const s=this.options.dragToSeek;this.isPlaying()?n=0:!0===s?n=200:s&&"object"==typeof s&&(n=null!==(i=s.debounceTime)&&void 0!==i?i:200),t=setTimeout((()=>{this.seekTo(e)}),n),this.emit("interaction",e*this.getDuration()),this.emit("drag",e)}));this.subscriptions.push((()=>{clearTimeout(t),e()}))}}initPlugins(){var t;(null===(t=this.options.plugins)||void 0===t?void 0:t.length)&&this.options.plugins.forEach((t=>{this.registerPlugin(t)}))}unsubscribePlayerEvents(){this.mediaSubscriptions.forEach((t=>t())),this.mediaSubscriptions=[]}setOptions(t){this.options=Object.assign({},this.options,t),t.duration&&!t.peaks&&(this.decodedData=i.createBuffer(this.exportPeaks(),t.duration)),t.peaks&&t.duration&&(this.decodedData=i.createBuffer(t.peaks,t.duration)),this.renderer.setOptions(this.options),t.audioRate&&this.setPlaybackRate(t.audioRate),null!=t.mediaControls&&(this.getMediaElement().controls=t.mediaControls)}registerPlugin(t){if(this.plugins.includes(t))return t;t._init(this),this.plugins.push(t);const e=t.once("destroy",(()=>{this.plugins=this.plugins.filter((e=>e!==t)),this.subscriptions=this.subscriptions.filter((t=>t!==e))}));return this.subscriptions.push(e),t}unregisterPlugin(t){this.plugins=this.plugins.filter((e=>e!==t)),t.destroy()}getWrapper(){return this.renderer.getWrapper()}getWidth(){return this.renderer.getWidth()}getScroll(){return this.renderer.getScroll()}setScroll(t){return this.renderer.setScroll(t)}setScrollTime(t){const e=t/this.getDuration();this.renderer.setScrollPercentage(e)}getActivePlugins(){return this.plugins}loadAudio(e,n,s,r){return t(this,void 0,void 0,(function*(){var t;const o=++this._loadVersion;if(this._isDestroyed=!1,this.emit("load",e),!this.options.media&&this.isPlaying()&&this.pause(),this.decodedData=null,this.stopAtPosition=null,null===(t=this.abortController)||void 0===t||t.abort(),this.abortController=null,!n&&!s){const t=this.options.fetchParams||{};window.AbortController&&!t.signal&&(this.abortController=new AbortController,t.signal=this.abortController.signal);const i=t=>this.emit("loading",t);if(n=yield a.fetchBlob(e,i,t),this._isDestroyed||o!==this._loadVersion)return;const s=this.options.blobMimeType;s&&(n=new Blob([n],{type:s}))}if(this._isDestroyed||o!==this._loadVersion)return;this.setSrc(e,n);const l=yield new Promise((t=>{const e=r||this.getDuration();e?t(e):this.mediaSubscriptions.push(this.onMediaEvent("loadedmetadata",(()=>t(this.getDuration())),{once:!0}))}));if(!this._isDestroyed&&o===this._loadVersion){if(!e&&!n){const t=this.getMediaElement();t instanceof E&&(t.duration=l)}if(s)this.decodedData=i.createBuffer(s,l||0);else if(n){const t=yield n.arrayBuffer();if(this._isDestroyed||o!==this._loadVersion)return;this.decodedData=yield i.decode(t,this.options.sampleRate)}this._isDestroyed||o!==this._loadVersion||(this.decodedData&&(this.emit("decode",this.getDuration()),this.renderer.render(this.decodedData)),this.emit("ready",this.getDuration()))}}))}load(e,i,n){return t(this,void 0,void 0,(function*(){try{return yield this.loadAudio(e,void 0,i,n)}catch(t){throw this.emit("error",t),t}}))}loadBlob(e,i,n){return t(this,void 0,void 0,(function*(){try{return yield this.loadAudio("",e,i,n)}catch(t){throw this.emit("error",t),t}}))}zoom(t){if(!this.decodedData)throw new Error("No audio loaded");this.renderer.zoom(t),this.emit("zoom",t)}getDecodedData(){return this.decodedData}exportPeaks({channels:t=2,maxLength:e=8e3,precision:i=1e4}={}){if(!this.decodedData)throw new Error("The audio has not been decoded yet");const n=Math.min(t,this.decodedData.numberOfChannels),s=[];for(let t=0;t<n;t++){const n=this.decodedData.getChannelData(t),r=[],o=n.length/e;for(let t=0;t<e;t++){const e=n.slice(Math.floor(t*o),Math.ceil((t+1)*o));let s=0;for(let t=0;t<e.length;t++){const i=e[t];Math.abs(i)>Math.abs(s)&&(s=i)}r.push(Math.round(s*i)/i)}s.push(r)}return s}getDuration(){let t=super.getDuration()||0;return 0!==t&&t!==1/0||!this.decodedData||(t=this.decodedData.duration),t}toggleInteraction(t){this.options.interact=t}setTime(t){this.stopAtPosition=null,super.setTime(t),this.updateProgress(t),this.emit("timeupdate",t)}seekTo(t){const e=this.getDuration()*t;this.setTime(e)}play(e,i){const n=Object.create(null,{play:{get:()=>super.play}});return t(this,void 0,void 0,(function*(){null!=e&&this.setTime(e);const t=yield n.play.call(this);return null!=i&&(this.media instanceof E?this.media.stopAt(i):this.stopAtPosition=i),t}))}playPause(){return t(this,void 0,void 0,(function*(){return this.isPlaying()?this.pause():this.play()}))}stop(){this.pause(),this.setTime(0)}skip(t){this.setTime(this.getCurrentTime()+t)}empty(){this.load("",[[0]],.001)}setMediaElement(t){this.unsubscribePlayerEvents(),super.setMediaElement(t),this.initPlayerEvents()}exportImage(){return t(this,arguments,void 0,(function*(t="image/png",e=1,i="dataURL"){return this.renderer.exportImage(t,e,i)}))}destroy(){var t;this._isDestroyed=!0,this.emit("destroy"),null===(t=this.abortController)||void 0===t||t.abort(),this.plugins.forEach((t=>t.destroy())),this.subscriptions.forEach((t=>t())),this.unsubscribePlayerEvents(),this.reactiveCleanups.forEach((t=>t())),this.reactiveCleanups=[],this.timer.destroy(),this.renderer.destroy(),super.destroy()}}w.BasePlugin=class extends e{constructor(t){super(),this.subscriptions=[],this.isDestroyed=!1,this.options=t}onInit(){}_init(t){this.isDestroyed&&(this.subscriptions=[],this.isDestroyed=!1),this.wavesurfer=t,this.onInit()}destroy(){this.emit("destroy"),this.subscriptions.forEach((t=>t())),this.subscriptions=[],this.isDestroyed=!0,this.wavesurfer=void 0}},w.dom=o;
@@ -5087,18 +5209,31 @@ function getBufferedProgress(bufferedRange, durationSec) {
5087
5209
  return clampTo0And1(bufferedRange.end(bufferedRange.length - 1) / durationSec);
5088
5210
  }
5089
5211
 
5212
+ /** Played bars: rest token on desktop, hover-played (brightest) on tape. Tape never uses hover or buffer fills. */
5213
+ function getPlayedWaveformColor(isTape = false) {
5214
+ return isTape ? WAVEFORM_COLOR_HOVER_PLAYED : WAVEFORM_COLOR_PLAYED;
5215
+ }
5216
+
5090
5217
  /**
5091
5218
  * progressColor paints left of the playhead (clipped by wavesurfer).
5092
5219
  * waveColor paints right of the playhead, including buffered vs not-yet-buffered.
5093
5220
  */
5094
5221
  function getWaveformFills({
5095
5222
  bufferProgress,
5096
- hoverProgress
5223
+ hoverProgress,
5224
+ isTape = false
5097
5225
  }) {
5098
5226
  const buffer = clampTo0And1(bufferProgress);
5227
+ const playedColor = getPlayedWaveformColor(isTape);
5228
+ if (isTape) {
5229
+ return {
5230
+ progressColor: playedColor,
5231
+ waveColor: WAVEFORM_COLOR_UNPLAYED
5232
+ };
5233
+ }
5099
5234
  if (hoverProgress == null) {
5100
5235
  return {
5101
- progressColor: WAVEFORM_COLOR_PLAYED,
5236
+ progressColor: playedColor,
5102
5237
  waveColor: gradientStopsFromColorRanges([{
5103
5238
  color: WAVEFORM_COLOR_UNPLAYED,
5104
5239
  end: buffer,
@@ -5275,6 +5410,7 @@ function prefersReducedMotion() {
5275
5410
  * every frame. WaveformView owns WaveSurfer, zoom, and the media rAF loop.
5276
5411
  */
5277
5412
  function usePlayheadCamera({
5413
+ cameraMode = 'desktop',
5278
5414
  mediaElRef,
5279
5415
  onViewportCommit,
5280
5416
  playheadRef,
@@ -5283,6 +5419,8 @@ function usePlayheadCamera({
5283
5419
  }) {
5284
5420
  const onViewportCommitRef = (0,external_react_.useRef)(onViewportCommit);
5285
5421
  onViewportCommitRef.current = onViewportCommit;
5422
+ const cameraModeRef = (0,external_react_.useRef)(cameraMode);
5423
+ cameraModeRef.current = cameraMode;
5286
5424
  const programmaticScrollRef = (0,external_react_.useRef)(false);
5287
5425
  const jumpAnimationRef = (0,external_react_.useRef)(0);
5288
5426
  const lastCameraScrollRef = (0,external_react_.useRef)(null);
@@ -5293,6 +5431,8 @@ function usePlayheadCamera({
5293
5431
  const scrollSettleTimerRef = (0,external_react_.useRef)(0);
5294
5432
  const applyRef = (0,external_react_.useRef)(null);
5295
5433
  const isFollowPinned = (0,external_react_.useCallback)(() => isFollowPinnedRef.current, []);
5434
+ const isUserPanning = (0,external_react_.useCallback)(() => userIsScrollingRef.current, []);
5435
+ const isTape = (0,external_react_.useCallback)(() => cameraModeRef.current === 'tape', []);
5296
5436
  const cancelJump = (0,external_react_.useCallback)(() => {
5297
5437
  if (!jumpAnimationRef.current) {
5298
5438
  return;
@@ -5321,15 +5461,22 @@ function usePlayheadCamera({
5321
5461
  const onSeek = (0,external_react_.useCallback)(timeSec => {
5322
5462
  mediaTimeRef.current = timeSec;
5323
5463
  cancelJump();
5324
- clearFollowPin();
5325
- }, [cancelJump, clearFollowPin]);
5464
+ if (!isTape()) {
5465
+ clearFollowPin();
5466
+ }
5467
+ }, [cancelJump, clearFollowPin, isTape]);
5326
5468
 
5327
5469
  /** Unpin and hold follow so zoom setScroll (slider center or pinch origin) is not stolen. */
5328
5470
  const onZoom = (0,external_react_.useCallback)(() => {
5329
5471
  cancelJump();
5472
+ if (isTape()) {
5473
+ isFollowPinnedRef.current = true;
5474
+ holdFollowUntilInsetRef.current = false;
5475
+ return;
5476
+ }
5330
5477
  isFollowPinnedRef.current = false;
5331
5478
  holdFollowUntilInsetRef.current = true;
5332
- }, [cancelJump]);
5479
+ }, [cancelJump, isTape]);
5333
5480
  const applyScrollLeft = (0,external_react_.useCallback)((scrollLeftPx, shouldCommitState) => {
5334
5481
  const wavesurfer = wavesurferRef.current;
5335
5482
  if (!wavesurfer || !wavesurfer.setScroll) {
@@ -5357,8 +5504,8 @@ function usePlayheadCamera({
5357
5504
  if (!playhead || !isPinned || !(viewport.widthPx > 0)) {
5358
5505
  return;
5359
5506
  }
5360
- playhead.style.left = getPinnedPlayheadLeft(viewport.widthPx);
5361
- }, []);
5507
+ playhead.style.left = isTape() ? getTapePinnedPlayheadLeft() : getPinnedPlayheadLeft(viewport.widthPx);
5508
+ }, [isTape]);
5362
5509
  const apply = (0,external_react_.useCallback)((timeSec, playJustStarted = false) => {
5363
5510
  mediaTimeRef.current = timeSec;
5364
5511
  const wavesurfer = wavesurferRef.current;
@@ -5366,7 +5513,7 @@ function usePlayheadCamera({
5366
5513
  if (!wavesurfer) {
5367
5514
  return;
5368
5515
  }
5369
- if (viewport.zoomLevel <= constants/* WAVEFORM_ZOOM_MIN */.LW) {
5516
+ if (!isTape() && viewport.zoomLevel <= constants/* WAVEFORM_ZOOM_MIN */.LW) {
5370
5517
  clearFollowPin();
5371
5518
  return;
5372
5519
  }
@@ -5378,12 +5525,13 @@ function usePlayheadCamera({
5378
5525
  }
5379
5526
  const liveViewport = getViewportAtScroll(viewport, getScrollLeft(wavesurfer, viewport.scrollLeftPx));
5380
5527
  const action = getPlayheadCameraAction({
5528
+ cameraMode: cameraModeRef.current,
5381
5529
  isPlaying: true,
5382
5530
  playJustStarted,
5383
5531
  timeSec,
5384
5532
  viewport: liveViewport
5385
5533
  });
5386
- if (holdFollowUntilInsetRef.current && !playJustStarted) {
5534
+ if (!isTape() && holdFollowUntilInsetRef.current && !playJustStarted) {
5387
5535
  if (action.type !== 'none') {
5388
5536
  return;
5389
5537
  }
@@ -5415,6 +5563,7 @@ function usePlayheadCamera({
5415
5563
  const tick = now => {
5416
5564
  const t = Math.min(1, (now - start) / constants/* WAVEFORM_PLAYHEAD_JUMP_MS */.Bq);
5417
5565
  const liveAction = getPlayheadCameraAction({
5566
+ cameraMode: cameraModeRef.current,
5418
5567
  isPlaying: true,
5419
5568
  playJustStarted: true,
5420
5569
  timeSec: mediaTimeRef.current,
@@ -5431,7 +5580,7 @@ function usePlayheadCamera({
5431
5580
  apply(mediaTimeRef.current, false);
5432
5581
  };
5433
5582
  jumpAnimationRef.current = window.requestAnimationFrame(tick);
5434
- }, [applyScrollLeft, cancelJump, clearFollowPin, pinFollowPlayhead, playheadRef, viewportRef, wavesurferRef]);
5583
+ }, [applyScrollLeft, cancelJump, clearFollowPin, isTape, pinFollowPlayhead, playheadRef, viewportRef, wavesurferRef]);
5435
5584
  applyRef.current = apply;
5436
5585
 
5437
5586
  /**
@@ -5454,9 +5603,12 @@ function usePlayheadCamera({
5454
5603
  }
5455
5604
  releaseUserPanHold();
5456
5605
  cancelJump();
5457
- isFollowPinnedRef.current = false;
5606
+ if (!isTape()) {
5607
+ isFollowPinnedRef.current = false;
5608
+ }
5458
5609
  const liveViewport = getViewportAtScroll(viewportRef.current, getScrollLeft(wavesurfer, viewportRef.current.scrollLeftPx));
5459
5610
  const action = getSeekCameraAction({
5611
+ cameraMode: cameraModeRef.current,
5460
5612
  timeSec,
5461
5613
  viewport: liveViewport
5462
5614
  });
@@ -5476,7 +5628,7 @@ function usePlayheadCamera({
5476
5628
  applyScrollLeft(from + (to - from) * (1 - (1 - t) * (1 - t)), false);
5477
5629
  const playhead = playheadRef.current;
5478
5630
  if (playhead) {
5479
- playhead.style.left = timeLeftPercent(mediaTimeRef.current, viewportRef.current.durationSec, viewportRef.current);
5631
+ playhead.style.left = isTape() ? getTapePinnedPlayheadLeft() : timeLeftPercent(mediaTimeRef.current, viewportRef.current.durationSec, viewportRef.current);
5480
5632
  }
5481
5633
  if (t < 1) {
5482
5634
  jumpAnimationRef.current = window.requestAnimationFrame(tick);
@@ -5486,7 +5638,7 @@ function usePlayheadCamera({
5486
5638
  applyScrollLeft(to, true);
5487
5639
  };
5488
5640
  jumpAnimationRef.current = window.requestAnimationFrame(tick);
5489
- }, [applyScrollLeft, cancelJump, playheadRef, releaseUserPanHold, viewportRef, wavesurferRef]);
5641
+ }, [applyScrollLeft, cancelJump, isTape, playheadRef, releaseUserPanHold, viewportRef, wavesurferRef]);
5490
5642
  const handleScroll = (0,external_react_.useCallback)(onUserPan => {
5491
5643
  const wavesurfer = wavesurferRef.current;
5492
5644
  if (!wavesurfer) {
@@ -5498,8 +5650,28 @@ function usePlayheadCamera({
5498
5650
  return;
5499
5651
  }
5500
5652
  viewportRef.current = getViewportAtScroll(viewportRef.current, scrollLeftPx);
5501
- const timeSec = readMediaTime();
5502
5653
  const playhead = playheadRef.current;
5654
+ if (isTape()) {
5655
+ if (playhead) {
5656
+ playhead.style.left = getTapePinnedPlayheadLeft();
5657
+ }
5658
+ const pinTimeSec = Math.min(viewportRef.current.durationSec, Math.max(0, timeFromPositionPx(viewportRef.current.widthPx / 2, viewportRef.current)));
5659
+ userIsScrollingRef.current = true;
5660
+ window.clearTimeout(scrollSettleTimerRef.current);
5661
+ scrollSettleTimerRef.current = window.setTimeout(() => {
5662
+ userIsScrollingRef.current = false;
5663
+ scrollSettleTimerRef.current = 0;
5664
+ if (!mediaElRef.current || mediaElRef.current.paused) {
5665
+ return;
5666
+ }
5667
+ applyRef.current?.(readMediaTime(), false);
5668
+ }, constants/* WAVEFORM_FOLLOW_SCROLL_SETTLE_MS */.Zs);
5669
+ cancelJump();
5670
+ programmaticScrollRef.current = false;
5671
+ onUserPan(pinTimeSec);
5672
+ return;
5673
+ }
5674
+ const timeSec = readMediaTime();
5503
5675
  if (playhead) {
5504
5676
  playhead.style.left = timeLeftPercent(timeSec, viewportRef.current.durationSec, viewportRef.current);
5505
5677
  }
@@ -5518,7 +5690,7 @@ function usePlayheadCamera({
5518
5690
  isFollowPinnedRef.current = false;
5519
5691
  programmaticScrollRef.current = false;
5520
5692
  onUserPan(timeSec);
5521
- }, [cancelJump, isUserPan, mediaElRef, playheadRef, readMediaTime, viewportRef, wavesurferRef]);
5693
+ }, [cancelJump, isTape, isUserPan, mediaElRef, playheadRef, readMediaTime, viewportRef, wavesurferRef]);
5522
5694
  (0,external_react_.useEffect)(() => () => {
5523
5695
  releaseUserPanHold();
5524
5696
  cancelJump();
@@ -5530,6 +5702,7 @@ function usePlayheadCamera({
5530
5702
  clearFollowPin,
5531
5703
  handleScroll,
5532
5704
  isFollowPinned,
5705
+ isUserPanning,
5533
5706
  onSeek,
5534
5707
  onZoom,
5535
5708
  releaseUserPanHold,
@@ -5555,6 +5728,8 @@ function WaveformView_toPrimitive(t, r) { if ("object" != typeof t || !t) return
5555
5728
 
5556
5729
 
5557
5730
 
5731
+
5732
+
5558
5733
  /** Pointer X in the view and the media time under it; zoom keeps this point fixed. */
5559
5734
 
5560
5735
  /** CSS width × devicePixelRatio, for canvas gradient fills. */
@@ -5584,6 +5759,25 @@ function disableScrollOverscroll(container) {
5584
5759
  }
5585
5760
  }
5586
5761
 
5762
+ /** Half-view margins so t=0 and duration can sit under the center pin. */
5763
+ function applyWaveformGutters(wavesurfer, gutterPx) {
5764
+ const wrapper = wavesurfer.getWrapper ? wavesurfer.getWrapper() : null;
5765
+ if (!wrapper || !wrapper.style) {
5766
+ return;
5767
+ }
5768
+ const pad = gutterPx > 0 ? `${gutterPx}px` : '';
5769
+ wrapper.style.marginLeft = pad;
5770
+ wrapper.style.marginRight = pad;
5771
+ // WaveSurfer sets overflow-x:hidden when duration * minPxPerSec <= view width
5772
+ // (tape 1x). Gutters live in these margins, so the scroller must stay pan-able.
5773
+ const scroll = wrapper.parentElement;
5774
+ if (scroll && scroll.style) {
5775
+ const scrollStyle = scroll.style;
5776
+ scrollStyle.overflowX = gutterPx > 0 ? 'auto' : '';
5777
+ scrollStyle.scrollbarWidth = gutterPx > 0 ? 'none' : '';
5778
+ }
5779
+ }
5780
+
5587
5781
  /** Tint WaveSurfer's zoomed tiles with played/unplayed/hover/buffer colors. */
5588
5782
  function tintZoomedWaveform(wavesurfer, fills, replaceSnapshot = false) {
5589
5783
  const wrapper = wavesurfer.getWrapper ? wavesurfer.getWrapper() : null;
@@ -5597,6 +5791,15 @@ function tintZoomedWaveform(wavesurfer, fills, replaceSnapshot = false) {
5597
5791
  totalWidthCss: wrapper.clientWidth
5598
5792
  });
5599
5793
  }
5794
+
5795
+ /** Touch (and the compatibility mouse events Chrome sends after a tap) must not drive hover fills. */
5796
+ function isTouchHoverInput(event) {
5797
+ if (event.pointerType === 'touch') {
5798
+ return true;
5799
+ }
5800
+ const native = event.nativeEvent;
5801
+ return !!native.sourceCapabilities?.firesTouchEvents;
5802
+ }
5600
5803
  function touchDistance(touches) {
5601
5804
  if (touches.length < 2) {
5602
5805
  return 0;
@@ -5627,11 +5830,14 @@ function zoomOriginAtPointer(pointerX, wavesurfer, durationSec, fallbackWidth) {
5627
5830
  */
5628
5831
  function WaveformView({
5629
5832
  bufferedRange,
5833
+ cameraMode = 'desktop',
5630
5834
  currentTime = 0,
5631
5835
  durationSec,
5632
5836
  height = constants/* WAVEFORM_HEIGHT */.oN,
5633
5837
  interactive = true,
5838
+ isPlaying = false,
5634
5839
  mediaEl,
5840
+ onPlayPause,
5635
5841
  onSeek,
5636
5842
  onViewportChange,
5637
5843
  onZoomChange,
@@ -5639,42 +5845,59 @@ function WaveformView({
5639
5845
  zoomLevel: zoomLevelProp
5640
5846
  }) {
5641
5847
  // DOM / WaveSurfer
5642
- const containerRef = (0,external_react_.useRef)(null);
5643
- const trackRef = (0,external_react_.useRef)(null);
5644
- const playheadRef = (0,external_react_.useRef)(null);
5645
- const playheadAnimationRef = (0,external_react_.useRef)(0);
5646
- const wavesurferRef = (0,external_react_.useRef)(null);
5848
+ const containerRef = (0,external_react_.useRef)(null); // WaveSurfer canvas host
5849
+ const playheadAnimationRef = (0,external_react_.useRef)(0); // animation frame id while the playhead follows playback
5850
+ const playheadRef = (0,external_react_.useRef)(null); // playhead element the camera positions
5851
+ const trackRef = (0,external_react_.useRef)(null); // hover / tap target around the canvas
5852
+ const wavesurferRef = (0,external_react_.useRef)(null); // WaveSurfer instance
5647
5853
 
5648
5854
  // Latest props for WaveSurfer + media listeners that must not re-subscribe each render.
5649
- const onSeekRef = (0,external_react_.useRef)(onSeek);
5650
- const interactiveRef = (0,external_react_.useRef)(interactive);
5651
- const hasZoomHandlersRef = (0,external_react_.useRef)(false);
5652
- const currentTimeRef = (0,external_react_.useRef)(currentTime);
5653
- const peaksRef = (0,external_react_.useRef)(peaks);
5654
- const durationSecRef = (0,external_react_.useRef)(durationSec);
5655
- const mediaElRef = (0,external_react_.useRef)(mediaEl);
5656
- const onViewportChangeRef = (0,external_react_.useRef)(onViewportChange);
5657
- const displayedPeaksRef = (0,external_react_.useRef)(null);
5658
- const peakTransitionAnimationRef = (0,external_react_.useRef)(0);
5855
+ const cameraModeRef = (0,external_react_.useRef)(cameraMode); // latest tape/desktop; WaveSurfer create() deps stay empty
5856
+ const currentTimeRef = (0,external_react_.useRef)(currentTime); // latest media time; zoom recenter reads this
5857
+ const durationSecRef = (0,external_react_.useRef)(durationSec); // latest duration; click-to-seek reads this
5858
+ const hasZoomHandlersRef = (0,external_react_.useRef)(false); // parent can zoom; wheel/pinch check this
5859
+ const interactiveRef = (0,external_react_.useRef)(interactive); // latest interactive; WaveSurfer listeners read this
5860
+ const isPlayingRef = (0,external_react_.useRef)(isPlaying); // latest isPlaying; tape tap toggles from this
5861
+ const mediaElRef = (0,external_react_.useRef)(mediaEl); // latest <audio>/<video>; camera + playhead tick read this
5862
+ const onPlayPauseRef = (0,external_react_.useRef)(onPlayPause); // latest play/pause; tape tap must not re-bind
5863
+ const onSeekRef = (0,external_react_.useRef)(onSeek); // latest onSeek; click/scroll handlers must not re-bind
5864
+ const onViewportChangeRef = (0,external_react_.useRef)(onViewportChange); // latest viewport callback; camera commits here
5865
+ const peaksRef = (0,external_react_.useRef)(peaks); // latest peaks; WaveSurfer create() + morph read this
5866
+
5867
+ const displayedPeaksRef = (0,external_react_.useRef)(null); // peaks currently drawn (for morph)
5868
+ const peakTransitionAnimationRef = (0,external_react_.useRef)(0); // animation frame id while peaks morph in
5659
5869
  // Zoom / pinch / tile tint (read from pointer + WaveSurfer handlers)
5660
- const zoomOriginRef = (0,external_react_.useRef)(null);
5661
- const pinchStartRef = (0,external_react_.useRef)(null);
5662
- const pointerZoomRef = (0,external_react_.useRef)(false);
5663
- const pointerZoomClearTimerRef = (0,external_react_.useRef)(0);
5664
- const applyZoomWindowRef = (0,external_react_.useRef)(null);
5665
- // WaveSurfer `setOptions` fires zoom/scroll before we apply the intended scroll.
5666
- const suppressViewportSyncRef = (0,external_react_.useRef)(false);
5667
- const bufferProgressRef = (0,external_react_.useRef)(0);
5668
- const hoverProgressRef = (0,external_react_.useRef)(null);
5669
- onSeekRef.current = onSeek;
5670
- interactiveRef.current = interactive;
5870
+ const applyZoomWindowRef = (0,external_react_.useRef)(null); // latest applyZoomWindow; resize observer calls this
5871
+ const bufferProgressRef = (0,external_react_.useRef)(0); // latest buffer fill; zoomed tile tint reads this
5872
+ const hideScrubTimeChipTimerRef = (0,external_react_.useRef)(0); // hides the tape scrub time chip after scroll settles
5873
+ const hoverProgressRef = (0,external_react_.useRef)(null); // latest hover fill; zoomed tile tint reads this
5874
+ const lastObservedWidthRef = (0,external_react_.useRef)(0); // last ResizeObserver width; skip no-op resizes
5875
+ const liveHeightRef = (0,external_react_.useRef)(height); // canvas height last applied to WaveSurfer
5876
+ const pinchStartRef = (0,external_react_.useRef)(null); // two-finger distance and zoom at pinch start
5877
+ const pointerZoomClearTimerRef = (0,external_react_.useRef)(0); // clears pointerZoomRef after WAVEFORM_ZOOM_DISMISS_MS
5878
+ const pointerZoomRef = (0,external_react_.useRef)(false); // pinch/wheel in progress; skip play/pause and tape recenter
5879
+ const queuedTapeSeekTimeSecRef = (0,external_react_.useRef)(null); // seek time waiting for the next animation frame
5880
+ const suppressNextTapPlayPauseRef = (0,external_react_.useRef)(false); // swallow play/pause after a swipe or pinch
5881
+ const suppressNextTapPlayPauseTimerRef = (0,external_react_.useRef)(0); // clears suppressNextTapPlayPauseRef
5882
+ const suppressViewportSyncRef = (0,external_react_.useRef)(false); // ignore WaveSurfer zoom/scroll while we setOptions
5883
+ const tapeSeekAnimationRef = (0,external_react_.useRef)(0); // coalesces swipe seeks to one seek per animation frame
5884
+ const toggleTapePlaybackRef = (0,external_react_.useRef)(null); // latest tap play/pause; click/pointerup call this
5885
+ const zoomOriginRef = (0,external_react_.useRef)(null); // pointer+time so zoom keeps that point fixed
5886
+
5887
+ cameraModeRef.current = cameraMode;
5671
5888
  currentTimeRef.current = currentTime;
5672
- peaksRef.current = peaks;
5673
5889
  durationSecRef.current = durationSec;
5890
+ interactiveRef.current = interactive;
5891
+ isPlayingRef.current = isPlaying;
5674
5892
  mediaElRef.current = mediaEl;
5893
+ onPlayPauseRef.current = onPlayPause;
5894
+ onSeekRef.current = onSeek;
5675
5895
  onViewportChangeRef.current = onViewportChange;
5896
+ peaksRef.current = peaks;
5676
5897
  const [internalZoom, setInternalZoom] = (0,external_react_.useState)(constants/* WAVEFORM_ZOOM_MIN */.LW);
5677
5898
  const [hoverProgress, setHoverProgress] = (0,external_react_.useState)(null);
5899
+ const [scrubPreviewTimeSec, setScrubPreviewTimeSec] = (0,external_react_.useState)(null);
5900
+ const [overlayPortalHost, setOverlayPortalHost] = (0,external_react_.useState)(null);
5678
5901
  const [canvasWidthPx, setCanvasWidthPx] = (0,external_react_.useState)(0);
5679
5902
  const [scrollLeft, setScrollLeft] = (0,external_react_.useState)(0);
5680
5903
  const isControlled = typeof zoomLevelProp === 'number';
@@ -5687,19 +5910,22 @@ function WaveformView({
5687
5910
  const zoomLevel = clampWaveformZoom(isControlled ? zoomLevelProp : internalZoom, maxZoom);
5688
5911
  const zoomRef = (0,external_react_.useRef)(zoomLevel); // latest zoom for WaveSurfer redraw/zoom handlers
5689
5912
  zoomRef.current = zoomLevel;
5690
- const prevZoomRef = (0,external_react_.useRef)(null);
5913
+ const prevZoomRef = (0,external_react_.useRef)(null); // last applied zoom; tape recenters only when this changes
5914
+ const isTape = cameraMode === 'tape';
5691
5915
  const isZoomed = zoomLevel > constants/* WAVEFORM_ZOOM_MIN */.LW;
5916
+ const isScrollableWindow = isZoomed || isTape;
5692
5917
  const bufferProgress = getBufferedProgress(bufferedRange, durationSec);
5693
5918
  bufferProgressRef.current = bufferProgress;
5694
5919
  hoverProgressRef.current = hoverProgress;
5695
5920
  const viewport = (0,external_react_.useMemo)(() => createWaveformViewport({
5696
5921
  durationSec,
5922
+ gutterPx: isTape ? getTapeGutterPx(canvasWidthPx) : 0,
5697
5923
  heightPx: height,
5698
5924
  maxZoom,
5699
5925
  scrollLeftPx: scrollLeft,
5700
5926
  widthPx: canvasWidthPx,
5701
5927
  zoomLevel
5702
- }), [canvasWidthPx, durationSec, height, maxZoom, scrollLeft, zoomLevel]);
5928
+ }), [canvasWidthPx, durationSec, height, isTape, maxZoom, scrollLeft, zoomLevel]);
5703
5929
  const viewportRef = (0,external_react_.useRef)(viewport); // live scroll window; prefer this over render-state while the camera is moving
5704
5930
  const onViewportCommit = (0,external_react_.useCallback)((scrollLeftPx, nextViewport, commitReactState = true) => {
5705
5931
  onViewportChangeRef.current?.(nextViewport);
@@ -5714,20 +5940,45 @@ function WaveformView({
5714
5940
  clearFollowPin,
5715
5941
  handleScroll: handleCameraScroll,
5716
5942
  isFollowPinned,
5943
+ isUserPanning,
5717
5944
  onSeek: onPlayheadSeek,
5718
5945
  onZoom,
5719
5946
  releaseUserPanHold,
5720
5947
  seekTo
5721
5948
  } = usePlayheadCamera({
5949
+ cameraMode,
5722
5950
  mediaElRef,
5723
5951
  onViewportCommit,
5724
5952
  playheadRef,
5725
5953
  viewportRef,
5726
5954
  wavesurferRef
5727
5955
  });
5956
+ const toggleTapePlayback = (0,external_react_.useCallback)(() => {
5957
+ if (!interactiveRef.current) {
5958
+ return;
5959
+ }
5960
+ if (isUserPanning() || pointerZoomRef.current) {
5961
+ return;
5962
+ }
5963
+ if (suppressNextTapPlayPauseRef.current) {
5964
+ suppressNextTapPlayPauseRef.current = false;
5965
+ window.clearTimeout(suppressNextTapPlayPauseTimerRef.current);
5966
+ suppressNextTapPlayPauseTimerRef.current = 0;
5967
+ return;
5968
+ }
5969
+ onPlayPauseRef.current?.(!isPlayingRef.current);
5970
+ suppressNextTapPlayPauseRef.current = true;
5971
+ window.clearTimeout(suppressNextTapPlayPauseTimerRef.current);
5972
+ suppressNextTapPlayPauseTimerRef.current = window.setTimeout(() => {
5973
+ suppressNextTapPlayPauseRef.current = false;
5974
+ suppressNextTapPlayPauseTimerRef.current = 0;
5975
+ }, constants/* WAVEFORM_TAPE_CLICK_SUPPRESS_MS */.fI);
5976
+ }, [isUserPanning]);
5977
+ toggleTapePlaybackRef.current = toggleTapePlayback;
5728
5978
  (0,external_react_.useLayoutEffect)(() => {
5729
5979
  viewportRef.current = createWaveformViewport({
5730
5980
  durationSec: viewport.durationSec,
5981
+ gutterPx: viewport.gutterPx,
5731
5982
  heightPx: viewport.heightPx,
5732
5983
  maxZoom: viewport.maxZoom,
5733
5984
  scrollLeftPx: viewportRef.current.scrollLeftPx,
@@ -5750,6 +6001,41 @@ function WaveformView({
5750
6001
  pointerZoomClearTimerRef.current = 0;
5751
6002
  }, constants/* WAVEFORM_ZOOM_DISMISS_MS */.m6);
5752
6003
  }, []);
6004
+
6005
+ /**
6006
+ * Seek the time under the pin (one seek per frame). Swallow the delayed iOS click;
6007
+ * show the scrub chip until the swipe settles.
6008
+ */
6009
+ const handleTapeSwipe = (0,external_react_.useCallback)(timeSec => {
6010
+ // Swallow the click WaveSurfer/iOS fires after a swipe or pinch.
6011
+ suppressNextTapPlayPauseRef.current = true;
6012
+ window.clearTimeout(suppressNextTapPlayPauseTimerRef.current);
6013
+ suppressNextTapPlayPauseTimerRef.current = window.setTimeout(() => {
6014
+ suppressNextTapPlayPauseRef.current = false;
6015
+ suppressNextTapPlayPauseTimerRef.current = 0;
6016
+ }, constants/* WAVEFORM_TAPE_CLICK_SUPPRESS_MS */.fI);
6017
+ queuedTapeSeekTimeSecRef.current = timeSec;
6018
+ if (!tapeSeekAnimationRef.current) {
6019
+ // One seek per frame; later scrolls just update the queued time.
6020
+ tapeSeekAnimationRef.current = window.requestAnimationFrame(() => {
6021
+ tapeSeekAnimationRef.current = 0;
6022
+ const next = queuedTapeSeekTimeSecRef.current;
6023
+ queuedTapeSeekTimeSecRef.current = null;
6024
+ if (next != null) {
6025
+ onSeekRef.current?.(next);
6026
+ }
6027
+ });
6028
+ }
6029
+ if (interactiveRef.current) {
6030
+ // Show the time under the pin; hide once scrolling has settled.
6031
+ setScrubPreviewTimeSec(timeSec);
6032
+ window.clearTimeout(hideScrubTimeChipTimerRef.current);
6033
+ hideScrubTimeChipTimerRef.current = window.setTimeout(() => {
6034
+ setScrubPreviewTimeSec(null);
6035
+ hideScrubTimeChipTimerRef.current = 0;
6036
+ }, constants/* WAVEFORM_FOLLOW_SCROLL_SETTLE_MS */.Zs);
6037
+ }
6038
+ }, []);
5753
6039
  const syncViewport = (0,external_react_.useCallback)(() => {
5754
6040
  const wavesurfer = wavesurferRef.current;
5755
6041
  if (!wavesurfer) {
@@ -5765,14 +6051,16 @@ function WaveformView({
5765
6051
  if (!playhead) {
5766
6052
  return;
5767
6053
  }
5768
- if (!isFollowPinned()) {
6054
+ if (isTape) {
6055
+ playhead.style.left = getTapePinnedPlayheadLeft();
6056
+ } else if (!isFollowPinned()) {
5769
6057
  playhead.style.left = timeLeftPercent(timeSec, durationSecRef.current, viewportRef.current);
5770
6058
  }
5771
6059
  const wavesurfer = wavesurferRef.current;
5772
6060
  if (wavesurfer && wavesurfer.setTime) {
5773
6061
  wavesurfer.setTime(timeSec);
5774
6062
  }
5775
- }, [isFollowPinned]);
6063
+ }, [isFollowPinned, isTape]);
5776
6064
 
5777
6065
  // Same zoom-window body as the zoom effect; extracted so resize can call it too.
5778
6066
  const applyZoomWindow = (0,external_react_.useCallback)(() => {
@@ -5784,11 +6072,13 @@ function WaveformView({
5784
6072
  const viewWidthPx = wavesurfer.getWidth ? wavesurfer.getWidth() : container.clientWidth;
5785
6073
  const minPxPerSec = getZoomedPixelsPerSecond({
5786
6074
  durationSec,
6075
+ isTape,
5787
6076
  maxZoom,
5788
6077
  viewWidthPx,
5789
6078
  zoomLevel
5790
6079
  });
5791
- const origin = zoomOriginRef.current;
6080
+ const gutterPx = isTape ? getTapeGutterPx(viewWidthPx) : 0;
6081
+ const origin = isTape ? null : zoomOriginRef.current;
5792
6082
  zoomOriginRef.current = null;
5793
6083
  const didZoomChange = prevZoomRef.current !== zoomLevel;
5794
6084
  if (origin || didZoomChange) {
@@ -5798,24 +6088,27 @@ function WaveformView({
5798
6088
  try {
5799
6089
  wavesurfer.setOptions(WaveformView_objectSpread({
5800
6090
  autoScroll: false,
6091
+ fillParent: !isTape,
5801
6092
  minPxPerSec
5802
- }, zoomLevel > constants/* WAVEFORM_ZOOM_MIN */.LW ? {
5803
- progressColor: WAVEFORM_COLOR_PLAYED,
6093
+ }, isScrollableWindow ? {
6094
+ progressColor: getPlayedWaveformColor(isTape),
5804
6095
  waveColor: WAVEFORM_COLOR_UNPLAYED
5805
6096
  } : {}));
6097
+ applyWaveformGutters(wavesurfer, gutterPx);
5806
6098
  if (minPxPerSec > 0) {
6099
+ const windowViewport = createWaveformViewport({
6100
+ durationSec,
6101
+ gutterPx,
6102
+ heightPx: liveHeightRef.current || height,
6103
+ maxZoom,
6104
+ scrollLeftPx: 0,
6105
+ widthPx: viewWidthPx,
6106
+ zoomLevel
6107
+ });
5807
6108
  if (origin) {
5808
- applyScrollLeft(origin.timeSec * minPxPerSec - origin.pointerX, true);
5809
- } else if (didZoomChange && !pointerZoomRef.current) {
5810
- const zoomedViewport = createWaveformViewport({
5811
- durationSec,
5812
- heightPx: height,
5813
- maxZoom,
5814
- scrollLeftPx: 0,
5815
- widthPx: viewWidthPx,
5816
- zoomLevel
5817
- });
5818
- applyScrollLeft(Math.min(maxScrollLeft(zoomedViewport), Math.max(0, currentTimeRef.current * minPxPerSec - viewWidthPx / 2)), true);
6109
+ applyScrollLeft(origin.timeSec * minPxPerSec + gutterPx - origin.pointerX, true);
6110
+ } else if (didZoomChange && (isTape || !pointerZoomRef.current)) {
6111
+ applyScrollLeft(getCenteredScrollLeft(currentTimeRef.current, windowViewport), true);
5819
6112
  }
5820
6113
  }
5821
6114
  wavesurfer.setTime(currentTimeRef.current);
@@ -5825,13 +6118,17 @@ function WaveformView({
5825
6118
  }
5826
6119
  syncViewport();
5827
6120
  updatePlayheadPosition(currentTimeRef.current);
5828
- }, [applyScrollLeft, durationSec, height, maxZoom, onZoom, syncViewport, updatePlayheadPosition, zoomLevel]);
6121
+ }, [applyScrollLeft, durationSec, height, isScrollableWindow, isTape, maxZoom, onZoom, syncViewport, updatePlayheadPosition, zoomLevel]);
5829
6122
  applyZoomWindowRef.current = applyZoomWindow;
5830
6123
  (0,external_react_.useEffect)(() => {
5831
6124
  const container = containerRef.current;
5832
6125
  if (!container) {
5833
6126
  return undefined;
5834
6127
  }
6128
+ const measuredHeight = cameraModeRef.current === 'tape' && container.clientHeight > 0 ? Math.round(container.clientHeight) : height;
6129
+ if (cameraModeRef.current === 'tape' && measuredHeight > 0) {
6130
+ liveHeightRef.current = measuredHeight;
6131
+ }
5835
6132
  const wavesurfer = w.create({
5836
6133
  autoScroll: false,
5837
6134
  barGap: constants/* WAVEFORM_BAR_GAP */.Lu,
@@ -5842,25 +6139,34 @@ function WaveformView({
5842
6139
  cursorWidth: 0,
5843
6140
  duration: durationSecRef.current,
5844
6141
  fillParent: true,
5845
- height,
6142
+ height: measuredHeight,
5846
6143
  hideScrollbar: true,
5847
- interact: interactiveRef.current,
6144
+ dragToSeek: false,
6145
+ interact: interactiveRef.current && cameraModeRef.current !== 'tape',
5848
6146
  normalize: false,
5849
6147
  peaks: toChannels(peaksRef.current),
5850
- progressColor: WAVEFORM_COLOR_PLAYED,
6148
+ progressColor: getPlayedWaveformColor(cameraModeRef.current === 'tape'),
5851
6149
  waveColor: WAVEFORM_COLOR_UNPLAYED
5852
6150
  });
5853
6151
  const unsubscribeClick = wavesurfer.on('click', relativeX => {
5854
6152
  if (!interactiveRef.current) {
5855
6153
  return;
5856
6154
  }
6155
+ if (cameraModeRef.current === 'tape') {
6156
+ toggleTapePlaybackRef.current?.();
6157
+ return;
6158
+ }
5857
6159
  onSeekRef.current?.(relativeX * durationSecRef.current);
5858
6160
  });
5859
6161
  const unsubscribeScroll = wavesurfer.on('scroll', () => {
5860
6162
  if (suppressViewportSyncRef.current) {
5861
6163
  return;
5862
6164
  }
5863
- handleCameraScroll(() => {
6165
+ // User pan: map scroll to the time under the playhead, then tape seeks it.
6166
+ handleCameraScroll(timeSec => {
6167
+ if (cameraModeRef.current === 'tape' && !pointerZoomRef.current) {
6168
+ handleTapeSwipe(timeSec);
6169
+ }
5864
6170
  syncViewport();
5865
6171
  });
5866
6172
  });
@@ -5871,12 +6177,13 @@ function WaveformView({
5871
6177
  syncViewport();
5872
6178
  });
5873
6179
  const unsubscribeRedraw = wavesurfer.on('redrawcomplete', () => {
5874
- if (!(zoomRef.current > constants/* WAVEFORM_ZOOM_MIN */.LW)) {
6180
+ if (!(zoomRef.current > constants/* WAVEFORM_ZOOM_MIN */.LW) && cameraModeRef.current !== 'tape') {
5875
6181
  return;
5876
6182
  }
5877
6183
  tintZoomedWaveform(wavesurfer, getWaveformFills({
5878
6184
  bufferProgress: bufferProgressRef.current,
5879
- hoverProgress: hoverProgressRef.current
6185
+ hoverProgress: cameraModeRef.current === 'tape' ? null : hoverProgressRef.current,
6186
+ isTape: cameraModeRef.current === 'tape'
5880
6187
  }), true);
5881
6188
  });
5882
6189
  wavesurferRef.current = wavesurfer;
@@ -5887,6 +6194,9 @@ function WaveformView({
5887
6194
  return () => {
5888
6195
  releaseUserPanHold();
5889
6196
  cancelJump();
6197
+ window.clearTimeout(hideScrubTimeChipTimerRef.current);
6198
+ window.clearTimeout(suppressNextTapPlayPauseTimerRef.current);
6199
+ window.cancelAnimationFrame(tapeSeekAnimationRef.current);
5890
6200
  window.cancelAnimationFrame(peakTransitionAnimationRef.current);
5891
6201
  unsubscribeClick();
5892
6202
  unsubscribeScroll();
@@ -5896,18 +6206,39 @@ function WaveformView({
5896
6206
  wavesurferRef.current = null;
5897
6207
  displayedPeaksRef.current = null;
5898
6208
  };
5899
- }, [cancelJump, handleCameraScroll, height, releaseUserPanHold, syncViewport]);
6209
+ }, [cancelJump, handleCameraScroll, handleTapeSwipe, height, isUserPanning, releaseUserPanHold, syncViewport]);
5900
6210
  (0,external_react_.useLayoutEffect)(() => {
5901
6211
  const el = containerRef.current;
5902
6212
  if (!el) {
5903
6213
  return undefined;
5904
6214
  }
5905
6215
  setCanvasWidthPx(el.clientWidth);
6216
+ lastObservedWidthRef.current = el.clientWidth;
6217
+ const mountedHeight = Math.round(el.clientHeight);
6218
+ if (cameraModeRef.current === 'tape' && mountedHeight > 0) {
6219
+ liveHeightRef.current = mountedHeight;
6220
+ }
5906
6221
  const observer = new ResizeObserver(entries => {
5907
6222
  entries.forEach(entry => {
5908
- setCanvasWidthPx(entry.contentRect.width);
6223
+ const nextWidth = entry.contentRect.width;
6224
+ const widthChanged = nextWidth !== lastObservedWidthRef.current;
6225
+ lastObservedWidthRef.current = nextWidth;
6226
+ setCanvasWidthPx(nextWidth);
6227
+ const nextHeight = Math.round(entry.contentRect.height);
6228
+ if (cameraModeRef.current === 'tape' && Number.isFinite(nextHeight) && nextHeight > 0 && nextHeight !== liveHeightRef.current) {
6229
+ liveHeightRef.current = nextHeight;
6230
+ const wavesurfer = wavesurferRef.current;
6231
+ if (wavesurfer && wavesurfer.setOptions) {
6232
+ wavesurfer.setOptions({
6233
+ height: nextHeight
6234
+ });
6235
+ }
6236
+ }
6237
+ syncViewport();
6238
+ if (widthChanged) {
6239
+ applyZoomWindowRef.current?.();
6240
+ }
5909
6241
  });
5910
- syncViewport();
5911
6242
  });
5912
6243
  observer.observe(el);
5913
6244
  return () => observer.disconnect();
@@ -5932,7 +6263,7 @@ function WaveformView({
5932
6263
  return;
5933
6264
  }
5934
6265
  wavesurfer.setOptions({
5935
- interact: interactive
6266
+ interact: interactive && cameraModeRef.current !== 'tape'
5936
6267
  });
5937
6268
  }, [interactive]);
5938
6269
  (0,external_react_.useLayoutEffect)(() => {
@@ -5963,13 +6294,17 @@ function WaveformView({
5963
6294
  window.cancelAnimationFrame(playheadAnimationRef.current);
5964
6295
  cancelJump();
5965
6296
  releaseUserPanHold();
5966
- clearFollowPin();
6297
+ if (cameraModeRef.current !== 'tape') {
6298
+ clearFollowPin();
6299
+ }
5967
6300
  syncViewport();
5968
6301
  updatePlayheadPosition(media.currentTime);
5969
6302
  };
5970
6303
  const handleSeeked = () => {
5971
6304
  onPlayheadSeek(media.currentTime);
5972
- seekTo(media.currentTime);
6305
+ if (!isUserPanning()) {
6306
+ seekTo(media.currentTime);
6307
+ }
5973
6308
  updatePlayheadPosition(media.currentTime);
5974
6309
  };
5975
6310
  if (!media.paused) {
@@ -5988,7 +6323,7 @@ function WaveformView({
5988
6323
  media.removeEventListener('pause', stopLoop);
5989
6324
  media.removeEventListener('seeked', handleSeeked);
5990
6325
  };
5991
- }, [applyPlayheadCamera, cancelJump, clearFollowPin, mediaEl, onPlayheadSeek, releaseUserPanHold, seekTo, syncViewport, updatePlayheadPosition]);
6326
+ }, [applyPlayheadCamera, cancelJump, clearFollowPin, isUserPanning, mediaEl, onPlayheadSeek, releaseUserPanHold, seekTo, syncViewport, updatePlayheadPosition]);
5992
6327
  (0,external_react_.useEffect)(() => {
5993
6328
  const wavesurfer = wavesurferRef.current;
5994
6329
  if (!wavesurfer || !wavesurfer.setOptions || !(durationSec > 0)) {
@@ -6023,9 +6358,10 @@ function WaveformView({
6023
6358
  }
6024
6359
  const fills = getWaveformFills({
6025
6360
  bufferProgress,
6026
- hoverProgress
6361
+ hoverProgress: isTape ? null : hoverProgress,
6362
+ isTape
6027
6363
  });
6028
- if (isZoomed) {
6364
+ if (isScrollableWindow) {
6029
6365
  tintZoomedWaveform(wavesurfer, fills);
6030
6366
  return;
6031
6367
  }
@@ -6034,7 +6370,7 @@ function WaveformView({
6034
6370
  waveColor: toCanvasFill(fills.waveColor, devicePixelWidth(canvasWidthPx))
6035
6371
  });
6036
6372
  wavesurfer.setTime(currentTimeRef.current);
6037
- }, [bufferProgress, canvasWidthPx, hoverProgress, isZoomed]);
6373
+ }, [bufferProgress, canvasWidthPx, hoverProgress, isScrollableWindow, isTape]);
6038
6374
  (0,external_react_.useEffect)(() => {
6039
6375
  const track = trackRef.current;
6040
6376
  if (!track) {
@@ -6115,7 +6451,7 @@ function WaveformView({
6115
6451
  };
6116
6452
  }, [durationSec, markPointerZoom, setZoomLevel]);
6117
6453
  const onHoverMove = (0,external_react_.useCallback)(event => {
6118
- if (!interactive) {
6454
+ if (!interactive || isTape || isTouchHoverInput(event)) {
6119
6455
  return;
6120
6456
  }
6121
6457
  const rect = event.currentTarget.getBoundingClientRect();
@@ -6132,19 +6468,48 @@ function WaveformView({
6132
6468
  return;
6133
6469
  }
6134
6470
  setHoverProgress(Math.min(1, Math.max(0, pointerX / rect.width)));
6135
- }, [durationSec, interactive]);
6471
+ }, [durationSec, interactive, isTape]);
6136
6472
  const onHoverLeave = (0,external_react_.useCallback)(() => {
6137
6473
  setHoverProgress(null);
6138
6474
  }, []);
6475
+ const bindOverlayPortalHost = (0,external_react_.useCallback)(node => {
6476
+ const host = node?.parentElement ?? null;
6477
+ setOverlayPortalHost(prev => prev === host ? prev : host);
6478
+ }, []);
6139
6479
  const hoverLeft = hoverProgress == null ? null : timeLeftPercent(hoverProgress * durationSec, durationSec, viewportRef.current);
6480
+ const scrubTimeChip = isTape && scrubPreviewTimeSec != null ? /*#__PURE__*/external_react_["default"].createElement("div", {
6481
+ className: "bp-WaveformView-hover bp-WaveformView-hover--tape",
6482
+ "data-testid": "bp-waveform-hover"
6483
+ }, /*#__PURE__*/external_react_["default"].createElement("div", {
6484
+ className: "bp-WaveformView-hoverTime",
6485
+ "data-testid": "bp-waveform-hover-time"
6486
+ }, formatTime(scrubPreviewTimeSec))) : null;
6140
6487
  return /*#__PURE__*/external_react_["default"].createElement("div", {
6141
- className: `bp-WaveformView${interactive ? '' : ' bp-WaveformView--inert'}${isZoomed ? ' bp-WaveformView--zoomed' : ''}`,
6488
+ ref: bindOverlayPortalHost,
6489
+ className: classnames_default()('bp-WaveformView', {
6490
+ 'bp-WaveformView--inert': !interactive,
6491
+ 'bp-WaveformView--tape': isTape,
6492
+ 'bp-WaveformView--zoomed': isZoomed
6493
+ }),
6142
6494
  "data-testid": "bp-waveform-view"
6143
6495
  }, /*#__PURE__*/external_react_["default"].createElement("div", {
6144
6496
  ref: trackRef,
6145
6497
  className: "bp-WaveformView-track",
6146
- onMouseLeave: interactive ? onHoverLeave : undefined,
6147
- onMouseMove: interactive ? onHoverMove : undefined
6498
+ onMouseLeave: interactive && !isTape ? onHoverLeave : undefined,
6499
+ onMouseMove: interactive && !isTape ? onHoverMove : undefined,
6500
+ onPointerMove: interactive && !isTape ? event => {
6501
+ if (event.pointerType === 'touch') {
6502
+ onHoverLeave();
6503
+ return;
6504
+ }
6505
+ onHoverMove(event);
6506
+ } : undefined,
6507
+ onPointerUp: isTape ? event => {
6508
+ if (event.button > 0) {
6509
+ return;
6510
+ }
6511
+ toggleTapePlaybackRef.current?.();
6512
+ } : undefined
6148
6513
  }, /*#__PURE__*/external_react_["default"].createElement("div", {
6149
6514
  ref: containerRef,
6150
6515
  className: "bp-WaveformView-canvas"
@@ -6162,12 +6527,9 @@ function WaveformView({
6162
6527
  }, /*#__PURE__*/external_react_["default"].createElement("div", {
6163
6528
  className: "bp-WaveformView-hoverTime",
6164
6529
  "data-testid": "bp-waveform-hover-time"
6165
- }, formatTime(hoverProgress * durationSec)))));
6530
+ }, formatTime(hoverProgress * durationSec)))), scrubTimeChip && overlayPortalHost ? /*#__PURE__*/__WEBPACK_EXTERNAL_MODULE_react_dom_7dac9eee_createPortal__(scrubTimeChip, overlayPortalHost) : null);
6166
6531
  }
6167
6532
  /* harmony default export */ const waveform_WaveformView = (/*#__PURE__*/external_react_["default"].memo(WaveformView));
6168
- // EXTERNAL MODULE: ./node_modules/classnames/index.js
6169
- var classnames = __webpack_require__(2485);
6170
- var classnames_default = /*#__PURE__*/__webpack_require__.n(classnames);
6171
6533
  ;// ./src/lib/viewers/controls/icons/IconZoomIn24.tsx
6172
6534
 
6173
6535
  function IconZoomIn24() {
@@ -6253,11 +6615,14 @@ function WaveformZoomControl({
6253
6615
  }) {
6254
6616
  const [isHovered, setHovered] = (0,external_react_.useState)(false);
6255
6617
  const [isFocused, setFocused] = (0,external_react_.useState)(false);
6256
- const dismissTimerRef = (0,external_react_.useRef)(0);
6618
+ const [isPinned, setPinned] = (0,external_react_.useState)(false);
6619
+ const wasFlyoutOpenOnPointerDownRef = (0,external_react_.useRef)(true); // first tap on + pins the flyout instead of zooming
6620
+ const zoomControlElRef = (0,external_react_.useRef)(null); // root; outside pointerdown unpins the flyout
6621
+ const dismissTimerRef = (0,external_react_.useRef)(0); // delay before hover-close of the flyout
6257
6622
  const sliderId = `bp-waveform-zoom-slider${(0,external_react_.useId)()}`;
6258
6623
  const zoom = clampWaveformZoom(zoomLevel, maxZoom);
6259
6624
  const zoomValue = Math.round(sliderValueFromZoom(zoom, maxZoom));
6260
- const isOpen = isHovered || isFocused || isRevealed;
6625
+ const isOpen = isHovered || isFocused || isRevealed || isPinned;
6261
6626
  const isAtMinZoom = zoomValue <= 0;
6262
6627
  const isAtMaxZoom = zoomValue >= constants/* WAVEFORM_ZOOM_SLIDER_MAX */.nh;
6263
6628
  const clearDismiss = (0,external_react_.useCallback)(() => {
@@ -6265,13 +6630,42 @@ function WaveformZoomControl({
6265
6630
  dismissTimerRef.current = 0;
6266
6631
  }, []);
6267
6632
  (0,external_react_.useEffect)(() => () => window.clearTimeout(dismissTimerRef.current), []);
6633
+ (0,external_react_.useEffect)(() => {
6634
+ if (!isPinned) {
6635
+ return undefined;
6636
+ }
6637
+ const closeIfOutside = event => {
6638
+ const zoomControlEl = zoomControlElRef.current;
6639
+ if (!zoomControlEl || zoomControlEl.contains(event.target)) {
6640
+ return;
6641
+ }
6642
+ setPinned(false);
6643
+ setFocused(false);
6644
+ setHovered(false);
6645
+ };
6646
+ document.addEventListener('pointerdown', closeIfOutside);
6647
+ return () => document.removeEventListener('pointerdown', closeIfOutside);
6648
+ }, [isPinned]);
6268
6649
  const handleSlider = (0,external_react_.useCallback)(newValue => {
6269
6650
  onZoomChange(zoomFromSliderValue(newValue, maxZoom));
6270
6651
  }, [maxZoom, onZoomChange]);
6271
6652
  const handleStep = (0,external_react_.useCallback)(delta => {
6272
6653
  onZoomChange(zoomFromSliderValue(zoomValue + delta, maxZoom));
6273
6654
  }, [maxZoom, onZoomChange, zoomValue]);
6655
+ const handleZoomInPointerDown = (0,external_react_.useCallback)(() => {
6656
+ wasFlyoutOpenOnPointerDownRef.current = isHovered || isFocused || isRevealed || isPinned;
6657
+ }, [isFocused, isHovered, isPinned, isRevealed]);
6658
+ const handleZoomInClick = (0,external_react_.useCallback)(() => {
6659
+ if (!wasFlyoutOpenOnPointerDownRef.current) {
6660
+ setPinned(true);
6661
+ return;
6662
+ }
6663
+ if (!isAtMaxZoom) {
6664
+ handleStep(constants/* WAVEFORM_ZOOM_BUTTON_STEP */.QD);
6665
+ }
6666
+ }, [handleStep, isAtMaxZoom]);
6274
6667
  return /*#__PURE__*/external_react_["default"].createElement("div", {
6668
+ ref: zoomControlElRef,
6275
6669
  "aria-label": "Zoom",
6276
6670
  className: classnames_default()('bp-WaveformZoomControl', {
6277
6671
  'bp-is-open': isOpen
@@ -6282,6 +6676,7 @@ function WaveformZoomControl({
6282
6676
  return;
6283
6677
  }
6284
6678
  setFocused(false);
6679
+ setPinned(false);
6285
6680
  },
6286
6681
  onFocus: () => {
6287
6682
  clearDismiss();
@@ -6299,15 +6694,12 @@ function WaveformZoomControl({
6299
6694
  },
6300
6695
  role: "group"
6301
6696
  }, /*#__PURE__*/external_react_["default"].createElement(MediaToggle/* default */.A, {
6302
- "aria-disabled": isAtMaxZoom,
6697
+ "aria-disabled": isOpen && isAtMaxZoom,
6303
6698
  className: "bp-WaveformZoomControl-button",
6304
6699
  "data-resin-target": "waveformZoomIn",
6305
6700
  "data-testid": "bp-waveform-zoom-in",
6306
- onClick: () => {
6307
- if (!isAtMaxZoom) {
6308
- handleStep(constants/* WAVEFORM_ZOOM_BUTTON_STEP */.QD);
6309
- }
6310
- },
6701
+ onClick: handleZoomInClick,
6702
+ onPointerDown: handleZoomInPointerDown,
6311
6703
  title: "Zoom in"
6312
6704
  }, /*#__PURE__*/external_react_["default"].createElement(icons_IconZoomIn24, null)), /*#__PURE__*/external_react_["default"].createElement("div", {
6313
6705
  "aria-hidden": !isOpen,
@@ -6361,6 +6753,7 @@ function WaveformZoomControl({
6361
6753
 
6362
6754
 
6363
6755
 
6756
+
6364
6757
  const PLACEHOLDER_PEAKS = placeholderPeaks();
6365
6758
  function MP3ControlsV2({
6366
6759
  autoplay,
@@ -6388,12 +6781,15 @@ function MP3ControlsV2({
6388
6781
  const [zoomLevel, setZoomLevel] = (0,external_react_.useState)(constants/* WAVEFORM_ZOOM_MIN */.LW);
6389
6782
  const [maxZoom, setMaxZoom] = (0,external_react_.useState)(constants/* WAVEFORM_ZOOM_MIN */.LW);
6390
6783
  const [isZoomRevealed, setIsZoomRevealed] = (0,external_react_.useState)(false);
6391
- const zoomRevealTimerRef = (0,external_react_.useRef)(0);
6784
+ const zoomRevealTimerRef = (0,external_react_.useRef)(0); // hides the zoom flyout after WAVEFORM_ZOOM_DISMISS_MS
6392
6785
  const hasRealPeaks = !!(peaks && peaks.length);
6393
6786
  const waveformPeaks = hasRealPeaks ? peaks : PLACEHOLDER_PEAKS;
6394
6787
  const waveformDurationSec = hasWaveformDuration ? durationValue : PLACEHOLDER_DURATION_SEC;
6395
6788
  const [playRequested, setPlayRequested] = (0,external_react_.useState)(false);
6396
6789
  const [viewport, setViewport] = (0,external_react_.useState)(null);
6790
+ const isTape = useTapeWaveform();
6791
+ const hasAppliedTapeDefaultZoomRef = (0,external_react_.useRef)(false); // ~10s window applied; reset to 1× when leaving tape
6792
+ const userChangedTapeZoomRef = (0,external_react_.useRef)(false); // pinch/wheel zoom; skip re-applying the 10s default
6397
6793
  const handleViewportChange = (0,external_react_.useCallback)(next => {
6398
6794
  setMaxZoom(prev => prev === next.maxZoom ? prev : next.maxZoom);
6399
6795
  setViewport(prev => viewportEquals(prev, next) ? prev : next);
@@ -6407,12 +6803,33 @@ function MP3ControlsV2({
6407
6803
  }, constants/* WAVEFORM_ZOOM_DISMISS_MS */.m6);
6408
6804
  }, []);
6409
6805
  const handleWaveformZoom = (0,external_react_.useCallback)(nextZoom => {
6806
+ userChangedTapeZoomRef.current = true;
6410
6807
  setZoomLevel(nextZoom);
6411
6808
  revealZoomControl();
6412
6809
  }, [revealZoomControl]);
6413
6810
  (0,external_react_.useEffect)(() => {
6414
6811
  setZoomLevel(prev => clampWaveformZoom(prev, maxZoom));
6415
6812
  }, [maxZoom]);
6813
+ (0,external_react_.useEffect)(() => {
6814
+ if (!isTape) {
6815
+ if (hasAppliedTapeDefaultZoomRef.current) {
6816
+ hasAppliedTapeDefaultZoomRef.current = false;
6817
+ userChangedTapeZoomRef.current = false;
6818
+ setZoomLevel(constants/* WAVEFORM_ZOOM_MIN */.LW);
6819
+ }
6820
+ return;
6821
+ }
6822
+ if (!hasWaveformDuration || !hasRealPeaks || !viewport || !(viewport.widthPx > 0)) {
6823
+ return;
6824
+ }
6825
+ if (userChangedTapeZoomRef.current) {
6826
+ hasAppliedTapeDefaultZoomRef.current = true;
6827
+ return;
6828
+ }
6829
+ const nextZoom = getTapeDefaultZoom(durationValue, Math.max(maxZoom, viewport.maxZoom));
6830
+ hasAppliedTapeDefaultZoomRef.current = true;
6831
+ setZoomLevel(prev => prev === nextZoom ? prev : nextZoom);
6832
+ }, [durationValue, hasRealPeaks, hasWaveformDuration, isTape, maxZoom, viewport]);
6416
6833
  (0,external_react_.useEffect)(() => () => window.clearTimeout(zoomRevealTimerRef.current), []);
6417
6834
  (0,external_react_.useEffect)(() => {
6418
6835
  if (isPlaying) {
@@ -6441,6 +6858,7 @@ function MP3ControlsV2({
6441
6858
  const showPlayOverlay = !playRequested && !isPlaying;
6442
6859
  const hasZoomHandlers = hasRealPeaks && !showPlayOverlay;
6443
6860
  const hasZoomControl = hasZoomHandlers && hasMediaMetadata && maxZoom > constants/* WAVEFORM_ZOOM_MIN */.LW;
6861
+ const waveformZoomLevel = isTape || hasZoomHandlers ? zoomLevel : constants/* WAVEFORM_ZOOM_MIN */.LW;
6444
6862
  return /*#__PURE__*/external_react_["default"].createElement("div", {
6445
6863
  className: "bp-MP3ControlsV2",
6446
6864
  "data-testid": "media-controls-wrapper-v2"
@@ -6450,18 +6868,22 @@ function MP3ControlsV2({
6450
6868
  className: "bp-MP3ControlsV2-waveform"
6451
6869
  }, /*#__PURE__*/external_react_["default"].createElement(waveform_WaveformView, {
6452
6870
  bufferedRange: bufferedRange,
6871
+ cameraMode: isTape ? 'tape' : 'desktop',
6453
6872
  currentTime: currentTime,
6454
6873
  durationSec: waveformDurationSec,
6455
6874
  interactive: isWaveformInteractive,
6875
+ isPlaying: isPlaying,
6456
6876
  mediaEl: mediaEl,
6877
+ onPlayPause: isWaveformInteractive ? onPlayPause : undefined,
6457
6878
  onSeek: isWaveformInteractive ? onTimeChange : undefined,
6458
6879
  onViewportChange: hasRealPeaks ? handleViewportChange : undefined,
6459
6880
  onZoomChange: hasZoomHandlers ? handleWaveformZoom : undefined,
6460
6881
  peaks: waveformPeaks,
6461
- zoomLevel: hasZoomHandlers ? zoomLevel : constants/* WAVEFORM_ZOOM_MIN */.LW
6882
+ zoomLevel: waveformZoomLevel
6462
6883
  }), /*#__PURE__*/external_react_["default"].createElement(WaveformCommentMarkers, {
6463
6884
  commentMarkers: waveformMarkers,
6464
6885
  durationSec: hasWaveformDuration ? durationValue : 0,
6886
+ isTape: isTape,
6465
6887
  onCommentMarkerClick: handleCommentMarkerClick,
6466
6888
  selectedId: selectedMarkerId,
6467
6889
  viewport: viewport
@@ -6470,7 +6892,7 @@ function MP3ControlsV2({
6470
6892
  }, /*#__PURE__*/external_react_["default"].createElement(WaveformZoomControl, {
6471
6893
  isRevealed: isZoomRevealed,
6472
6894
  maxZoom: maxZoom,
6473
- onZoomChange: setZoomLevel,
6895
+ onZoomChange: handleWaveformZoom,
6474
6896
  zoomLevel: zoomLevel
6475
6897
  })), showPlayOverlay && /*#__PURE__*/external_react_["default"].createElement("button", {
6476
6898
  className: "bp-MP3ControlsV2-playOverlay"
@@ -6590,6 +7012,7 @@ function isFpsAvailable(player) {
6590
7012
  (__unused_webpack_module, __webpack_exports__, __webpack_require__) {
6591
7013
 
6592
7014
  /* harmony export */ __webpack_require__.d(__webpack_exports__, {
7015
+ /* harmony export */ Av: () => (/* binding */ WAVEFORM_TAPE_DEFAULT_WINDOW_SEC),
6593
7016
  /* harmony export */ Bq: () => (/* binding */ WAVEFORM_PLAYHEAD_JUMP_MS),
6594
7017
  /* harmony export */ DY: () => (/* binding */ WAVEFORM_BAR_RADIUS),
6595
7018
  /* harmony export */ EB: () => (/* binding */ CLIENT_DECODE_MAX_COMPRESSED_BYTES),
@@ -6597,12 +7020,14 @@ function isFpsAvailable(player) {
6597
7020
  /* harmony export */ GQ: () => (/* binding */ DURATION_MISMATCH_TOLERANCE_SEC),
6598
7021
  /* harmony export */ Kl: () => (/* binding */ WAVEFORM_MIN_VIEW_WINDOW_SEC),
6599
7022
  /* harmony export */ LW: () => (/* binding */ WAVEFORM_ZOOM_MIN),
7023
+ /* harmony export */ Le: () => (/* binding */ TAPE_POINTER_MEDIA_QUERY),
6600
7024
  /* harmony export */ Lu: () => (/* binding */ WAVEFORM_BAR_GAP),
6601
7025
  /* harmony export */ NM: () => (/* binding */ WAVEFORM_FOLLOW_INSET_PX),
6602
7026
  /* harmony export */ QD: () => (/* binding */ WAVEFORM_ZOOM_BUTTON_STEP),
6603
7027
  /* harmony export */ XS: () => (/* binding */ WAVEFORM_BAR_MIN_HEIGHT),
6604
7028
  /* harmony export */ Zs: () => (/* binding */ WAVEFORM_FOLLOW_SCROLL_SETTLE_MS),
6605
7029
  /* harmony export */ f3: () => (/* binding */ CLIENT_DECODE_PEAK_COUNT),
7030
+ /* harmony export */ fI: () => (/* binding */ WAVEFORM_TAPE_CLICK_SUPPRESS_MS),
6606
7031
  /* harmony export */ i8: () => (/* binding */ PEAK_UNIT_MAX),
6607
7032
  /* harmony export */ m6: () => (/* binding */ WAVEFORM_ZOOM_DISMISS_MS),
6608
7033
  /* harmony export */ mJ: () => (/* binding */ PEAK_UNIT_MIN),
@@ -6642,6 +7067,10 @@ const WAVEFORM_ZOOM_MIN = 1;
6642
7067
  const WAVEFORM_ZOOM_MAX = 24;
6643
7068
  /** Visible window never shorter than this. */
6644
7069
  const WAVEFORM_MIN_VIEW_WINDOW_SEC = 4;
7070
+ /** Tape (phone/tablet) default: seconds of audio visible in the viewport. */
7071
+ const WAVEFORM_TAPE_DEFAULT_WINDOW_SEC = 10;
7072
+ /** Primary pointer is a finger, not a mouse/trackpad. */
7073
+ const TAPE_POINTER_MEDIA_QUERY = '(hover: none) and (pointer: coarse)';
6645
7074
  const WAVEFORM_ZOOM_SLIDER_MAX = 100;
6646
7075
  /** One click on zoom in/out moves this many units on the 0–100 slider. */
6647
7076
  const WAVEFORM_ZOOM_BUTTON_STEP = 10;
@@ -6650,6 +7079,8 @@ const WAVEFORM_FOLLOW_INSET_PX = 200;
6650
7079
  const WAVEFORM_PLAYHEAD_JUMP_MS = 400;
6651
7080
  /** After the last user pan, wait this long before the camera may pin again. */
6652
7081
  const WAVEFORM_FOLLOW_SCROLL_SETTLE_MS = 150;
7082
+ /** Ignore a WaveSurfer click after a tape swipe (iOS delayed click). Must outlast the scrub-chip hide. */
7083
+ const WAVEFORM_TAPE_CLICK_SUPPRESS_MS = 200;
6653
7084
  const WAVEFORM_BAR_GAP = 2;
6654
7085
  const WAVEFORM_BAR_WIDTH = 2;
6655
7086
  const WAVEFORM_BAR_RADIUS = WAVEFORM_BAR_WIDTH / 2;
@@ -6701,6 +7132,12 @@ function isWaveformErrorCode(value) {
6701
7132
 
6702
7133
  /** Visible slice of the timeline. Emitted whenever zoom, scroll, or width changes. */
6703
7134
 
7135
+ /** Navigator fields used to detect iPad, including iPadOS reporting itself as Mac. */
7136
+
7137
+ /** Window bits needed to choose tape vs desktop (coarse pointer + iPad). */
7138
+
7139
+ /** Desktop walks then pins near the right inset. Tape keeps the playhead at center. */
7140
+
6704
7141
  /** One canvas linear-gradient stop. `offset` is 0–1 along the bar. */
6705
7142
 
6706
7143
  /**
@@ -23055,7 +23492,7 @@ class Browser {
23055
23492
  ;// ./src/lib/Logger.js
23056
23493
  /* eslint-disable no-undef */
23057
23494
  const CLIENT_NAME = "box-content-preview";
23058
- const CLIENT_VERSION = "3.90.0";
23495
+ const CLIENT_VERSION = "3.91.0";
23059
23496
  /* eslint-enable no-undef */
23060
23497
 
23061
23498
  class Logger {
@@ -34434,10 +34871,9 @@ class MediaBaseViewer extends viewers_BaseViewer {
34434
34871
  const template = this.options.representation.content.url_template;
34435
34872
  this.mediaEl.addEventListener('error', this.errorHandler);
34436
34873
  this.mediaEl.setAttribute('title', this.options.file.name);
34437
- if (lib_Browser.isIOS()) {
34438
- // iOS doesn't fire loadeddata event until some data loads
34439
- // Adding autoplay prevents this but won't actually autoplay the video.
34440
- // https://webkit.org/blog/6784/new-video-policies-for-ios/
34874
+ if (lib_Browser.isIOS() && this.mediaEl.tagName === 'VIDEO') {
34875
+ // Unblocks loadeddata on iOS <video>. Do not set this on <audio>:
34876
+ // after tap-to-open, Safari will start playback.
34441
34877
  this.mediaEl.autoplay = true;
34442
34878
  }
34443
34879
  const contentUrl = this.createContentUrlV2(template);
@@ -35930,7 +36366,7 @@ class MP3Viewer extends media_MediaBaseViewer {
35930
36366
  * @return {Promise<{ default: Function }>} MP3ControlsV2 module
35931
36367
  */
35932
36368
  importV2Controls() {
35933
- return Promise.resolve(/* import() */).then(__webpack_require__.bind(__webpack_require__, 6180));
36369
+ return Promise.resolve(/* import() */).then(__webpack_require__.bind(__webpack_require__, 2668));
35934
36370
  }
35935
36371
 
35936
36372
  /**