@hyperframes/studio 0.8.92 → 0.8.94

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.
Files changed (43) hide show
  1. package/dist/assets/{hyperframes-player-DKJPDHUp.js → hyperframes-player-Bkm-3G56.js} +3 -3
  2. package/dist/assets/{index-DpzpqJ_j.js → index-BUn5H_np.js} +1 -1
  3. package/dist/assets/{index-C5_j5-3u.js → index-BupvpoiM.js} +188 -188
  4. package/dist/assets/{index-CHZ4kFWm.js → index-n2ZRiO44.js} +1 -1
  5. package/dist/index.d.ts +62 -46
  6. package/dist/index.html +1 -1
  7. package/dist/index.js +156 -102
  8. package/dist/index.js.map +1 -1
  9. package/package.json +8 -8
  10. package/src/components/TimelineToolbar.test.tsx +17 -0
  11. package/src/components/TimelineToolbar.tsx +3 -1
  12. package/src/components/editor/ConnectedDomEditOverlay.test.tsx +2 -0
  13. package/src/components/editor/ConnectedDomEditOverlay.tsx +3 -1
  14. package/src/components/editor/DomEditCropHandles.test.tsx +87 -1
  15. package/src/components/editor/DomEditCropHandles.tsx +64 -62
  16. package/src/components/editor/DomEditOverlay.test.ts +39 -0
  17. package/src/components/editor/DomEditOverlay.tsx +6 -1
  18. package/src/components/editor/DomEditSelectionChrome.test.tsx +114 -1
  19. package/src/components/editor/DomEditSelectionChrome.tsx +17 -10
  20. package/src/components/editor/PreviewOverlayProvider.test.tsx +19 -0
  21. package/src/components/editor/PreviewOverlayProvider.tsx +1 -1
  22. package/src/components/editor/SnapToolbar.tsx +1 -1
  23. package/src/components/editor/domEditOverlayGeometry.ts +2 -0
  24. package/src/components/nle/PreviewOverlays.tsx +3 -3
  25. package/src/hooks/gsapResizeIntercept.test.ts +40 -0
  26. package/src/hooks/gsapResizeIntercept.ts +8 -4
  27. package/src/index.ts +4 -0
  28. package/src/player/components/PlayerControls.tsx +2 -15
  29. package/src/player/components/ShortcutsPanel.test.tsx +33 -1
  30. package/src/player/components/ShortcutsPanel.tsx +26 -0
  31. package/src/player/components/Timeline.trackPadding.test.tsx +157 -0
  32. package/src/player/components/TimelineCanvas.tsx +16 -19
  33. package/src/player/components/TimelineTypes.ts +2 -0
  34. package/src/player/components/timelineLayout.test.ts +47 -0
  35. package/src/player/components/timelineLayout.ts +22 -15
  36. package/src/player/components/useTimelineProviderState.tsx +2 -0
  37. package/src/player/components/useTimelineTrackLayout.ts +11 -2
  38. package/src/player/components/useTimelineVirtualRows.test.tsx +32 -0
  39. package/src/player/components/useTimelineVirtualRows.ts +2 -2
  40. package/src/player/hooks/useTimelinePlayer.seek.test.ts +29 -1
  41. package/src/player/hooks/useTimelinePlayer.ts +6 -0
  42. package/src/player/hooks/useTimelinePlayerLoop.ts +6 -2
  43. package/src/player/lib/playbackTypes.ts +1 -0
@@ -247,6 +247,53 @@ describe("track-area breathing pad y-math", () => {
247
247
  });
248
248
  });
249
249
 
250
+ describe("host track padding", () => {
251
+ const geometry = createTimelineRowGeometry([0, 1, 2], baseRows(3), { top: 0, bottom: TRACK_H });
252
+ // Row 1 with no top pad, but still row 0 if any reader keeps the default pad.
253
+ const yInRow1 = RULER_H + TRACK_H + 10;
254
+
255
+ it("moves the first row up and sizes the canvas from the host pads", () => {
256
+ expect(geometry.getRowTop(0)).toBe(RULER_H);
257
+ expect(getTimelineRowTop(1, geometry.rowHeights)).toBe(RULER_H + TRACK_H);
258
+ expect(getTimelineCanvasHeight(geometry.rowHeights)).toBe(RULER_H + 4 * TRACK_H);
259
+ });
260
+
261
+ it("maps a drag pointer y to the row under it", () => {
262
+ expect(Math.floor(geometry.getRowFromY(yInRow1))).toBe(1);
263
+ expect(Math.floor(getTimelineRowFromY(yInRow1, geometry.rowHeights))).toBe(1);
264
+ });
265
+
266
+ it("drops onto the row under the pointer, and a new track in the bottom pad", () => {
267
+ const drop = {
268
+ rectLeft: 0,
269
+ rectTop: 0,
270
+ scrollLeft: 0,
271
+ scrollTop: 0,
272
+ contentOrigin: GUTTER,
273
+ pixelsPerSecond: 100,
274
+ rowHeights: geometry.rowHeights,
275
+ trackOrder: [0, 1, 2],
276
+ };
277
+ expect(resolveTimelineAssetDrop(drop, GUTTER, yInRow1).track).toBe(1);
278
+ const inBottomPad = RULER_H + 3 * TRACK_H + TRACK_H / 2;
279
+ expect(resolveTimelineAssetDrop(drop, GUTTER, inBottomPad).track).toBe(3);
280
+ });
281
+
282
+ it("clamps a negative pad to 0 and replaces a non-finite one with the default", () => {
283
+ const odd = createTimelineRowGeometry([0], baseRows(1), { top: -20, bottom: Number.NaN });
284
+ expect(odd.padding).toEqual({ top: 0, bottom: TRACKS_BOTTOM_PAD });
285
+ expect(odd.getRowTop(0)).toBe(RULER_H);
286
+ expect(odd.canvasHeight).toBe(RULER_H + TRACK_H + TRACKS_BOTTOM_PAD);
287
+ const infinite = createTimelineRowGeometry([0], baseRows(1), { top: Infinity });
288
+ expect(infinite.padding.top).toBe(TRACKS_TOP_PAD);
289
+ });
290
+
291
+ it("keeps the default pads when a host passes none", () => {
292
+ const defaults = createTimelineRowGeometry([0], baseRows(1));
293
+ expect(defaults.padding).toEqual({ top: TRACKS_TOP_PAD, bottom: TRACKS_BOTTOM_PAD });
294
+ });
295
+ });
296
+
250
297
  describe("getTimelineScrubTime", () => {
251
298
  const at = (clientX: number, duration = 10) =>
252
299
  getTimelineScrubTime({
@@ -55,9 +55,8 @@ export function getTimelineLaneTop(laneIndex: number): number {
55
55
  return TRACK_H + Math.max(0, Math.trunc(laneIndex)) * LANE_H;
56
56
  }
57
57
  /**
58
- * Breathing room INSIDE the scroll area (CapCut-style), threaded through every
59
- * track-row y computation via {@link getTimelineRowTop} — never inline a magic
60
- * offset; a track row's top is always ruler + top pad + cumulative row heights.
58
+ * Default breathing room INSIDE the scroll area (CapCut-style). A host overrides
59
+ * it per Timeline; rows always read TimelineRowGeometry.padding, never these.
61
60
  *
62
61
  * - TRACKS_TOP_PAD: empty space between the (sticky) ruler and the first track
63
62
  * (~half a track height) so the first clip isn't jammed under the ruler.
@@ -66,6 +65,10 @@ export function getTimelineLaneTop(laneIndex: number): number {
66
65
  */
67
66
  export const TRACKS_TOP_PAD = 50;
68
67
  export const TRACKS_BOTTOM_PAD = Math.round(TRACK_H * 1.5);
68
+ export interface TimelineTrackPadding {
69
+ top?: number;
70
+ bottom?: number;
71
+ }
69
72
  /**
70
73
  * Breathing room LEFT of t=0 (CapCut-style), inside the scroll content — the
71
74
  * horizontal sibling of TRACKS_TOP_PAD: empty lane surface between the sticky
@@ -115,12 +118,17 @@ function validRowHeight(height: number | undefined): number {
115
118
  return height;
116
119
  }
117
120
 
121
+ function validPad(pad: number | undefined, fallback: number): number {
122
+ return pad !== undefined && Number.isFinite(pad) ? Math.max(0, pad) : fallback;
123
+ }
124
+
118
125
  export interface TimelineRowGeometry {
119
126
  readonly rowKeys: readonly number[];
120
127
  readonly rowHeights: readonly number[];
121
128
  /** Cumulative row boundaries, including the final bottom boundary. */
122
129
  readonly rowOffsets: readonly number[];
123
130
  readonly rowsHeight: number;
131
+ readonly padding: Readonly<Required<TimelineTrackPadding>>;
124
132
  readonly canvasHeight: number;
125
133
  getRowIndex(rowKey: number): number;
126
134
  getRowHeight(row: number): number;
@@ -141,7 +149,10 @@ const EMPTY_ROW_HEIGHTS: readonly number[] = Object.freeze([]);
141
149
  export function createTimelineRowGeometry(
142
150
  rowKeys: readonly number[],
143
151
  rowHeights: readonly number[],
152
+ padding: TimelineTrackPadding = {},
144
153
  ): TimelineRowGeometry {
154
+ const topPad = validPad(padding.top, TRACKS_TOP_PAD);
155
+ const bottomPad = validPad(padding.bottom, TRACKS_BOTTOM_PAD);
145
156
  const heights = Object.freeze(rowHeights.map(validRowHeight));
146
157
  const keys = Object.freeze(
147
158
  heights.map((_, row) => {
@@ -165,7 +176,7 @@ export function createTimelineRowGeometry(
165
176
  return (offsets[wholeRow] ?? 0) + (row - wholeRow) * getRowHeight(wholeRow);
166
177
  };
167
178
  const getRowFromY = (contentY: number) => {
168
- const y = contentY - RULER_H - TRACKS_TOP_PAD;
179
+ const y = contentY - RULER_H - topPad;
169
180
  if (heights.length === 0) return y / TRACK_H;
170
181
  if (y < 0) return y / getRowHeight(0);
171
182
  const rowsHeight = offsets[heights.length] ?? 0;
@@ -188,10 +199,11 @@ export function createTimelineRowGeometry(
188
199
  rowHeights: heights,
189
200
  rowOffsets: offsets,
190
201
  rowsHeight: offsets.at(-1) ?? 0,
191
- canvasHeight: RULER_H + TRACKS_TOP_PAD + (offsets.at(-1) ?? 0) + TRACKS_BOTTOM_PAD,
202
+ padding: Object.freeze({ top: topPad, bottom: bottomPad }),
203
+ canvasHeight: RULER_H + topPad + (offsets.at(-1) ?? 0) + bottomPad,
192
204
  getRowIndex: (rowKey) => rowIndexByKey.get(rowKey) ?? -1,
193
205
  getRowHeight,
194
- getRowTop: (row) => RULER_H + TRACKS_TOP_PAD + getRowOffset(row),
206
+ getRowTop: (row) => RULER_H + topPad + getRowOffset(row),
195
207
  getRowFromY,
196
208
  getRowPositionFromY: (contentY) => {
197
209
  const rowFloat = getRowFromY(contentY);
@@ -204,7 +216,7 @@ export function createTimelineRowGeometry(
204
216
  return frozenGeometry;
205
217
  }
206
218
 
207
- /** Compatibility accessor; repeated calls for one height-array reuse one snapshot. */
219
+ /** Compatibility accessor: a geometry's own rowHeights resolves back to it, pads included. */
208
220
  export function getTimelineRowGeometry(rowHeights: readonly number[]): TimelineRowGeometry {
209
221
  const cached = rowGeometryCache.get(rowHeights);
210
222
  if (cached) return cached;
@@ -228,10 +240,6 @@ export function getTimelineRowHeight(
228
240
  return validRowHeight(rowHeights[row]);
229
241
  }
230
242
 
231
- function getTimelineRowOffset(row: number, rowHeights: readonly number[]): number {
232
- return getTimelineRowGeometry(rowHeights).getRowTop(row) - RULER_H - TRACKS_TOP_PAD;
233
- }
234
-
235
243
  /**
236
244
  * The y (content-space) of the top edge of track ROW index `row` (0 = first
237
245
  * displayed lane). The single source of truth for row->y: the ruler height plus
@@ -243,7 +251,7 @@ export function getTimelineRowTop(
243
251
  row: number,
244
252
  rowHeights: readonly number[] = EMPTY_ROW_HEIGHTS,
245
253
  ): number {
246
- return RULER_H + TRACKS_TOP_PAD + getTimelineRowOffset(row, rowHeights);
254
+ return getTimelineRowGeometry(rowHeights).getRowTop(row);
247
255
  }
248
256
 
249
257
  /**
@@ -484,9 +492,8 @@ export function getTimelinePlaybackFollowScrollLeft(input: {
484
492
  }
485
493
 
486
494
  export function getTimelineCanvasHeight(rowHeights: readonly number[]): number {
487
- // RULER_H + top pad + lanes + bottom pad. The old TIMELINE_SCROLL_BUFFER is
488
- // subsumed by TRACKS_BOTTOM_PAD (which is larger), so the drag-into-void space
489
- // below the last lane is real scrollable surface, not a hidden buffer.
495
+ // RULER_H + top pad + lanes + bottom pad, read from the geometry's padding.
496
+ // The drag-into-void space below the last lane is real scrollable surface.
490
497
  return getTimelineRowGeometry(rowHeights).canvasHeight;
491
498
  }
492
499
 
@@ -65,6 +65,7 @@ export function useTimelineProviderState({
65
65
  theme: themeOverrides,
66
66
  showAudioEffects = true,
67
67
  showKeyframes = true,
68
+ trackPadding,
68
69
  sessionEpoch = 0,
69
70
  previewIframeRef,
70
71
  onZIndexReorder,
@@ -143,6 +144,7 @@ export function useTimelineProviderState({
143
144
  gsapAnimations,
144
145
  selectedElementId,
145
146
  selectedElementIds,
147
+ trackPadding,
146
148
  );
147
149
  const timelineElementsRef = useRef(timelineElements);
148
150
  timelineElementsRef.current = timelineElements; // oxlint-disable-line react/refs -- event handlers read the latest elements
@@ -10,6 +10,7 @@ import {
10
10
  TRACK_H,
11
11
  createTimelineRowGeometry,
12
12
  type TimelineRowGeometry,
13
+ type TimelineTrackPadding,
13
14
  trackHeights,
14
15
  type TimelineTrackHeightClip,
15
16
  } from "./timelineLayout";
@@ -164,7 +165,10 @@ function useTimelineRowHeights(
164
165
  selectedElementId: string | null,
165
166
  selectedElementIds: ReadonlySet<string>,
166
167
  groups: readonly TimelineTrackGroupInfo[],
168
+ trackPadding: TimelineTrackPadding | undefined,
167
169
  ) {
170
+ const padTop = trackPadding?.top;
171
+ const padBottom = trackPadding?.bottom;
168
172
  const expandedClipIds = usePlayerStore((s) => s.expandedClipIds);
169
173
  const expandedLaneOwnerIds = usePlayerStore((s) => s.expandedLaneOwnerIds);
170
174
  const { laneCounts, rowGeometry } = useMemo(() => {
@@ -207,9 +211,12 @@ function useTimelineRowHeights(
207
211
  rowGeometry: createTimelineRowGeometry(
208
212
  tracks.map(([track]) => track),
209
213
  rowHeights,
214
+ { top: padTop, bottom: padBottom },
210
215
  ),
211
216
  };
212
217
  }, [
218
+ padTop,
219
+ padBottom,
213
220
  expandedClipIds,
214
221
  expandedLaneOwnerIds,
215
222
  gsapAnimations,
@@ -233,6 +240,7 @@ export function useTimelineTrackLayout(
233
240
  gsapAnimations: Map<string, GsapAnimation[]>,
234
241
  selectedElementId: string | null,
235
242
  selectedElementIds: ReadonlySet<string>,
243
+ trackPadding?: TimelineTrackPadding,
236
244
  ) {
237
245
  const { tracks, trackStyles, trackOrder, groups, trackGroupOf } =
238
246
  useTimelineTrackDerivations(expandedElements);
@@ -244,6 +252,7 @@ export function useTimelineTrackLayout(
244
252
  selectedElementId,
245
253
  selectedElementIds,
246
254
  groups,
255
+ trackPadding,
247
256
  );
248
257
 
249
258
  return {
@@ -305,8 +314,8 @@ export function useTimelineDisplayLayout(
305
314
  const displayTrackOrder = useDisplayTrackOrder(draggedClip, trackOrder);
306
315
  const displayRowHeights = useDisplayRowHeights(displayTrackOrder, rowGeometry);
307
316
  const displayRowGeometry = useMemo(
308
- () => createTimelineRowGeometry(displayTrackOrder, displayRowHeights),
309
- [displayTrackOrder, displayRowHeights],
317
+ () => createTimelineRowGeometry(displayTrackOrder, displayRowHeights, rowGeometry.padding),
318
+ [displayTrackOrder, displayRowHeights, rowGeometry.padding],
310
319
  );
311
320
  return {
312
321
  displayTrackOrder,
@@ -171,4 +171,36 @@ describe("useTimelineVirtualRows", () => {
171
171
 
172
172
  act(() => root.unmount());
173
173
  });
174
+
175
+ it("offsets the mounted range by the geometry's own top pad", () => {
176
+ const keys = Array.from({ length: 1_000 }, (_, index) => index);
177
+ const topPad = 200 * 48;
178
+ const geometry = createTimelineRowGeometry(
179
+ keys,
180
+ keys.map(() => 48),
181
+ { top: topPad },
182
+ );
183
+ const scroll = createScrollElement(RULER_H + topPad + 300 * 48);
184
+ const scrollRef = { current: scroll };
185
+ let rows: ReturnType<typeof useTimelineVirtualRows> = [];
186
+
187
+ function Probe() {
188
+ rows = useTimelineVirtualRows({
189
+ enabled: true,
190
+ scrollRef,
191
+ viewport: viewport(scroll),
192
+ rowGeometry: geometry,
193
+ sessionEpoch: 1,
194
+ pinnedRowKeys: [],
195
+ });
196
+ return null;
197
+ }
198
+
199
+ const root = createRoot(document.createElement("div"));
200
+ act(() => root.render(React.createElement(Probe)));
201
+ act(() => {});
202
+ expect(rows.some((row) => row.index === 300)).toBe(true);
203
+ expect(rows.some((row) => row.index === 500)).toBe(false);
204
+ act(() => root.unmount());
205
+ });
174
206
  });
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, type RefObject } from "react";
2
2
  import { defaultRangeExtractor, useVirtualizer, type Range } from "@tanstack/react-virtual";
3
3
  import { TIMELINE_VIEWPORT_BUDGETS } from "../lib/timelineViewportBudgets";
4
4
  import type { TimelineScrollViewportSnapshot } from "./useTimelineScrollViewport";
5
- import { RULER_H, TRACKS_TOP_PAD, type TimelineRowGeometry } from "./timelineLayout";
5
+ import { RULER_H, type TimelineRowGeometry } from "./timelineLayout";
6
6
 
7
7
  export interface TimelineVirtualRow {
8
8
  readonly index: number;
@@ -84,7 +84,7 @@ export function useTimelineVirtualRows({
84
84
  getItemKey,
85
85
  overscan: TIMELINE_VIEWPORT_BUDGETS.rowOverscanPerSide,
86
86
  rangeExtractor,
87
- scrollMargin: RULER_H + TRACKS_TOP_PAD,
87
+ scrollMargin: RULER_H + rowGeometry.padding.top,
88
88
  initialRect: { width: viewport.clientWidth, height: viewport.clientHeight },
89
89
  initialOffset,
90
90
  });
@@ -353,7 +353,11 @@ describe("useTimelinePlayer seek keepPlaying option (#834)", () => {
353
353
  describe("useTimelinePlayer RAF loop wrap-around", () => {
354
354
  type SeekCall = { time: number; options?: { keepPlaying?: boolean } };
355
355
 
356
- function attachInstrumentedAdapter(api: ReturnType<typeof useTimelinePlayer>, duration = 30) {
356
+ function attachInstrumentedAdapter(
357
+ api: ReturnType<typeof useTimelinePlayer>,
358
+ duration = 30,
359
+ hooks: object = {},
360
+ ) {
357
361
  let currentTime = 0;
358
362
  let playing = false;
359
363
  const seekCalls: SeekCall[] = [];
@@ -377,6 +381,7 @@ describe("useTimelinePlayer RAF loop wrap-around", () => {
377
381
  };
378
382
  attachIframeWindow(api, {
379
383
  __player: adapter,
384
+ __hf: hooks,
380
385
  postMessage: () => {},
381
386
  scrollTo: () => {},
382
387
  addEventListener: () => {},
@@ -446,6 +451,29 @@ describe("useTimelinePlayer RAF loop wrap-around", () => {
446
451
  }
447
452
  });
448
453
 
454
+ it("tells the preview where the loop wraps to while it plays, and that it stopped looping", () => {
455
+ const raf = installRafCapture();
456
+ try {
457
+ const { api, root } = renderTimelinePlayerHarness();
458
+ const setLoopStart = vi.fn();
459
+ const { adapter } = attachInstrumentedAdapter(api, 30, { setLoopStart });
460
+ act(() => {
461
+ usePlayerStore.getState().setInPoint(2);
462
+ usePlayerStore.getState().setOutPoint(5);
463
+ api.play();
464
+ });
465
+ adapter.setTime(3);
466
+ act(() => void raf.flushOne());
467
+ expect(setLoopStart).toHaveBeenLastCalledWith(2);
468
+ act(() => usePlayerStore.getState().setLoopEnabled(false));
469
+ act(() => void raf.flushOne());
470
+ expect(setLoopStart).toHaveBeenLastCalledWith(null);
471
+ unmountWithAct(root);
472
+ } finally {
473
+ raf.restore();
474
+ }
475
+ });
476
+
449
477
  it("does not seek and pauses cleanly when forward playback reaches the end without loop", () => {
450
478
  const raf = installRafCapture();
451
479
  try {
@@ -152,12 +152,18 @@ export function useTimelinePlayer({
152
152
  [],
153
153
  );
154
154
 
155
+ const setLoopStart = useCallback((seconds: number | null) => {
156
+ try {
157
+ (iframeRef.current?.contentWindow as IframeWindow | null)?.__hf?.setLoopStart?.(seconds);
158
+ } catch {}
159
+ }, []);
155
160
  const { startRAFLoop, stopRAFLoop, stopReverseLoop } = useTimelinePlayerLoop({
156
161
  rafRef,
157
162
  reverseRafRef,
158
163
  getAdapter,
159
164
  setCurrentTime,
160
165
  setIsPlaying,
166
+ setLoopStart,
161
167
  });
162
168
 
163
169
  const applyPlaybackRate = useCallback((rate: number) => {
@@ -22,6 +22,7 @@ interface UseTimelinePlayerLoopParams {
22
22
  getAdapter: () => PlaybackAdapter | null;
23
23
  setCurrentTime: (v: number) => void;
24
24
  setIsPlaying: (v: boolean) => void;
25
+ setLoopStart: (seconds: number | null) => void;
25
26
  }
26
27
 
27
28
  interface UseTimelinePlayerLoopResult {
@@ -36,6 +37,7 @@ export function useTimelinePlayerLoop({
36
37
  getAdapter,
37
38
  setCurrentTime,
38
39
  setIsPlaying,
40
+ setLoopStart,
39
41
  }: UseTimelinePlayerLoopParams): UseTimelinePlayerLoopResult {
40
42
  const stopReverseLoop = useCallback(() => {
41
43
  cancelAnimationFrame(reverseRafRef.current);
@@ -55,8 +57,10 @@ export function useTimelinePlayerLoop({
55
57
  const rawLoopStart = inPoint !== null ? inPoint : 0;
56
58
  const loopEnd = rawLoopStart < rawLoopEnd ? rawLoopEnd : dur;
57
59
  const loopStart = rawLoopStart < rawLoopEnd ? rawLoopStart : 0;
60
+ const looping = usePlayerStore.getState().loopEnabled && dur > 0;
61
+ setLoopStart(looping ? loopStart : null);
58
62
  if (time >= loopEnd) {
59
- if (usePlayerStore.getState().loopEnabled && dur > 0) {
63
+ if (looping) {
60
64
  // keepPlaying skips the adapter's implicit pause; play() below is then a no-op.
61
65
  adapter.seek(loopStart, { keepPlaying: true });
62
66
  liveTime.notify(loopStart);
@@ -75,7 +79,7 @@ export function useTimelinePlayerLoop({
75
79
  rafRef.current = requestAnimationFrame(tick);
76
80
  };
77
81
  rafRef.current = requestAnimationFrame(tick);
78
- }, [rafRef, getAdapter, setCurrentTime, setIsPlaying]);
82
+ }, [rafRef, getAdapter, setCurrentTime, setIsPlaying, setLoopStart]);
79
83
 
80
84
  const stopRAFLoop = useCallback(() => {
81
85
  cancelAnimationFrame(rafRef.current);
@@ -69,5 +69,6 @@ export type IframeWindow = Window & {
69
69
  __hf?: {
70
70
  leasePausedMedia?: (el: HTMLMediaElement) => void;
71
71
  releasePausedMedia?: (el: HTMLMediaElement) => void;
72
+ setLoopStart?: (seconds: number | null) => void;
72
73
  };
73
74
  };