box-content-preview 3.90.0 → 3.92.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.92.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 = {};
@@ -2915,10 +2916,10 @@ function TimestampControl({
2915
2916
 
2916
2917
  // EXPORTS
2917
2918
  __webpack_require__.d(__webpack_exports__, {
2918
- A: () => (/* binding */ VolumeControls)
2919
+ Ay: () => (/* binding */ VolumeControls)
2919
2920
  });
2920
2921
 
2921
- // UNUSED EXPORTS: getIcon
2922
+ // UNUSED EXPORTS: VOLUME_FLYOUT_DISMISS_MS, getIcon
2922
2923
 
2923
2924
  // EXTERNAL MODULE: external "react"
2924
2925
  var external_react_ = __webpack_require__(1649);
@@ -2980,12 +2981,26 @@ function IconVolumeMuted24(props) {
2980
2981
  var MediaToggle = __webpack_require__(8997);
2981
2982
  ;// ./src/lib/viewers/controls/hooks/useAttention.ts
2982
2983
 
2984
+ /** True when focus/hover is moving to a descendant, not leaving the host. */
2985
+ function isRelatedTargetInside(event) {
2986
+ return event.currentTarget.contains(event.relatedTarget);
2987
+ }
2983
2988
  function useAttention() {
2984
2989
  const [isFocused, setFocused] = external_react_.useState(false);
2985
2990
  const [isHovered, setHovered] = external_react_.useState(false);
2986
- const handleBlur = () => setFocused(false);
2991
+ const handleBlur = event => {
2992
+ if (isRelatedTargetInside(event)) {
2993
+ return;
2994
+ }
2995
+ setFocused(false);
2996
+ };
2987
2997
  const handleFocus = () => setFocused(true);
2988
- const handleMouseOut = () => setHovered(false);
2998
+ const handleMouseOut = event => {
2999
+ if (isRelatedTargetInside(event)) {
3000
+ return;
3001
+ }
3002
+ setHovered(false);
3003
+ };
2989
3004
  const handleMouseOver = () => setHovered(true);
2990
3005
  return [isFocused || isHovered, {
2991
3006
  onBlur: handleBlur,
@@ -3022,7 +3037,7 @@ var util = __webpack_require__(4410);
3022
3037
  // extracted by mini-css-extract-plugin
3023
3038
 
3024
3039
  ;// ./src/lib/viewers/controls/slider/VolumeSliderControl.tsx
3025
- const _excluded = ["className", "max", "min", "onMove", "onUpdate", "step", "title", "value", "onMouseOver"];
3040
+ const _excluded = ["className", "max", "min", "onMove", "onUpdate", "step", "title", "value", "onMouseOver", "tabIndex"];
3026
3041
  function VolumeSliderControl_extends() { return VolumeSliderControl_extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, VolumeSliderControl_extends.apply(null, arguments); }
3027
3042
  function _objectWithoutProperties(e, t) { if (null == e) return {}; var o, r, i = _objectWithoutPropertiesLoose(e, t); if (Object.getOwnPropertySymbols) { var n = Object.getOwnPropertySymbols(e); for (r = 0; r < n.length; r++) o = n[r], -1 === t.indexOf(o) && {}.propertyIsEnumerable.call(e, o) && (i[o] = e[o]); } return i; }
3028
3043
  function _objectWithoutPropertiesLoose(r, e) { if (null == r) return {}; var t = {}; for (var n in r) if ({}.hasOwnProperty.call(r, n)) { if (-1 !== e.indexOf(n)) continue; t[n] = r[n]; } return t; }
@@ -3048,7 +3063,8 @@ function VolumeSliderControl(_ref) {
3048
3063
  step = 1,
3049
3064
  title,
3050
3065
  value,
3051
- onMouseOver
3066
+ onMouseOver,
3067
+ tabIndex = 0
3052
3068
  } = _ref,
3053
3069
  rest = _objectWithoutProperties(_ref, _excluded);
3054
3070
  const [isScrubbing, setIsScrubbing] = external_react_["default"].useState(false);
@@ -3159,33 +3175,29 @@ function VolumeSliderControl(_ref) {
3159
3175
  className: "bp-VolumeVerticalSliderControl-track-container"
3160
3176
  }, /*#__PURE__*/external_react_["default"].createElement("div", {
3161
3177
  ref: sliderElRef,
3162
- className: "bp-VolumeVerticalSliderControl-track-background",
3163
- onMouseDown: handleMouseDown,
3164
- role: "button",
3165
- tabIndex: 0
3166
- }, /*#__PURE__*/external_react_["default"].createElement("div", {
3167
3178
  "aria-label": title,
3168
3179
  "aria-valuemax": max,
3169
3180
  "aria-valuemin": min,
3170
3181
  "aria-valuenow": value,
3171
- className: "bp-VolumeVerticalSliderControl-track",
3182
+ className: "bp-VolumeVerticalSliderControl-track-background",
3172
3183
  "data-resin-target": "volumeSlider",
3173
3184
  "data-testid": "bp-volume-slider-control-track",
3174
- onFocus: onMouseOver,
3185
+ onFocus: (noop_default()),
3175
3186
  onKeyDown: handleKeydown,
3176
3187
  onMouseDown: handleMouseDown,
3177
3188
  onMouseMove: handleMouseMove,
3178
3189
  onMouseOver: onMouseOver,
3179
3190
  onTouchStart: handleTouchStart,
3180
3191
  role: "slider",
3192
+ tabIndex: tabIndex
3193
+ }, /*#__PURE__*/external_react_["default"].createElement("div", {
3194
+ className: "bp-VolumeVerticalSliderControl-track",
3181
3195
  style: {
3182
3196
  height: `${heightValueBasedOnVolume / max * 100}%`
3183
- },
3184
- tabIndex: 0
3197
+ }
3185
3198
  }))));
3186
3199
  }
3187
3200
  ;// ./src/lib/viewers/controls/media/VolumeControls.tsx
3188
- function VolumeControls_extends() { return VolumeControls_extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, VolumeControls_extends.apply(null, arguments); }
3189
3201
 
3190
3202
 
3191
3203
 
@@ -3196,6 +3208,9 @@ function VolumeControls_extends() { return VolumeControls_extends = Object.assig
3196
3208
 
3197
3209
 
3198
3210
 
3211
+
3212
+ /** Keyboard volume keeps the flyout open longer than the 200ms max-width transition. */
3213
+ const VOLUME_FLYOUT_DISMISS_MS = 1000;
3199
3214
  function getIcon(volume) {
3200
3215
  let Icon = icons_IconVolumeMuted24;
3201
3216
  if (volume >= 0.66) {
@@ -3208,16 +3223,32 @@ function getIcon(volume) {
3208
3223
  return Icon;
3209
3224
  }
3210
3225
  function VolumeControls({
3226
+ keyboardVolumeStep = 0,
3211
3227
  onMuteChange,
3212
3228
  onVolumeChange,
3213
3229
  volume = 1
3214
3230
  }) {
3215
3231
  const [isActive, handlers] = useAttention();
3232
+ const [isRevealed, setIsRevealed] = (0,external_react_.useState)(false);
3233
+ const revealTimerRef = (0,external_react_.useRef)(0);
3216
3234
  const isMuted = !volume;
3217
3235
  const Icon = isMuted ? icons_IconVolumeMuted24 : getIcon(volume);
3218
3236
  const title = isMuted ? "Unmute" : "Mute";
3219
3237
  const value = Math.round(volume * 100);
3220
- const handleVolume = external_react_["default"].useCallback(newValue => {
3238
+ const isOpen = isActive || isRevealed;
3239
+ (0,external_react_.useEffect)(() => {
3240
+ if (!keyboardVolumeStep) {
3241
+ return;
3242
+ }
3243
+ setIsRevealed(true);
3244
+ window.clearTimeout(revealTimerRef.current);
3245
+ revealTimerRef.current = window.setTimeout(() => {
3246
+ setIsRevealed(false);
3247
+ revealTimerRef.current = 0;
3248
+ }, VOLUME_FLYOUT_DISMISS_MS);
3249
+ }, [keyboardVolumeStep]);
3250
+ (0,external_react_.useEffect)(() => () => window.clearTimeout(revealTimerRef.current), []);
3251
+ const handleVolume = (0,external_react_.useCallback)(newValue => {
3221
3252
  const newValueToUse = newValue <= 5 ? 0 : newValue;
3222
3253
  // make sure the value is always between 0 and 1 since the value passed in could be
3223
3254
  // greater than 100 if the user drags the mouse above the top of the sider when adjusting the volume
@@ -3227,27 +3258,39 @@ function VolumeControls({
3227
3258
  return /*#__PURE__*/external_react_["default"].createElement("div", {
3228
3259
  className: "bp-VolumeControls",
3229
3260
  "data-testid": "bp-volume-controls",
3230
- onBlur: handlers.onBlur,
3261
+ onBlur: event => {
3262
+ handlers.onBlur(event);
3263
+ if (event.currentTarget.contains(event.relatedTarget)) {
3264
+ return;
3265
+ }
3266
+ window.clearTimeout(revealTimerRef.current);
3267
+ revealTimerRef.current = 0;
3268
+ setIsRevealed(false);
3269
+ },
3231
3270
  onFocus: handlers.onFocus,
3232
3271
  onMouseOut: handlers.onMouseOut,
3233
3272
  onMouseOver: handlers.onMouseOver
3234
- }, /*#__PURE__*/external_react_["default"].createElement(MediaToggle/* default */.A, VolumeControls_extends({
3273
+ }, /*#__PURE__*/external_react_["default"].createElement(MediaToggle/* default */.A, {
3235
3274
  className: "bp-VolumeControls-toggle",
3236
3275
  "data-resin-target": "volumeToggle",
3237
3276
  onClick: () => onMuteChange(!isMuted),
3238
3277
  title: title
3239
- }, handlers), /*#__PURE__*/external_react_["default"].createElement(Icon, null)), /*#__PURE__*/external_react_["default"].createElement("div", {
3278
+ }, /*#__PURE__*/external_react_["default"].createElement(Icon, null)), /*#__PURE__*/external_react_["default"].createElement("div", {
3279
+ "aria-hidden": !isOpen,
3240
3280
  className: classnames_default()('bp-VolumeControls-flyout', {
3241
- 'bp-is-open': isActive
3281
+ 'bp-is-open': isOpen
3242
3282
  })
3243
- }, /*#__PURE__*/external_react_["default"].createElement(VolumeSliderControl, VolumeControls_extends({
3283
+ }, /*#__PURE__*/external_react_["default"].createElement(VolumeSliderControl, {
3284
+ "aria-hidden": !isOpen,
3244
3285
  className: "bp-VolumeControls-slider",
3245
3286
  max: 100,
3287
+ onMouseOver: handlers.onMouseOver,
3246
3288
  onUpdate: handleVolume,
3247
3289
  step: 1,
3290
+ tabIndex: isOpen ? 0 : -1,
3248
3291
  title: "Volume Slider",
3249
3292
  value: value
3250
- }, handlers))));
3293
+ })));
3251
3294
  }
3252
3295
 
3253
3296
  /***/ },
@@ -4487,7 +4530,7 @@ function getPdfjsWorkerSrc() {
4487
4530
 
4488
4531
  /***/ },
4489
4532
 
4490
- /***/ 6180
4533
+ /***/ 7450
4491
4534
  (__unused_webpack_module, __webpack_exports__, __webpack_require__) {
4492
4535
 
4493
4536
 
@@ -4621,6 +4664,7 @@ function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e =
4621
4664
  /** Visible window: start/end times, pixels-per-second, and the current zoom. */
4622
4665
  function createWaveformViewport({
4623
4666
  durationSec,
4667
+ gutterPx = 0,
4624
4668
  heightPx,
4625
4669
  maxZoom,
4626
4670
  scrollLeftPx,
@@ -4630,11 +4674,16 @@ function createWaveformViewport({
4630
4674
  const zoom = Number.isFinite(zoomLevel) && zoomLevel > 0 ? zoomLevel : constants/* WAVEFORM_ZOOM_MIN */.LW;
4631
4675
  const viewDurationSec = durationSec > 0 ? durationSec / zoom : 0; // seconds visible at this zoom
4632
4676
  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;
4677
+ const leadingGutterPx = Number.isFinite(gutterPx) && gutterPx > 0 ? gutterPx : 0;
4678
+ const unclampedStartSec = pixelsPerSecond > 0 ? (scrollLeftPx - leadingGutterPx) / pixelsPerSecond : 0;
4679
+ const gutterDurationSec = pixelsPerSecond > 0 ? leadingGutterPx / pixelsPerSecond : 0;
4680
+ const latestWindowStartSec = leadingGutterPx > 0 ? durationSec + gutterDurationSec - viewDurationSec : Math.max(0, durationSec - viewDurationSec);
4681
+ const earliestWindowStartSec = leadingGutterPx > 0 ? -gutterDurationSec : 0;
4682
+ const startSec = pixelsPerSecond > 0 ? Math.min(latestWindowStartSec, Math.max(earliestWindowStartSec, unclampedStartSec)) : 0;
4635
4683
  return {
4636
4684
  durationSec,
4637
4685
  endSec: startSec + viewDurationSec,
4686
+ gutterPx: leadingGutterPx,
4638
4687
  heightPx,
4639
4688
  maxZoom,
4640
4689
  pixelsPerSecond,
@@ -4654,7 +4703,7 @@ function getViewportAtScroll(viewport, scrollLeftPx) {
4654
4703
 
4655
4704
  /** True when the visible window has not moved — skip a React state update. */
4656
4705
  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;
4706
+ 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
4707
  }
4659
4708
 
4660
4709
  /** How many CSS pixels from the left of the visible window this time sits. */
@@ -4701,17 +4750,34 @@ function clampWaveformZoom(zoomLevel, maxZoom = constants/* WAVEFORM_ZOOM_MIN */
4701
4750
  /** WaveSurfer zoom density. 0 = fit the whole file in the view. */
4702
4751
  function getZoomedPixelsPerSecond({
4703
4752
  durationSec,
4753
+ isTape = false,
4704
4754
  maxZoom = constants/* WAVEFORM_ZOOM_MIN */.LW,
4705
4755
  viewWidthPx,
4706
4756
  zoomLevel
4707
4757
  }) {
4708
4758
  const zoom = clampWaveformZoom(zoomLevel, maxZoom);
4709
- if (zoom <= constants/* WAVEFORM_ZOOM_MIN */.LW || !(durationSec > 0) || !(viewWidthPx > 0)) {
4759
+ if (!(durationSec > 0) || !(viewWidthPx > 0)) {
4760
+ return 0;
4761
+ }
4762
+ if (!isTape && zoom <= constants/* WAVEFORM_ZOOM_MIN */.LW) {
4710
4763
  return 0;
4711
4764
  }
4712
4765
  return viewWidthPx / durationSec * zoom;
4713
4766
  }
4714
4767
 
4768
+ /** Half-view empty space so t=0 and duration can sit under the center pin. */
4769
+ function getTapeGutterPx(widthPx) {
4770
+ return widthPx > 0 ? widthPx / 2 : 0;
4771
+ }
4772
+
4773
+ /** Tape first paint: 10s on screen, or 1× when the file is shorter than that. */
4774
+ function getTapeDefaultZoom(durationSec, maxZoom = constants/* WAVEFORM_ZOOM_MIN */.LW) {
4775
+ if (!(durationSec > 0)) {
4776
+ return constants/* WAVEFORM_ZOOM_MIN */.LW;
4777
+ }
4778
+ return clampWaveformZoom(durationSec / constants/* WAVEFORM_TAPE_DEFAULT_WINDOW_SEC */.Av, maxZoom);
4779
+ }
4780
+
4715
4781
  /** Map zoom (1…max) onto the 0–100 slider. */
4716
4782
  function sliderValueFromZoom(zoomLevel, maxZoom = constants/* WAVEFORM_ZOOM_MIN */.LW) {
4717
4783
  const max = Math.max(constants/* WAVEFORM_ZOOM_MIN */.LW, maxZoom);
@@ -4732,9 +4798,15 @@ function zoomFromSliderValue(value, maxZoom = constants/* WAVEFORM_ZOOM_MIN */.L
4732
4798
  return clampWaveformZoom(constants/* WAVEFORM_ZOOM_MIN */.LW + t * (max - constants/* WAVEFORM_ZOOM_MIN */.LW), max);
4733
4799
  }
4734
4800
 
4735
- /** Max scroll that still shows a full window (no overscroll). */
4801
+ /** One zoom-in/out button step on the 0–100 slider, mapped back to zoom. */
4802
+ function stepWaveformZoom(zoomLevel, maxZoom, sliderDelta) {
4803
+ return zoomFromSliderValue(Math.round(sliderValueFromZoom(zoomLevel, maxZoom)) + sliderDelta, maxZoom);
4804
+ }
4805
+
4806
+ /** Max scroll that still shows a full window (no overscroll). Gutters add empty lead/trail. */
4736
4807
  function maxScrollLeft(viewport) {
4737
- return Math.max(0, viewport.durationSec * viewport.pixelsPerSecond - viewport.widthPx);
4808
+ const gutterPx = viewport.gutterPx || 0;
4809
+ return Math.max(0, viewport.durationSec * viewport.pixelsPerSecond + 2 * gutterPx - viewport.widthPx);
4738
4810
  }
4739
4811
 
4740
4812
  /** Scroll offset that still shows a full window (no overscroll). */
@@ -4747,7 +4819,7 @@ function clampScrollLeft(scrollLeftPx, viewport) {
4747
4819
 
4748
4820
  /** Center this time in the view, clamped so the window still fills the canvas. */
4749
4821
  function getCenteredScrollLeft(timeSec, viewport) {
4750
- return clampScrollLeft(timeSec * viewport.pixelsPerSecond - viewport.widthPx / 2, viewport);
4822
+ return clampScrollLeft(timeSec * viewport.pixelsPerSecond + (viewport.gutterPx || 0) - viewport.widthPx / 2, viewport);
4751
4823
  }
4752
4824
 
4753
4825
  /**
@@ -4755,10 +4827,28 @@ function getCenteredScrollLeft(timeSec, viewport) {
4755
4827
  * No-op at fit-to-width or when that time is already visible.
4756
4828
  */
4757
4829
  function getSeekCameraAction({
4830
+ cameraMode = 'desktop',
4758
4831
  timeSec,
4759
4832
  viewport
4760
4833
  }) {
4761
- if (viewport.zoomLevel <= constants/* WAVEFORM_ZOOM_MIN */.LW || !(viewport.widthPx > 0) || !(viewport.pixelsPerSecond > 0)) {
4834
+ if (!(viewport.widthPx > 0) || !(viewport.pixelsPerSecond > 0)) {
4835
+ return {
4836
+ type: 'none'
4837
+ };
4838
+ }
4839
+ if (cameraMode === 'tape') {
4840
+ const scrollLeftPx = getCenteredScrollLeft(timeSec, viewport);
4841
+ if (Math.abs(scrollLeftPx - viewport.scrollLeftPx) < 1) {
4842
+ return {
4843
+ type: 'none'
4844
+ };
4845
+ }
4846
+ return {
4847
+ type: 'jump',
4848
+ scrollLeftPx
4849
+ };
4850
+ }
4851
+ if (viewport.zoomLevel <= constants/* WAVEFORM_ZOOM_MIN */.LW) {
4762
4852
  return {
4763
4853
  type: 'none'
4764
4854
  };
@@ -4774,6 +4864,23 @@ function getSeekCameraAction({
4774
4864
  };
4775
4865
  }
4776
4866
 
4867
+ /** Always keep this time under the center pin. Reuses followRight so the camera hook can pin. */
4868
+ function getTapeCameraAction({
4869
+ timeSec,
4870
+ viewport
4871
+ }) {
4872
+ if (!(viewport.widthPx > 0) || !(viewport.pixelsPerSecond > 0)) {
4873
+ return {
4874
+ type: 'none'
4875
+ };
4876
+ }
4877
+ return {
4878
+ isPlayheadPinned: true,
4879
+ scrollLeftPx: getCenteredScrollLeft(timeSec, viewport),
4880
+ type: 'followRight'
4881
+ };
4882
+ }
4883
+
4777
4884
  /** Follow inset in CSS px, capped at one third of the view so a narrow player still has room. */
4778
4885
  function getFollowInsetPx(widthPx, insetPx = constants/* WAVEFORM_FOLLOW_INSET_PX */.NM) {
4779
4886
  if (!(widthPx > 0)) {
@@ -4791,6 +4898,11 @@ function getPinnedPlayheadLeft(widthPx, insetPx = constants/* WAVEFORM_FOLLOW_IN
4791
4898
  return `${(widthPx - inset) / widthPx * 100}%`;
4792
4899
  }
4793
4900
 
4901
+ /** CSS left for a playhead locked to the center of the view. */
4902
+ function getTapePinnedPlayheadLeft() {
4903
+ return '50%';
4904
+ }
4905
+
4794
4906
  /** CSS left % of the playhead from the left of the visible window. */
4795
4907
  function timeLeftPercent(timeSec, durationSec, viewport) {
4796
4908
  if (viewport.widthPx > 0 && viewport.pixelsPerSecond > 0) {
@@ -4805,12 +4917,19 @@ function timeLeftPercent(timeSec, durationSec, viewport) {
4805
4917
  * Off-screen at play start jumps in; off-screen right while playing keeps following.
4806
4918
  */
4807
4919
  function getPlayheadCameraAction({
4920
+ cameraMode = 'desktop',
4808
4921
  insetPx = constants/* WAVEFORM_FOLLOW_INSET_PX */.NM,
4809
4922
  isPlaying,
4810
4923
  playJustStarted,
4811
4924
  timeSec,
4812
4925
  viewport
4813
4926
  }) {
4927
+ if (cameraMode === 'tape') {
4928
+ return getTapeCameraAction({
4929
+ timeSec,
4930
+ viewport
4931
+ });
4932
+ }
4814
4933
  if (!isPlaying || viewport.zoomLevel <= constants/* WAVEFORM_ZOOM_MIN */.LW || !(viewport.widthPx > 0) || !(viewport.pixelsPerSecond > 0)) {
4815
4934
  return {
4816
4935
  type: 'none'
@@ -4850,6 +4969,46 @@ function getPlayheadCameraAction({
4850
4969
  type: 'none'
4851
4970
  };
4852
4971
  }
4972
+ ;// ./src/lib/viewers/media/waveform/useTapeWaveform.ts
4973
+
4974
+
4975
+ function isIPadNavigator(tapeNavigator) {
4976
+ return /iPad/i.test(tapeNavigator.userAgent || '') || tapeNavigator.platform === 'MacIntel' && (tapeNavigator.maxTouchPoints || 0) > 1;
4977
+ }
4978
+ function isCoarsePrimaryPointer(tapeWindow) {
4979
+ return typeof tapeWindow.matchMedia === 'function' && tapeWindow.matchMedia(constants/* TAPE_POINTER_MEDIA_QUERY */.Le).matches;
4980
+ }
4981
+
4982
+ /**
4983
+ * Tape vs desktop camera: coarse primary pointer (finger) or iPad
4984
+ * (including iPadOS reporting itself as Mac). Not width, not hasTouch,
4985
+ * not Browser.isMobile().
4986
+ */
4987
+ function isTapeWaveformInput(tapeWindow = window) {
4988
+ return isCoarsePrimaryPointer(tapeWindow) || isIPadNavigator(tapeWindow.navigator);
4989
+ }
4990
+
4991
+ /** Subscribe to the pointer media query; re-read the iPad heuristic on change. */
4992
+ function useTapeWaveform() {
4993
+ const [isTape, setIsTape] = (0,external_react_.useState)(() => typeof window !== 'undefined' && isTapeWaveformInput(window));
4994
+ (0,external_react_.useEffect)(() => {
4995
+ if (typeof window.matchMedia !== 'function') {
4996
+ setIsTape(isTapeWaveformInput(window));
4997
+ return undefined;
4998
+ }
4999
+ const mediaQuery = window.matchMedia(constants/* TAPE_POINTER_MEDIA_QUERY */.Le);
5000
+ const sync = () => {
5001
+ setIsTape(isTapeWaveformInput(window));
5002
+ };
5003
+ sync();
5004
+ mediaQuery.addEventListener('change', sync);
5005
+ return () => mediaQuery.removeEventListener('change', sync);
5006
+ }, []);
5007
+ return isTape;
5008
+ }
5009
+ // EXTERNAL MODULE: ./node_modules/classnames/index.js
5010
+ var classnames = __webpack_require__(2485);
5011
+ var classnames_default = /*#__PURE__*/__webpack_require__.n(classnames);
4853
5012
  // EXTERNAL MODULE: ./src/lib/viewers/controls/media/markers/helpers/useDismissableMarkerSelection.ts
4854
5013
  var useDismissableMarkerSelection = __webpack_require__(2378);
4855
5014
  // EXTERNAL MODULE: ./src/lib/viewers/controls/media/markers/helpers/buildClusters.ts
@@ -4869,6 +5028,7 @@ var utils = __webpack_require__(5346);
4869
5028
 
4870
5029
 
4871
5030
 
5031
+
4872
5032
  const WAVEFORM_MARKER_SIZE_PX = 20;
4873
5033
  function hasMappedWindow(viewport) {
4874
5034
  return !!viewport && Number.isFinite(viewport.startSec) && viewport.endSec > viewport.startSec;
@@ -4907,11 +5067,12 @@ function clustersByExactTime(markers, durationSec) {
4907
5067
  function WaveformCommentMarkers({
4908
5068
  commentMarkers,
4909
5069
  durationSec,
5070
+ isTape = false,
4910
5071
  onCommentMarkerClick,
4911
5072
  selectedId: hostSelectedId = null,
4912
5073
  viewport = null
4913
5074
  }) {
4914
- const trackRef = (0,external_react_.useRef)(null);
5075
+ const trackRef = (0,external_react_.useRef)(null); // width source for clustering overlapping badges
4915
5076
  const {
4916
5077
  containerRef,
4917
5078
  selectMarker,
@@ -4920,7 +5081,7 @@ function WaveformCommentMarkers({
4920
5081
  const [trackWidth, setTrackWidth] = (0,external_react_.useState)(0);
4921
5082
  const canShowTrack = durationSec > 0 && commentMarkers.length > 0;
4922
5083
  const zoomLevel = viewport?.zoomLevel ?? constants/* WAVEFORM_ZOOM_MIN */.LW;
4923
- const isZoomed = hasMappedWindow(viewport) && viewport.zoomLevel > constants/* WAVEFORM_ZOOM_MIN */.LW;
5084
+ const alignsMarkersToVisibleWindow = hasMappedWindow(viewport) && (viewport.zoomLevel > constants/* WAVEFORM_ZOOM_MIN */.LW || isTape);
4924
5085
  (0,external_react_.useLayoutEffect)(() => {
4925
5086
  if (!canShowTrack) {
4926
5087
  setTrackWidth(0);
@@ -4956,7 +5117,10 @@ function WaveformCommentMarkers({
4956
5117
  }
4957
5118
  return /*#__PURE__*/external_react_["default"].createElement("div", {
4958
5119
  ref: containerRef,
4959
- className: `bp-WaveformCommentMarkers${isZoomed ? ' bp-WaveformCommentMarkers--zoomed' : ''}`,
5120
+ className: classnames_default()('bp-WaveformCommentMarkers', {
5121
+ 'bp-WaveformCommentMarkers--tape': isTape,
5122
+ 'bp-WaveformCommentMarkers--zoomed': alignsMarkersToVisibleWindow
5123
+ }),
4960
5124
  "data-testid": "bp-waveform-comment-markers"
4961
5125
  }, /*#__PURE__*/external_react_["default"].createElement("div", {
4962
5126
  ref: trackRef,
@@ -4965,7 +5129,10 @@ function WaveformCommentMarkers({
4965
5129
  const marker = cluster.markers[0];
4966
5130
  const isGroup = cluster.markers.length > 1;
4967
5131
  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' : ''}`;
5132
+ const className = classnames_default()('bp-WaveformCommentMarkers-marker', {
5133
+ 'bp-WaveformCommentMarkers-marker--group': isGroup,
5134
+ 'bp-WaveformCommentMarkers-marker--selected': isSelected
5135
+ });
4969
5136
  const left = `${markerLeftPercent(marker.time, durationSec, viewport)}%`;
4970
5137
  if (isGroup) {
4971
5138
  return /*#__PURE__*/external_react_["default"].createElement("div", {
@@ -5003,6 +5170,8 @@ function WaveformCommentMarkers({
5003
5170
  }));
5004
5171
  })));
5005
5172
  }
5173
+ ;// external "react-dom"
5174
+
5006
5175
  ;// ./node_modules/wavesurfer.js/dist/wavesurfer.esm.js
5007
5176
  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
5177
  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 +5256,86 @@ function getBufferedProgress(bufferedRange, durationSec) {
5087
5256
  return clampTo0And1(bufferedRange.end(bufferedRange.length - 1) / durationSec);
5088
5257
  }
5089
5258
 
5259
+ /** Played bars: rest token on desktop, hover-played (brightest) on tape. Tape never uses hover or buffer fills. */
5260
+ function getPlayedWaveformColor(isTape = false) {
5261
+ return isTape ? WAVEFORM_COLOR_HOVER_PLAYED : WAVEFORM_COLOR_PLAYED;
5262
+ }
5263
+ function rangeFills({
5264
+ buffer,
5265
+ hover,
5266
+ rangeEnd,
5267
+ rangeStart
5268
+ }) {
5269
+ const start = clampTo0And1(rangeStart);
5270
+ const end = clampTo0And1(rangeEnd);
5271
+ const hoverInside = hover != null && hover > start && hover < end;
5272
+ const outsideAfter = [{
5273
+ color: WAVEFORM_COLOR_UNPLAYED,
5274
+ end: Math.max(end, buffer),
5275
+ start: end
5276
+ }, {
5277
+ color: WAVEFORM_COLOR_BUFFER,
5278
+ end: 1,
5279
+ start: Math.max(end, buffer)
5280
+ }];
5281
+ const ranges = hoverInside ? [{
5282
+ color: WAVEFORM_COLOR_UNPLAYED,
5283
+ end: start,
5284
+ start: 0
5285
+ }, {
5286
+ color: WAVEFORM_COLOR_HOVER_PLAYED,
5287
+ end: hover,
5288
+ start
5289
+ }, {
5290
+ color: WAVEFORM_COLOR_HOVER_AREA,
5291
+ end,
5292
+ start: hover
5293
+ }, ...outsideAfter] : [{
5294
+ color: WAVEFORM_COLOR_UNPLAYED,
5295
+ end: start,
5296
+ start: 0
5297
+ }, {
5298
+ color: WAVEFORM_COLOR_PLAYED,
5299
+ end,
5300
+ start
5301
+ }, ...outsideAfter];
5302
+ const fill = gradientStopsFromColorRanges(ranges);
5303
+ return {
5304
+ progressColor: fill,
5305
+ waveColor: fill
5306
+ };
5307
+ }
5308
+
5090
5309
  /**
5091
5310
  * progressColor paints left of the playhead (clipped by wavesurfer).
5092
5311
  * waveColor paints right of the playhead, including buffered vs not-yet-buffered.
5312
+ * An open comment range replaces the played/unplayed split with in-range / out-of-range.
5093
5313
  */
5094
5314
  function getWaveformFills({
5095
5315
  bufferProgress,
5096
- hoverProgress
5316
+ hoverProgress,
5317
+ isTape = false,
5318
+ rangeProgress
5097
5319
  }) {
5098
5320
  const buffer = clampTo0And1(bufferProgress);
5321
+ const playedColor = getPlayedWaveformColor(isTape);
5322
+ if (isTape) {
5323
+ return {
5324
+ progressColor: playedColor,
5325
+ waveColor: WAVEFORM_COLOR_UNPLAYED
5326
+ };
5327
+ }
5328
+ if (rangeProgress && rangeProgress.end > rangeProgress.start) {
5329
+ return rangeFills({
5330
+ buffer,
5331
+ hover: hoverProgress == null ? null : clampTo0And1(hoverProgress),
5332
+ rangeEnd: rangeProgress.end,
5333
+ rangeStart: rangeProgress.start
5334
+ });
5335
+ }
5099
5336
  if (hoverProgress == null) {
5100
5337
  return {
5101
- progressColor: WAVEFORM_COLOR_PLAYED,
5338
+ progressColor: playedColor,
5102
5339
  waveColor: gradientStopsFromColorRanges([{
5103
5340
  color: WAVEFORM_COLOR_UNPLAYED,
5104
5341
  end: buffer,
@@ -5259,6 +5496,200 @@ function tintWaveformTiles({
5259
5496
  });
5260
5497
  });
5261
5498
  }
5499
+ ;// ./src/lib/viewers/media/waveform/range.ts
5500
+
5501
+
5502
+ function clampToRange(value, min, max) {
5503
+ if (!Number.isFinite(value)) {
5504
+ return min;
5505
+ }
5506
+ return Math.min(max, Math.max(min, value));
5507
+ }
5508
+
5509
+ /** Integer ms in `[0, durationMs]`. */
5510
+ function clampTimeMs(timeMs, durationMs) {
5511
+ const duration = Math.max(0, durationMs);
5512
+ return Math.round(clampToRange(timeMs, 0, duration));
5513
+ }
5514
+ function durationMsFromSec(durationSec) {
5515
+ if (!Number.isFinite(durationSec) || durationSec <= 0) {
5516
+ return 0;
5517
+ }
5518
+ return Math.round(durationSec * 1000);
5519
+ }
5520
+ function isRangeCollapsed(range) {
5521
+ return !range || range.endMs == null || range.endMs <= range.startMs;
5522
+ }
5523
+
5524
+ /** Clamp both edges onto the file. Collapsed drafts stay a point. */
5525
+ function resolveRange(range, durationMs) {
5526
+ const startMs = clampTimeMs(range.startMs, durationMs);
5527
+ if (range.endMs == null) {
5528
+ return {
5529
+ endMs: startMs,
5530
+ startMs
5531
+ };
5532
+ }
5533
+ const endMs = clampTimeMs(range.endMs, durationMs);
5534
+ if (endMs <= startMs) {
5535
+ return {
5536
+ endMs: startMs,
5537
+ startMs
5538
+ };
5539
+ }
5540
+ return {
5541
+ endMs,
5542
+ startMs
5543
+ };
5544
+ }
5545
+
5546
+ /**
5547
+ * Which handle should receive this pointer. Overlapping hit areas (collapsed or
5548
+ * a span narrower than the 24px target) split at the midpoint so each edge stays reachable.
5549
+ */
5550
+ function pickRangeHandle({
5551
+ endX,
5552
+ pointerX,
5553
+ startX
5554
+ }) {
5555
+ const mid = (startX + endX) / 2;
5556
+ if (!Number.isFinite(mid)) {
5557
+ return 'end';
5558
+ }
5559
+ return pointerX < mid ? 'start' : 'end';
5560
+ }
5561
+
5562
+ /** Screen x of a handle. Collapsed drafts sit 1px left/right of the playhead, not on it. */
5563
+ function visualRangeHandlePx({
5564
+ handle,
5565
+ range,
5566
+ viewport
5567
+ }) {
5568
+ const timeSec = (handle === 'start' ? range.startMs : range.endMs) / 1000;
5569
+ const x = positionPxFromTime(timeSec, viewport);
5570
+ if (range.startMs !== range.endMs) {
5571
+ return x;
5572
+ }
5573
+ return handle === 'start' ? x - constants/* WAVEFORM_RANGE_COLLAPSED_OFFSET_PX */.BG : x + constants/* WAVEFORM_RANGE_COLLAPSED_OFFSET_PX */.BG;
5574
+ }
5575
+
5576
+ /** True when the pointer is over the range rectangle or its handle hit targets. */
5577
+ function isPointerOverRange({
5578
+ pointerX,
5579
+ range,
5580
+ viewport
5581
+ }) {
5582
+ const resolved = resolveRange(range, durationMsFromSec(viewport.durationSec));
5583
+ const startX = visualRangeHandlePx({
5584
+ handle: 'start',
5585
+ range: resolved,
5586
+ viewport
5587
+ });
5588
+ const endX = visualRangeHandlePx({
5589
+ handle: 'end',
5590
+ range: resolved,
5591
+ viewport
5592
+ });
5593
+ const pad = constants/* WAVEFORM_RANGE_HIT_AREA_PX */.r0 / 2;
5594
+ return pointerX >= Math.min(startX, endX) - pad && pointerX <= Math.max(startX, endX) + pad;
5595
+ }
5596
+
5597
+ /** Snap to the playhead when the pointer is within `WAVEFORM_RANGE_SNAP_PX`. */
5598
+ function snapTimeMs({
5599
+ pixelsPerSecond,
5600
+ playheadMs,
5601
+ timeMs
5602
+ }) {
5603
+ if (!(pixelsPerSecond > 0) || !Number.isFinite(playheadMs)) {
5604
+ return timeMs;
5605
+ }
5606
+ const deltaPx = Math.abs(timeMs - playheadMs) / 1000 * pixelsPerSecond;
5607
+ if (deltaPx <= constants/* WAVEFORM_RANGE_SNAP_PX */.IQ) {
5608
+ return Math.round(playheadMs);
5609
+ }
5610
+ return timeMs;
5611
+ }
5612
+
5613
+ /**
5614
+ * Move one edge. Crossing the other edge swaps roles so a collapsed pair can
5615
+ * expand in either direction. Enforces `[0, duration]` and the minimum span
5616
+ * once the drag has actually opened a range. An already-open range stays a
5617
+ * span — snapping one edge onto the other clamps to the minimum duration.
5618
+ */
5619
+ function dragRangeHandle({
5620
+ durationMs,
5621
+ handle,
5622
+ pointerMs,
5623
+ range
5624
+ }) {
5625
+ const duration = Math.max(0, durationMs);
5626
+ const pointer = clampTimeMs(pointerMs, duration);
5627
+ let {
5628
+ startMs,
5629
+ endMs
5630
+ } = range;
5631
+ let nextHandle = handle;
5632
+ if (handle === 'start') {
5633
+ if (pointer > endMs) {
5634
+ startMs = endMs;
5635
+ endMs = pointer;
5636
+ nextHandle = 'end';
5637
+ } else {
5638
+ startMs = pointer;
5639
+ }
5640
+ } else if (pointer < startMs) {
5641
+ endMs = startMs;
5642
+ startMs = pointer;
5643
+ nextHandle = 'start';
5644
+ } else {
5645
+ endMs = pointer;
5646
+ }
5647
+ const wasOpen = range.startMs !== range.endMs;
5648
+ const opened = wasOpen || startMs !== endMs;
5649
+ if (opened && endMs - startMs < constants/* WAVEFORM_RANGE_MIN_DURATION_MS */.bX) {
5650
+ if (nextHandle === 'end') {
5651
+ endMs = clampTimeMs(startMs + constants/* WAVEFORM_RANGE_MIN_DURATION_MS */.bX, duration);
5652
+ if (endMs - startMs < constants/* WAVEFORM_RANGE_MIN_DURATION_MS */.bX) {
5653
+ startMs = clampTimeMs(endMs - constants/* WAVEFORM_RANGE_MIN_DURATION_MS */.bX, duration);
5654
+ }
5655
+ } else {
5656
+ startMs = clampTimeMs(endMs - constants/* WAVEFORM_RANGE_MIN_DURATION_MS */.bX, duration);
5657
+ if (endMs - startMs < constants/* WAVEFORM_RANGE_MIN_DURATION_MS */.bX) {
5658
+ endMs = clampTimeMs(startMs + constants/* WAVEFORM_RANGE_MIN_DURATION_MS */.bX, duration);
5659
+ }
5660
+ }
5661
+ }
5662
+ return {
5663
+ handle: nextHandle,
5664
+ range: {
5665
+ endMs,
5666
+ startMs
5667
+ }
5668
+ };
5669
+ }
5670
+ function pointerTimeMs(pointerX, viewport) {
5671
+ return Math.round(timeFromPositionPx(pointerX, viewport) * 1000);
5672
+ }
5673
+
5674
+ /** Sidebar payload after mouseup. Null when the draft is still a single timestamp. */
5675
+ function commitRangeChange(range) {
5676
+ if (range.endMs <= range.startMs) {
5677
+ return null;
5678
+ }
5679
+ return {
5680
+ endMs: range.endMs,
5681
+ startMs: range.startMs
5682
+ };
5683
+ }
5684
+ function rangeProgress(range, durationMs) {
5685
+ if (!(durationMs > 0) || range.endMs <= range.startMs) {
5686
+ return null;
5687
+ }
5688
+ return {
5689
+ end: range.endMs / durationMs,
5690
+ start: range.startMs / durationMs
5691
+ };
5692
+ }
5262
5693
  ;// ./src/lib/viewers/media/waveform/usePlayheadCamera.ts
5263
5694
 
5264
5695
 
@@ -5275,6 +5706,7 @@ function prefersReducedMotion() {
5275
5706
  * every frame. WaveformView owns WaveSurfer, zoom, and the media rAF loop.
5276
5707
  */
5277
5708
  function usePlayheadCamera({
5709
+ cameraMode = 'desktop',
5278
5710
  mediaElRef,
5279
5711
  onViewportCommit,
5280
5712
  playheadRef,
@@ -5283,6 +5715,8 @@ function usePlayheadCamera({
5283
5715
  }) {
5284
5716
  const onViewportCommitRef = (0,external_react_.useRef)(onViewportCommit);
5285
5717
  onViewportCommitRef.current = onViewportCommit;
5718
+ const cameraModeRef = (0,external_react_.useRef)(cameraMode);
5719
+ cameraModeRef.current = cameraMode;
5286
5720
  const programmaticScrollRef = (0,external_react_.useRef)(false);
5287
5721
  const jumpAnimationRef = (0,external_react_.useRef)(0);
5288
5722
  const lastCameraScrollRef = (0,external_react_.useRef)(null);
@@ -5293,6 +5727,8 @@ function usePlayheadCamera({
5293
5727
  const scrollSettleTimerRef = (0,external_react_.useRef)(0);
5294
5728
  const applyRef = (0,external_react_.useRef)(null);
5295
5729
  const isFollowPinned = (0,external_react_.useCallback)(() => isFollowPinnedRef.current, []);
5730
+ const isUserPanning = (0,external_react_.useCallback)(() => userIsScrollingRef.current, []);
5731
+ const isTape = (0,external_react_.useCallback)(() => cameraModeRef.current === 'tape', []);
5296
5732
  const cancelJump = (0,external_react_.useCallback)(() => {
5297
5733
  if (!jumpAnimationRef.current) {
5298
5734
  return;
@@ -5321,15 +5757,22 @@ function usePlayheadCamera({
5321
5757
  const onSeek = (0,external_react_.useCallback)(timeSec => {
5322
5758
  mediaTimeRef.current = timeSec;
5323
5759
  cancelJump();
5324
- clearFollowPin();
5325
- }, [cancelJump, clearFollowPin]);
5760
+ if (!isTape()) {
5761
+ clearFollowPin();
5762
+ }
5763
+ }, [cancelJump, clearFollowPin, isTape]);
5326
5764
 
5327
5765
  /** Unpin and hold follow so zoom setScroll (slider center or pinch origin) is not stolen. */
5328
5766
  const onZoom = (0,external_react_.useCallback)(() => {
5329
5767
  cancelJump();
5768
+ if (isTape()) {
5769
+ isFollowPinnedRef.current = true;
5770
+ holdFollowUntilInsetRef.current = false;
5771
+ return;
5772
+ }
5330
5773
  isFollowPinnedRef.current = false;
5331
5774
  holdFollowUntilInsetRef.current = true;
5332
- }, [cancelJump]);
5775
+ }, [cancelJump, isTape]);
5333
5776
  const applyScrollLeft = (0,external_react_.useCallback)((scrollLeftPx, shouldCommitState) => {
5334
5777
  const wavesurfer = wavesurferRef.current;
5335
5778
  if (!wavesurfer || !wavesurfer.setScroll) {
@@ -5357,8 +5800,8 @@ function usePlayheadCamera({
5357
5800
  if (!playhead || !isPinned || !(viewport.widthPx > 0)) {
5358
5801
  return;
5359
5802
  }
5360
- playhead.style.left = getPinnedPlayheadLeft(viewport.widthPx);
5361
- }, []);
5803
+ playhead.style.left = isTape() ? getTapePinnedPlayheadLeft() : getPinnedPlayheadLeft(viewport.widthPx);
5804
+ }, [isTape]);
5362
5805
  const apply = (0,external_react_.useCallback)((timeSec, playJustStarted = false) => {
5363
5806
  mediaTimeRef.current = timeSec;
5364
5807
  const wavesurfer = wavesurferRef.current;
@@ -5366,7 +5809,7 @@ function usePlayheadCamera({
5366
5809
  if (!wavesurfer) {
5367
5810
  return;
5368
5811
  }
5369
- if (viewport.zoomLevel <= constants/* WAVEFORM_ZOOM_MIN */.LW) {
5812
+ if (!isTape() && viewport.zoomLevel <= constants/* WAVEFORM_ZOOM_MIN */.LW) {
5370
5813
  clearFollowPin();
5371
5814
  return;
5372
5815
  }
@@ -5378,12 +5821,13 @@ function usePlayheadCamera({
5378
5821
  }
5379
5822
  const liveViewport = getViewportAtScroll(viewport, getScrollLeft(wavesurfer, viewport.scrollLeftPx));
5380
5823
  const action = getPlayheadCameraAction({
5824
+ cameraMode: cameraModeRef.current,
5381
5825
  isPlaying: true,
5382
5826
  playJustStarted,
5383
5827
  timeSec,
5384
5828
  viewport: liveViewport
5385
5829
  });
5386
- if (holdFollowUntilInsetRef.current && !playJustStarted) {
5830
+ if (!isTape() && holdFollowUntilInsetRef.current && !playJustStarted) {
5387
5831
  if (action.type !== 'none') {
5388
5832
  return;
5389
5833
  }
@@ -5415,6 +5859,7 @@ function usePlayheadCamera({
5415
5859
  const tick = now => {
5416
5860
  const t = Math.min(1, (now - start) / constants/* WAVEFORM_PLAYHEAD_JUMP_MS */.Bq);
5417
5861
  const liveAction = getPlayheadCameraAction({
5862
+ cameraMode: cameraModeRef.current,
5418
5863
  isPlaying: true,
5419
5864
  playJustStarted: true,
5420
5865
  timeSec: mediaTimeRef.current,
@@ -5431,7 +5876,7 @@ function usePlayheadCamera({
5431
5876
  apply(mediaTimeRef.current, false);
5432
5877
  };
5433
5878
  jumpAnimationRef.current = window.requestAnimationFrame(tick);
5434
- }, [applyScrollLeft, cancelJump, clearFollowPin, pinFollowPlayhead, playheadRef, viewportRef, wavesurferRef]);
5879
+ }, [applyScrollLeft, cancelJump, clearFollowPin, isTape, pinFollowPlayhead, playheadRef, viewportRef, wavesurferRef]);
5435
5880
  applyRef.current = apply;
5436
5881
 
5437
5882
  /**
@@ -5454,9 +5899,12 @@ function usePlayheadCamera({
5454
5899
  }
5455
5900
  releaseUserPanHold();
5456
5901
  cancelJump();
5457
- isFollowPinnedRef.current = false;
5902
+ if (!isTape()) {
5903
+ isFollowPinnedRef.current = false;
5904
+ }
5458
5905
  const liveViewport = getViewportAtScroll(viewportRef.current, getScrollLeft(wavesurfer, viewportRef.current.scrollLeftPx));
5459
5906
  const action = getSeekCameraAction({
5907
+ cameraMode: cameraModeRef.current,
5460
5908
  timeSec,
5461
5909
  viewport: liveViewport
5462
5910
  });
@@ -5476,7 +5924,7 @@ function usePlayheadCamera({
5476
5924
  applyScrollLeft(from + (to - from) * (1 - (1 - t) * (1 - t)), false);
5477
5925
  const playhead = playheadRef.current;
5478
5926
  if (playhead) {
5479
- playhead.style.left = timeLeftPercent(mediaTimeRef.current, viewportRef.current.durationSec, viewportRef.current);
5927
+ playhead.style.left = isTape() ? getTapePinnedPlayheadLeft() : timeLeftPercent(mediaTimeRef.current, viewportRef.current.durationSec, viewportRef.current);
5480
5928
  }
5481
5929
  if (t < 1) {
5482
5930
  jumpAnimationRef.current = window.requestAnimationFrame(tick);
@@ -5486,7 +5934,7 @@ function usePlayheadCamera({
5486
5934
  applyScrollLeft(to, true);
5487
5935
  };
5488
5936
  jumpAnimationRef.current = window.requestAnimationFrame(tick);
5489
- }, [applyScrollLeft, cancelJump, playheadRef, releaseUserPanHold, viewportRef, wavesurferRef]);
5937
+ }, [applyScrollLeft, cancelJump, isTape, playheadRef, releaseUserPanHold, viewportRef, wavesurferRef]);
5490
5938
  const handleScroll = (0,external_react_.useCallback)(onUserPan => {
5491
5939
  const wavesurfer = wavesurferRef.current;
5492
5940
  if (!wavesurfer) {
@@ -5498,8 +5946,28 @@ function usePlayheadCamera({
5498
5946
  return;
5499
5947
  }
5500
5948
  viewportRef.current = getViewportAtScroll(viewportRef.current, scrollLeftPx);
5501
- const timeSec = readMediaTime();
5502
5949
  const playhead = playheadRef.current;
5950
+ if (isTape()) {
5951
+ if (playhead) {
5952
+ playhead.style.left = getTapePinnedPlayheadLeft();
5953
+ }
5954
+ const pinTimeSec = Math.min(viewportRef.current.durationSec, Math.max(0, timeFromPositionPx(viewportRef.current.widthPx / 2, viewportRef.current)));
5955
+ userIsScrollingRef.current = true;
5956
+ window.clearTimeout(scrollSettleTimerRef.current);
5957
+ scrollSettleTimerRef.current = window.setTimeout(() => {
5958
+ userIsScrollingRef.current = false;
5959
+ scrollSettleTimerRef.current = 0;
5960
+ if (!mediaElRef.current || mediaElRef.current.paused) {
5961
+ return;
5962
+ }
5963
+ applyRef.current?.(readMediaTime(), false);
5964
+ }, constants/* WAVEFORM_FOLLOW_SCROLL_SETTLE_MS */.Zs);
5965
+ cancelJump();
5966
+ programmaticScrollRef.current = false;
5967
+ onUserPan(pinTimeSec);
5968
+ return;
5969
+ }
5970
+ const timeSec = readMediaTime();
5503
5971
  if (playhead) {
5504
5972
  playhead.style.left = timeLeftPercent(timeSec, viewportRef.current.durationSec, viewportRef.current);
5505
5973
  }
@@ -5518,7 +5986,7 @@ function usePlayheadCamera({
5518
5986
  isFollowPinnedRef.current = false;
5519
5987
  programmaticScrollRef.current = false;
5520
5988
  onUserPan(timeSec);
5521
- }, [cancelJump, isUserPan, mediaElRef, playheadRef, readMediaTime, viewportRef, wavesurferRef]);
5989
+ }, [cancelJump, isTape, isUserPan, mediaElRef, playheadRef, readMediaTime, viewportRef, wavesurferRef]);
5522
5990
  (0,external_react_.useEffect)(() => () => {
5523
5991
  releaseUserPanHold();
5524
5992
  cancelJump();
@@ -5530,12 +5998,314 @@ function usePlayheadCamera({
5530
5998
  clearFollowPin,
5531
5999
  handleScroll,
5532
6000
  isFollowPinned,
6001
+ isUserPanning,
5533
6002
  onSeek,
5534
6003
  onZoom,
5535
6004
  releaseUserPanHold,
5536
6005
  seekTo
5537
6006
  };
5538
6007
  }
6008
+ ;// ./src/lib/viewers/media/waveform/WaveformRangeSelection.scss
6009
+ // extracted by mini-css-extract-plugin
6010
+
6011
+ ;// ./src/lib/viewers/media/waveform/WaveformRangeSelection.tsx
6012
+
6013
+
6014
+
6015
+
6016
+
6017
+
6018
+ function offsetLeftCss(base, offsetPx) {
6019
+ if (!offsetPx) {
6020
+ return base;
6021
+ }
6022
+ const op = offsetPx < 0 ? '-' : '+';
6023
+ return `calc(${base} ${op} ${Math.abs(offsetPx)}px)`;
6024
+ }
6025
+ function applyHandleLeft(el, timeSec, durationSec, viewport, collapsedOffsetPx = 0) {
6026
+ if (!el) {
6027
+ return;
6028
+ }
6029
+ el.style.left = offsetLeftCss(timeLeftPercent(timeSec, durationSec, viewport), collapsedOffsetPx);
6030
+ }
6031
+ function applyRegion(el, startSec, endSec, durationSec, viewport, collapsed) {
6032
+ if (!el) {
6033
+ return;
6034
+ }
6035
+ const start = timeLeftPercent(startSec, durationSec, viewport);
6036
+ const halfLine = constants/* WAVEFORM_RANGE_HANDLE_LINE_PX */.h1 / 2;
6037
+ if (collapsed) {
6038
+ const extentPx = constants/* WAVEFORM_RANGE_COLLAPSED_OFFSET_PX */.BG + halfLine;
6039
+ el.style.left = offsetLeftCss(start, -extentPx);
6040
+ el.style.width = `${extentPx * 2}px`;
6041
+ return;
6042
+ }
6043
+ const end = timeLeftPercent(endSec, durationSec, viewport);
6044
+ el.style.left = offsetLeftCss(start, -halfLine);
6045
+ el.style.width = `calc(${end} - ${start} + ${constants/* WAVEFORM_RANGE_HANDLE_LINE_PX */.h1}px)`;
6046
+ }
6047
+ const WaveformRangeSelection = /*#__PURE__*/(0,external_react_.forwardRef)(function WaveformRangeSelection({
6048
+ currentTimeSec = 0,
6049
+ durationSec,
6050
+ getPlayheadSec,
6051
+ interactive = true,
6052
+ isHighlighted = false,
6053
+ keepHighlight = true,
6054
+ onDragChange,
6055
+ onPreviewChange,
6056
+ onRangeChange,
6057
+ range,
6058
+ viewport
6059
+ }, ref) {
6060
+ const rootRef = (0,external_react_.useRef)(null);
6061
+ const regionRef = (0,external_react_.useRef)(null);
6062
+ const startHandleRef = (0,external_react_.useRef)(null);
6063
+ const endHandleRef = (0,external_react_.useRef)(null);
6064
+ const tooltipRef = (0,external_react_.useRef)(null);
6065
+ const dragRef = (0,external_react_.useRef)(null);
6066
+ const rangeRef = (0,external_react_.useRef)(range);
6067
+ const displayedRef = (0,external_react_.useRef)(resolveRange(range, durationMsFromSec(durationSec)));
6068
+ const viewportRef = (0,external_react_.useRef)(viewport);
6069
+ const durationSecRef = (0,external_react_.useRef)(durationSec);
6070
+ const onDragChangeRef = (0,external_react_.useRef)(onDragChange);
6071
+ const onPreviewChangeRef = (0,external_react_.useRef)(onPreviewChange);
6072
+ const onRangeChangeRef = (0,external_react_.useRef)(onRangeChange);
6073
+ const getPlayheadSecRef = (0,external_react_.useRef)(getPlayheadSec);
6074
+ const [dragRange, setDragRange] = (0,external_react_.useState)(null);
6075
+ const [activeHandle, setActiveHandle] = (0,external_react_.useState)(null);
6076
+ const activeHandleRef = (0,external_react_.useRef)(null);
6077
+ rangeRef.current = range;
6078
+ durationSecRef.current = durationSec;
6079
+ onDragChangeRef.current = onDragChange;
6080
+ onPreviewChangeRef.current = onPreviewChange;
6081
+ onRangeChangeRef.current = onRangeChange;
6082
+ getPlayheadSecRef.current = getPlayheadSec;
6083
+ activeHandleRef.current = activeHandle;
6084
+ const durationMs = durationMsFromSec(durationSec);
6085
+ const displayed = dragRange ?? resolveRange(range, durationMs);
6086
+ displayedRef.current = displayed;
6087
+ const syncPositions = (0,external_react_.useCallback)((next, nextViewport, nextDurationSec) => {
6088
+ displayedRef.current = next;
6089
+ const collapsed = next.startMs === next.endMs;
6090
+ applyRegion(regionRef.current, next.startMs / 1000, next.endMs / 1000, nextDurationSec, nextViewport, collapsed);
6091
+ applyHandleLeft(startHandleRef.current, next.startMs / 1000, nextDurationSec, nextViewport, collapsed ? -constants/* WAVEFORM_RANGE_COLLAPSED_OFFSET_PX */.BG : 0);
6092
+ applyHandleLeft(endHandleRef.current, next.endMs / 1000, nextDurationSec, nextViewport, collapsed ? constants/* WAVEFORM_RANGE_COLLAPSED_OFFSET_PX */.BG : 0);
6093
+ const tooltip = tooltipRef.current;
6094
+ if (tooltip) {
6095
+ const tooltipHandle = activeHandleRef.current ?? 'end';
6096
+ const tooltipMs = tooltipHandle === 'start' ? next.startMs : next.endMs;
6097
+ tooltip.style.left = timeLeftPercent(tooltipMs / 1000, nextDurationSec, nextViewport);
6098
+ }
6099
+ }, []);
6100
+ (0,external_react_.useImperativeHandle)(ref, () => ({
6101
+ applyViewport: nextViewport => {
6102
+ viewportRef.current = nextViewport;
6103
+ syncPositions(displayedRef.current, nextViewport, durationSecRef.current);
6104
+ }
6105
+ }), [syncPositions]);
6106
+ (0,external_react_.useLayoutEffect)(() => {
6107
+ syncPositions(displayedRef.current, viewportRef.current, durationSec);
6108
+ }, [activeHandle, displayed.endMs, displayed.startMs, durationSec, syncPositions]);
6109
+ (0,external_react_.useLayoutEffect)(() => {
6110
+ const next = createWaveformViewport({
6111
+ durationSec: viewport.durationSec,
6112
+ heightPx: viewport.heightPx,
6113
+ maxZoom: viewport.maxZoom,
6114
+ scrollLeftPx: viewportRef.current.scrollLeftPx,
6115
+ widthPx: viewport.widthPx,
6116
+ zoomLevel: viewport.zoomLevel
6117
+ });
6118
+ viewportRef.current = next;
6119
+ syncPositions(displayedRef.current, next, durationSecRef.current);
6120
+ }, [syncPositions, viewport.durationSec, viewport.heightPx, viewport.maxZoom, viewport.widthPx, viewport.zoomLevel]);
6121
+ const endDrag = (0,external_react_.useCallback)(event => {
6122
+ const drag = dragRef.current;
6123
+ if (!drag || event.pointerId && drag.pointerId !== event.pointerId) {
6124
+ return;
6125
+ }
6126
+ [startHandleRef.current, endHandleRef.current].forEach(handleEl => {
6127
+ if (handleEl && handleEl.hasPointerCapture(event.pointerId)) {
6128
+ handleEl.releasePointerCapture(event.pointerId);
6129
+ }
6130
+ });
6131
+ dragRef.current = null;
6132
+ setActiveHandle(null);
6133
+ setDragRange(null);
6134
+ onDragChangeRef.current?.(false);
6135
+ const didChange = drag.range.startMs !== drag.originRange.startMs || drag.range.endMs !== drag.originRange.endMs;
6136
+ if (!didChange) {
6137
+ return;
6138
+ }
6139
+ const committed = commitRangeChange(drag.range);
6140
+ if (committed) {
6141
+ onRangeChangeRef.current?.(committed);
6142
+ }
6143
+ }, []);
6144
+ const moveDrag = (0,external_react_.useCallback)(event => {
6145
+ const drag = dragRef.current;
6146
+ if (!drag || event.pointerId && drag.pointerId !== event.pointerId) {
6147
+ return;
6148
+ }
6149
+ const root = rootRef.current;
6150
+ if (!root) {
6151
+ return;
6152
+ }
6153
+ const rect = root.getBoundingClientRect();
6154
+ const pointerX = event.clientX - rect.left;
6155
+ const vp = viewportRef.current;
6156
+ const duration = durationMsFromSec(durationSecRef.current);
6157
+ const playheadSec = getPlayheadSecRef.current?.() ?? currentTimeSec;
6158
+ const pointerMs = snapTimeMs({
6159
+ pixelsPerSecond: vp.pixelsPerSecond,
6160
+ playheadMs: playheadSec * 1000,
6161
+ timeMs: pointerTimeMs(pointerX, vp)
6162
+ });
6163
+ if (drag.originMs === drag.range.startMs && drag.originMs === drag.range.endMs && pointerMs === drag.originMs) {
6164
+ return;
6165
+ }
6166
+ const next = dragRangeHandle({
6167
+ durationMs: duration,
6168
+ handle: drag.handle,
6169
+ pointerMs,
6170
+ range: {
6171
+ endMs: drag.range.endMs,
6172
+ startMs: drag.range.startMs
6173
+ }
6174
+ });
6175
+ drag.handle = next.handle;
6176
+ drag.range = next.range;
6177
+ setActiveHandle(next.handle);
6178
+ setDragRange(next.range);
6179
+ syncPositions(next.range, vp, durationSecRef.current);
6180
+ onPreviewChangeRef.current?.({
6181
+ endMs: next.range.endMs,
6182
+ startMs: next.range.startMs
6183
+ });
6184
+ }, [currentTimeSec, syncPositions]);
6185
+ const startDrag = (0,external_react_.useCallback)((handle, event) => {
6186
+ if (!interactive || event.button > 1 || event.ctrlKey || event.metaKey) {
6187
+ return;
6188
+ }
6189
+ event.preventDefault();
6190
+ event.stopPropagation();
6191
+ const resolved = resolveRange(rangeRef.current, durationMsFromSec(durationSecRef.current));
6192
+ dragRef.current = {
6193
+ handle,
6194
+ originMs: handle === 'start' ? resolved.startMs : resolved.endMs,
6195
+ originRange: {
6196
+ endMs: resolved.endMs,
6197
+ startMs: resolved.startMs
6198
+ },
6199
+ pointerId: event.pointerId,
6200
+ range: resolved
6201
+ };
6202
+ event.currentTarget.setPointerCapture(event.pointerId);
6203
+ setActiveHandle(handle);
6204
+ setDragRange(resolved);
6205
+ onDragChangeRef.current?.(true);
6206
+ onPreviewChangeRef.current?.({
6207
+ endMs: isRangeCollapsed(rangeRef.current) ? null : resolved.endMs,
6208
+ startMs: resolved.startMs
6209
+ });
6210
+ }, [interactive]);
6211
+ const onHandlePointerDown = (0,external_react_.useCallback)(event => {
6212
+ const root = rootRef.current;
6213
+ if (!root) {
6214
+ return;
6215
+ }
6216
+ const rect = root.getBoundingClientRect();
6217
+ const pointerX = event.clientX - rect.left;
6218
+ const vp = viewportRef.current;
6219
+ const resolved = resolveRange(rangeRef.current, durationMsFromSec(durationSecRef.current));
6220
+ const handle = pickRangeHandle({
6221
+ endX: visualRangeHandlePx({
6222
+ handle: 'end',
6223
+ range: resolved,
6224
+ viewport: vp
6225
+ }),
6226
+ pointerX,
6227
+ startX: visualRangeHandlePx({
6228
+ handle: 'start',
6229
+ range: resolved,
6230
+ viewport: vp
6231
+ })
6232
+ });
6233
+ startDrag(handle, event);
6234
+ }, [startDrag]);
6235
+ (0,external_react_.useEffect)(() => {
6236
+ const onMove = event => {
6237
+ if (!dragRef.current) {
6238
+ return;
6239
+ }
6240
+ event.preventDefault();
6241
+ moveDrag(event);
6242
+ };
6243
+ const onUp = event => {
6244
+ if (!dragRef.current) {
6245
+ return;
6246
+ }
6247
+ endDrag(event);
6248
+ };
6249
+ window.addEventListener('pointermove', onMove);
6250
+ window.addEventListener('pointerup', onUp);
6251
+ window.addEventListener('pointercancel', onUp);
6252
+ return () => {
6253
+ window.removeEventListener('pointermove', onMove);
6254
+ window.removeEventListener('pointerup', onUp);
6255
+ window.removeEventListener('pointercancel', onUp);
6256
+ };
6257
+ }, [endDrag, moveDrag]);
6258
+ (0,external_react_.useEffect)(() => {
6259
+ return () => {
6260
+ if (!dragRef.current) {
6261
+ return;
6262
+ }
6263
+ dragRef.current = null;
6264
+ onDragChangeRef.current?.(false);
6265
+ };
6266
+ }, []);
6267
+ if (!(durationSec > 0) || !(durationMs >= 0)) {
6268
+ return null;
6269
+ }
6270
+ const tooltipHandle = activeHandle ?? 'end';
6271
+ const tooltipMs = tooltipHandle === 'start' ? displayed.startMs : displayed.endMs;
6272
+ const collapsed = displayed.startMs === displayed.endMs;
6273
+ const highlight = isHighlighted || collapsed;
6274
+ return /*#__PURE__*/external_react_["default"].createElement("div", {
6275
+ ref: rootRef,
6276
+ className: `bp-WaveformRange${keepHighlight ? ' bp-WaveformRange--persist' : ''}${highlight ? ' bp-WaveformRange--highlight' : ''}`,
6277
+ "data-collapsed": collapsed ? 'true' : 'false',
6278
+ "data-highlighted": highlight ? 'true' : 'false',
6279
+ "data-testid": "bp-waveform-range"
6280
+ }, /*#__PURE__*/external_react_["default"].createElement("div", {
6281
+ ref: regionRef,
6282
+ className: "bp-WaveformRange-region",
6283
+ "data-testid": "bp-waveform-range-region"
6284
+ }), /*#__PURE__*/external_react_["default"].createElement("div", {
6285
+ ref: startHandleRef,
6286
+ className: `bp-WaveformRange-handle bp-WaveformRange-handle--start${activeHandle === 'start' ? ' bp-WaveformRange-handle--dragging' : ''}`,
6287
+ "data-testid": "bp-waveform-range-handle-start",
6288
+ onMouseMove: event => event.stopPropagation(),
6289
+ onPointerDown: onHandlePointerDown
6290
+ }, /*#__PURE__*/external_react_["default"].createElement("div", {
6291
+ className: "bp-WaveformRange-handleGrip"
6292
+ })), /*#__PURE__*/external_react_["default"].createElement("div", {
6293
+ ref: endHandleRef,
6294
+ className: `bp-WaveformRange-handle bp-WaveformRange-handle--end${activeHandle === 'end' ? ' bp-WaveformRange-handle--dragging' : ''}`,
6295
+ "data-testid": "bp-waveform-range-handle-end",
6296
+ onMouseMove: event => event.stopPropagation(),
6297
+ onPointerDown: onHandlePointerDown
6298
+ }, /*#__PURE__*/external_react_["default"].createElement("div", {
6299
+ className: "bp-WaveformRange-handleGrip"
6300
+ })), activeHandle && /*#__PURE__*/external_react_["default"].createElement("div", {
6301
+ ref: tooltipRef,
6302
+ className: "bp-WaveformRange-tooltip",
6303
+ "data-testid": "bp-waveform-range-tooltip"
6304
+ }, /*#__PURE__*/external_react_["default"].createElement("div", {
6305
+ className: "bp-WaveformRange-tooltipTime"
6306
+ }, formatTime(tooltipMs / 1000))));
6307
+ });
6308
+ /* harmony default export */ const waveform_WaveformRangeSelection = (/*#__PURE__*/external_react_["default"].memo(WaveformRangeSelection));
5539
6309
  ;// ./src/lib/viewers/media/waveform/WaveformView.scss
5540
6310
  // extracted by mini-css-extract-plugin
5541
6311
 
@@ -5555,6 +6325,10 @@ function WaveformView_toPrimitive(t, r) { if ("object" != typeof t || !t) return
5555
6325
 
5556
6326
 
5557
6327
 
6328
+
6329
+
6330
+
6331
+
5558
6332
  /** Pointer X in the view and the media time under it; zoom keeps this point fixed. */
5559
6333
 
5560
6334
  /** CSS width × devicePixelRatio, for canvas gradient fills. */
@@ -5584,6 +6358,25 @@ function disableScrollOverscroll(container) {
5584
6358
  }
5585
6359
  }
5586
6360
 
6361
+ /** Half-view margins so t=0 and duration can sit under the center pin. */
6362
+ function applyWaveformGutters(wavesurfer, gutterPx) {
6363
+ const wrapper = wavesurfer.getWrapper ? wavesurfer.getWrapper() : null;
6364
+ if (!wrapper || !wrapper.style) {
6365
+ return;
6366
+ }
6367
+ const pad = gutterPx > 0 ? `${gutterPx}px` : '';
6368
+ wrapper.style.marginLeft = pad;
6369
+ wrapper.style.marginRight = pad;
6370
+ // WaveSurfer sets overflow-x:hidden when duration * minPxPerSec <= view width
6371
+ // (tape 1x). Gutters live in these margins, so the scroller must stay pan-able.
6372
+ const scroll = wrapper.parentElement;
6373
+ if (scroll && scroll.style) {
6374
+ const scrollStyle = scroll.style;
6375
+ scrollStyle.overflowX = gutterPx > 0 ? 'auto' : '';
6376
+ scrollStyle.scrollbarWidth = gutterPx > 0 ? 'none' : '';
6377
+ }
6378
+ }
6379
+
5587
6380
  /** Tint WaveSurfer's zoomed tiles with played/unplayed/hover/buffer colors. */
5588
6381
  function tintZoomedWaveform(wavesurfer, fills, replaceSnapshot = false) {
5589
6382
  const wrapper = wavesurfer.getWrapper ? wavesurfer.getWrapper() : null;
@@ -5597,6 +6390,15 @@ function tintZoomedWaveform(wavesurfer, fills, replaceSnapshot = false) {
5597
6390
  totalWidthCss: wrapper.clientWidth
5598
6391
  });
5599
6392
  }
6393
+
6394
+ /** Touch (and the compatibility mouse events Chrome sends after a tap) must not drive hover fills. */
6395
+ function isTouchHoverInput(event) {
6396
+ if (event.pointerType === 'touch') {
6397
+ return true;
6398
+ }
6399
+ const native = event.nativeEvent;
6400
+ return !!native.sourceCapabilities?.firesTouchEvents;
6401
+ }
5600
6402
  function touchDistance(touches) {
5601
6403
  if (touches.length < 2) {
5602
6404
  return 0;
@@ -5627,56 +6429,87 @@ function zoomOriginAtPointer(pointerX, wavesurfer, durationSec, fallbackWidth) {
5627
6429
  */
5628
6430
  function WaveformView({
5629
6431
  bufferedRange,
6432
+ cameraMode = 'desktop',
5630
6433
  currentTime = 0,
5631
6434
  durationSec,
5632
6435
  height = constants/* WAVEFORM_HEIGHT */.oN,
5633
6436
  interactive = true,
6437
+ isPlaying = false,
5634
6438
  mediaEl,
6439
+ onPlayPause,
6440
+ onRangeChange,
6441
+ onRangeDragChange,
5635
6442
  onSeek,
5636
6443
  onViewportChange,
5637
6444
  onZoomChange,
5638
6445
  peaks,
6446
+ range = null,
5639
6447
  zoomLevel: zoomLevelProp
5640
6448
  }) {
5641
6449
  // 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);
6450
+ const containerRef = (0,external_react_.useRef)(null); // WaveSurfer canvas host
6451
+ const playheadAnimationRef = (0,external_react_.useRef)(0); // animation frame id while the playhead follows playback
6452
+ const playheadRef = (0,external_react_.useRef)(null); // playhead element the camera positions
6453
+ const trackRef = (0,external_react_.useRef)(null); // hover / tap target around the canvas
6454
+ const wavesurferRef = (0,external_react_.useRef)(null); // WaveSurfer instance
6455
+ const rangeLayerRef = (0,external_react_.useRef)(null);
5647
6456
 
5648
6457
  // 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);
6458
+ const cameraModeRef = (0,external_react_.useRef)(cameraMode); // latest tape/desktop; WaveSurfer create() deps stay empty
6459
+ const currentTimeRef = (0,external_react_.useRef)(currentTime); // latest media time; zoom recenter reads this
6460
+ const durationSecRef = (0,external_react_.useRef)(durationSec); // latest duration; click-to-seek reads this
6461
+ const hasZoomHandlersRef = (0,external_react_.useRef)(false); // parent can zoom; wheel/pinch check this
6462
+ const interactiveRef = (0,external_react_.useRef)(interactive); // latest interactive; WaveSurfer listeners read this
6463
+ const isPlayingRef = (0,external_react_.useRef)(isPlaying); // latest isPlaying; tape tap toggles from this
6464
+ const mediaElRef = (0,external_react_.useRef)(mediaEl); // latest <audio>/<video>; camera + playhead tick read this
6465
+ const onPlayPauseRef = (0,external_react_.useRef)(onPlayPause); // latest play/pause; tape tap must not re-bind
6466
+ const onSeekRef = (0,external_react_.useRef)(onSeek); // latest onSeek; click/scroll handlers must not re-bind
6467
+ const onViewportChangeRef = (0,external_react_.useRef)(onViewportChange); // latest viewport callback; camera commits here
6468
+ const peaksRef = (0,external_react_.useRef)(peaks); // latest peaks; WaveSurfer create() + morph read this
6469
+
6470
+ const displayedPeaksRef = (0,external_react_.useRef)(null); // peaks currently drawn (for morph)
6471
+ const peakTransitionAnimationRef = (0,external_react_.useRef)(0); // animation frame id while peaks morph in
5659
6472
  // 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;
6473
+ const applyZoomWindowRef = (0,external_react_.useRef)(null); // latest applyZoomWindow; resize observer calls this
6474
+ const bufferProgressRef = (0,external_react_.useRef)(0); // latest buffer fill; zoomed tile tint reads this
6475
+ const hideScrubTimeChipTimerRef = (0,external_react_.useRef)(0); // hides the tape scrub time chip after scroll settles
6476
+ const hoverProgressRef = (0,external_react_.useRef)(null); // latest hover fill; zoomed tile tint reads this
6477
+ const lastObservedWidthRef = (0,external_react_.useRef)(0); // last ResizeObserver width; skip no-op resizes
6478
+ const liveHeightRef = (0,external_react_.useRef)(height); // canvas height last applied to WaveSurfer
6479
+ const pinchStartRef = (0,external_react_.useRef)(null); // two-finger distance and zoom at pinch start
6480
+ const pointerZoomClearTimerRef = (0,external_react_.useRef)(0); // clears pointerZoomRef after WAVEFORM_ZOOM_DISMISS_MS
6481
+ const pointerZoomRef = (0,external_react_.useRef)(false); // pinch/wheel in progress; skip play/pause and tape recenter
6482
+ const queuedTapeSeekTimeSecRef = (0,external_react_.useRef)(null); // seek time waiting for the next animation frame
6483
+ const rangeProgressRef = (0,external_react_.useRef)(null); // latest range progress; zoomed tile tint reads this
6484
+ const suppressNextTapPlayPauseRef = (0,external_react_.useRef)(false); // swallow play/pause after a swipe or pinch
6485
+ const suppressNextTapPlayPauseTimerRef = (0,external_react_.useRef)(0); // clears suppressNextTapPlayPauseRef
6486
+ const suppressViewportSyncRef = (0,external_react_.useRef)(false); // ignore WaveSurfer zoom/scroll while we setOptions
6487
+ const tapeSeekAnimationRef = (0,external_react_.useRef)(0); // coalesces swipe seeks to one seek per animation frame
6488
+ const toggleTapePlaybackRef = (0,external_react_.useRef)(null); // latest tap play/pause; click/pointerup call this
6489
+ const zoomOriginRef = (0,external_react_.useRef)(null); // pointer+time so zoom keeps that point fixed
6490
+
6491
+ cameraModeRef.current = cameraMode;
5671
6492
  currentTimeRef.current = currentTime;
5672
- peaksRef.current = peaks;
5673
6493
  durationSecRef.current = durationSec;
6494
+ interactiveRef.current = interactive;
6495
+ isPlayingRef.current = isPlaying;
5674
6496
  mediaElRef.current = mediaEl;
6497
+ onPlayPauseRef.current = onPlayPause;
6498
+ onSeekRef.current = onSeek;
5675
6499
  onViewportChangeRef.current = onViewportChange;
6500
+ peaksRef.current = peaks;
5676
6501
  const [internalZoom, setInternalZoom] = (0,external_react_.useState)(constants/* WAVEFORM_ZOOM_MIN */.LW);
5677
6502
  const [hoverProgress, setHoverProgress] = (0,external_react_.useState)(null);
6503
+ const [scrubPreviewTimeSec, setScrubPreviewTimeSec] = (0,external_react_.useState)(null);
6504
+ const [overlayPortalHost, setOverlayPortalHost] = (0,external_react_.useState)(null);
5678
6505
  const [canvasWidthPx, setCanvasWidthPx] = (0,external_react_.useState)(0);
5679
6506
  const [scrollLeft, setScrollLeft] = (0,external_react_.useState)(0);
6507
+ const [previewRange, setPreviewRange] = (0,external_react_.useState)(null);
6508
+ const [isRangeDragging, setIsRangeDragging] = (0,external_react_.useState)(false);
6509
+ const isRangeDraggingRef = (0,external_react_.useRef)(false);
6510
+ const skipNextSeekRef = (0,external_react_.useRef)(false);
6511
+ const lastSetTimeSecRef = (0,external_react_.useRef)(null);
6512
+ const [isRangeHovered, setIsRangeHovered] = (0,external_react_.useState)(false);
5680
6513
  const isControlled = typeof zoomLevelProp === 'number';
5681
6514
  const maxZoom = getWaveformZoomMax({
5682
6515
  durationSec,
@@ -5687,21 +6520,41 @@ function WaveformView({
5687
6520
  const zoomLevel = clampWaveformZoom(isControlled ? zoomLevelProp : internalZoom, maxZoom);
5688
6521
  const zoomRef = (0,external_react_.useRef)(zoomLevel); // latest zoom for WaveSurfer redraw/zoom handlers
5689
6522
  zoomRef.current = zoomLevel;
5690
- const prevZoomRef = (0,external_react_.useRef)(null);
6523
+ const prevZoomRef = (0,external_react_.useRef)(null); // last applied zoom; tape recenters only when this changes
6524
+ const isTape = cameraMode === 'tape';
5691
6525
  const isZoomed = zoomLevel > constants/* WAVEFORM_ZOOM_MIN */.LW;
6526
+ const isScrollableWindow = isZoomed || isTape;
5692
6527
  const bufferProgress = getBufferedProgress(bufferedRange, durationSec);
5693
6528
  bufferProgressRef.current = bufferProgress;
5694
- hoverProgressRef.current = hoverProgress;
6529
+ hoverProgressRef.current = isRangeDragging ? null : hoverProgress;
6530
+ const activeRange = isRangeDragging ? previewRange ?? range : range;
6531
+ const activeRangeRef = (0,external_react_.useRef)(activeRange);
6532
+ activeRangeRef.current = activeRange;
6533
+ const durationMs = durationMsFromSec(durationSec);
6534
+ const rangeStartMs = activeRange?.startMs;
6535
+ const rangeEndMs = activeRange?.endMs;
6536
+ const activeRangeProgress = (0,external_react_.useMemo)(() => {
6537
+ if (rangeStartMs == null || !Number.isFinite(rangeStartMs)) {
6538
+ return null;
6539
+ }
6540
+ return rangeProgress(resolveRange({
6541
+ endMs: rangeEndMs ?? null,
6542
+ startMs: rangeStartMs
6543
+ }, durationMs), durationMs);
6544
+ }, [durationMs, rangeEndMs, rangeStartMs]);
6545
+ rangeProgressRef.current = activeRangeProgress;
5695
6546
  const viewport = (0,external_react_.useMemo)(() => createWaveformViewport({
5696
6547
  durationSec,
6548
+ gutterPx: isTape ? getTapeGutterPx(canvasWidthPx) : 0,
5697
6549
  heightPx: height,
5698
6550
  maxZoom,
5699
6551
  scrollLeftPx: scrollLeft,
5700
6552
  widthPx: canvasWidthPx,
5701
6553
  zoomLevel
5702
- }), [canvasWidthPx, durationSec, height, maxZoom, scrollLeft, zoomLevel]);
6554
+ }), [canvasWidthPx, durationSec, height, isTape, maxZoom, scrollLeft, zoomLevel]);
5703
6555
  const viewportRef = (0,external_react_.useRef)(viewport); // live scroll window; prefer this over render-state while the camera is moving
5704
6556
  const onViewportCommit = (0,external_react_.useCallback)((scrollLeftPx, nextViewport, commitReactState = true) => {
6557
+ rangeLayerRef.current?.applyViewport(nextViewport);
5705
6558
  onViewportChangeRef.current?.(nextViewport);
5706
6559
  if (commitReactState) {
5707
6560
  setScrollLeft(scrollLeftPx);
@@ -5714,20 +6567,45 @@ function WaveformView({
5714
6567
  clearFollowPin,
5715
6568
  handleScroll: handleCameraScroll,
5716
6569
  isFollowPinned,
6570
+ isUserPanning,
5717
6571
  onSeek: onPlayheadSeek,
5718
6572
  onZoom,
5719
6573
  releaseUserPanHold,
5720
6574
  seekTo
5721
6575
  } = usePlayheadCamera({
6576
+ cameraMode,
5722
6577
  mediaElRef,
5723
6578
  onViewportCommit,
5724
6579
  playheadRef,
5725
6580
  viewportRef,
5726
6581
  wavesurferRef
5727
6582
  });
6583
+ const toggleTapePlayback = (0,external_react_.useCallback)(() => {
6584
+ if (!interactiveRef.current) {
6585
+ return;
6586
+ }
6587
+ if (isUserPanning() || pointerZoomRef.current) {
6588
+ return;
6589
+ }
6590
+ if (suppressNextTapPlayPauseRef.current) {
6591
+ suppressNextTapPlayPauseRef.current = false;
6592
+ window.clearTimeout(suppressNextTapPlayPauseTimerRef.current);
6593
+ suppressNextTapPlayPauseTimerRef.current = 0;
6594
+ return;
6595
+ }
6596
+ onPlayPauseRef.current?.(!isPlayingRef.current);
6597
+ suppressNextTapPlayPauseRef.current = true;
6598
+ window.clearTimeout(suppressNextTapPlayPauseTimerRef.current);
6599
+ suppressNextTapPlayPauseTimerRef.current = window.setTimeout(() => {
6600
+ suppressNextTapPlayPauseRef.current = false;
6601
+ suppressNextTapPlayPauseTimerRef.current = 0;
6602
+ }, constants/* WAVEFORM_TAPE_CLICK_SUPPRESS_MS */.fI);
6603
+ }, [isUserPanning]);
6604
+ toggleTapePlaybackRef.current = toggleTapePlayback;
5728
6605
  (0,external_react_.useLayoutEffect)(() => {
5729
6606
  viewportRef.current = createWaveformViewport({
5730
6607
  durationSec: viewport.durationSec,
6608
+ gutterPx: viewport.gutterPx,
5731
6609
  heightPx: viewport.heightPx,
5732
6610
  maxZoom: viewport.maxZoom,
5733
6611
  scrollLeftPx: viewportRef.current.scrollLeftPx,
@@ -5750,6 +6628,41 @@ function WaveformView({
5750
6628
  pointerZoomClearTimerRef.current = 0;
5751
6629
  }, constants/* WAVEFORM_ZOOM_DISMISS_MS */.m6);
5752
6630
  }, []);
6631
+
6632
+ /**
6633
+ * Seek the time under the pin (one seek per frame). Swallow the delayed iOS click;
6634
+ * show the scrub chip until the swipe settles.
6635
+ */
6636
+ const handleTapeSwipe = (0,external_react_.useCallback)(timeSec => {
6637
+ // Swallow the click WaveSurfer/iOS fires after a swipe or pinch.
6638
+ suppressNextTapPlayPauseRef.current = true;
6639
+ window.clearTimeout(suppressNextTapPlayPauseTimerRef.current);
6640
+ suppressNextTapPlayPauseTimerRef.current = window.setTimeout(() => {
6641
+ suppressNextTapPlayPauseRef.current = false;
6642
+ suppressNextTapPlayPauseTimerRef.current = 0;
6643
+ }, constants/* WAVEFORM_TAPE_CLICK_SUPPRESS_MS */.fI);
6644
+ queuedTapeSeekTimeSecRef.current = timeSec;
6645
+ if (!tapeSeekAnimationRef.current) {
6646
+ // One seek per frame; later scrolls just update the queued time.
6647
+ tapeSeekAnimationRef.current = window.requestAnimationFrame(() => {
6648
+ tapeSeekAnimationRef.current = 0;
6649
+ const next = queuedTapeSeekTimeSecRef.current;
6650
+ queuedTapeSeekTimeSecRef.current = null;
6651
+ if (next != null) {
6652
+ onSeekRef.current?.(next);
6653
+ }
6654
+ });
6655
+ }
6656
+ if (interactiveRef.current) {
6657
+ // Show the time under the pin; hide once scrolling has settled.
6658
+ setScrubPreviewTimeSec(timeSec);
6659
+ window.clearTimeout(hideScrubTimeChipTimerRef.current);
6660
+ hideScrubTimeChipTimerRef.current = window.setTimeout(() => {
6661
+ setScrubPreviewTimeSec(null);
6662
+ hideScrubTimeChipTimerRef.current = 0;
6663
+ }, constants/* WAVEFORM_FOLLOW_SCROLL_SETTLE_MS */.Zs);
6664
+ }
6665
+ }, []);
5753
6666
  const syncViewport = (0,external_react_.useCallback)(() => {
5754
6667
  const wavesurfer = wavesurferRef.current;
5755
6668
  if (!wavesurfer) {
@@ -5757,6 +6670,7 @@ function WaveformView({
5757
6670
  }
5758
6671
  const scrollLeftPx = WaveformView_getScrollLeft(wavesurfer);
5759
6672
  viewportRef.current = getViewportAtScroll(viewportRef.current, scrollLeftPx);
6673
+ rangeLayerRef.current?.applyViewport(viewportRef.current);
5760
6674
  onViewportChangeRef.current?.(viewportRef.current);
5761
6675
  setScrollLeft(scrollLeftPx);
5762
6676
  }, []);
@@ -5765,14 +6679,18 @@ function WaveformView({
5765
6679
  if (!playhead) {
5766
6680
  return;
5767
6681
  }
5768
- if (!isFollowPinned()) {
6682
+ if (isTape) {
6683
+ playhead.style.left = getTapePinnedPlayheadLeft();
6684
+ } else if (!isFollowPinned()) {
5769
6685
  playhead.style.left = timeLeftPercent(timeSec, durationSecRef.current, viewportRef.current);
5770
6686
  }
6687
+ rangeLayerRef.current?.applyViewport(viewportRef.current);
5771
6688
  const wavesurfer = wavesurferRef.current;
5772
- if (wavesurfer && wavesurfer.setTime) {
6689
+ if (wavesurfer && wavesurfer.setTime && lastSetTimeSecRef.current !== timeSec) {
6690
+ lastSetTimeSecRef.current = timeSec;
5773
6691
  wavesurfer.setTime(timeSec);
5774
6692
  }
5775
- }, [isFollowPinned]);
6693
+ }, [isFollowPinned, isTape]);
5776
6694
 
5777
6695
  // Same zoom-window body as the zoom effect; extracted so resize can call it too.
5778
6696
  const applyZoomWindow = (0,external_react_.useCallback)(() => {
@@ -5784,11 +6702,13 @@ function WaveformView({
5784
6702
  const viewWidthPx = wavesurfer.getWidth ? wavesurfer.getWidth() : container.clientWidth;
5785
6703
  const minPxPerSec = getZoomedPixelsPerSecond({
5786
6704
  durationSec,
6705
+ isTape,
5787
6706
  maxZoom,
5788
6707
  viewWidthPx,
5789
6708
  zoomLevel
5790
6709
  });
5791
- const origin = zoomOriginRef.current;
6710
+ const gutterPx = isTape ? getTapeGutterPx(viewWidthPx) : 0;
6711
+ const origin = isTape ? null : zoomOriginRef.current;
5792
6712
  zoomOriginRef.current = null;
5793
6713
  const didZoomChange = prevZoomRef.current !== zoomLevel;
5794
6714
  if (origin || didZoomChange) {
@@ -5798,26 +6718,30 @@ function WaveformView({
5798
6718
  try {
5799
6719
  wavesurfer.setOptions(WaveformView_objectSpread({
5800
6720
  autoScroll: false,
6721
+ fillParent: !isTape,
5801
6722
  minPxPerSec
5802
- }, zoomLevel > constants/* WAVEFORM_ZOOM_MIN */.LW ? {
5803
- progressColor: WAVEFORM_COLOR_PLAYED,
6723
+ }, isScrollableWindow ? {
6724
+ progressColor: getPlayedWaveformColor(isTape),
5804
6725
  waveColor: WAVEFORM_COLOR_UNPLAYED
5805
6726
  } : {}));
6727
+ applyWaveformGutters(wavesurfer, gutterPx);
5806
6728
  if (minPxPerSec > 0) {
6729
+ const windowViewport = createWaveformViewport({
6730
+ durationSec,
6731
+ gutterPx,
6732
+ heightPx: liveHeightRef.current || height,
6733
+ maxZoom,
6734
+ scrollLeftPx: 0,
6735
+ widthPx: viewWidthPx,
6736
+ zoomLevel
6737
+ });
5807
6738
  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);
6739
+ applyScrollLeft(origin.timeSec * minPxPerSec + gutterPx - origin.pointerX, true);
6740
+ } else if (didZoomChange && (isTape || !pointerZoomRef.current)) {
6741
+ applyScrollLeft(getCenteredScrollLeft(currentTimeRef.current, windowViewport), true);
5819
6742
  }
5820
6743
  }
6744
+ lastSetTimeSecRef.current = currentTimeRef.current;
5821
6745
  wavesurfer.setTime(currentTimeRef.current);
5822
6746
  } finally {
5823
6747
  suppressViewportSyncRef.current = false;
@@ -5825,13 +6749,17 @@ function WaveformView({
5825
6749
  }
5826
6750
  syncViewport();
5827
6751
  updatePlayheadPosition(currentTimeRef.current);
5828
- }, [applyScrollLeft, durationSec, height, maxZoom, onZoom, syncViewport, updatePlayheadPosition, zoomLevel]);
6752
+ }, [applyScrollLeft, durationSec, height, isScrollableWindow, isTape, maxZoom, onZoom, syncViewport, updatePlayheadPosition, zoomLevel]);
5829
6753
  applyZoomWindowRef.current = applyZoomWindow;
5830
6754
  (0,external_react_.useEffect)(() => {
5831
6755
  const container = containerRef.current;
5832
6756
  if (!container) {
5833
6757
  return undefined;
5834
6758
  }
6759
+ const measuredHeight = cameraModeRef.current === 'tape' && container.clientHeight > 0 ? Math.round(container.clientHeight) : height;
6760
+ if (cameraModeRef.current === 'tape' && measuredHeight > 0) {
6761
+ liveHeightRef.current = measuredHeight;
6762
+ }
5835
6763
  const wavesurfer = w.create({
5836
6764
  autoScroll: false,
5837
6765
  barGap: constants/* WAVEFORM_BAR_GAP */.Lu,
@@ -5842,16 +6770,25 @@ function WaveformView({
5842
6770
  cursorWidth: 0,
5843
6771
  duration: durationSecRef.current,
5844
6772
  fillParent: true,
5845
- height,
6773
+ height: measuredHeight,
5846
6774
  hideScrollbar: true,
5847
- interact: interactiveRef.current,
6775
+ dragToSeek: false,
6776
+ interact: interactiveRef.current && cameraModeRef.current !== 'tape',
5848
6777
  normalize: false,
5849
6778
  peaks: toChannels(peaksRef.current),
5850
- progressColor: WAVEFORM_COLOR_PLAYED,
6779
+ progressColor: getPlayedWaveformColor(cameraModeRef.current === 'tape'),
5851
6780
  waveColor: WAVEFORM_COLOR_UNPLAYED
5852
6781
  });
5853
6782
  const unsubscribeClick = wavesurfer.on('click', relativeX => {
5854
- if (!interactiveRef.current) {
6783
+ if (skipNextSeekRef.current) {
6784
+ skipNextSeekRef.current = false;
6785
+ return;
6786
+ }
6787
+ if (!interactiveRef.current || isRangeDraggingRef.current) {
6788
+ return;
6789
+ }
6790
+ if (cameraModeRef.current === 'tape') {
6791
+ toggleTapePlaybackRef.current?.();
5855
6792
  return;
5856
6793
  }
5857
6794
  onSeekRef.current?.(relativeX * durationSecRef.current);
@@ -5860,9 +6797,14 @@ function WaveformView({
5860
6797
  if (suppressViewportSyncRef.current) {
5861
6798
  return;
5862
6799
  }
5863
- handleCameraScroll(() => {
6800
+ // User pan: map scroll to the time under the playhead, then tape seeks it.
6801
+ handleCameraScroll(timeSec => {
6802
+ if (cameraModeRef.current === 'tape' && !pointerZoomRef.current) {
6803
+ handleTapeSwipe(timeSec);
6804
+ }
5864
6805
  syncViewport();
5865
6806
  });
6807
+ rangeLayerRef.current?.applyViewport(viewportRef.current);
5866
6808
  });
5867
6809
  const unsubscribeZoom = wavesurfer.on('zoom', () => {
5868
6810
  if (suppressViewportSyncRef.current) {
@@ -5871,22 +6813,28 @@ function WaveformView({
5871
6813
  syncViewport();
5872
6814
  });
5873
6815
  const unsubscribeRedraw = wavesurfer.on('redrawcomplete', () => {
5874
- if (!(zoomRef.current > constants/* WAVEFORM_ZOOM_MIN */.LW)) {
6816
+ if (!(zoomRef.current > constants/* WAVEFORM_ZOOM_MIN */.LW) && cameraModeRef.current !== 'tape') {
5875
6817
  return;
5876
6818
  }
5877
6819
  tintZoomedWaveform(wavesurfer, getWaveformFills({
5878
6820
  bufferProgress: bufferProgressRef.current,
5879
- hoverProgress: hoverProgressRef.current
6821
+ hoverProgress: cameraModeRef.current === 'tape' ? null : hoverProgressRef.current,
6822
+ isTape: cameraModeRef.current === 'tape',
6823
+ rangeProgress: rangeProgressRef.current
5880
6824
  }), true);
5881
6825
  });
5882
6826
  wavesurferRef.current = wavesurfer;
5883
6827
  displayedPeaksRef.current = peaksRef.current;
6828
+ lastSetTimeSecRef.current = null;
5884
6829
  disableScrollOverscroll(container);
5885
6830
  syncViewport();
5886
6831
  applyZoomWindowRef.current?.();
5887
6832
  return () => {
5888
6833
  releaseUserPanHold();
5889
6834
  cancelJump();
6835
+ window.clearTimeout(hideScrubTimeChipTimerRef.current);
6836
+ window.clearTimeout(suppressNextTapPlayPauseTimerRef.current);
6837
+ window.cancelAnimationFrame(tapeSeekAnimationRef.current);
5890
6838
  window.cancelAnimationFrame(peakTransitionAnimationRef.current);
5891
6839
  unsubscribeClick();
5892
6840
  unsubscribeScroll();
@@ -5895,19 +6843,41 @@ function WaveformView({
5895
6843
  wavesurfer.destroy();
5896
6844
  wavesurferRef.current = null;
5897
6845
  displayedPeaksRef.current = null;
6846
+ lastSetTimeSecRef.current = null;
5898
6847
  };
5899
- }, [cancelJump, handleCameraScroll, height, releaseUserPanHold, syncViewport]);
6848
+ }, [cancelJump, handleCameraScroll, handleTapeSwipe, height, isUserPanning, releaseUserPanHold, syncViewport]);
5900
6849
  (0,external_react_.useLayoutEffect)(() => {
5901
6850
  const el = containerRef.current;
5902
6851
  if (!el) {
5903
6852
  return undefined;
5904
6853
  }
5905
6854
  setCanvasWidthPx(el.clientWidth);
6855
+ lastObservedWidthRef.current = el.clientWidth;
6856
+ const mountedHeight = Math.round(el.clientHeight);
6857
+ if (cameraModeRef.current === 'tape' && mountedHeight > 0) {
6858
+ liveHeightRef.current = mountedHeight;
6859
+ }
5906
6860
  const observer = new ResizeObserver(entries => {
5907
6861
  entries.forEach(entry => {
5908
- setCanvasWidthPx(entry.contentRect.width);
6862
+ const nextWidth = entry.contentRect.width;
6863
+ const widthChanged = nextWidth !== lastObservedWidthRef.current;
6864
+ lastObservedWidthRef.current = nextWidth;
6865
+ setCanvasWidthPx(nextWidth);
6866
+ const nextHeight = Math.round(entry.contentRect.height);
6867
+ if (cameraModeRef.current === 'tape' && Number.isFinite(nextHeight) && nextHeight > 0 && nextHeight !== liveHeightRef.current) {
6868
+ liveHeightRef.current = nextHeight;
6869
+ const wavesurfer = wavesurferRef.current;
6870
+ if (wavesurfer && wavesurfer.setOptions) {
6871
+ wavesurfer.setOptions({
6872
+ height: nextHeight
6873
+ });
6874
+ }
6875
+ }
6876
+ syncViewport();
6877
+ if (widthChanged) {
6878
+ applyZoomWindowRef.current?.();
6879
+ }
5909
6880
  });
5910
- syncViewport();
5911
6881
  });
5912
6882
  observer.observe(el);
5913
6883
  return () => observer.disconnect();
@@ -5932,7 +6902,7 @@ function WaveformView({
5932
6902
  return;
5933
6903
  }
5934
6904
  wavesurfer.setOptions({
5935
- interact: interactive
6905
+ interact: interactive && cameraModeRef.current !== 'tape'
5936
6906
  });
5937
6907
  }, [interactive]);
5938
6908
  (0,external_react_.useLayoutEffect)(() => {
@@ -5963,13 +6933,17 @@ function WaveformView({
5963
6933
  window.cancelAnimationFrame(playheadAnimationRef.current);
5964
6934
  cancelJump();
5965
6935
  releaseUserPanHold();
5966
- clearFollowPin();
6936
+ if (cameraModeRef.current !== 'tape') {
6937
+ clearFollowPin();
6938
+ }
5967
6939
  syncViewport();
5968
6940
  updatePlayheadPosition(media.currentTime);
5969
6941
  };
5970
6942
  const handleSeeked = () => {
5971
6943
  onPlayheadSeek(media.currentTime);
5972
- seekTo(media.currentTime);
6944
+ if (!isUserPanning()) {
6945
+ seekTo(media.currentTime);
6946
+ }
5973
6947
  updatePlayheadPosition(media.currentTime);
5974
6948
  };
5975
6949
  if (!media.paused) {
@@ -5988,7 +6962,7 @@ function WaveformView({
5988
6962
  media.removeEventListener('pause', stopLoop);
5989
6963
  media.removeEventListener('seeked', handleSeeked);
5990
6964
  };
5991
- }, [applyPlayheadCamera, cancelJump, clearFollowPin, mediaEl, onPlayheadSeek, releaseUserPanHold, seekTo, syncViewport, updatePlayheadPosition]);
6965
+ }, [applyPlayheadCamera, cancelJump, clearFollowPin, isUserPanning, mediaEl, onPlayheadSeek, releaseUserPanHold, seekTo, syncViewport, updatePlayheadPosition]);
5992
6966
  (0,external_react_.useEffect)(() => {
5993
6967
  const wavesurfer = wavesurferRef.current;
5994
6968
  if (!wavesurfer || !wavesurfer.setOptions || !(durationSec > 0)) {
@@ -6023,9 +6997,11 @@ function WaveformView({
6023
6997
  }
6024
6998
  const fills = getWaveformFills({
6025
6999
  bufferProgress,
6026
- hoverProgress
7000
+ hoverProgress: isTape ? null : hoverProgress,
7001
+ isTape,
7002
+ rangeProgress: activeRangeProgress
6027
7003
  });
6028
- if (isZoomed) {
7004
+ if (isScrollableWindow) {
6029
7005
  tintZoomedWaveform(wavesurfer, fills);
6030
7006
  return;
6031
7007
  }
@@ -6033,8 +7009,9 @@ function WaveformView({
6033
7009
  progressColor: toCanvasFill(fills.progressColor, devicePixelWidth(canvasWidthPx)),
6034
7010
  waveColor: toCanvasFill(fills.waveColor, devicePixelWidth(canvasWidthPx))
6035
7011
  });
7012
+ lastSetTimeSecRef.current = currentTimeRef.current;
6036
7013
  wavesurfer.setTime(currentTimeRef.current);
6037
- }, [bufferProgress, canvasWidthPx, hoverProgress, isZoomed]);
7014
+ }, [activeRangeProgress, bufferProgress, canvasWidthPx, hoverProgress, isRangeDragging, isScrollableWindow, isTape, isZoomed]);
6038
7015
  (0,external_react_.useEffect)(() => {
6039
7016
  const track = trackRef.current;
6040
7017
  if (!track) {
@@ -6115,11 +7092,11 @@ function WaveformView({
6115
7092
  };
6116
7093
  }, [durationSec, markPointerZoom, setZoomLevel]);
6117
7094
  const onHoverMove = (0,external_react_.useCallback)(event => {
6118
- if (!interactive) {
7095
+ if (isTape || isTouchHoverInput(event)) {
6119
7096
  return;
6120
7097
  }
6121
7098
  const rect = event.currentTarget.getBoundingClientRect();
6122
- if (!(rect.width > 0) || !(durationSec > 0)) {
7099
+ if (!(rect.width > 0)) {
6123
7100
  return;
6124
7101
  }
6125
7102
  const pointerX = event.clientX - rect.left;
@@ -6127,24 +7104,81 @@ function WaveformView({
6127
7104
  return;
6128
7105
  }
6129
7106
  const vp = viewportRef.current;
7107
+ const draft = activeRangeRef.current;
7108
+ setIsRangeHovered(!!draft && isPointerOverRange({
7109
+ pointerX,
7110
+ range: draft,
7111
+ viewport: vp
7112
+ }));
7113
+ if (!interactive || isRangeDraggingRef.current || !(durationSec > 0)) {
7114
+ return;
7115
+ }
6130
7116
  if (vp.pixelsPerSecond > 0) {
6131
7117
  setHoverProgress(Math.min(1, Math.max(0, timeFromPositionPx(pointerX, vp) / durationSec)));
6132
7118
  return;
6133
7119
  }
6134
7120
  setHoverProgress(Math.min(1, Math.max(0, pointerX / rect.width)));
6135
- }, [durationSec, interactive]);
7121
+ }, [durationSec, interactive, isTape]);
6136
7122
  const onHoverLeave = (0,external_react_.useCallback)(() => {
6137
7123
  setHoverProgress(null);
7124
+ setIsRangeHovered(false);
7125
+ }, []);
7126
+ const handleRangeDragChange = (0,external_react_.useCallback)(isDragging => {
7127
+ isRangeDraggingRef.current = isDragging;
7128
+ setIsRangeDragging(isDragging);
7129
+ if (isDragging) {
7130
+ setHoverProgress(null);
7131
+ } else {
7132
+ skipNextSeekRef.current = true;
7133
+ window.setTimeout(() => {
7134
+ skipNextSeekRef.current = false;
7135
+ }, 0);
7136
+ setPreviewRange(null);
7137
+ }
7138
+ onRangeDragChange?.(isDragging);
7139
+ }, [onRangeDragChange]);
7140
+ const getPlayheadSec = (0,external_react_.useCallback)(() => {
7141
+ return mediaElRef.current ? mediaElRef.current.currentTime : currentTimeRef.current;
7142
+ }, []);
7143
+ const bindOverlayPortalHost = (0,external_react_.useCallback)(node => {
7144
+ const host = node?.parentElement ?? null;
7145
+ setOverlayPortalHost(prev => prev === host ? prev : host);
6138
7146
  }, []);
6139
- const hoverLeft = hoverProgress == null ? null : timeLeftPercent(hoverProgress * durationSec, durationSec, viewportRef.current);
7147
+ const hoverLeft = isRangeDragging || hoverProgress == null ? null : timeLeftPercent(hoverProgress * durationSec, durationSec, viewportRef.current);
7148
+ const showRangeOverlay = Boolean(range) && (isRangeDragging || !isPlaying || !isRangeCollapsed(range));
7149
+ const scrubTimeChip = isTape && scrubPreviewTimeSec != null ? /*#__PURE__*/external_react_["default"].createElement("div", {
7150
+ className: "bp-WaveformView-hover bp-WaveformView-hover--tape",
7151
+ "data-testid": "bp-waveform-hover"
7152
+ }, /*#__PURE__*/external_react_["default"].createElement("div", {
7153
+ className: "bp-WaveformView-hoverTime",
7154
+ "data-testid": "bp-waveform-hover-time"
7155
+ }, formatTime(scrubPreviewTimeSec))) : null;
6140
7156
  return /*#__PURE__*/external_react_["default"].createElement("div", {
6141
- className: `bp-WaveformView${interactive ? '' : ' bp-WaveformView--inert'}${isZoomed ? ' bp-WaveformView--zoomed' : ''}`,
7157
+ ref: bindOverlayPortalHost,
7158
+ className: classnames_default()('bp-WaveformView', {
7159
+ 'bp-WaveformView--inert': !interactive,
7160
+ 'bp-WaveformView--tape': isTape,
7161
+ 'bp-WaveformView--zoomed': isZoomed
7162
+ }),
6142
7163
  "data-testid": "bp-waveform-view"
6143
7164
  }, /*#__PURE__*/external_react_["default"].createElement("div", {
6144
7165
  ref: trackRef,
6145
7166
  className: "bp-WaveformView-track",
6146
- onMouseLeave: interactive ? onHoverLeave : undefined,
6147
- onMouseMove: interactive ? onHoverMove : undefined
7167
+ onMouseLeave: interactive && !isTape ? onHoverLeave : undefined,
7168
+ onMouseMove: interactive && !isTape ? onHoverMove : undefined,
7169
+ onPointerMove: interactive && !isTape ? event => {
7170
+ if (event.pointerType === 'touch') {
7171
+ onHoverLeave();
7172
+ return;
7173
+ }
7174
+ onHoverMove(event);
7175
+ } : undefined,
7176
+ onPointerUp: isTape ? event => {
7177
+ if (event.button > 0) {
7178
+ return;
7179
+ }
7180
+ toggleTapePlaybackRef.current?.();
7181
+ } : undefined
6148
7182
  }, /*#__PURE__*/external_react_["default"].createElement("div", {
6149
7183
  ref: containerRef,
6150
7184
  className: "bp-WaveformView-canvas"
@@ -6153,7 +7187,20 @@ function WaveformView({
6153
7187
  "aria-hidden": "true",
6154
7188
  className: "bp-WaveformView-playhead",
6155
7189
  "data-testid": "bp-waveform-playhead"
6156
- }), hoverLeft != null && hoverProgress != null && /*#__PURE__*/external_react_["default"].createElement("div", {
7190
+ }), showRangeOverlay && range && /*#__PURE__*/external_react_["default"].createElement(waveform_WaveformRangeSelection, {
7191
+ ref: rangeLayerRef,
7192
+ currentTimeSec: currentTime,
7193
+ durationSec: durationSec,
7194
+ getPlayheadSec: getPlayheadSec,
7195
+ interactive: interactive,
7196
+ isHighlighted: isRangeDragging || isRangeHovered,
7197
+ keepHighlight: true,
7198
+ onDragChange: handleRangeDragChange,
7199
+ onPreviewChange: setPreviewRange,
7200
+ onRangeChange: interactive ? onRangeChange : undefined,
7201
+ range: range,
7202
+ viewport: viewport
7203
+ }), hoverLeft != null && hoverProgress != null && !isRangeDragging && /*#__PURE__*/external_react_["default"].createElement("div", {
6157
7204
  className: "bp-WaveformView-hover",
6158
7205
  "data-testid": "bp-waveform-hover",
6159
7206
  style: {
@@ -6162,12 +7209,9 @@ function WaveformView({
6162
7209
  }, /*#__PURE__*/external_react_["default"].createElement("div", {
6163
7210
  className: "bp-WaveformView-hoverTime",
6164
7211
  "data-testid": "bp-waveform-hover-time"
6165
- }, formatTime(hoverProgress * durationSec)))));
7212
+ }, formatTime(hoverProgress * durationSec)))), scrubTimeChip && overlayPortalHost ? /*#__PURE__*/__WEBPACK_EXTERNAL_MODULE_react_dom_7dac9eee_createPortal__(scrubTimeChip, overlayPortalHost) : null);
6166
7213
  }
6167
7214
  /* 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
7215
  ;// ./src/lib/viewers/controls/icons/IconZoomIn24.tsx
6172
7216
 
6173
7217
  function IconZoomIn24() {
@@ -6253,11 +7297,14 @@ function WaveformZoomControl({
6253
7297
  }) {
6254
7298
  const [isHovered, setHovered] = (0,external_react_.useState)(false);
6255
7299
  const [isFocused, setFocused] = (0,external_react_.useState)(false);
6256
- const dismissTimerRef = (0,external_react_.useRef)(0);
7300
+ const [isPinned, setPinned] = (0,external_react_.useState)(false);
7301
+ const wasFlyoutOpenOnPointerDownRef = (0,external_react_.useRef)(true); // first tap on + pins the flyout instead of zooming
7302
+ const zoomControlElRef = (0,external_react_.useRef)(null); // root; outside pointerdown unpins the flyout
7303
+ const dismissTimerRef = (0,external_react_.useRef)(0); // delay before hover-close of the flyout
6257
7304
  const sliderId = `bp-waveform-zoom-slider${(0,external_react_.useId)()}`;
6258
7305
  const zoom = clampWaveformZoom(zoomLevel, maxZoom);
6259
7306
  const zoomValue = Math.round(sliderValueFromZoom(zoom, maxZoom));
6260
- const isOpen = isHovered || isFocused || isRevealed;
7307
+ const isOpen = isHovered || isFocused || isRevealed || isPinned;
6261
7308
  const isAtMinZoom = zoomValue <= 0;
6262
7309
  const isAtMaxZoom = zoomValue >= constants/* WAVEFORM_ZOOM_SLIDER_MAX */.nh;
6263
7310
  const clearDismiss = (0,external_react_.useCallback)(() => {
@@ -6265,13 +7312,42 @@ function WaveformZoomControl({
6265
7312
  dismissTimerRef.current = 0;
6266
7313
  }, []);
6267
7314
  (0,external_react_.useEffect)(() => () => window.clearTimeout(dismissTimerRef.current), []);
7315
+ (0,external_react_.useEffect)(() => {
7316
+ if (!isPinned) {
7317
+ return undefined;
7318
+ }
7319
+ const closeIfOutside = event => {
7320
+ const zoomControlEl = zoomControlElRef.current;
7321
+ if (!zoomControlEl || zoomControlEl.contains(event.target)) {
7322
+ return;
7323
+ }
7324
+ setPinned(false);
7325
+ setFocused(false);
7326
+ setHovered(false);
7327
+ };
7328
+ document.addEventListener('pointerdown', closeIfOutside);
7329
+ return () => document.removeEventListener('pointerdown', closeIfOutside);
7330
+ }, [isPinned]);
6268
7331
  const handleSlider = (0,external_react_.useCallback)(newValue => {
6269
7332
  onZoomChange(zoomFromSliderValue(newValue, maxZoom));
6270
7333
  }, [maxZoom, onZoomChange]);
6271
7334
  const handleStep = (0,external_react_.useCallback)(delta => {
6272
- onZoomChange(zoomFromSliderValue(zoomValue + delta, maxZoom));
6273
- }, [maxZoom, onZoomChange, zoomValue]);
7335
+ onZoomChange(stepWaveformZoom(zoom, maxZoom, delta));
7336
+ }, [maxZoom, onZoomChange, zoom]);
7337
+ const handleZoomInPointerDown = (0,external_react_.useCallback)(() => {
7338
+ wasFlyoutOpenOnPointerDownRef.current = isHovered || isFocused || isRevealed || isPinned;
7339
+ }, [isFocused, isHovered, isPinned, isRevealed]);
7340
+ const handleZoomInClick = (0,external_react_.useCallback)(() => {
7341
+ if (!wasFlyoutOpenOnPointerDownRef.current) {
7342
+ setPinned(true);
7343
+ return;
7344
+ }
7345
+ if (!isAtMaxZoom) {
7346
+ handleStep(constants/* WAVEFORM_ZOOM_BUTTON_STEP */.QD);
7347
+ }
7348
+ }, [handleStep, isAtMaxZoom]);
6274
7349
  return /*#__PURE__*/external_react_["default"].createElement("div", {
7350
+ ref: zoomControlElRef,
6275
7351
  "aria-label": "Zoom",
6276
7352
  className: classnames_default()('bp-WaveformZoomControl', {
6277
7353
  'bp-is-open': isOpen
@@ -6282,6 +7358,7 @@ function WaveformZoomControl({
6282
7358
  return;
6283
7359
  }
6284
7360
  setFocused(false);
7361
+ setPinned(false);
6285
7362
  },
6286
7363
  onFocus: () => {
6287
7364
  clearDismiss();
@@ -6299,15 +7376,12 @@ function WaveformZoomControl({
6299
7376
  },
6300
7377
  role: "group"
6301
7378
  }, /*#__PURE__*/external_react_["default"].createElement(MediaToggle/* default */.A, {
6302
- "aria-disabled": isAtMaxZoom,
7379
+ "aria-disabled": isOpen && isAtMaxZoom,
6303
7380
  className: "bp-WaveformZoomControl-button",
6304
7381
  "data-resin-target": "waveformZoomIn",
6305
7382
  "data-testid": "bp-waveform-zoom-in",
6306
- onClick: () => {
6307
- if (!isAtMaxZoom) {
6308
- handleStep(constants/* WAVEFORM_ZOOM_BUTTON_STEP */.QD);
6309
- }
6310
- },
7383
+ onClick: handleZoomInClick,
7384
+ onPointerDown: handleZoomInPointerDown,
6311
7385
  title: "Zoom in"
6312
7386
  }, /*#__PURE__*/external_react_["default"].createElement(icons_IconZoomIn24, null)), /*#__PURE__*/external_react_["default"].createElement("div", {
6313
7387
  "aria-hidden": !isOpen,
@@ -6361,6 +7435,7 @@ function WaveformZoomControl({
6361
7435
 
6362
7436
 
6363
7437
 
7438
+
6364
7439
  const PLACEHOLDER_PEAKS = placeholderPeaks();
6365
7440
  function MP3ControlsV2({
6366
7441
  autoplay,
@@ -6368,7 +7443,10 @@ function MP3ControlsV2({
6368
7443
  commentMarkers,
6369
7444
  currentTime,
6370
7445
  durationTime,
7446
+ hasStartedPlayback = false,
6371
7447
  isPlaying,
7448
+ keyboardVolumeStep = 0,
7449
+ keyboardZoomStep = 0,
6372
7450
  mediaEl,
6373
7451
  onAutoplayChange,
6374
7452
  onCommentMarkerClick,
@@ -6388,12 +7466,16 @@ function MP3ControlsV2({
6388
7466
  const [zoomLevel, setZoomLevel] = (0,external_react_.useState)(constants/* WAVEFORM_ZOOM_MIN */.LW);
6389
7467
  const [maxZoom, setMaxZoom] = (0,external_react_.useState)(constants/* WAVEFORM_ZOOM_MIN */.LW);
6390
7468
  const [isZoomRevealed, setIsZoomRevealed] = (0,external_react_.useState)(false);
6391
- const zoomRevealTimerRef = (0,external_react_.useRef)(0);
7469
+ const zoomRevealTimerRef = (0,external_react_.useRef)(0); // hides the zoom flyout after WAVEFORM_ZOOM_DISMISS_MS
6392
7470
  const hasRealPeaks = !!(peaks && peaks.length);
6393
7471
  const waveformPeaks = hasRealPeaks ? peaks : PLACEHOLDER_PEAKS;
6394
7472
  const waveformDurationSec = hasWaveformDuration ? durationValue : PLACEHOLDER_DURATION_SEC;
6395
- const [playRequested, setPlayRequested] = (0,external_react_.useState)(false);
7473
+ const [playRequested, setPlayRequested] = (0,external_react_.useState)(hasStartedPlayback);
6396
7474
  const [viewport, setViewport] = (0,external_react_.useState)(null);
7475
+ const isTape = useTapeWaveform();
7476
+ const hasAppliedTapeDefaultZoomRef = (0,external_react_.useRef)(false); // ~10s window applied; reset to 1× when leaving tape
7477
+ const lastKeyboardZoomStepRef = (0,external_react_.useRef)(0); // last +/− step applied; skip re-step when maxZoom retriggers the effect
7478
+ const userChangedTapeZoomRef = (0,external_react_.useRef)(false); // pinch/wheel zoom; skip re-applying the 10s default
6397
7479
  const handleViewportChange = (0,external_react_.useCallback)(next => {
6398
7480
  setMaxZoom(prev => prev === next.maxZoom ? prev : next.maxZoom);
6399
7481
  setViewport(prev => viewportEquals(prev, next) ? prev : next);
@@ -6407,18 +7489,52 @@ function MP3ControlsV2({
6407
7489
  }, constants/* WAVEFORM_ZOOM_DISMISS_MS */.m6);
6408
7490
  }, []);
6409
7491
  const handleWaveformZoom = (0,external_react_.useCallback)(nextZoom => {
7492
+ userChangedTapeZoomRef.current = true;
6410
7493
  setZoomLevel(nextZoom);
6411
7494
  revealZoomControl();
6412
7495
  }, [revealZoomControl]);
7496
+ (0,external_react_.useEffect)(() => {
7497
+ const steps = keyboardZoomStep - lastKeyboardZoomStepRef.current;
7498
+ if (!steps) {
7499
+ return;
7500
+ }
7501
+ lastKeyboardZoomStepRef.current = keyboardZoomStep;
7502
+ if (!(maxZoom > constants/* WAVEFORM_ZOOM_MIN */.LW)) {
7503
+ return;
7504
+ }
7505
+ userChangedTapeZoomRef.current = true;
7506
+ setZoomLevel(prev => stepWaveformZoom(prev, maxZoom, steps * constants/* WAVEFORM_ZOOM_BUTTON_STEP */.QD));
7507
+ revealZoomControl();
7508
+ }, [keyboardZoomStep, maxZoom, revealZoomControl]);
6413
7509
  (0,external_react_.useEffect)(() => {
6414
7510
  setZoomLevel(prev => clampWaveformZoom(prev, maxZoom));
6415
7511
  }, [maxZoom]);
7512
+ (0,external_react_.useEffect)(() => {
7513
+ if (!isTape) {
7514
+ if (hasAppliedTapeDefaultZoomRef.current) {
7515
+ hasAppliedTapeDefaultZoomRef.current = false;
7516
+ userChangedTapeZoomRef.current = false;
7517
+ setZoomLevel(constants/* WAVEFORM_ZOOM_MIN */.LW);
7518
+ }
7519
+ return;
7520
+ }
7521
+ if (!hasWaveformDuration || !hasRealPeaks || !viewport || !(viewport.widthPx > 0)) {
7522
+ return;
7523
+ }
7524
+ if (userChangedTapeZoomRef.current) {
7525
+ hasAppliedTapeDefaultZoomRef.current = true;
7526
+ return;
7527
+ }
7528
+ const nextZoom = getTapeDefaultZoom(durationValue, Math.max(maxZoom, viewport.maxZoom));
7529
+ hasAppliedTapeDefaultZoomRef.current = true;
7530
+ setZoomLevel(prev => prev === nextZoom ? prev : nextZoom);
7531
+ }, [durationValue, hasRealPeaks, hasWaveformDuration, isTape, maxZoom, viewport]);
6416
7532
  (0,external_react_.useEffect)(() => () => window.clearTimeout(zoomRevealTimerRef.current), []);
6417
7533
  (0,external_react_.useEffect)(() => {
6418
- if (isPlaying) {
7534
+ if (isPlaying || hasStartedPlayback) {
6419
7535
  setPlayRequested(true);
6420
7536
  }
6421
- }, [isPlaying]);
7537
+ }, [hasStartedPlayback, isPlaying]);
6422
7538
  const handlePlayOverlayClick = (0,external_react_.useCallback)(() => {
6423
7539
  setPlayRequested(true);
6424
7540
  onPlayPause(true);
@@ -6436,11 +7552,13 @@ function MP3ControlsV2({
6436
7552
  }
6437
7553
  onTimeChange(marker.time);
6438
7554
  }, [onCommentMarkerClick, onPlayPause, onTimeChange]);
6439
- const isWaveformInteractive = playRequested && hasMediaMetadata;
6440
- const isWaitingToPlay = playRequested && !hasMediaMetadata;
6441
- const showPlayOverlay = !playRequested && !isPlaying;
7555
+ const hasStarted = playRequested || hasStartedPlayback;
7556
+ const isWaveformInteractive = hasStarted && hasMediaMetadata;
7557
+ const isWaitingToPlay = hasStarted && !hasMediaMetadata;
7558
+ const showPlayOverlay = !hasStarted && !isPlaying;
6442
7559
  const hasZoomHandlers = hasRealPeaks && !showPlayOverlay;
6443
7560
  const hasZoomControl = hasZoomHandlers && hasMediaMetadata && maxZoom > constants/* WAVEFORM_ZOOM_MIN */.LW;
7561
+ const waveformZoomLevel = isTape || hasZoomHandlers ? zoomLevel : constants/* WAVEFORM_ZOOM_MIN */.LW;
6444
7562
  return /*#__PURE__*/external_react_["default"].createElement("div", {
6445
7563
  className: "bp-MP3ControlsV2",
6446
7564
  "data-testid": "media-controls-wrapper-v2"
@@ -6450,18 +7568,22 @@ function MP3ControlsV2({
6450
7568
  className: "bp-MP3ControlsV2-waveform"
6451
7569
  }, /*#__PURE__*/external_react_["default"].createElement(waveform_WaveformView, {
6452
7570
  bufferedRange: bufferedRange,
7571
+ cameraMode: isTape ? 'tape' : 'desktop',
6453
7572
  currentTime: currentTime,
6454
7573
  durationSec: waveformDurationSec,
6455
7574
  interactive: isWaveformInteractive,
7575
+ isPlaying: isPlaying,
6456
7576
  mediaEl: mediaEl,
7577
+ onPlayPause: isWaveformInteractive ? onPlayPause : undefined,
6457
7578
  onSeek: isWaveformInteractive ? onTimeChange : undefined,
6458
7579
  onViewportChange: hasRealPeaks ? handleViewportChange : undefined,
6459
7580
  onZoomChange: hasZoomHandlers ? handleWaveformZoom : undefined,
6460
7581
  peaks: waveformPeaks,
6461
- zoomLevel: hasZoomHandlers ? zoomLevel : constants/* WAVEFORM_ZOOM_MIN */.LW
7582
+ zoomLevel: waveformZoomLevel
6462
7583
  }), /*#__PURE__*/external_react_["default"].createElement(WaveformCommentMarkers, {
6463
7584
  commentMarkers: waveformMarkers,
6464
7585
  durationSec: hasWaveformDuration ? durationValue : 0,
7586
+ isTape: isTape,
6465
7587
  onCommentMarkerClick: handleCommentMarkerClick,
6466
7588
  selectedId: selectedMarkerId,
6467
7589
  viewport: viewport
@@ -6470,7 +7592,7 @@ function MP3ControlsV2({
6470
7592
  }, /*#__PURE__*/external_react_["default"].createElement(WaveformZoomControl, {
6471
7593
  isRevealed: isZoomRevealed,
6472
7594
  maxZoom: maxZoom,
6473
- onZoomChange: setZoomLevel,
7595
+ onZoomChange: handleWaveformZoom,
6474
7596
  zoomLevel: zoomLevel
6475
7597
  })), showPlayOverlay && /*#__PURE__*/external_react_["default"].createElement("button", {
6476
7598
  className: "bp-MP3ControlsV2-playOverlay"
@@ -6504,7 +7626,8 @@ function MP3ControlsV2({
6504
7626
  durationTime: durationValue
6505
7627
  })), /*#__PURE__*/external_react_["default"].createElement("div", {
6506
7628
  className: "bp-MP3ControlsV2-group"
6507
- }, /*#__PURE__*/external_react_["default"].createElement(VolumeControls/* default */.A, {
7629
+ }, /*#__PURE__*/external_react_["default"].createElement(VolumeControls/* default */.Ay, {
7630
+ keyboardVolumeStep: keyboardVolumeStep,
6508
7631
  onMuteChange: onMuteChange,
6509
7632
  onVolumeChange: onVolumeChange,
6510
7633
  volume: volume
@@ -6590,19 +7713,26 @@ function isFpsAvailable(player) {
6590
7713
  (__unused_webpack_module, __webpack_exports__, __webpack_require__) {
6591
7714
 
6592
7715
  /* harmony export */ __webpack_require__.d(__webpack_exports__, {
7716
+ /* harmony export */ Av: () => (/* binding */ WAVEFORM_TAPE_DEFAULT_WINDOW_SEC),
7717
+ /* harmony export */ BG: () => (/* binding */ WAVEFORM_RANGE_COLLAPSED_OFFSET_PX),
6593
7718
  /* harmony export */ Bq: () => (/* binding */ WAVEFORM_PLAYHEAD_JUMP_MS),
6594
7719
  /* harmony export */ DY: () => (/* binding */ WAVEFORM_BAR_RADIUS),
6595
7720
  /* harmony export */ EB: () => (/* binding */ CLIENT_DECODE_MAX_COMPRESSED_BYTES),
6596
7721
  /* harmony export */ FY: () => (/* binding */ MAX_PEAK_COUNT),
6597
7722
  /* harmony export */ GQ: () => (/* binding */ DURATION_MISMATCH_TOLERANCE_SEC),
7723
+ /* harmony export */ IQ: () => (/* binding */ WAVEFORM_RANGE_SNAP_PX),
6598
7724
  /* harmony export */ Kl: () => (/* binding */ WAVEFORM_MIN_VIEW_WINDOW_SEC),
6599
7725
  /* harmony export */ LW: () => (/* binding */ WAVEFORM_ZOOM_MIN),
7726
+ /* harmony export */ Le: () => (/* binding */ TAPE_POINTER_MEDIA_QUERY),
6600
7727
  /* harmony export */ Lu: () => (/* binding */ WAVEFORM_BAR_GAP),
6601
7728
  /* harmony export */ NM: () => (/* binding */ WAVEFORM_FOLLOW_INSET_PX),
6602
7729
  /* harmony export */ QD: () => (/* binding */ WAVEFORM_ZOOM_BUTTON_STEP),
6603
7730
  /* harmony export */ XS: () => (/* binding */ WAVEFORM_BAR_MIN_HEIGHT),
6604
7731
  /* harmony export */ Zs: () => (/* binding */ WAVEFORM_FOLLOW_SCROLL_SETTLE_MS),
7732
+ /* harmony export */ bX: () => (/* binding */ WAVEFORM_RANGE_MIN_DURATION_MS),
6605
7733
  /* harmony export */ f3: () => (/* binding */ CLIENT_DECODE_PEAK_COUNT),
7734
+ /* harmony export */ fI: () => (/* binding */ WAVEFORM_TAPE_CLICK_SUPPRESS_MS),
7735
+ /* harmony export */ h1: () => (/* binding */ WAVEFORM_RANGE_HANDLE_LINE_PX),
6606
7736
  /* harmony export */ i8: () => (/* binding */ PEAK_UNIT_MAX),
6607
7737
  /* harmony export */ m6: () => (/* binding */ WAVEFORM_ZOOM_DISMISS_MS),
6608
7738
  /* harmony export */ mJ: () => (/* binding */ PEAK_UNIT_MIN),
@@ -6610,10 +7740,12 @@ function isFpsAvailable(player) {
6610
7740
  /* harmony export */ nh: () => (/* binding */ WAVEFORM_ZOOM_SLIDER_MAX),
6611
7741
  /* harmony export */ oN: () => (/* binding */ WAVEFORM_HEIGHT),
6612
7742
  /* harmony export */ qY: () => (/* binding */ MAX_PAYLOAD_BYTES),
7743
+ /* harmony export */ r0: () => (/* binding */ WAVEFORM_RANGE_HIT_AREA_PX),
6613
7744
  /* harmony export */ sh: () => (/* binding */ WAVEFORM_PAYLOAD_VERSION),
6614
7745
  /* harmony export */ tK: () => (/* binding */ WAVEFORM_ZOOM_MAX),
6615
7746
  /* harmony export */ zo: () => (/* binding */ WAVEFORM_BAR_WIDTH)
6616
7747
  /* harmony export */ });
7748
+ /* unused harmony exports WAVEFORM_RANGE_PLAYHEAD_GAP_PX, WAVEFORM_RANGE_GRIP_IN_MS, WAVEFORM_RANGE_GRIP_OUT_MS */
6617
7749
  /** Current waveform payload schema version. Bump only with a migration path. */
6618
7750
  const WAVEFORM_PAYLOAD_VERSION = 1;
6619
7751
 
@@ -6642,6 +7774,10 @@ const WAVEFORM_ZOOM_MIN = 1;
6642
7774
  const WAVEFORM_ZOOM_MAX = 24;
6643
7775
  /** Visible window never shorter than this. */
6644
7776
  const WAVEFORM_MIN_VIEW_WINDOW_SEC = 4;
7777
+ /** Tape (phone/tablet) default: seconds of audio visible in the viewport. */
7778
+ const WAVEFORM_TAPE_DEFAULT_WINDOW_SEC = 10;
7779
+ /** Primary pointer is a finger, not a mouse/trackpad. */
7780
+ const TAPE_POINTER_MEDIA_QUERY = '(hover: none) and (pointer: coarse)';
6645
7781
  const WAVEFORM_ZOOM_SLIDER_MAX = 100;
6646
7782
  /** One click on zoom in/out moves this many units on the 0–100 slider. */
6647
7783
  const WAVEFORM_ZOOM_BUTTON_STEP = 10;
@@ -6650,6 +7786,8 @@ const WAVEFORM_FOLLOW_INSET_PX = 200;
6650
7786
  const WAVEFORM_PLAYHEAD_JUMP_MS = 400;
6651
7787
  /** After the last user pan, wait this long before the camera may pin again. */
6652
7788
  const WAVEFORM_FOLLOW_SCROLL_SETTLE_MS = 150;
7789
+ /** Ignore a WaveSurfer click after a tape swipe (iOS delayed click). Must outlast the scrub-chip hide. */
7790
+ const WAVEFORM_TAPE_CLICK_SUPPRESS_MS = 200;
6653
7791
  const WAVEFORM_BAR_GAP = 2;
6654
7792
  const WAVEFORM_BAR_WIDTH = 2;
6655
7793
  const WAVEFORM_BAR_RADIUS = WAVEFORM_BAR_WIDTH / 2;
@@ -6657,6 +7795,24 @@ const WAVEFORM_BAR_RADIUS = WAVEFORM_BAR_WIDTH / 2;
6657
7795
  const WAVEFORM_BAR_MIN_HEIGHT = WAVEFORM_BAR_WIDTH;
6658
7796
  const WAVEFORM_HEIGHT = 140;
6659
7797
 
7798
+ /** Hit target centered on each range handle line. */
7799
+ const WAVEFORM_RANGE_HIT_AREA_PX = 24;
7800
+ /** Visible handle line and playhead width. */
7801
+ const WAVEFORM_RANGE_HANDLE_LINE_PX = 2;
7802
+ /** Screen gap between the playhead and each collapsed handle line. */
7803
+ const WAVEFORM_RANGE_PLAYHEAD_GAP_PX = 1;
7804
+ /**
7805
+ * Offset of each collapsed handle from the timestamp/playhead center so the
7806
+ * 2px lines sit 1px left and right of the 2px playhead.
7807
+ */
7808
+ const WAVEFORM_RANGE_COLLAPSED_OFFSET_PX = WAVEFORM_RANGE_HANDLE_LINE_PX / 2 + WAVEFORM_RANGE_PLAYHEAD_GAP_PX + WAVEFORM_RANGE_HANDLE_LINE_PX / 2;
7809
+ /** Snap a dragging handle to the playhead when this close. */
7810
+ const WAVEFORM_RANGE_SNAP_PX = 8;
7811
+ /** Shortest committed range. Collapsed drafts (`endMs: null`) are exempt. */
7812
+ const WAVEFORM_RANGE_MIN_DURATION_MS = 250;
7813
+ const WAVEFORM_RANGE_GRIP_IN_MS = 150;
7814
+ const WAVEFORM_RANGE_GRIP_OUT_MS = 100;
7815
+
6660
7816
  /***/ },
6661
7817
 
6662
7818
  /***/ 4971
@@ -6701,6 +7857,12 @@ function isWaveformErrorCode(value) {
6701
7857
 
6702
7858
  /** Visible slice of the timeline. Emitted whenever zoom, scroll, or width changes. */
6703
7859
 
7860
+ /** Navigator fields used to detect iPad, including iPadOS reporting itself as Mac. */
7861
+
7862
+ /** Window bits needed to choose tape vs desktop (coarse pointer + iPad). */
7863
+
7864
+ /** Desktop walks then pins near the right inset. Tape keeps the playhead at center. */
7865
+
6704
7866
  /** One canvas linear-gradient stop. `offset` is 0–1 along the bar. */
6705
7867
 
6706
7868
  /**
@@ -14161,7 +15323,7 @@ var x = (y) => {
14161
15323
  var x = {}; __webpack_require__.d(x, y); return x
14162
15324
  }
14163
15325
  var y = (x) => (() => (x))
14164
- module.exports = x({ ["createElement"]: () => (__WEBPACK_EXTERNAL_MODULE_react__.createElement), ["default"]: () => (__WEBPACK_EXTERNAL_MODULE_react__["default"]), ["useCallback"]: () => (__WEBPACK_EXTERNAL_MODULE_react__.useCallback), ["useEffect"]: () => (__WEBPACK_EXTERNAL_MODULE_react__.useEffect), ["useId"]: () => (__WEBPACK_EXTERNAL_MODULE_react__.useId), ["useLayoutEffect"]: () => (__WEBPACK_EXTERNAL_MODULE_react__.useLayoutEffect), ["useMemo"]: () => (__WEBPACK_EXTERNAL_MODULE_react__.useMemo), ["useRef"]: () => (__WEBPACK_EXTERNAL_MODULE_react__.useRef), ["useState"]: () => (__WEBPACK_EXTERNAL_MODULE_react__.useState) });
15326
+ module.exports = x({ ["createElement"]: () => (__WEBPACK_EXTERNAL_MODULE_react__.createElement), ["default"]: () => (__WEBPACK_EXTERNAL_MODULE_react__["default"]), ["forwardRef"]: () => (__WEBPACK_EXTERNAL_MODULE_react__.forwardRef), ["useCallback"]: () => (__WEBPACK_EXTERNAL_MODULE_react__.useCallback), ["useEffect"]: () => (__WEBPACK_EXTERNAL_MODULE_react__.useEffect), ["useId"]: () => (__WEBPACK_EXTERNAL_MODULE_react__.useId), ["useImperativeHandle"]: () => (__WEBPACK_EXTERNAL_MODULE_react__.useImperativeHandle), ["useLayoutEffect"]: () => (__WEBPACK_EXTERNAL_MODULE_react__.useLayoutEffect), ["useMemo"]: () => (__WEBPACK_EXTERNAL_MODULE_react__.useMemo), ["useRef"]: () => (__WEBPACK_EXTERNAL_MODULE_react__.useRef), ["useState"]: () => (__WEBPACK_EXTERNAL_MODULE_react__.useState) });
14165
15327
 
14166
15328
  /***/ },
14167
15329
 
@@ -23055,7 +24217,7 @@ class Browser {
23055
24217
  ;// ./src/lib/Logger.js
23056
24218
  /* eslint-disable no-undef */
23057
24219
  const CLIENT_NAME = "box-content-preview";
23058
- const CLIENT_VERSION = "3.90.0";
24220
+ const CLIENT_VERSION = "3.92.0";
23059
24221
  /* eslint-enable no-undef */
23060
24222
 
23061
24223
  class Logger {
@@ -34434,10 +35596,9 @@ class MediaBaseViewer extends viewers_BaseViewer {
34434
35596
  const template = this.options.representation.content.url_template;
34435
35597
  this.mediaEl.addEventListener('error', this.errorHandler);
34436
35598
  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/
35599
+ if (lib_Browser.isIOS() && this.mediaEl.tagName === 'VIDEO') {
35600
+ // Unblocks loadeddata on iOS <video>. Do not set this on <audio>:
35601
+ // after tap-to-open, Safari will start playback.
34441
35602
  this.mediaEl.autoplay = true;
34442
35603
  }
34443
35604
  const contentUrl = this.createContentUrlV2(template);
@@ -35693,7 +36854,7 @@ function MP3Controls({
35693
36854
  isPlaying: isPlaying,
35694
36855
  movePlayback: movePlayback,
35695
36856
  onPlayPause: onPlayPause
35696
- }), /*#__PURE__*/external_react_["default"].createElement(VolumeControls/* default */.A, {
36857
+ }), /*#__PURE__*/external_react_["default"].createElement(VolumeControls/* default */.Ay, {
35697
36858
  onMuteChange: onMuteChange,
35698
36859
  onVolumeChange: onVolumeChange,
35699
36860
  volume: volume
@@ -35766,6 +36927,57 @@ function MP3Viewer_toPrimitive(t, r) { if ("object" != typeof t || !t) return t;
35766
36927
 
35767
36928
 
35768
36929
  const CSS_CLASS_MP3 = 'bp-media-mp3';
36930
+ const AUDIO_V2_SHUTTLE_RATES = [2, 4, 8, 16];
36931
+ const AUDIO_V2_SHUTTLE_TICK_MS = 50;
36932
+ const AUDIO_V2_SKIP_SEC = 1;
36933
+ const AUDIO_V2_SHIFT_SKIP_SEC = 5;
36934
+
36935
+ /**
36936
+ * Next J/L shuttle rate. Opposite direction starts at 2×.
36937
+ *
36938
+ * @param {number} currentRate
36939
+ * @param {boolean} sameDirection
36940
+ * @return {number} 2, 4, 8, or 16
36941
+ */
36942
+ function nextShuttleRate(currentRate, sameDirection) {
36943
+ if (!sameDirection) {
36944
+ return AUDIO_V2_SHUTTLE_RATES[0];
36945
+ }
36946
+ const index = AUDIO_V2_SHUTTLE_RATES.indexOf(currentRate);
36947
+ if (index < 0) {
36948
+ return AUDIO_V2_SHUTTLE_RATES[0];
36949
+ }
36950
+ return AUDIO_V2_SHUTTLE_RATES[Math.min(index + 1, AUDIO_V2_SHUTTLE_RATES.length - 1)];
36951
+ }
36952
+
36953
+ /**
36954
+ * Next/previous comment marker for ↑/↓. Walks by id so same-time stacks each get a stop.
36955
+ *
36956
+ * @param {Array<Object>} markers
36957
+ * @param {Object} options
36958
+ * @param {number} options.direction -1 previous, 1 next
36959
+ * @param {string|null} options.selectedId
36960
+ * @param {number} options.time current playhead
36961
+ * @return {Object|null} marker to select
36962
+ */
36963
+ function nextCommentMarker(markers, {
36964
+ direction,
36965
+ selectedId,
36966
+ time
36967
+ }) {
36968
+ const sorted = (markers || []).filter(marker => Number.isFinite(marker.time)).sort((left, right) => left.time - right.time || String(left.id).localeCompare(String(right.id)));
36969
+ if (!sorted.length) {
36970
+ return null;
36971
+ }
36972
+ const selectedIndex = sorted.findIndex(marker => marker.id === selectedId);
36973
+ if (selectedIndex >= 0) {
36974
+ return sorted[Math.max(0, Math.min(sorted.length - 1, selectedIndex + direction))];
36975
+ }
36976
+ if (direction > 0) {
36977
+ return sorted.find(marker => marker.time >= time) || sorted[sorted.length - 1];
36978
+ }
36979
+ return [...sorted].reverse().find(marker => marker.time <= time) || sorted[0];
36980
+ }
35769
36981
  function createLoadFailedError(message) {
35770
36982
  const error = new Error(message);
35771
36983
  error.name = 'LOAD_FAILED';
@@ -35778,6 +36990,7 @@ class MP3Viewer extends media_MediaBaseViewer {
35778
36990
  * @inheritdoc
35779
36991
  */
35780
36992
  MP3Viewer_defineProperty(this, "handlePlayRequest", () => {
36993
+ this.exitShuttle();
35781
36994
  this.userRequestedPlay = true;
35782
36995
  this.togglePlay();
35783
36996
  if (this.shouldRaceClientWaveformDecode() && this.isWaveformDecodeRetryPending && !this.hasUsedWaveformDecodePlayRetry) {
@@ -35794,6 +37007,7 @@ class MP3Viewer extends media_MediaBaseViewer {
35794
37007
  * @return {void}
35795
37008
  */
35796
37009
  MP3Viewer_defineProperty(this, "handlePlayPause", shouldPlay => {
37010
+ this.exitShuttle();
35797
37011
  if (shouldPlay) {
35798
37012
  this.handlePlayRequest();
35799
37013
  return;
@@ -35803,19 +37017,25 @@ class MP3Viewer extends media_MediaBaseViewer {
35803
37017
  MP3Viewer_defineProperty(this, "handleCommentMarkersUpdated", (markers = []) => {
35804
37018
  this.commentMarkers = markers;
35805
37019
  const selected = markers.find(marker => marker.isSelected);
35806
- const selectedId = selected ? selected.id : null;
35807
- if (selectedId !== this.hostSelectedMarkerId) {
35808
- this.hostSelectedMarkerId = selectedId;
35809
- if (selected && Number.isFinite(selected.time)) {
35810
- this.pendingHostSelectedSeek = selected;
35811
- this.applyPendingHostSelectedSeek();
35812
- } else {
35813
- this.pendingHostSelectedSeek = null;
37020
+
37021
+ // Host echoes often omit isSelected. Keep hostSelectedMarkerId so ↑/↓ can
37022
+ // still walk other comments at the same time. Clear only when that id is gone.
37023
+ if (selected) {
37024
+ if (selected.id !== this.hostSelectedMarkerId) {
37025
+ this.hostSelectedMarkerId = selected.id;
37026
+ if (Number.isFinite(selected.time)) {
37027
+ this.pendingHostSelectedSeek = selected;
37028
+ this.applyPendingHostSelectedSeek();
37029
+ }
35814
37030
  }
37031
+ } else if (this.hostSelectedMarkerId && !markers.some(marker => marker.id === this.hostSelectedMarkerId)) {
37032
+ this.hostSelectedMarkerId = null;
37033
+ this.pendingHostSelectedSeek = null;
35815
37034
  }
35816
37035
  this.renderUI();
35817
37036
  });
35818
37037
  MP3Viewer_defineProperty(this, "handleCommentMarkerClick", marker => {
37038
+ this.exitShuttle();
35819
37039
  this.hostSelectedMarkerId = marker.id;
35820
37040
  this.pendingHostSelectedSeek = marker;
35821
37041
  if (this.mediaEl) {
@@ -35869,6 +37089,12 @@ class MP3Viewer extends media_MediaBaseViewer {
35869
37089
  this.mediaEl = this.mediaContainerEl.appendChild(document.createElement('audio'));
35870
37090
  this.mediaEl.setAttribute('preload', 'auto');
35871
37091
  this.commentMarkers = [];
37092
+ this.hostSelectedMarkerId = null;
37093
+ this.shuttleDirection = null;
37094
+ this.shuttleRate = 0;
37095
+ this.reverseShuttleTimer = 0;
37096
+ this.keyboardVolumeStep = 0;
37097
+ this.keyboardZoomStep = 0;
35872
37098
  }
35873
37099
  bindCommentMarkersListener() {
35874
37100
  if (!this.isAudioPlayerV2) {
@@ -35888,6 +37114,212 @@ class MP3Viewer extends media_MediaBaseViewer {
35888
37114
  return this.featureEnabled(constants/* AUDIO_PLAYER_V2 */.Ku) && this.useReactControls();
35889
37115
  }
35890
37116
 
37117
+ /**
37118
+ * V2 audio keymap. Flag off and video keep MediaBaseViewer.handleKeydownReact.
37119
+ * Alt is not in Preview's key decoder — read it from the native event.
37120
+ *
37121
+ * @param {string} key
37122
+ * @param {KeyboardEvent} [event]
37123
+ * @return {boolean} consumed
37124
+ */
37125
+ onKeydown(key, event) {
37126
+ if (this.isAudioPlayerV2 && this.handleKeydownAudioV2(key, event)) {
37127
+ return true;
37128
+ }
37129
+ return super.onKeydown(key, event);
37130
+ }
37131
+ handleKeydownAudioV2(key, event) {
37132
+ const decoded = (key || '').toLowerCase();
37133
+ const altKey = !!(event && event.altKey);
37134
+ switch (decoded) {
37135
+ case 'space':
37136
+ this.toggleV2Play();
37137
+ return true;
37138
+ case 'k':
37139
+ this.exitShuttle();
37140
+ this.pause(undefined, true);
37141
+ return true;
37142
+ case 'j':
37143
+ this.shuttle('reverse');
37144
+ return true;
37145
+ case 'l':
37146
+ this.shuttle('forward');
37147
+ return true;
37148
+ case 'arrowleft':
37149
+ this.quickSeek(-AUDIO_V2_SKIP_SEC);
37150
+ return true;
37151
+ case 'arrowright':
37152
+ this.quickSeek(AUDIO_V2_SKIP_SEC);
37153
+ return true;
37154
+ case ',':
37155
+ this.frameStep('back');
37156
+ return true;
37157
+ case '.':
37158
+ this.frameStep('forward');
37159
+ return true;
37160
+ case 'arrowup':
37161
+ if (altKey || !(this.commentMarkers || []).some(marker => Number.isFinite(marker.time))) {
37162
+ this.stepKeyboardVolume(1);
37163
+ return true;
37164
+ }
37165
+ this.jumpToCommentMarker(-1);
37166
+ return true;
37167
+ case 'arrowdown':
37168
+ if (altKey || !(this.commentMarkers || []).some(marker => Number.isFinite(marker.time))) {
37169
+ this.stepKeyboardVolume(-1);
37170
+ return true;
37171
+ }
37172
+ this.jumpToCommentMarker(1);
37173
+ return true;
37174
+ case 'end':
37175
+ if (!this.mediaEl || !Number.isFinite(this.mediaEl.duration)) {
37176
+ return false;
37177
+ }
37178
+ this.exitShuttle();
37179
+ this.setMediaTime(this.mediaEl.duration);
37180
+ return true;
37181
+ case '0':
37182
+ case 'home':
37183
+ this.exitShuttle();
37184
+ return false;
37185
+ case 'shift+arrowleft':
37186
+ this.quickSeek(-AUDIO_V2_SHIFT_SKIP_SEC);
37187
+ return true;
37188
+ case 'shift+arrowright':
37189
+ this.quickSeek(AUDIO_V2_SHIFT_SKIP_SEC);
37190
+ return true;
37191
+ case 'i':
37192
+ case 'o':
37193
+ case '/':
37194
+ case 'shift+l':
37195
+ // Range UI is not on this branch yet. Consume so I/O/loop cannot
37196
+ // fall through to shuttle or Preview navigation.
37197
+ return true;
37198
+ case 'shift++':
37199
+ case '+':
37200
+ case '=':
37201
+ this.stepKeyboardZoom(1);
37202
+ return true;
37203
+ case '-':
37204
+ case 'shift+_':
37205
+ this.stepKeyboardZoom(-1);
37206
+ return true;
37207
+ default:
37208
+ return false;
37209
+ }
37210
+ }
37211
+ toggleV2Play() {
37212
+ const reverseShuttle = this.shuttleDirection === 'reverse';
37213
+ const wasPlaying = !!(this.mediaEl && !this.mediaEl.paused);
37214
+ this.exitShuttle();
37215
+ if (reverseShuttle || wasPlaying) {
37216
+ this.pause(undefined, true);
37217
+ return;
37218
+ }
37219
+ this.handlePlayRequest();
37220
+ }
37221
+ shuttle(direction) {
37222
+ const sameDirection = this.shuttleDirection === direction;
37223
+ const rate = nextShuttleRate(this.shuttleRate, sameDirection);
37224
+ if (direction === 'forward') {
37225
+ this.startForwardShuttle(rate);
37226
+ return;
37227
+ }
37228
+ this.startReverseShuttle(rate);
37229
+ }
37230
+ startForwardShuttle(rate) {
37231
+ this.stopReverseShuttle();
37232
+ this.shuttleDirection = 'forward';
37233
+ this.shuttleRate = rate;
37234
+ this.userRequestedPlay = true;
37235
+ const playPromise = this.play();
37236
+ if (this.mediaEl) {
37237
+ this.mediaEl.playbackRate = this.shuttleRate;
37238
+ }
37239
+ if (playPromise && typeof playPromise.then === 'function') {
37240
+ playPromise.then(() => {
37241
+ if (this.mediaEl && this.shuttleDirection === 'forward') {
37242
+ this.mediaEl.playbackRate = this.shuttleRate;
37243
+ }
37244
+ }).catch(() => {});
37245
+ }
37246
+ }
37247
+ startReverseShuttle(rate) {
37248
+ this.stopReverseShuttle();
37249
+ this.shuttleDirection = 'reverse';
37250
+ this.shuttleRate = rate;
37251
+ this.userRequestedPlay = true;
37252
+ this.pause(undefined, true);
37253
+ this.handleRate();
37254
+ this.reverseShuttleTimer = window.setInterval(() => {
37255
+ if (!this.mediaEl || this.mediaEl.currentTime <= 0) {
37256
+ this.exitShuttle();
37257
+ return;
37258
+ }
37259
+ this.quickSeek(-(this.shuttleRate * AUDIO_V2_SHUTTLE_TICK_MS / 1000));
37260
+ if (this.mediaEl.currentTime <= 0) {
37261
+ this.exitShuttle();
37262
+ }
37263
+ }, AUDIO_V2_SHUTTLE_TICK_MS);
37264
+ }
37265
+ stopReverseShuttle() {
37266
+ if (this.reverseShuttleTimer) {
37267
+ window.clearInterval(this.reverseShuttleTimer);
37268
+ this.reverseShuttleTimer = 0;
37269
+ }
37270
+ }
37271
+ exitShuttle() {
37272
+ const wasShuttling = !!this.shuttleDirection;
37273
+ this.stopReverseShuttle();
37274
+ this.shuttleDirection = null;
37275
+ this.shuttleRate = 0;
37276
+ if (wasShuttling) {
37277
+ this.handleRate();
37278
+ }
37279
+ }
37280
+
37281
+ /**
37282
+ * Keep the settings rate unless forward shuttle owns playbackRate.
37283
+ *
37284
+ * @inheritdoc
37285
+ */
37286
+ handleRate() {
37287
+ if (this.shuttleDirection === 'forward' && this.shuttleRate > 0 && this.mediaEl) {
37288
+ this.mediaEl.playbackRate = this.shuttleRate;
37289
+ if (this.controls) {
37290
+ this.renderUI();
37291
+ }
37292
+ return;
37293
+ }
37294
+ super.handleRate();
37295
+ }
37296
+ jumpToCommentMarker(direction) {
37297
+ if (!this.mediaEl) {
37298
+ return;
37299
+ }
37300
+ const target = nextCommentMarker(this.commentMarkers, {
37301
+ direction,
37302
+ selectedId: this.hostSelectedMarkerId,
37303
+ time: this.mediaEl.currentTime
37304
+ });
37305
+ if (!target || target.id === this.hostSelectedMarkerId) {
37306
+ return;
37307
+ }
37308
+ this.handleCommentMarkerClick(target);
37309
+ }
37310
+ stepKeyboardVolume(direction) {
37311
+ this.keyboardVolumeStep += 1;
37312
+ if (direction > 0) {
37313
+ this.increaseVolume();
37314
+ return;
37315
+ }
37316
+ this.decreaseVolume();
37317
+ }
37318
+ stepKeyboardZoom(direction) {
37319
+ this.keyboardZoomStep += direction;
37320
+ this.renderUI();
37321
+ }
37322
+
35891
37323
  /**
35892
37324
  * Load v2 controls (and wavesurfer) only when the audio player v2 gate is on.
35893
37325
  *
@@ -35930,7 +37362,7 @@ class MP3Viewer extends media_MediaBaseViewer {
35930
37362
  * @return {Promise<{ default: Function }>} MP3ControlsV2 module
35931
37363
  */
35932
37364
  importV2Controls() {
35933
- return Promise.resolve(/* import() */).then(__webpack_require__.bind(__webpack_require__, 6180));
37365
+ return Promise.resolve(/* import() */).then(__webpack_require__.bind(__webpack_require__, 7450));
35934
37366
  }
35935
37367
 
35936
37368
  /**
@@ -35953,6 +37385,9 @@ class MP3Viewer extends media_MediaBaseViewer {
35953
37385
  */
35954
37386
  destroy() {
35955
37387
  this.removeListener('comment_markers', this.handleCommentMarkersUpdated);
37388
+ this.stopReverseShuttle();
37389
+ this.shuttleDirection = null;
37390
+ this.shuttleRate = 0;
35956
37391
  this.abortWaveformLoads();
35957
37392
  super.destroy();
35958
37393
  }
@@ -36328,6 +37763,7 @@ class MP3Viewer extends media_MediaBaseViewer {
36328
37763
  return;
36329
37764
  }
36330
37765
  this.pendingHostSelectedSeek = null;
37766
+ this.exitShuttle();
36331
37767
  this.mediaEl.pause();
36332
37768
  if (this.mediaEl.currentTime !== marker.time) {
36333
37769
  this.mediaEl.currentTime = marker.time;
@@ -36370,6 +37806,9 @@ class MP3Viewer extends media_MediaBaseViewer {
36370
37806
  const Mp3ControlsV2 = this.MP3ControlsV2;
36371
37807
  this.controls.render(/*#__PURE__*/external_react_["default"].createElement(Mp3ControlsV2, MP3Viewer_extends({}, sharedProps, {
36372
37808
  commentMarkers: this.commentMarkers || [],
37809
+ hasStartedPlayback: !!this.userRequestedPlay,
37810
+ keyboardVolumeStep: this.keyboardVolumeStep,
37811
+ keyboardZoomStep: this.keyboardZoomStep,
36373
37812
  mediaEl: this.mediaEl,
36374
37813
  onCommentMarkerClick: this.handleCommentMarkerClick,
36375
37814
  peaks: this.waveformPeaks
@@ -37968,7 +39407,7 @@ function VideoControls({
37968
39407
  onPlayPause: onPlayPause
37969
39408
  })), /*#__PURE__*/external_react_["default"].createElement("div", {
37970
39409
  className: "bp-VideoControls-group bp-VideoControls-group--media-controls"
37971
- }, /*#__PURE__*/external_react_["default"].createElement(VolumeControls/* default */.A, {
39410
+ }, /*#__PURE__*/external_react_["default"].createElement(VolumeControls/* default */.Ay, {
37972
39411
  onMuteChange: onMuteChange,
37973
39412
  onVolumeChange: onVolumeChange,
37974
39413
  volume: volume
@@ -38527,7 +39966,7 @@ function VideoControlsV2({
38527
39966
  mediaEl: mediaEl
38528
39967
  })), /*#__PURE__*/external_react_["default"].createElement("div", {
38529
39968
  className: "bp-VideoControlsV2-group bp-VideoControlsV2-group--right"
38530
- }, /*#__PURE__*/external_react_["default"].createElement(VolumeControls/* default */.A, {
39969
+ }, /*#__PURE__*/external_react_["default"].createElement(VolumeControls/* default */.Ay, {
38531
39970
  onMuteChange: onMuteChange,
38532
39971
  onVolumeChange: onVolumeChange,
38533
39972
  volume: volume