@hyperframes/studio 0.6.7 → 0.6.9

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 (30) hide show
  1. package/dist/assets/index-CGWN-iUB.js +115 -0
  2. package/dist/index.html +1 -1
  3. package/package.json +4 -4
  4. package/src/App.tsx +5 -10
  5. package/src/components/StudioLeftSidebar.tsx +16 -2
  6. package/src/components/StudioRightPanel.tsx +15 -2
  7. package/src/components/editor/MotionPanel.tsx +8 -8
  8. package/src/components/editor/SourceEditor.tsx +14 -0
  9. package/src/components/editor/manualEdits.ts +2 -0
  10. package/src/components/editor/manualEditsDom.ts +56 -0
  11. package/src/components/editor/studioMotion.ts +96 -0
  12. package/src/components/editor/studioMotionOps.test.ts +445 -0
  13. package/src/components/editor/studioMotionOps.ts +78 -4
  14. package/src/components/renders/RenderQueue.tsx +20 -6
  15. package/src/components/renders/renderSettings.ts +38 -0
  16. package/src/components/renders/useRenderQueue.ts +11 -1
  17. package/src/components/sidebar/CompositionsTab.tsx +43 -1
  18. package/src/components/sidebar/LeftSidebar.tsx +6 -0
  19. package/src/contexts/FileManagerContext.tsx +6 -0
  20. package/src/hooks/useDomEditCommits.ts +45 -33
  21. package/src/hooks/useDomEditSession.ts +26 -25
  22. package/src/hooks/useFileManager.ts +45 -2
  23. package/src/hooks/useManifestPersistence.ts +40 -218
  24. package/src/hooks/usePreviewInteraction.ts +7 -0
  25. package/src/player/components/Player.tsx +12 -3
  26. package/src/player/components/PlayerControls.tsx +29 -2
  27. package/src/player/components/useTimelineRangeSelection.ts +30 -3
  28. package/src/utils/sourcePatcher.test.ts +285 -0
  29. package/src/utils/sourcePatcher.ts +26 -6
  30. package/dist/assets/index-Yvtxngdi.js +0 -116
package/dist/index.html CHANGED
@@ -5,7 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
6
6
  <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
7
7
  <title>HyperFrames Studio</title>
8
- <script type="module" crossorigin src="/assets/index-Yvtxngdi.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-CGWN-iUB.js"></script>
9
9
  <link rel="stylesheet" crossorigin href="/assets/index-Ckqo37Co.css">
10
10
  </head>
11
11
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/studio",
3
- "version": "0.6.7",
3
+ "version": "0.6.9",
4
4
  "description": "",
5
5
  "repository": {
6
6
  "type": "git",
@@ -32,8 +32,8 @@
32
32
  "@phosphor-icons/react": "^2.1.10",
33
33
  "codemirror": "^6.0.1",
34
34
  "motion": "^12.38.0",
35
- "@hyperframes/core": "0.6.7",
36
- "@hyperframes/player": "0.6.7"
35
+ "@hyperframes/core": "0.6.9",
36
+ "@hyperframes/player": "0.6.9"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@types/react": "19",
@@ -47,7 +47,7 @@
47
47
  "vite": "^6.4.2",
48
48
  "vitest": "^3.2.4",
49
49
  "zustand": "^5.0.0",
50
- "@hyperframes/producer": "0.6.7"
50
+ "@hyperframes/producer": "0.6.9"
51
51
  },
52
52
  "peerDependencies": {
53
53
  "react": "19",
package/src/App.tsx CHANGED
@@ -1,5 +1,5 @@
1
1
  import { useState, useCallback, useRef, useMemo, useEffect } from "react";
2
- import type { LeftSidebarHandle } from "./components/sidebar/LeftSidebar";
2
+ import type { LeftSidebarHandle, SidebarTab } from "./components/sidebar/LeftSidebar";
3
3
  import { useRenderQueue } from "./components/renders/useRenderQueue";
4
4
  import { usePlayerStore } from "./player";
5
5
  import { LintModal } from "./components/LintModal";
@@ -25,7 +25,7 @@ import {
25
25
  STUDIO_INSPECTOR_PANELS_ENABLED,
26
26
  STUDIO_MOTION_PANEL_ENABLED,
27
27
  } from "./components/editor/manualEditingAvailability";
28
- import { getStudioMotionForSelection } from "./components/editor/studioMotion";
28
+ import { readStudioMotionFromElement } from "./components/editor/studioMotion";
29
29
  import type { DomEditSelection } from "./components/editor/domEditing";
30
30
  import { AskAgentModal } from "./components/AskAgentModal";
31
31
  import { StudioGlobalDragOverlay } from "./components/StudioGlobalDragOverlay";
@@ -200,9 +200,6 @@ export function StudioApp() {
200
200
  showToast,
201
201
  refreshPreviewDocumentVersion,
202
202
  queueDomEditSave: manifestPersistence.queueDomEditSave,
203
- commitStudioMotionManifestOptimistically:
204
- manifestPersistence.commitStudioMotionManifestOptimistically,
205
- applyCurrentStudioMotionToPreview: manifestPersistence.applyCurrentStudioMotionToPreview,
206
203
  readProjectFile: fileManager.readProjectFile,
207
204
  writeProjectFile: fileManager.writeProjectFile,
208
205
  domEditSaveTimestampRef,
@@ -215,10 +212,11 @@ export function StudioApp() {
215
212
  refreshKey,
216
213
  rightPanelTab: panelLayout.rightPanelTab,
217
214
  applyStudioManualEditsToPreviewRef: manifestPersistence.applyStudioManualEditsToPreviewRef,
218
- applyStudioMotionToPreviewRef: manifestPersistence.applyStudioMotionToPreviewRef,
219
215
  syncPreviewHistoryHotkey: appHotkeys.syncPreviewHistoryHotkey,
220
216
  reloadPreview,
221
217
  setRefreshKey,
218
+ openSourceForSelection: fileManager.openSourceForSelection,
219
+ selectSidebarTab: (tab: SidebarTab) => leftSidebarRef.current?.selectTab(tab),
222
220
  });
223
221
 
224
222
  domEditSelectionBridgeRef.current = domEditSession.domEditSelection;
@@ -292,10 +290,7 @@ export function StudioApp() {
292
290
 
293
291
  const selectedStudioMotion =
294
292
  STUDIO_INSPECTOR_PANELS_ENABLED && domEditSession.domEditSelection
295
- ? getStudioMotionForSelection(
296
- manifestPersistence.studioMotionManifestRef.current,
297
- domEditSession.domEditSelection,
298
- )
293
+ ? readStudioMotionFromElement(domEditSession.domEditSelection.element)
299
294
  : null;
300
295
  const layersPanelActive =
301
296
  STUDIO_INSPECTOR_PANELS_ENABLED && panelLayout.rightPanelTab === "layers";
@@ -1,4 +1,4 @@
1
- import type { RefObject } from "react";
1
+ import { useCallback, type RefObject } from "react";
2
2
  import { SourceEditor } from "./editor/SourceEditor";
3
3
  import { LeftSidebar, type LeftSidebarHandle } from "./sidebar/LeftSidebar";
4
4
  import { MediaPreview } from "./MediaPreview";
@@ -6,6 +6,7 @@ import { isMediaFile } from "../utils/mediaTypes";
6
6
  import { usePanelLayoutContext } from "../contexts/PanelLayoutContext";
7
7
  import { useStudioContext } from "../contexts/StudioContext";
8
8
  import { useFileManagerContext } from "../contexts/FileManagerContext";
9
+ import { getPersistedRenderSettings } from "./renders/renderSettings";
9
10
 
10
11
  export interface StudioLeftSidebarProps {
11
12
  leftSidebarRef: RefObject<LeftSidebarHandle | null>;
@@ -28,12 +29,13 @@ export function StudioLeftSidebar({
28
29
  handlePanelResizeMove,
29
30
  handlePanelResizeEnd,
30
31
  } = usePanelLayoutContext();
31
- const { projectId } = useStudioContext();
32
+ const { projectId, renderQueue, waitForPendingDomEditSaves } = useStudioContext();
32
33
  const {
33
34
  compositions,
34
35
  assets,
35
36
  editingFile,
36
37
  fileTree,
38
+ revealSourceOffset,
37
39
  handleFileSelect,
38
40
  handleCreateFile,
39
41
  handleCreateFolder,
@@ -45,6 +47,15 @@ export function StudioLeftSidebar({
45
47
  handleContentChange,
46
48
  } = useFileManagerContext();
47
49
 
50
+ const handleRenderComposition = useCallback(
51
+ async (comp: string) => {
52
+ await waitForPendingDomEditSaves();
53
+ const { format, quality, fps } = getPersistedRenderSettings();
54
+ await renderQueue.startRender({ composition: comp, format, quality, fps });
55
+ },
56
+ [renderQueue, waitForPendingDomEditSaves],
57
+ );
58
+
48
59
  if (leftCollapsed) {
49
60
  return (
50
61
  <div className="flex w-10 flex-shrink-0 flex-col items-center border-r border-neutral-800/50 bg-neutral-950 pt-1">
@@ -103,10 +114,13 @@ export function StudioLeftSidebar({
103
114
  content={editingFile.content ?? ""}
104
115
  filePath={editingFile.path}
105
116
  onChange={handleContentChange}
117
+ revealOffset={revealSourceOffset}
106
118
  />
107
119
  )
108
120
  ) : undefined
109
121
  }
122
+ onRenderComposition={handleRenderComposition}
123
+ isRendering={renderQueue.isRendering}
110
124
  onLint={onLint}
111
125
  linting={linting}
112
126
  onToggleCollapse={toggleLeftSidebar}
@@ -9,6 +9,9 @@ import {
9
9
  STUDIO_INSPECTOR_PANELS_ENABLED,
10
10
  STUDIO_MOTION_PANEL_ENABLED,
11
11
  } from "./editor/manualEditingAvailability";
12
+
13
+ /** Motion data without targeting metadata. */
14
+ type StudioMotionData = Omit<StudioGsapMotion, "kind" | "target" | "updatedAt">;
12
15
  import { useCallback } from "react";
13
16
  import { resolveDomEditSelection, type DomEditLayerItem } from "./editor/domEditing";
14
17
  import { useStudioContext } from "../contexts/StudioContext";
@@ -17,7 +20,7 @@ import { useFileManagerContext } from "../contexts/FileManagerContext";
17
20
  import { useDomEditContext } from "../contexts/DomEditContext";
18
21
 
19
22
  export interface StudioRightPanelProps {
20
- selectedStudioMotion: StudioGsapMotion | null;
23
+ selectedStudioMotion: StudioMotionData | null;
21
24
  designPanelActive: boolean;
22
25
  motionPanelActive: boolean;
23
26
  }
@@ -195,7 +198,17 @@ export function StudioRightPanel({
195
198
  onClearCompleted={renderQueue.clearCompleted}
196
199
  onStartRender={async (format, quality, resolution, fps) => {
197
200
  await waitForPendingDomEditSaves();
198
- await renderQueue.startRender({ fps, quality, format, resolution });
201
+ const composition =
202
+ activeCompPath && activeCompPath !== "index.html"
203
+ ? activeCompPath
204
+ : undefined;
205
+ await renderQueue.startRender({
206
+ fps,
207
+ quality,
208
+ format,
209
+ resolution,
210
+ composition,
211
+ });
199
212
  }}
200
213
  compositionDimensions={compositionDimensions}
201
214
  isRendering={renderQueue.isRendering}
@@ -24,14 +24,14 @@ import {
24
24
  } from "./MotionPanelFields";
25
25
  import { EaseCurveEditor } from "./EaseCurveEditor";
26
26
 
27
+ /** Motion data without targeting metadata (kind/target/updatedAt are derived from context). */
28
+ type StudioMotionData = Omit<StudioGsapMotion, "kind" | "target" | "updatedAt">;
29
+
27
30
  interface MotionPanelProps {
28
31
  element: DomEditSelection | null;
29
- motion: StudioGsapMotion | null;
32
+ motion: StudioMotionData | null;
30
33
  onClearSelection: () => void;
31
- onSetMotion: (
32
- element: DomEditSelection,
33
- motion: Omit<StudioGsapMotion, "kind" | "target" | "updatedAt">,
34
- ) => void;
34
+ onSetMotion: (element: DomEditSelection, motion: StudioMotionData) => void;
35
35
  onClearMotion: (element: DomEditSelection) => void;
36
36
  }
37
37
 
@@ -43,19 +43,19 @@ const MOTION_PRESET_OPTIONS: Array<{ label: string; value: StudioGsapMotionPrese
43
43
 
44
44
  const MOTION_DIRECTION_OPTIONS: StudioGsapMotionDirection[] = ["up", "down", "left", "right"];
45
45
 
46
- function motionValueDistance(motion: StudioGsapMotion | null): number {
46
+ function motionValueDistance(motion: StudioMotionData | null): number {
47
47
  if (!motion) return 32;
48
48
  return Math.max(Math.abs(motion.from.x ?? 0), Math.abs(motion.from.y ?? 0), 1);
49
49
  }
50
50
 
51
- function inferMotionPreset(motion: StudioGsapMotion | null): StudioGsapMotionPreset {
51
+ function inferMotionPreset(motion: StudioMotionData | null): StudioGsapMotionPreset {
52
52
  if (!motion) return "fade-up";
53
53
  if (motion.from.scale != null || motion.to.scale != null) return "pop";
54
54
  if (motion.from.x != null || motion.to.x != null) return "slide";
55
55
  return "fade-up";
56
56
  }
57
57
 
58
- function inferMotionDirection(motion: StudioGsapMotion | null): StudioGsapMotionDirection {
58
+ function inferMotionDirection(motion: StudioMotionData | null): StudioGsapMotionDirection {
59
59
  if (!motion) return "up";
60
60
  const x = motion.from.x ?? 0;
61
61
  const y = motion.from.y ?? 0;
@@ -55,6 +55,7 @@ interface SourceEditorProps {
55
55
  language?: string;
56
56
  onChange?: (content: string) => void;
57
57
  readOnly?: boolean;
58
+ revealOffset?: number | null;
58
59
  }
59
60
 
60
61
  export const SourceEditor = memo(function SourceEditor({
@@ -63,6 +64,7 @@ export const SourceEditor = memo(function SourceEditor({
63
64
  language,
64
65
  onChange,
65
66
  readOnly = false,
67
+ revealOffset,
66
68
  }: SourceEditorProps) {
67
69
  const editorRef = useRef<EditorView | null>(null);
68
70
  const containerRef = useRef<HTMLDivElement | null>(null);
@@ -132,5 +134,17 @@ export const SourceEditor = memo(function SourceEditor({
132
134
  }
133
135
  }, [content]);
134
136
 
137
+ useEffect(() => {
138
+ const view = editorRef.current;
139
+ if (!view || revealOffset == null || revealOffset < 0) return;
140
+ const docLen = view.state.doc.length;
141
+ const pos = Math.min(revealOffset, docLen);
142
+ view.dispatch({
143
+ selection: { anchor: pos },
144
+ effects: EditorView.scrollIntoView(pos, { y: "center" }),
145
+ });
146
+ view.focus();
147
+ }, [revealOffset]);
148
+
135
149
  return <div ref={mountEditor} className="h-full w-full overflow-hidden" />;
136
150
  });
@@ -30,6 +30,8 @@ export {
30
30
  clearStudioRotation,
31
31
  clearStudioBoxSize,
32
32
  reapplyPositionEditsAfterSeek,
33
+ buildMotionPatches,
34
+ buildClearMotionPatches,
33
35
  } from "./manualEditsDom";
34
36
 
35
37
  export {
@@ -31,6 +31,13 @@ import {
31
31
  STUDIO_ROTATION_TRANSFORM_ORIGIN,
32
32
  } from "./manualEditsTypes";
33
33
  import { roundRotationAngle } from "./manualEditsParsing";
34
+ import {
35
+ STUDIO_MOTION_ATTR,
36
+ STUDIO_MOTION_ORIGINAL_TRANSFORM_ATTR,
37
+ STUDIO_MOTION_ORIGINAL_OPACITY_ATTR,
38
+ STUDIO_MOTION_ORIGINAL_VISIBILITY_ATTR,
39
+ } from "./studioMotionTypes";
40
+ import { applyStudioMotionFromDom } from "./studioMotion";
34
41
 
35
42
  /* ── Gesture tracking ─────────────────────────────────────────────── */
36
43
  let studioManualEditGestureId = 0;
@@ -755,6 +762,52 @@ export function buildClearRotationPatches(element: HTMLElement): PatchOperation[
755
762
  return ops;
756
763
  }
757
764
 
765
+ /* ── Motion HTML patch builders ──────────────────────────────────── */
766
+
767
+ export function buildMotionPatches(element: HTMLElement): PatchOperation[] {
768
+ const motionJson = element.getAttribute(STUDIO_MOTION_ATTR);
769
+ if (!motionJson) return [];
770
+ const ops: PatchOperation[] = [
771
+ { type: "attribute", property: STUDIO_MOTION_ATTR, value: motionJson },
772
+ ];
773
+ const origTransform = element.getAttribute(STUDIO_MOTION_ORIGINAL_TRANSFORM_ATTR);
774
+ if (origTransform !== null) {
775
+ ops.push({
776
+ type: "attribute",
777
+ property: STUDIO_MOTION_ORIGINAL_TRANSFORM_ATTR,
778
+ value: origTransform,
779
+ });
780
+ }
781
+ const origOpacity = element.getAttribute(STUDIO_MOTION_ORIGINAL_OPACITY_ATTR);
782
+ if (origOpacity !== null) {
783
+ ops.push({
784
+ type: "attribute",
785
+ property: STUDIO_MOTION_ORIGINAL_OPACITY_ATTR,
786
+ value: origOpacity,
787
+ });
788
+ }
789
+ const origVisibility = element.getAttribute(STUDIO_MOTION_ORIGINAL_VISIBILITY_ATTR);
790
+ if (origVisibility !== null) {
791
+ ops.push({
792
+ type: "attribute",
793
+ property: STUDIO_MOTION_ORIGINAL_VISIBILITY_ATTR,
794
+ value: origVisibility,
795
+ });
796
+ }
797
+ return ops;
798
+ }
799
+
800
+ export function buildClearMotionPatches(_element: HTMLElement): PatchOperation[] {
801
+ return [
802
+ { type: "attribute", property: STUDIO_MOTION_ATTR, value: null },
803
+ { type: "attribute", property: STUDIO_MOTION_ORIGINAL_TRANSFORM_ATTR, value: null },
804
+ { type: "attribute", property: STUDIO_MOTION_ORIGINAL_OPACITY_ATTR, value: null },
805
+ { type: "attribute", property: STUDIO_MOTION_ORIGINAL_VISIBILITY_ATTR, value: null },
806
+ ];
807
+ }
808
+
809
+ /* ── Seek reapply (position + motion) ────────────────────────────── */
810
+
758
811
  export function reapplyPositionEditsAfterSeek(doc: Document): void {
759
812
  const htmlElement = doc.defaultView?.HTMLElement;
760
813
  if (!htmlElement) return;
@@ -793,4 +846,7 @@ export function reapplyPositionEditsAfterSeek(doc: Document): void {
793
846
  applyStudioRotation(el, { angle });
794
847
  }
795
848
  }
849
+
850
+ // Reapply DOM-backed motion timeline after seek
851
+ applyStudioMotionFromDom(doc);
796
852
  }
@@ -30,8 +30,12 @@ export {
30
30
  upsertStudioGsapMotion,
31
31
  removeStudioMotionForSelection,
32
32
  getStudioMotionForSelection,
33
+ readStudioMotionFromElement,
34
+ writeStudioMotionToElement,
35
+ clearStudioMotionFromElement,
33
36
  } from "./studioMotionOps";
34
37
 
38
+ import { readStudioMotionFromElement as readMotionAttr } from "./studioMotionOps";
35
39
  import {
36
40
  STUDIO_MOTION_ATTR,
37
41
  STUDIO_MOTION_ORIGINAL_TRANSFORM_ATTR,
@@ -39,6 +43,7 @@ import {
39
43
  STUDIO_MOTION_ORIGINAL_VISIBILITY_ATTR,
40
44
  STUDIO_MOTION_TIMELINE_ID,
41
45
  type StudioGsapMotion,
46
+ type StudioGsapMotionValues,
42
47
  type StudioMotionManifest,
43
48
  type StudioMotionTarget,
44
49
  type StudioMotionWindow,
@@ -220,6 +225,97 @@ export function applyStudioMotionManifest(
220
225
  return applied;
221
226
  }
222
227
 
228
+ /**
229
+ * Reads motion data from `data-hf-studio-motion` JSON attributes in the DOM,
230
+ * builds a GSAP timeline, and seeks to the current time.
231
+ * This replaces the manifest-based `applyStudioMotionManifest` for the studio preview.
232
+ */
233
+ export function applyStudioMotionFromDom(document: Document, currentTime?: number): number {
234
+ const win = document.defaultView as StudioMotionWindow | null;
235
+ if (!win) return 0;
236
+ const gsap = win.gsap;
237
+ win.__timelines = win.__timelines ?? {};
238
+ win.__timelines[STUDIO_MOTION_TIMELINE_ID]?.kill?.();
239
+ delete win.__timelines[STUDIO_MOTION_TIMELINE_ID];
240
+
241
+ // Restore elements that had GSAP motion applied previously but whose attribute
242
+ // is now just the legacy marker "true" (i.e. they were restored/cleared).
243
+ const HTMLElementCtor = document.defaultView?.HTMLElement;
244
+ if (!HTMLElementCtor) return 0;
245
+
246
+ // Collect elements that have JSON motion data in their attribute
247
+ const motionElements: Array<{
248
+ element: HTMLElement;
249
+ motion: {
250
+ start: number;
251
+ duration: number;
252
+ ease: string;
253
+ customEase?: { id: string; data: string };
254
+ from: StudioGsapMotionValues;
255
+ to: StudioGsapMotionValues;
256
+ };
257
+ }> = [];
258
+
259
+ for (const el of Array.from(document.querySelectorAll(`[${STUDIO_MOTION_ATTR}]`))) {
260
+ if (!(el instanceof HTMLElementCtor)) continue;
261
+ const motionData = readMotionAttr(el);
262
+ if (motionData) {
263
+ motionElements.push({ element: el, motion: motionData });
264
+ }
265
+ }
266
+
267
+ if (!gsap?.timeline || motionElements.length === 0) return 0;
268
+
269
+ const timeline = gsap.timeline({
270
+ paused: true,
271
+ defaults: { overwrite: "auto" },
272
+ });
273
+ let applied = 0;
274
+ for (const { element, motion } of motionElements) {
275
+ if (!timeline.fromTo) continue;
276
+ // Original styles are already captured when writeStudioMotionToElement was called
277
+ const fromVars: Record<string, unknown> = { ...motion.from };
278
+ const ease = resolveGsapEaseFromPayload(win, motion);
279
+ const toVars: Record<string, unknown> = {
280
+ ...motion.to,
281
+ duration: motion.duration,
282
+ ease,
283
+ overwrite: "auto",
284
+ immediateRender: false,
285
+ };
286
+ timeline.fromTo(element, fromVars, toVars, motion.start);
287
+ applied += 1;
288
+ }
289
+
290
+ if (applied === 0) {
291
+ timeline.kill?.();
292
+ return 0;
293
+ }
294
+ win.__timelines[STUDIO_MOTION_TIMELINE_ID] = timeline;
295
+ timeline.pause?.();
296
+ const safeTime = readCurrentTime(win, currentTime);
297
+ if (timeline.totalTime) timeline.totalTime(safeTime, false);
298
+ else timeline.time?.(safeTime);
299
+ return applied;
300
+ }
301
+
302
+ function resolveGsapEaseFromPayload(
303
+ win: StudioMotionWindow,
304
+ motion: { ease: string; customEase?: { id: string; data: string } },
305
+ ): string {
306
+ const customEase = motion.customEase;
307
+ if (!customEase) return motion.ease;
308
+ const customEasePlugin = win.CustomEase;
309
+ if (typeof customEasePlugin?.create !== "function") return motion.ease;
310
+ try {
311
+ win.gsap?.registerPlugin?.(customEasePlugin);
312
+ customEasePlugin.create(customEase.id, customEase.data);
313
+ return customEase.id;
314
+ } catch {
315
+ return motion.ease;
316
+ }
317
+ }
318
+
223
319
  export function installStudioMotionSeekReapply(win: Window, apply: () => void): boolean {
224
320
  const studioWin = win as StudioMotionWindow;
225
321
  studioWin.__hfStudioMotionApply = () => {