box-content-preview 3.91.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
@@ -1018,7 +1018,7 @@ function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e =
1018
1018
  const CLIENT_NAME = "box-content-preview"; // eslint-disable-line no-undef
1019
1019
  const CLIENT_NAME_KEY = 'box_client_name';
1020
1020
  const CLIENT_VERSION_KEY = 'box_client_version';
1021
- const CLIENT_VERSION = "3.91.0"; // eslint-disable-line no-undef
1021
+ const CLIENT_VERSION = "3.92.0"; // eslint-disable-line no-undef
1022
1022
  const HEADER_CLIENT_NAME = 'X-Box-Client-Name';
1023
1023
  const HEADER_CLIENT_VERSION = 'X-Box-Client-Version';
1024
1024
  const PROMISE_MAP = {};
@@ -2916,10 +2916,10 @@ function TimestampControl({
2916
2916
 
2917
2917
  // EXPORTS
2918
2918
  __webpack_require__.d(__webpack_exports__, {
2919
- A: () => (/* binding */ VolumeControls)
2919
+ Ay: () => (/* binding */ VolumeControls)
2920
2920
  });
2921
2921
 
2922
- // UNUSED EXPORTS: getIcon
2922
+ // UNUSED EXPORTS: VOLUME_FLYOUT_DISMISS_MS, getIcon
2923
2923
 
2924
2924
  // EXTERNAL MODULE: external "react"
2925
2925
  var external_react_ = __webpack_require__(1649);
@@ -2981,12 +2981,26 @@ function IconVolumeMuted24(props) {
2981
2981
  var MediaToggle = __webpack_require__(8997);
2982
2982
  ;// ./src/lib/viewers/controls/hooks/useAttention.ts
2983
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
+ }
2984
2988
  function useAttention() {
2985
2989
  const [isFocused, setFocused] = external_react_.useState(false);
2986
2990
  const [isHovered, setHovered] = external_react_.useState(false);
2987
- const handleBlur = () => setFocused(false);
2991
+ const handleBlur = event => {
2992
+ if (isRelatedTargetInside(event)) {
2993
+ return;
2994
+ }
2995
+ setFocused(false);
2996
+ };
2988
2997
  const handleFocus = () => setFocused(true);
2989
- const handleMouseOut = () => setHovered(false);
2998
+ const handleMouseOut = event => {
2999
+ if (isRelatedTargetInside(event)) {
3000
+ return;
3001
+ }
3002
+ setHovered(false);
3003
+ };
2990
3004
  const handleMouseOver = () => setHovered(true);
2991
3005
  return [isFocused || isHovered, {
2992
3006
  onBlur: handleBlur,
@@ -3023,7 +3037,7 @@ var util = __webpack_require__(4410);
3023
3037
  // extracted by mini-css-extract-plugin
3024
3038
 
3025
3039
  ;// ./src/lib/viewers/controls/slider/VolumeSliderControl.tsx
3026
- const _excluded = ["className", "max", "min", "onMove", "onUpdate", "step", "title", "value", "onMouseOver"];
3040
+ const _excluded = ["className", "max", "min", "onMove", "onUpdate", "step", "title", "value", "onMouseOver", "tabIndex"];
3027
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); }
3028
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; }
3029
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; }
@@ -3049,7 +3063,8 @@ function VolumeSliderControl(_ref) {
3049
3063
  step = 1,
3050
3064
  title,
3051
3065
  value,
3052
- onMouseOver
3066
+ onMouseOver,
3067
+ tabIndex = 0
3053
3068
  } = _ref,
3054
3069
  rest = _objectWithoutProperties(_ref, _excluded);
3055
3070
  const [isScrubbing, setIsScrubbing] = external_react_["default"].useState(false);
@@ -3160,33 +3175,29 @@ function VolumeSliderControl(_ref) {
3160
3175
  className: "bp-VolumeVerticalSliderControl-track-container"
3161
3176
  }, /*#__PURE__*/external_react_["default"].createElement("div", {
3162
3177
  ref: sliderElRef,
3163
- className: "bp-VolumeVerticalSliderControl-track-background",
3164
- onMouseDown: handleMouseDown,
3165
- role: "button",
3166
- tabIndex: 0
3167
- }, /*#__PURE__*/external_react_["default"].createElement("div", {
3168
3178
  "aria-label": title,
3169
3179
  "aria-valuemax": max,
3170
3180
  "aria-valuemin": min,
3171
3181
  "aria-valuenow": value,
3172
- className: "bp-VolumeVerticalSliderControl-track",
3182
+ className: "bp-VolumeVerticalSliderControl-track-background",
3173
3183
  "data-resin-target": "volumeSlider",
3174
3184
  "data-testid": "bp-volume-slider-control-track",
3175
- onFocus: onMouseOver,
3185
+ onFocus: (noop_default()),
3176
3186
  onKeyDown: handleKeydown,
3177
3187
  onMouseDown: handleMouseDown,
3178
3188
  onMouseMove: handleMouseMove,
3179
3189
  onMouseOver: onMouseOver,
3180
3190
  onTouchStart: handleTouchStart,
3181
3191
  role: "slider",
3192
+ tabIndex: tabIndex
3193
+ }, /*#__PURE__*/external_react_["default"].createElement("div", {
3194
+ className: "bp-VolumeVerticalSliderControl-track",
3182
3195
  style: {
3183
3196
  height: `${heightValueBasedOnVolume / max * 100}%`
3184
- },
3185
- tabIndex: 0
3197
+ }
3186
3198
  }))));
3187
3199
  }
3188
3200
  ;// ./src/lib/viewers/controls/media/VolumeControls.tsx
3189
- 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); }
3190
3201
 
3191
3202
 
3192
3203
 
@@ -3197,6 +3208,9 @@ function VolumeControls_extends() { return VolumeControls_extends = Object.assig
3197
3208
 
3198
3209
 
3199
3210
 
3211
+
3212
+ /** Keyboard volume keeps the flyout open longer than the 200ms max-width transition. */
3213
+ const VOLUME_FLYOUT_DISMISS_MS = 1000;
3200
3214
  function getIcon(volume) {
3201
3215
  let Icon = icons_IconVolumeMuted24;
3202
3216
  if (volume >= 0.66) {
@@ -3209,16 +3223,32 @@ function getIcon(volume) {
3209
3223
  return Icon;
3210
3224
  }
3211
3225
  function VolumeControls({
3226
+ keyboardVolumeStep = 0,
3212
3227
  onMuteChange,
3213
3228
  onVolumeChange,
3214
3229
  volume = 1
3215
3230
  }) {
3216
3231
  const [isActive, handlers] = useAttention();
3232
+ const [isRevealed, setIsRevealed] = (0,external_react_.useState)(false);
3233
+ const revealTimerRef = (0,external_react_.useRef)(0);
3217
3234
  const isMuted = !volume;
3218
3235
  const Icon = isMuted ? icons_IconVolumeMuted24 : getIcon(volume);
3219
3236
  const title = isMuted ? "Unmute" : "Mute";
3220
3237
  const value = Math.round(volume * 100);
3221
- 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 => {
3222
3252
  const newValueToUse = newValue <= 5 ? 0 : newValue;
3223
3253
  // make sure the value is always between 0 and 1 since the value passed in could be
3224
3254
  // greater than 100 if the user drags the mouse above the top of the sider when adjusting the volume
@@ -3228,27 +3258,39 @@ function VolumeControls({
3228
3258
  return /*#__PURE__*/external_react_["default"].createElement("div", {
3229
3259
  className: "bp-VolumeControls",
3230
3260
  "data-testid": "bp-volume-controls",
3231
- 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
+ },
3232
3270
  onFocus: handlers.onFocus,
3233
3271
  onMouseOut: handlers.onMouseOut,
3234
3272
  onMouseOver: handlers.onMouseOver
3235
- }, /*#__PURE__*/external_react_["default"].createElement(MediaToggle/* default */.A, VolumeControls_extends({
3273
+ }, /*#__PURE__*/external_react_["default"].createElement(MediaToggle/* default */.A, {
3236
3274
  className: "bp-VolumeControls-toggle",
3237
3275
  "data-resin-target": "volumeToggle",
3238
3276
  onClick: () => onMuteChange(!isMuted),
3239
3277
  title: title
3240
- }, 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,
3241
3280
  className: classnames_default()('bp-VolumeControls-flyout', {
3242
- 'bp-is-open': isActive
3281
+ 'bp-is-open': isOpen
3243
3282
  })
3244
- }, /*#__PURE__*/external_react_["default"].createElement(VolumeSliderControl, VolumeControls_extends({
3283
+ }, /*#__PURE__*/external_react_["default"].createElement(VolumeSliderControl, {
3284
+ "aria-hidden": !isOpen,
3245
3285
  className: "bp-VolumeControls-slider",
3246
3286
  max: 100,
3287
+ onMouseOver: handlers.onMouseOver,
3247
3288
  onUpdate: handleVolume,
3248
3289
  step: 1,
3290
+ tabIndex: isOpen ? 0 : -1,
3249
3291
  title: "Volume Slider",
3250
3292
  value: value
3251
- }, handlers))));
3293
+ })));
3252
3294
  }
3253
3295
 
3254
3296
  /***/ },
@@ -4488,7 +4530,7 @@ function getPdfjsWorkerSrc() {
4488
4530
 
4489
4531
  /***/ },
4490
4532
 
4491
- /***/ 2668
4533
+ /***/ 7450
4492
4534
  (__unused_webpack_module, __webpack_exports__, __webpack_require__) {
4493
4535
 
4494
4536
 
@@ -4756,6 +4798,11 @@ function zoomFromSliderValue(value, maxZoom = constants/* WAVEFORM_ZOOM_MIN */.L
4756
4798
  return clampWaveformZoom(constants/* WAVEFORM_ZOOM_MIN */.LW + t * (max - constants/* WAVEFORM_ZOOM_MIN */.LW), max);
4757
4799
  }
4758
4800
 
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
+
4759
4806
  /** Max scroll that still shows a full window (no overscroll). Gutters add empty lead/trail. */
4760
4807
  function maxScrollLeft(viewport) {
4761
4808
  const gutterPx = viewport.gutterPx || 0;
@@ -5213,15 +5260,62 @@ function getBufferedProgress(bufferedRange, durationSec) {
5213
5260
  function getPlayedWaveformColor(isTape = false) {
5214
5261
  return isTape ? WAVEFORM_COLOR_HOVER_PLAYED : WAVEFORM_COLOR_PLAYED;
5215
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
+ }
5216
5308
 
5217
5309
  /**
5218
5310
  * progressColor paints left of the playhead (clipped by wavesurfer).
5219
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.
5220
5313
  */
5221
5314
  function getWaveformFills({
5222
5315
  bufferProgress,
5223
5316
  hoverProgress,
5224
- isTape = false
5317
+ isTape = false,
5318
+ rangeProgress
5225
5319
  }) {
5226
5320
  const buffer = clampTo0And1(bufferProgress);
5227
5321
  const playedColor = getPlayedWaveformColor(isTape);
@@ -5231,6 +5325,14 @@ function getWaveformFills({
5231
5325
  waveColor: WAVEFORM_COLOR_UNPLAYED
5232
5326
  };
5233
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
+ }
5234
5336
  if (hoverProgress == null) {
5235
5337
  return {
5236
5338
  progressColor: playedColor,
@@ -5394,6 +5496,200 @@ function tintWaveformTiles({
5394
5496
  });
5395
5497
  });
5396
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
+ }
5397
5693
  ;// ./src/lib/viewers/media/waveform/usePlayheadCamera.ts
5398
5694
 
5399
5695
 
@@ -5709,6 +6005,307 @@ function usePlayheadCamera({
5709
6005
  seekTo
5710
6006
  };
5711
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));
5712
6309
  ;// ./src/lib/viewers/media/waveform/WaveformView.scss
5713
6310
  // extracted by mini-css-extract-plugin
5714
6311
 
@@ -5730,6 +6327,8 @@ function WaveformView_toPrimitive(t, r) { if ("object" != typeof t || !t) return
5730
6327
 
5731
6328
 
5732
6329
 
6330
+
6331
+
5733
6332
  /** Pointer X in the view and the media time under it; zoom keeps this point fixed. */
5734
6333
 
5735
6334
  /** CSS width × devicePixelRatio, for canvas gradient fills. */
@@ -5838,10 +6437,13 @@ function WaveformView({
5838
6437
  isPlaying = false,
5839
6438
  mediaEl,
5840
6439
  onPlayPause,
6440
+ onRangeChange,
6441
+ onRangeDragChange,
5841
6442
  onSeek,
5842
6443
  onViewportChange,
5843
6444
  onZoomChange,
5844
6445
  peaks,
6446
+ range = null,
5845
6447
  zoomLevel: zoomLevelProp
5846
6448
  }) {
5847
6449
  // DOM / WaveSurfer
@@ -5850,6 +6452,7 @@ function WaveformView({
5850
6452
  const playheadRef = (0,external_react_.useRef)(null); // playhead element the camera positions
5851
6453
  const trackRef = (0,external_react_.useRef)(null); // hover / tap target around the canvas
5852
6454
  const wavesurferRef = (0,external_react_.useRef)(null); // WaveSurfer instance
6455
+ const rangeLayerRef = (0,external_react_.useRef)(null);
5853
6456
 
5854
6457
  // Latest props for WaveSurfer + media listeners that must not re-subscribe each render.
5855
6458
  const cameraModeRef = (0,external_react_.useRef)(cameraMode); // latest tape/desktop; WaveSurfer create() deps stay empty
@@ -5877,6 +6480,7 @@ function WaveformView({
5877
6480
  const pointerZoomClearTimerRef = (0,external_react_.useRef)(0); // clears pointerZoomRef after WAVEFORM_ZOOM_DISMISS_MS
5878
6481
  const pointerZoomRef = (0,external_react_.useRef)(false); // pinch/wheel in progress; skip play/pause and tape recenter
5879
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
5880
6484
  const suppressNextTapPlayPauseRef = (0,external_react_.useRef)(false); // swallow play/pause after a swipe or pinch
5881
6485
  const suppressNextTapPlayPauseTimerRef = (0,external_react_.useRef)(0); // clears suppressNextTapPlayPauseRef
5882
6486
  const suppressViewportSyncRef = (0,external_react_.useRef)(false); // ignore WaveSurfer zoom/scroll while we setOptions
@@ -5900,6 +6504,12 @@ function WaveformView({
5900
6504
  const [overlayPortalHost, setOverlayPortalHost] = (0,external_react_.useState)(null);
5901
6505
  const [canvasWidthPx, setCanvasWidthPx] = (0,external_react_.useState)(0);
5902
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);
5903
6513
  const isControlled = typeof zoomLevelProp === 'number';
5904
6514
  const maxZoom = getWaveformZoomMax({
5905
6515
  durationSec,
@@ -5916,7 +6526,23 @@ function WaveformView({
5916
6526
  const isScrollableWindow = isZoomed || isTape;
5917
6527
  const bufferProgress = getBufferedProgress(bufferedRange, durationSec);
5918
6528
  bufferProgressRef.current = bufferProgress;
5919
- 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;
5920
6546
  const viewport = (0,external_react_.useMemo)(() => createWaveformViewport({
5921
6547
  durationSec,
5922
6548
  gutterPx: isTape ? getTapeGutterPx(canvasWidthPx) : 0,
@@ -5928,6 +6554,7 @@ function WaveformView({
5928
6554
  }), [canvasWidthPx, durationSec, height, isTape, maxZoom, scrollLeft, zoomLevel]);
5929
6555
  const viewportRef = (0,external_react_.useRef)(viewport); // live scroll window; prefer this over render-state while the camera is moving
5930
6556
  const onViewportCommit = (0,external_react_.useCallback)((scrollLeftPx, nextViewport, commitReactState = true) => {
6557
+ rangeLayerRef.current?.applyViewport(nextViewport);
5931
6558
  onViewportChangeRef.current?.(nextViewport);
5932
6559
  if (commitReactState) {
5933
6560
  setScrollLeft(scrollLeftPx);
@@ -6043,6 +6670,7 @@ function WaveformView({
6043
6670
  }
6044
6671
  const scrollLeftPx = WaveformView_getScrollLeft(wavesurfer);
6045
6672
  viewportRef.current = getViewportAtScroll(viewportRef.current, scrollLeftPx);
6673
+ rangeLayerRef.current?.applyViewport(viewportRef.current);
6046
6674
  onViewportChangeRef.current?.(viewportRef.current);
6047
6675
  setScrollLeft(scrollLeftPx);
6048
6676
  }, []);
@@ -6056,8 +6684,10 @@ function WaveformView({
6056
6684
  } else if (!isFollowPinned()) {
6057
6685
  playhead.style.left = timeLeftPercent(timeSec, durationSecRef.current, viewportRef.current);
6058
6686
  }
6687
+ rangeLayerRef.current?.applyViewport(viewportRef.current);
6059
6688
  const wavesurfer = wavesurferRef.current;
6060
- if (wavesurfer && wavesurfer.setTime) {
6689
+ if (wavesurfer && wavesurfer.setTime && lastSetTimeSecRef.current !== timeSec) {
6690
+ lastSetTimeSecRef.current = timeSec;
6061
6691
  wavesurfer.setTime(timeSec);
6062
6692
  }
6063
6693
  }, [isFollowPinned, isTape]);
@@ -6111,6 +6741,7 @@ function WaveformView({
6111
6741
  applyScrollLeft(getCenteredScrollLeft(currentTimeRef.current, windowViewport), true);
6112
6742
  }
6113
6743
  }
6744
+ lastSetTimeSecRef.current = currentTimeRef.current;
6114
6745
  wavesurfer.setTime(currentTimeRef.current);
6115
6746
  } finally {
6116
6747
  suppressViewportSyncRef.current = false;
@@ -6149,7 +6780,11 @@ function WaveformView({
6149
6780
  waveColor: WAVEFORM_COLOR_UNPLAYED
6150
6781
  });
6151
6782
  const unsubscribeClick = wavesurfer.on('click', relativeX => {
6152
- if (!interactiveRef.current) {
6783
+ if (skipNextSeekRef.current) {
6784
+ skipNextSeekRef.current = false;
6785
+ return;
6786
+ }
6787
+ if (!interactiveRef.current || isRangeDraggingRef.current) {
6153
6788
  return;
6154
6789
  }
6155
6790
  if (cameraModeRef.current === 'tape') {
@@ -6169,6 +6804,7 @@ function WaveformView({
6169
6804
  }
6170
6805
  syncViewport();
6171
6806
  });
6807
+ rangeLayerRef.current?.applyViewport(viewportRef.current);
6172
6808
  });
6173
6809
  const unsubscribeZoom = wavesurfer.on('zoom', () => {
6174
6810
  if (suppressViewportSyncRef.current) {
@@ -6183,11 +6819,13 @@ function WaveformView({
6183
6819
  tintZoomedWaveform(wavesurfer, getWaveformFills({
6184
6820
  bufferProgress: bufferProgressRef.current,
6185
6821
  hoverProgress: cameraModeRef.current === 'tape' ? null : hoverProgressRef.current,
6186
- isTape: cameraModeRef.current === 'tape'
6822
+ isTape: cameraModeRef.current === 'tape',
6823
+ rangeProgress: rangeProgressRef.current
6187
6824
  }), true);
6188
6825
  });
6189
6826
  wavesurferRef.current = wavesurfer;
6190
6827
  displayedPeaksRef.current = peaksRef.current;
6828
+ lastSetTimeSecRef.current = null;
6191
6829
  disableScrollOverscroll(container);
6192
6830
  syncViewport();
6193
6831
  applyZoomWindowRef.current?.();
@@ -6205,6 +6843,7 @@ function WaveformView({
6205
6843
  wavesurfer.destroy();
6206
6844
  wavesurferRef.current = null;
6207
6845
  displayedPeaksRef.current = null;
6846
+ lastSetTimeSecRef.current = null;
6208
6847
  };
6209
6848
  }, [cancelJump, handleCameraScroll, handleTapeSwipe, height, isUserPanning, releaseUserPanHold, syncViewport]);
6210
6849
  (0,external_react_.useLayoutEffect)(() => {
@@ -6359,7 +6998,8 @@ function WaveformView({
6359
6998
  const fills = getWaveformFills({
6360
6999
  bufferProgress,
6361
7000
  hoverProgress: isTape ? null : hoverProgress,
6362
- isTape
7001
+ isTape,
7002
+ rangeProgress: activeRangeProgress
6363
7003
  });
6364
7004
  if (isScrollableWindow) {
6365
7005
  tintZoomedWaveform(wavesurfer, fills);
@@ -6369,8 +7009,9 @@ function WaveformView({
6369
7009
  progressColor: toCanvasFill(fills.progressColor, devicePixelWidth(canvasWidthPx)),
6370
7010
  waveColor: toCanvasFill(fills.waveColor, devicePixelWidth(canvasWidthPx))
6371
7011
  });
7012
+ lastSetTimeSecRef.current = currentTimeRef.current;
6372
7013
  wavesurfer.setTime(currentTimeRef.current);
6373
- }, [bufferProgress, canvasWidthPx, hoverProgress, isScrollableWindow, isTape]);
7014
+ }, [activeRangeProgress, bufferProgress, canvasWidthPx, hoverProgress, isRangeDragging, isScrollableWindow, isTape, isZoomed]);
6374
7015
  (0,external_react_.useEffect)(() => {
6375
7016
  const track = trackRef.current;
6376
7017
  if (!track) {
@@ -6451,11 +7092,11 @@ function WaveformView({
6451
7092
  };
6452
7093
  }, [durationSec, markPointerZoom, setZoomLevel]);
6453
7094
  const onHoverMove = (0,external_react_.useCallback)(event => {
6454
- if (!interactive || isTape || isTouchHoverInput(event)) {
7095
+ if (isTape || isTouchHoverInput(event)) {
6455
7096
  return;
6456
7097
  }
6457
7098
  const rect = event.currentTarget.getBoundingClientRect();
6458
- if (!(rect.width > 0) || !(durationSec > 0)) {
7099
+ if (!(rect.width > 0)) {
6459
7100
  return;
6460
7101
  }
6461
7102
  const pointerX = event.clientX - rect.left;
@@ -6463,6 +7104,15 @@ function WaveformView({
6463
7104
  return;
6464
7105
  }
6465
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
+ }
6466
7116
  if (vp.pixelsPerSecond > 0) {
6467
7117
  setHoverProgress(Math.min(1, Math.max(0, timeFromPositionPx(pointerX, vp) / durationSec)));
6468
7118
  return;
@@ -6471,12 +7121,31 @@ function WaveformView({
6471
7121
  }, [durationSec, interactive, isTape]);
6472
7122
  const onHoverLeave = (0,external_react_.useCallback)(() => {
6473
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;
6474
7142
  }, []);
6475
7143
  const bindOverlayPortalHost = (0,external_react_.useCallback)(node => {
6476
7144
  const host = node?.parentElement ?? null;
6477
7145
  setOverlayPortalHost(prev => prev === host ? prev : host);
6478
7146
  }, []);
6479
- 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));
6480
7149
  const scrubTimeChip = isTape && scrubPreviewTimeSec != null ? /*#__PURE__*/external_react_["default"].createElement("div", {
6481
7150
  className: "bp-WaveformView-hover bp-WaveformView-hover--tape",
6482
7151
  "data-testid": "bp-waveform-hover"
@@ -6518,7 +7187,20 @@ function WaveformView({
6518
7187
  "aria-hidden": "true",
6519
7188
  className: "bp-WaveformView-playhead",
6520
7189
  "data-testid": "bp-waveform-playhead"
6521
- }), 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", {
6522
7204
  className: "bp-WaveformView-hover",
6523
7205
  "data-testid": "bp-waveform-hover",
6524
7206
  style: {
@@ -6650,8 +7332,8 @@ function WaveformZoomControl({
6650
7332
  onZoomChange(zoomFromSliderValue(newValue, maxZoom));
6651
7333
  }, [maxZoom, onZoomChange]);
6652
7334
  const handleStep = (0,external_react_.useCallback)(delta => {
6653
- onZoomChange(zoomFromSliderValue(zoomValue + delta, maxZoom));
6654
- }, [maxZoom, onZoomChange, zoomValue]);
7335
+ onZoomChange(stepWaveformZoom(zoom, maxZoom, delta));
7336
+ }, [maxZoom, onZoomChange, zoom]);
6655
7337
  const handleZoomInPointerDown = (0,external_react_.useCallback)(() => {
6656
7338
  wasFlyoutOpenOnPointerDownRef.current = isHovered || isFocused || isRevealed || isPinned;
6657
7339
  }, [isFocused, isHovered, isPinned, isRevealed]);
@@ -6761,7 +7443,10 @@ function MP3ControlsV2({
6761
7443
  commentMarkers,
6762
7444
  currentTime,
6763
7445
  durationTime,
7446
+ hasStartedPlayback = false,
6764
7447
  isPlaying,
7448
+ keyboardVolumeStep = 0,
7449
+ keyboardZoomStep = 0,
6765
7450
  mediaEl,
6766
7451
  onAutoplayChange,
6767
7452
  onCommentMarkerClick,
@@ -6785,10 +7470,11 @@ function MP3ControlsV2({
6785
7470
  const hasRealPeaks = !!(peaks && peaks.length);
6786
7471
  const waveformPeaks = hasRealPeaks ? peaks : PLACEHOLDER_PEAKS;
6787
7472
  const waveformDurationSec = hasWaveformDuration ? durationValue : PLACEHOLDER_DURATION_SEC;
6788
- const [playRequested, setPlayRequested] = (0,external_react_.useState)(false);
7473
+ const [playRequested, setPlayRequested] = (0,external_react_.useState)(hasStartedPlayback);
6789
7474
  const [viewport, setViewport] = (0,external_react_.useState)(null);
6790
7475
  const isTape = useTapeWaveform();
6791
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
6792
7478
  const userChangedTapeZoomRef = (0,external_react_.useRef)(false); // pinch/wheel zoom; skip re-applying the 10s default
6793
7479
  const handleViewportChange = (0,external_react_.useCallback)(next => {
6794
7480
  setMaxZoom(prev => prev === next.maxZoom ? prev : next.maxZoom);
@@ -6807,6 +7493,19 @@ function MP3ControlsV2({
6807
7493
  setZoomLevel(nextZoom);
6808
7494
  revealZoomControl();
6809
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]);
6810
7509
  (0,external_react_.useEffect)(() => {
6811
7510
  setZoomLevel(prev => clampWaveformZoom(prev, maxZoom));
6812
7511
  }, [maxZoom]);
@@ -6832,10 +7531,10 @@ function MP3ControlsV2({
6832
7531
  }, [durationValue, hasRealPeaks, hasWaveformDuration, isTape, maxZoom, viewport]);
6833
7532
  (0,external_react_.useEffect)(() => () => window.clearTimeout(zoomRevealTimerRef.current), []);
6834
7533
  (0,external_react_.useEffect)(() => {
6835
- if (isPlaying) {
7534
+ if (isPlaying || hasStartedPlayback) {
6836
7535
  setPlayRequested(true);
6837
7536
  }
6838
- }, [isPlaying]);
7537
+ }, [hasStartedPlayback, isPlaying]);
6839
7538
  const handlePlayOverlayClick = (0,external_react_.useCallback)(() => {
6840
7539
  setPlayRequested(true);
6841
7540
  onPlayPause(true);
@@ -6853,9 +7552,10 @@ function MP3ControlsV2({
6853
7552
  }
6854
7553
  onTimeChange(marker.time);
6855
7554
  }, [onCommentMarkerClick, onPlayPause, onTimeChange]);
6856
- const isWaveformInteractive = playRequested && hasMediaMetadata;
6857
- const isWaitingToPlay = playRequested && !hasMediaMetadata;
6858
- const showPlayOverlay = !playRequested && !isPlaying;
7555
+ const hasStarted = playRequested || hasStartedPlayback;
7556
+ const isWaveformInteractive = hasStarted && hasMediaMetadata;
7557
+ const isWaitingToPlay = hasStarted && !hasMediaMetadata;
7558
+ const showPlayOverlay = !hasStarted && !isPlaying;
6859
7559
  const hasZoomHandlers = hasRealPeaks && !showPlayOverlay;
6860
7560
  const hasZoomControl = hasZoomHandlers && hasMediaMetadata && maxZoom > constants/* WAVEFORM_ZOOM_MIN */.LW;
6861
7561
  const waveformZoomLevel = isTape || hasZoomHandlers ? zoomLevel : constants/* WAVEFORM_ZOOM_MIN */.LW;
@@ -6926,7 +7626,8 @@ function MP3ControlsV2({
6926
7626
  durationTime: durationValue
6927
7627
  })), /*#__PURE__*/external_react_["default"].createElement("div", {
6928
7628
  className: "bp-MP3ControlsV2-group"
6929
- }, /*#__PURE__*/external_react_["default"].createElement(VolumeControls/* default */.A, {
7629
+ }, /*#__PURE__*/external_react_["default"].createElement(VolumeControls/* default */.Ay, {
7630
+ keyboardVolumeStep: keyboardVolumeStep,
6930
7631
  onMuteChange: onMuteChange,
6931
7632
  onVolumeChange: onVolumeChange,
6932
7633
  volume: volume
@@ -7013,11 +7714,13 @@ function isFpsAvailable(player) {
7013
7714
 
7014
7715
  /* harmony export */ __webpack_require__.d(__webpack_exports__, {
7015
7716
  /* harmony export */ Av: () => (/* binding */ WAVEFORM_TAPE_DEFAULT_WINDOW_SEC),
7717
+ /* harmony export */ BG: () => (/* binding */ WAVEFORM_RANGE_COLLAPSED_OFFSET_PX),
7016
7718
  /* harmony export */ Bq: () => (/* binding */ WAVEFORM_PLAYHEAD_JUMP_MS),
7017
7719
  /* harmony export */ DY: () => (/* binding */ WAVEFORM_BAR_RADIUS),
7018
7720
  /* harmony export */ EB: () => (/* binding */ CLIENT_DECODE_MAX_COMPRESSED_BYTES),
7019
7721
  /* harmony export */ FY: () => (/* binding */ MAX_PEAK_COUNT),
7020
7722
  /* harmony export */ GQ: () => (/* binding */ DURATION_MISMATCH_TOLERANCE_SEC),
7723
+ /* harmony export */ IQ: () => (/* binding */ WAVEFORM_RANGE_SNAP_PX),
7021
7724
  /* harmony export */ Kl: () => (/* binding */ WAVEFORM_MIN_VIEW_WINDOW_SEC),
7022
7725
  /* harmony export */ LW: () => (/* binding */ WAVEFORM_ZOOM_MIN),
7023
7726
  /* harmony export */ Le: () => (/* binding */ TAPE_POINTER_MEDIA_QUERY),
@@ -7026,8 +7729,10 @@ function isFpsAvailable(player) {
7026
7729
  /* harmony export */ QD: () => (/* binding */ WAVEFORM_ZOOM_BUTTON_STEP),
7027
7730
  /* harmony export */ XS: () => (/* binding */ WAVEFORM_BAR_MIN_HEIGHT),
7028
7731
  /* harmony export */ Zs: () => (/* binding */ WAVEFORM_FOLLOW_SCROLL_SETTLE_MS),
7732
+ /* harmony export */ bX: () => (/* binding */ WAVEFORM_RANGE_MIN_DURATION_MS),
7029
7733
  /* harmony export */ f3: () => (/* binding */ CLIENT_DECODE_PEAK_COUNT),
7030
7734
  /* harmony export */ fI: () => (/* binding */ WAVEFORM_TAPE_CLICK_SUPPRESS_MS),
7735
+ /* harmony export */ h1: () => (/* binding */ WAVEFORM_RANGE_HANDLE_LINE_PX),
7031
7736
  /* harmony export */ i8: () => (/* binding */ PEAK_UNIT_MAX),
7032
7737
  /* harmony export */ m6: () => (/* binding */ WAVEFORM_ZOOM_DISMISS_MS),
7033
7738
  /* harmony export */ mJ: () => (/* binding */ PEAK_UNIT_MIN),
@@ -7035,10 +7740,12 @@ function isFpsAvailable(player) {
7035
7740
  /* harmony export */ nh: () => (/* binding */ WAVEFORM_ZOOM_SLIDER_MAX),
7036
7741
  /* harmony export */ oN: () => (/* binding */ WAVEFORM_HEIGHT),
7037
7742
  /* harmony export */ qY: () => (/* binding */ MAX_PAYLOAD_BYTES),
7743
+ /* harmony export */ r0: () => (/* binding */ WAVEFORM_RANGE_HIT_AREA_PX),
7038
7744
  /* harmony export */ sh: () => (/* binding */ WAVEFORM_PAYLOAD_VERSION),
7039
7745
  /* harmony export */ tK: () => (/* binding */ WAVEFORM_ZOOM_MAX),
7040
7746
  /* harmony export */ zo: () => (/* binding */ WAVEFORM_BAR_WIDTH)
7041
7747
  /* harmony export */ });
7748
+ /* unused harmony exports WAVEFORM_RANGE_PLAYHEAD_GAP_PX, WAVEFORM_RANGE_GRIP_IN_MS, WAVEFORM_RANGE_GRIP_OUT_MS */
7042
7749
  /** Current waveform payload schema version. Bump only with a migration path. */
7043
7750
  const WAVEFORM_PAYLOAD_VERSION = 1;
7044
7751
 
@@ -7088,6 +7795,24 @@ const WAVEFORM_BAR_RADIUS = WAVEFORM_BAR_WIDTH / 2;
7088
7795
  const WAVEFORM_BAR_MIN_HEIGHT = WAVEFORM_BAR_WIDTH;
7089
7796
  const WAVEFORM_HEIGHT = 140;
7090
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
+
7091
7816
  /***/ },
7092
7817
 
7093
7818
  /***/ 4971
@@ -14598,7 +15323,7 @@ var x = (y) => {
14598
15323
  var x = {}; __webpack_require__.d(x, y); return x
14599
15324
  }
14600
15325
  var y = (x) => (() => (x))
14601
- 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) });
14602
15327
 
14603
15328
  /***/ },
14604
15329
 
@@ -23492,7 +24217,7 @@ class Browser {
23492
24217
  ;// ./src/lib/Logger.js
23493
24218
  /* eslint-disable no-undef */
23494
24219
  const CLIENT_NAME = "box-content-preview";
23495
- const CLIENT_VERSION = "3.91.0";
24220
+ const CLIENT_VERSION = "3.92.0";
23496
24221
  /* eslint-enable no-undef */
23497
24222
 
23498
24223
  class Logger {
@@ -36129,7 +36854,7 @@ function MP3Controls({
36129
36854
  isPlaying: isPlaying,
36130
36855
  movePlayback: movePlayback,
36131
36856
  onPlayPause: onPlayPause
36132
- }), /*#__PURE__*/external_react_["default"].createElement(VolumeControls/* default */.A, {
36857
+ }), /*#__PURE__*/external_react_["default"].createElement(VolumeControls/* default */.Ay, {
36133
36858
  onMuteChange: onMuteChange,
36134
36859
  onVolumeChange: onVolumeChange,
36135
36860
  volume: volume
@@ -36202,6 +36927,57 @@ function MP3Viewer_toPrimitive(t, r) { if ("object" != typeof t || !t) return t;
36202
36927
 
36203
36928
 
36204
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
+ }
36205
36981
  function createLoadFailedError(message) {
36206
36982
  const error = new Error(message);
36207
36983
  error.name = 'LOAD_FAILED';
@@ -36214,6 +36990,7 @@ class MP3Viewer extends media_MediaBaseViewer {
36214
36990
  * @inheritdoc
36215
36991
  */
36216
36992
  MP3Viewer_defineProperty(this, "handlePlayRequest", () => {
36993
+ this.exitShuttle();
36217
36994
  this.userRequestedPlay = true;
36218
36995
  this.togglePlay();
36219
36996
  if (this.shouldRaceClientWaveformDecode() && this.isWaveformDecodeRetryPending && !this.hasUsedWaveformDecodePlayRetry) {
@@ -36230,6 +37007,7 @@ class MP3Viewer extends media_MediaBaseViewer {
36230
37007
  * @return {void}
36231
37008
  */
36232
37009
  MP3Viewer_defineProperty(this, "handlePlayPause", shouldPlay => {
37010
+ this.exitShuttle();
36233
37011
  if (shouldPlay) {
36234
37012
  this.handlePlayRequest();
36235
37013
  return;
@@ -36239,19 +37017,25 @@ class MP3Viewer extends media_MediaBaseViewer {
36239
37017
  MP3Viewer_defineProperty(this, "handleCommentMarkersUpdated", (markers = []) => {
36240
37018
  this.commentMarkers = markers;
36241
37019
  const selected = markers.find(marker => marker.isSelected);
36242
- const selectedId = selected ? selected.id : null;
36243
- if (selectedId !== this.hostSelectedMarkerId) {
36244
- this.hostSelectedMarkerId = selectedId;
36245
- if (selected && Number.isFinite(selected.time)) {
36246
- this.pendingHostSelectedSeek = selected;
36247
- this.applyPendingHostSelectedSeek();
36248
- } else {
36249
- 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
+ }
36250
37030
  }
37031
+ } else if (this.hostSelectedMarkerId && !markers.some(marker => marker.id === this.hostSelectedMarkerId)) {
37032
+ this.hostSelectedMarkerId = null;
37033
+ this.pendingHostSelectedSeek = null;
36251
37034
  }
36252
37035
  this.renderUI();
36253
37036
  });
36254
37037
  MP3Viewer_defineProperty(this, "handleCommentMarkerClick", marker => {
37038
+ this.exitShuttle();
36255
37039
  this.hostSelectedMarkerId = marker.id;
36256
37040
  this.pendingHostSelectedSeek = marker;
36257
37041
  if (this.mediaEl) {
@@ -36305,6 +37089,12 @@ class MP3Viewer extends media_MediaBaseViewer {
36305
37089
  this.mediaEl = this.mediaContainerEl.appendChild(document.createElement('audio'));
36306
37090
  this.mediaEl.setAttribute('preload', 'auto');
36307
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;
36308
37098
  }
36309
37099
  bindCommentMarkersListener() {
36310
37100
  if (!this.isAudioPlayerV2) {
@@ -36324,6 +37114,212 @@ class MP3Viewer extends media_MediaBaseViewer {
36324
37114
  return this.featureEnabled(constants/* AUDIO_PLAYER_V2 */.Ku) && this.useReactControls();
36325
37115
  }
36326
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
+
36327
37323
  /**
36328
37324
  * Load v2 controls (and wavesurfer) only when the audio player v2 gate is on.
36329
37325
  *
@@ -36366,7 +37362,7 @@ class MP3Viewer extends media_MediaBaseViewer {
36366
37362
  * @return {Promise<{ default: Function }>} MP3ControlsV2 module
36367
37363
  */
36368
37364
  importV2Controls() {
36369
- return Promise.resolve(/* import() */).then(__webpack_require__.bind(__webpack_require__, 2668));
37365
+ return Promise.resolve(/* import() */).then(__webpack_require__.bind(__webpack_require__, 7450));
36370
37366
  }
36371
37367
 
36372
37368
  /**
@@ -36389,6 +37385,9 @@ class MP3Viewer extends media_MediaBaseViewer {
36389
37385
  */
36390
37386
  destroy() {
36391
37387
  this.removeListener('comment_markers', this.handleCommentMarkersUpdated);
37388
+ this.stopReverseShuttle();
37389
+ this.shuttleDirection = null;
37390
+ this.shuttleRate = 0;
36392
37391
  this.abortWaveformLoads();
36393
37392
  super.destroy();
36394
37393
  }
@@ -36764,6 +37763,7 @@ class MP3Viewer extends media_MediaBaseViewer {
36764
37763
  return;
36765
37764
  }
36766
37765
  this.pendingHostSelectedSeek = null;
37766
+ this.exitShuttle();
36767
37767
  this.mediaEl.pause();
36768
37768
  if (this.mediaEl.currentTime !== marker.time) {
36769
37769
  this.mediaEl.currentTime = marker.time;
@@ -36806,6 +37806,9 @@ class MP3Viewer extends media_MediaBaseViewer {
36806
37806
  const Mp3ControlsV2 = this.MP3ControlsV2;
36807
37807
  this.controls.render(/*#__PURE__*/external_react_["default"].createElement(Mp3ControlsV2, MP3Viewer_extends({}, sharedProps, {
36808
37808
  commentMarkers: this.commentMarkers || [],
37809
+ hasStartedPlayback: !!this.userRequestedPlay,
37810
+ keyboardVolumeStep: this.keyboardVolumeStep,
37811
+ keyboardZoomStep: this.keyboardZoomStep,
36809
37812
  mediaEl: this.mediaEl,
36810
37813
  onCommentMarkerClick: this.handleCommentMarkerClick,
36811
37814
  peaks: this.waveformPeaks
@@ -38404,7 +39407,7 @@ function VideoControls({
38404
39407
  onPlayPause: onPlayPause
38405
39408
  })), /*#__PURE__*/external_react_["default"].createElement("div", {
38406
39409
  className: "bp-VideoControls-group bp-VideoControls-group--media-controls"
38407
- }, /*#__PURE__*/external_react_["default"].createElement(VolumeControls/* default */.A, {
39410
+ }, /*#__PURE__*/external_react_["default"].createElement(VolumeControls/* default */.Ay, {
38408
39411
  onMuteChange: onMuteChange,
38409
39412
  onVolumeChange: onVolumeChange,
38410
39413
  volume: volume
@@ -38963,7 +39966,7 @@ function VideoControlsV2({
38963
39966
  mediaEl: mediaEl
38964
39967
  })), /*#__PURE__*/external_react_["default"].createElement("div", {
38965
39968
  className: "bp-VideoControlsV2-group bp-VideoControlsV2-group--right"
38966
- }, /*#__PURE__*/external_react_["default"].createElement(VolumeControls/* default */.A, {
39969
+ }, /*#__PURE__*/external_react_["default"].createElement(VolumeControls/* default */.Ay, {
38967
39970
  onMuteChange: onMuteChange,
38968
39971
  onVolumeChange: onVolumeChange,
38969
39972
  volume: volume