@pixodesk/svg-animator-rn 1.0.30 โ†’ 1.0.35

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/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # animator-rn
2
2
 
3
+ > ๐Ÿ“– Full user guide: [docs/library/react-native.md](../../docs/library/react-native.md) ยท [all docs](../../README.md#documentation)
4
+
3
5
  [![CI](https://github.com/pixodesk/pixodesk-svg-animator/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/pixodesk/pixodesk-svg-animator/actions/workflows/ci.yml)
4
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
7
 
@@ -21,8 +23,8 @@ native SVG, driven on the UI thread.
21
23
  - **One prop to render:** put the `.json` next to your component, import it, pass
22
24
  it as `doc`.
23
25
  - **Playback control:** declarative props (`autoplay`, `play`, `pause`,
24
- `timeMs`) or an imperative ref (`play()`, `pause()`, `setCurrentTime()`, โ€ฆ).
25
- - **Sized by its container:** wrap it in a `View` with the dimensions you want.
26
+ `time`) or an imperative ref (`play()`, `pause()`, `setCurrentTime()`, โ€ฆ).
27
+ - **No size of its own:** it fills whatever `View` you put it in โ€” give that `View` a `width` and `height`.
26
28
 
27
29
  ## Quick start
28
30
 
@@ -49,7 +51,7 @@ document says so.
49
51
 
50
52
  ```tsx
51
53
  // Play once when a screen opens, then hold the last frame
52
- <PixodeskSvgAnimator doc={doc} autoplay iterations={1} fill="forwards" />
54
+ <PixodeskSvgAnimator doc={doc} autoplay iterations={1} config={{ timeline: { fillMode: 'forwards' } }} />
53
55
 
54
56
  // Loop forever regardless of what the document says
55
57
  <PixodeskSvgAnimator doc={doc} autoplay iterations="infinite" />
@@ -153,13 +155,13 @@ const api = useRef<RnAnimatorApi>(null);
153
155
 
154
156
  ### Controlled time
155
157
 
156
- Scrub through the animation or pin a fixed frame:
158
+ Move through the animation with a slider, or show one fixed frame:
157
159
 
158
160
  ```tsx
159
- const [timeMs, setTimeMs] = useState(0);
161
+ const [time, setTime] = useState(0);
160
162
 
161
- <PixodeskSvgAnimator doc={doc} timeMs={timeMs} />
162
- <Slider minimumValue={0} maximumValue={2000} value={timeMs} onValueChange={setTimeMs} />
163
+ <PixodeskSvgAnimator doc={doc} time={time} />
164
+ <Slider minimumValue={0} maximumValue={2000} value={time} onValueChange={setTime} />
163
165
  ```
164
166
 
165
167
  ## Props
@@ -171,15 +173,14 @@ const [timeMs, setTimeMs] = useState(0);
171
173
  | `play` | `boolean` | Start playback, ignoring document triggers |
172
174
  | `pause` | `boolean` | Pause current playback |
173
175
  | `apiRef` | `RefObject<RnAnimatorApi>` | Ref for imperative control |
174
- | `time` | `number` | Seek to a fraction (0โ€“1) of the whole timeline (duration ร— iterations) |
175
- | `timeMs` | `number` | Seek to a time in milliseconds |
176
- | `duration` | `number` | Duration override (ms) |
177
- | `delay` | `number` | Delay before start (ms) |
178
- | `iterations` | `number \| 'infinite'` | Loop count |
179
- | `fill` | `FillMode` | Fill behaviour |
180
- | `direction` | `PlaybackDirection` | Playback direction |
181
- | `resetOnFinish` | `boolean` | Snap back to the start after a natural finish |
182
- | `outAction` | `OutAction` | What a second tap does with the `click` trigger (default: the document's, else `pause`) |
176
+ | `progress` | `number` | show the frame at this position in the whole timeline (duration ร— iterations): `0` is the first frame, `0.5` the middle, `1` the last |
177
+ | `time` | `number` | show the frame at that time, in milliseconds from the start |
178
+ | `config` | `object \| string` | Per-instance override of the document's `animator` block, deep-merged over it โ€” same shape as the file (`{ timeline: { fillMode, direction, trigger: { outAction, finishAction, โ€ฆ } } }`); `null` at a slot deletes that key. A JSON string is accepted too. `timeline.engine` is accepted but ignored here |
179
+ | `resetDocDefaults` | `boolean` | Ignore the document's playback settings and start from the player's defaults, with `config` on top |
180
+ | `duration` | `number` | Shortcut for `config.timeline.duration` (ms) |
181
+ | `delay` | `number` | Shortcut for `config.timeline.delay` (ms) |
182
+ | `iterations` | `number \| 'infinite'` | Shortcut for `config.timeline.iterations` |
183
+ | `startOn` | `StartOn` | Shortcut for `config.timeline.trigger.startOn`. `mouseOver` has no touch equivalent and is ignored |
183
184
  | `onPlay` | `() => void` | Called on play/resume |
184
185
  | `onPause` | `() => void` | Called on pause |
185
186
  | `onFinish` | `() => void` | Called on natural finish |
@@ -188,7 +189,7 @@ const [timeMs, setTimeMs] = useState(0);
188
189
  | `onError` | `(error, componentStack?) => void` | Called when a document cannot be compiled or rendered |
189
190
  | `fallback` | `(error) => ReactElement \| null` | Rendered in place of a failed animation (default: nothing) |
190
191
 
191
- With none of `autoplay` / `play` / `pause` / `time` / `timeMs` set, the component
192
+ With none of `autoplay` / `play` / `pause` / `progress` / `time` set, the component
192
193
  renders the animation statically (initial state, no playback).
193
194
 
194
195
  ### Failure handling
@@ -215,11 +216,11 @@ renderer never reaches JavaScript and cannot be caught โ€” see
215
216
 
216
217
  | Prop | Why it differs |
217
218
  |---|---|
218
- | `mode` | Not accepted. There is no Web Animations API on RN; playback is always native-driven. |
219
- | `frameRate` | Not accepted. Reanimated runs at the display refresh rate. The analogous knob is sampling density โ€” see `compileTracks`. |
220
- | `startOn` | Not accepted as a prop โ€” the document's trigger is honoured via `autoplay` (`load`, `click`, `scrollIntoView`, `programmatic`). `mouseOver` has no touch equivalent. |
221
- | `className` / `style` | Not accepted. Size the animation with the container `View`. (`node.style` *inside* the document is supported.) |
222
- | `onRemove` | Not emitted. Use React's own unmount cleanup. |
219
+ | `timeline.engine` | Accepted inside `config` but ignored. There is no Web Animations API on React Native; playback is always native-driven. |
220
+ | `timeline.frameRate` | Ignored. The screen's own refresh rate is used. The player does not compute values frame by frame: when the document loads it works out the animated values in advance, as a list of snapshots (60 per second of animation), and each screen refresh shows the nearest one. The closest thing to a frame rate is how many snapshots per second are prepared โ€” `compileTracks({ sampleRate })`, only available when you use the lower-level API instead of the component. |
221
+ | `startOn: 'mouseOver'` | Has no touch equivalent, so it is not honoured. The other four values (`load`, `click`, `scrollIntoView`, `programmatic`) work as they do on the web, from the file or from the `startOn` prop. |
222
+ | `className` / `style` | Not accepted โ€” you cannot style the component itself. It fills whatever `View` you put it in, so to set its size, give that `View` a `width` and `height`. Styling *inside* the document (`style` on an element in the JSON) is supported. |
223
+ | `onRemove` | Never called. On the web it tells you the animator was thrown away; here there is nothing to tell โ€” when the component leaves the screen, React removes it and everything it created. To run code at that moment, use a `useEffect` cleanup function in your own component. |
223
224
 
224
225
  ## How playback works
225
226
 
@@ -255,11 +256,11 @@ properties actually change over time.
255
256
  | `text`, `tspan` | โœ… | content via the `text` attribute |
256
257
  | `textPath` | โœ… | see *Text along a path* below |
257
258
  | `image` | โœ… | `href` accepts `data:` URIs; remote URLs are blocked by the sanitiser |
258
- | `use`, `symbol` | โœ… | animated targets are **inlined into real clones** before render โ€” `<use>` does not propagate animation natively in RN |
259
+ | `use`, `symbol` | โœ… | animated targets are **inlined into real clones** before render โ€” `<use>` does not propagate animation natively in React Native |
259
260
  | `linearGradient`, `radialGradient`, `stop` | โœ… | |
260
261
  | `pattern`, `marker` | โœ… | static geometry verified; complex cases unverified on device |
261
262
  | `mask`, `clipPath` | โœ… | |
262
- | `filter` + all 22 `fe*` primitives | โœ… | `feGaussianBlur`, `feDropShadow`, `feColorMatrix`, `feMerge`, `feComponentTransfer` + `feFunc*`, โ€ฆ Requires the New Architecture; **visual parity with the web is unverified on device** |
263
+ | `filter` + all 22 `fe*` primitives | โœ… | `feGaussianBlur`, `feDropShadow`, `feColorMatrix`, `feMerge`, `feComponentTransfer` + `feFunc*`, โ€ฆ **The visual result has not yet been checked on a real device** |
263
264
  | `foreignObject` | โŒ | blocked by the shared sanitiser (embeds arbitrary host content) |
264
265
  | `script` | โŒ | blocked by the shared sanitiser |
265
266
 
@@ -269,7 +270,7 @@ properties actually change over time.
269
270
  |---|---|---|
270
271
  | `opacity`, `fill-opacity`, `stroke-opacity` | โœ… | |
271
272
  | `fill`, `stroke`, `stop-color` | โœ… | interpolated as RGBA |
272
- | `stroke-width`, `stroke-dasharray`, `stroke-dashoffset` | โœ… | dash arrays are converted to the numeric form RN expects |
273
+ | `stroke-width`, `stroke-dasharray`, `stroke-dashoffset` | โœ… | dash arrays are converted to the numeric form React Native expects |
273
274
  | `x`, `y`, `width`, `height`, `cx`, `cy`, `r`, `rx`, `ry` | โœ… | |
274
275
  | `d` (**path morphing**) | โœ… | keyframes must share command structure |
275
276
  | `transform` (unified parts record) | โœ… | `translate`, `rotate`, `skew`, `scale`, `origin` |
@@ -281,7 +282,7 @@ properties actually change over time.
281
282
 
282
283
  ### Effects (`node.effects`)
283
284
 
284
- All effects are materialised by the shared core before rendering, so the RN
285
+ All effects are materialised by the shared core before rendering, so the React Native
285
286
  player sees plain nodes. **All are supported:**
286
287
 
287
288
  | Effect | Status | Notes |
@@ -294,8 +295,7 @@ player sees plain nodes. **All are supported:**
294
295
  | `clone` + `retime` | โœ… | each clone keeps its own time shift, incl. `retime.timeCrop` (a visibility window on the document timeline) |
295
296
  | `fillGradient` / `strokeGradient` | โœ… | animated stops **and animated geometry** (`animate.gradientX1`/`Cx`/`R`, โ€ฆ); `gradientTransform` is static (core-wide) |
296
297
  | `textPath` | โœ… | incl. animated `startOffset` |
297
- | `text.useGlyphs` | โœ… | text becomes `<path>` outlines from `definitions.glyphs` โ€” no font needed |
298
- | `isCombinedShape` | โœ… | |
298
+ | `text.useGlyphs` | โœ… | text becomes `<path>` outlines from `definitions.fonts` โ€” no font needed |
299
299
 
300
300
  ### Motion, timing and references
301
301
 
@@ -305,8 +305,8 @@ player sees plain nodes. **All are supported:**
305
305
  | **Text along a path** | โœ… two ways | native `textPath` (incl. animated `startOffset`), or **per-letter motion paths** for smooth results โ€” the example app uses the latter, since animating native `startOffset` is janky in `react-native-svg` |
306
306
  | Per-property `loop` (incl. `alternate` pingpong) | โœ… | expanded before playback |
307
307
  | Easing (cubic-bezier and named refs) | โœ… | baked into the sampled tracks |
308
- | `definitions.animations` / `easings` / `styles` / `glyphs` | โœ… | named refs resolved; `style` presets applied as props |
309
- | `node.style` (inline or named) | โœ… | resolved to props โ€” RN has no CSS, so explicit attributes win |
308
+ | `definitions.animations` / `easings` / `styles` / `fonts` | โœ… | named refs resolved; `style` presets applied as props |
309
+ | `node.style` (inline or named) | โœ… | resolved to props โ€” React Native has no CSS, so explicit attributes win |
310
310
 
311
311
  ### Playback and triggers
312
312
 
@@ -314,17 +314,17 @@ player sees plain nodes. **All are supported:**
314
314
  |---|---|---|
315
315
  | `duration`, `delay`, `iterations` (incl. `'infinite'`) | โœ… | |
316
316
  | `direction` โ€” all four values | โœ… | |
317
- | `fill` โ€” `forwards` / `backwards` / `both` / `none` | โœ… | |
318
- | `resetOnFinish` | โœ… | |
317
+ | `fillMode` โ€” `forwards` / `backwards` / `both` / `none` | โœ… | |
318
+ | `trigger.finishAction: 'reset'` | โœ… | |
319
319
  | `play` / `pause` / `cancel` / `finish` | โœ… | |
320
- | `setCurrentTime` โ€” seek, including **while playing** | โœ… | playback continues from the new position |
320
+ | `setCurrentTime` โ€” jump to a time, including **while playing** | โœ… | playback continues from the new point |
321
321
  | `setPlaybackRate` โ€” faster, slower and **reverse** (negative) | โœ… | composes with `direction` |
322
322
  | Trigger `load` / `programmatic` | โœ… | |
323
323
  | Trigger `click` | โœ… | wrapped in a `Pressable`; a second tap applies `outAction` |
324
- | Trigger `scrollIntoView` | โœ… | visibility sampled by measuring against the window (RN has no `IntersectionObserver`); honours `scrollIntoViewThreshold` and `outAction` |
324
+ | Trigger `scrollIntoView` | โœ… | visibility sampled by measuring against the window (React Native has no `IntersectionObserver`); honours `scrollIntoViewThreshold` and `outAction` |
325
325
  | Trigger `mouseOver` | โŒ | no touch equivalent โ€” use `click`, or drive `play` yourself |
326
- | `frameRate` | n/a | reanimated runs at the display refresh rate; use `compileTracks({sampleRate})` to trade memory for temporal precision |
327
- | `mode` (`waapi` / `frames`) | n/a | there is no Web Animations API on RN โ€” playback is always native-driven |
326
+ | `timeline.frameRate` | n/a | reanimated runs at the display refresh rate; use `compileTracks({sampleRate})` to trade memory for temporal precision |
327
+ | `timeline.engine` (`auto` / `native` / `js`) | n/a | there is no Web Animations API on React Native โ€” playback is always native-driven |
328
328
 
329
329
  ### Known limitations
330
330
 
package/dist/index.cjs CHANGED
@@ -199,7 +199,7 @@ function compileTracks(doc, opts) {
199
199
  ((_a2 = n == null ? void 0 : n.children) != null ? _a2 : []).forEach(indexTags);
200
200
  };
201
201
  indexTags(doc);
202
- const bindings = (0, import_svg_animator_core2.getNormalisedBindings)(doc, import_svg_animator_core2.PxAnimatorEngine.frames) || [];
202
+ const bindings = (0, import_svg_animator_core2.getNormalisedBindings)(doc, import_svg_animator_core2.PxTimelineEngine.js) || [];
203
203
  const elements = [];
204
204
  for (const binding of bindings) {
205
205
  const animDef = binding.animate;
@@ -562,43 +562,39 @@ var EMPTY_TRACKS = {
562
562
  elements: []
563
563
  };
564
564
  function compileDocument(doc, overrides) {
565
- const { duration, delay, iterations, fill, direction, resetOnFinish } = overrides;
565
+ const { config, resetDocDefaults, duration, delay, iterations, startOn } = overrides;
566
566
  const warnings = (0, import_svg_animator_core5.validateNodeEffects)(doc);
567
567
  for (const w of warnings) console.warn("[PixodeskSvgAnimator] effects shape warning:", w);
568
- let prepared = (0, import_svg_animator_core5.materialiseAllInTree)(doc, import_svg_animator_core5.PxAnimatorEngine.waapi);
568
+ (0, import_svg_animator_core5.reportDocumentDiagnostics)(doc, "[PixodeskSvgAnimator]");
569
+ const patch = (0, import_svg_animator_core5.foldAnimatorConfigShortcuts)(config, { duration, delay, iterations, startOn });
570
+ if (patch !== void 0 || resetDocDefaults) {
571
+ const applied = (0, import_svg_animator_core5.applyAnimatorConfig)(doc, patch != null ? patch : {}, { resetDefaults: !!resetDocDefaults });
572
+ for (const w of applied.warnings) console.warn("[PixodeskSvgAnimator] config override:", w);
573
+ doc = applied.doc;
574
+ }
575
+ let prepared = (0, import_svg_animator_core5.materialiseAllInTree)(doc, import_svg_animator_core5.PxTimelineEngine.native);
569
576
  if (NATIVE_SVG_VIEWS) {
570
577
  prepared = openClosedTextPathTargets(prepared);
571
578
  }
572
- const animator = (0, import_svg_animator_core5.getAnimatorConfig)(prepared) || {};
573
- prepared = __spreadProps(__spreadValues({}, prepared), {
574
- animator: __spreadProps(__spreadValues({}, animator), {
575
- duration: duration !== void 0 ? duration : animator.duration,
576
- delay: delay !== void 0 ? delay : animator.delay,
577
- iterations: iterations !== void 0 ? iterations : animator.iterations,
578
- fill: fill !== void 0 ? fill : animator.fill,
579
- direction: direction !== void 0 ? direction : animator.direction,
580
- resetOnFinish: resetOnFinish !== void 0 ? resetOnFinish : animator.resetOnFinish
581
- })
582
- });
583
579
  prepared = (0, import_svg_animator_core5.generateNewIds)(prepared);
584
580
  const tracks = compileTracks(prepared, { native: NATIVE_SVG_VIEWS });
585
581
  return { doc: prepared, tracks, error: null };
586
582
  }
587
583
  function PixodeskSvgAnimator({
588
584
  doc,
585
+ config,
586
+ resetDocDefaults,
589
587
  duration,
590
588
  delay,
591
589
  iterations,
592
- fill,
593
- direction,
594
- resetOnFinish,
595
- outAction: outActionProp,
590
+ startOn,
591
+ // (`progress` prop aliased โ€” the name is taken by the internal reanimated SharedValue)
596
592
  autoplay,
597
593
  play,
598
594
  pause,
599
595
  apiRef,
596
+ progress: progressProp,
600
597
  time,
601
- timeMs,
602
598
  onPlay,
603
599
  onStop,
604
600
  onPause,
@@ -608,18 +604,19 @@ function PixodeskSvgAnimator({
608
604
  fallback
609
605
  }) {
610
606
  var _a, _b, _c, _d;
607
+ const configKey = typeof config === "string" ? config : JSON.stringify(config != null ? config : null);
611
608
  const compiled = (0, import_react3.useMemo)(() => {
612
609
  try {
613
610
  return compileDocument(
614
611
  doc,
615
- { duration, delay, iterations, fill, direction, resetOnFinish }
612
+ { config, resetDocDefaults, duration, delay, iterations, startOn }
616
613
  );
617
614
  } catch (e) {
618
615
  const error = e instanceof Error ? e : new Error(String(e));
619
616
  console.warn("[PixodeskSvgAnimator] could not compile the document:", error.message);
620
617
  return { doc: null, tracks: EMPTY_TRACKS, error };
621
618
  }
622
- }, [doc, duration, delay, iterations, fill, direction, resetOnFinish]);
619
+ }, [doc, configKey, resetDocDefaults, duration, delay, iterations, startOn]);
623
620
  const tracks = compiled.tracks;
624
621
  const totalDuration = tracks.duration * (tracks.iterations === Infinity ? 1 : tracks.iterations);
625
622
  const progress = (0, import_react_native_reanimated.useSharedValue)(0);
@@ -713,11 +710,11 @@ function PixodeskSvgAnimator({
713
710
  };
714
711
  (0, import_react3.useImperativeHandle)(apiRef, () => api, [compiled]);
715
712
  const trigger = compiled.doc ? (_a = (0, import_svg_animator_core5.getAnimatorConfig)(compiled.doc)) == null ? void 0 : _a.trigger : void 0;
716
- const startOn = (_b = trigger == null ? void 0 : trigger.startOn) != null ? _b : "load";
717
- const outAction = (_c = outActionProp != null ? outActionProp : trigger == null ? void 0 : trigger.outAction) != null ? _c : "pause";
713
+ const effectiveStartOn = (_b = trigger == null ? void 0 : trigger.startOn) != null ? _b : "load";
714
+ const effectiveOutAction = (_c = trigger == null ? void 0 : trigger.outAction) != null ? _c : "pause";
718
715
  (0, import_react3.useEffect)(() => {
719
- if (time !== void 0 || timeMs !== void 0) {
720
- const seekMs = timeMs !== void 0 ? timeMs : (time != null ? time : 0) * totalDuration;
716
+ if (progressProp !== void 0 || time !== void 0) {
717
+ const seekMs = time !== void 0 ? time : (progressProp != null ? progressProp : 0) * totalDuration;
721
718
  api.setCurrentTime(seekMs);
722
719
  return;
723
720
  }
@@ -728,15 +725,15 @@ function PixodeskSvgAnimator({
728
725
  else api.play();
729
726
  return;
730
727
  }
731
- if (autoplay && startOn === "load") {
728
+ if (autoplay && effectiveStartOn === "load") {
732
729
  api.play();
733
730
  }
734
- }, [compiled, autoplay, play, pause, time, timeMs]);
731
+ }, [compiled, autoplay, play, pause, progressProp, time]);
735
732
  const scrollRef = (0, import_react3.useRef)(null);
736
733
  const inViewRef = (0, import_react3.useRef)(false);
737
734
  (0, import_react3.useEffect)(() => {
738
735
  var _a2;
739
- if (!autoplay || startOn !== "scrollIntoView") return;
736
+ if (!autoplay || effectiveStartOn !== "scrollIntoView") return;
740
737
  const threshold = (_a2 = trigger == null ? void 0 : trigger.scrollIntoViewThreshold) != null ? _a2 : 0;
741
738
  inViewRef.current = false;
742
739
  const check = () => {
@@ -753,17 +750,17 @@ function PixodeskSvgAnimator({
753
750
  if (isIn) {
754
751
  if (rateRef.current < 0) api.setPlaybackRate(Math.abs(rateRef.current));
755
752
  api.play();
756
- } else if (outAction === "reset") api.cancel();
757
- else if (outAction === "reverse") {
753
+ } else if (effectiveOutAction === "reset") api.cancel();
754
+ else if (effectiveOutAction === "reverse") {
758
755
  api.setPlaybackRate(-Math.abs(rateRef.current || 1));
759
756
  api.play();
760
- } else if (outAction !== "continue") api.pause();
757
+ } else if (effectiveOutAction !== "continue") api.pause();
761
758
  });
762
759
  };
763
760
  check();
764
761
  const id = setInterval(check, 200);
765
762
  return () => clearInterval(id);
766
- }, [compiled, autoplay, startOn, outAction]);
763
+ }, [compiled, autoplay, effectiveStartOn, effectiveOutAction]);
767
764
  (0, import_react3.useEffect)(() => {
768
765
  return () => {
769
766
  (0, import_react_native_reanimated.cancelAnimation)(progress);
@@ -836,19 +833,19 @@ function PixodeskSvgAnimator({
836
833
  }, [failure]);
837
834
  if (failure) return fallback ? fallback(failure) : null;
838
835
  let content = root;
839
- if (autoplay && startOn === "scrollIntoView" && root) {
836
+ if (autoplay && effectiveStartOn === "scrollIntoView" && root) {
840
837
  content = /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native.View, { ref: scrollRef, collapsable: false, children: root });
841
- } else if (autoplay && startOn === "click" && root) {
838
+ } else if (autoplay && effectiveStartOn === "click" && root) {
842
839
  content = /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
843
840
  import_react_native.Pressable,
844
841
  {
845
842
  onPress: () => {
846
843
  if (playingRef.current) {
847
- if (outAction === "reset") api.cancel();
848
- else if (outAction === "reverse") {
844
+ if (effectiveOutAction === "reset") api.cancel();
845
+ else if (effectiveOutAction === "reverse") {
849
846
  api.setPlaybackRate(-Math.abs(rateRef.current || 1));
850
847
  api.play();
851
- } else if (outAction !== "continue") api.pause();
848
+ } else if (effectiveOutAction !== "continue") api.pause();
852
849
  } else {
853
850
  if (rateRef.current < 0) api.setPlaybackRate(Math.abs(rateRef.current));
854
851
  api.play();