@hyperframes/studio 0.8.97 → 0.8.99

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 (75) hide show
  1. package/dist/assets/{hyperframes-player-Biu5_W0l.js → hyperframes-player-Rml34FRW.js} +1 -1
  2. package/dist/assets/{index-BdV5T-QI.js → index-CVDArg3m.js} +188 -188
  3. package/dist/assets/{index-B5Z-1RW9.js → index-C_fi-jTe.js} +1 -1
  4. package/dist/assets/{index-CWuozokW.js → index-D9YzDuBx.js} +1 -1
  5. package/dist/index.d.ts +16 -4
  6. package/dist/index.html +1 -1
  7. package/dist/index.js +758 -607
  8. package/dist/index.js.map +1 -1
  9. package/package.json +10 -9
  10. package/src/components/EditorShell.tsx +7 -0
  11. package/src/components/editor/DomEditCropHandles.test.tsx +34 -66
  12. package/src/components/editor/DomEditCropHandles.tsx +30 -62
  13. package/src/components/editor/DomEditOverlay.test.ts +2 -2
  14. package/src/components/editor/DomEditOverlay.tsx +0 -27
  15. package/src/components/editor/DomEditSelectionChrome.test.tsx +0 -5
  16. package/src/components/editor/DomEditSelectionChrome.tsx +0 -6
  17. package/src/components/editor/clickAfterPreventedPress.test.tsx +122 -0
  18. package/src/components/editor/cropResize.test.ts +129 -0
  19. package/src/components/editor/cropResize.ts +143 -0
  20. package/src/components/editor/domEditOverlayCrop.ts +65 -15
  21. package/src/components/editor/domEditOverlayCropLift.test.ts +44 -0
  22. package/src/components/editor/domEditOverlayGestures.ts +2 -5
  23. package/src/components/editor/elementPositionOffset.ts +5 -4
  24. package/src/components/editor/manualEdits.test.ts +22 -8
  25. package/src/components/editor/manualEditsDom.ts +27 -11
  26. package/src/components/editor/manualEditsParsing.ts +0 -4
  27. package/src/components/editor/manualEditsSnapshot.ts +3 -0
  28. package/src/components/editor/manualOffsetDrag.test.ts +14 -0
  29. package/src/components/editor/propertyPanelFont.test.tsx +48 -5
  30. package/src/components/editor/propertyPanelFont.tsx +2 -1
  31. package/src/components/editor/shiftDragAxisLock.test.tsx +0 -2
  32. package/src/components/nle/TimelinePane.tsx +4 -0
  33. package/src/hooks/domSelectionTestHarness.ts +8 -0
  34. package/src/hooks/draggedGsapPosition.ts +4 -2
  35. package/src/hooks/gsapDragCommit.ts +6 -6
  36. package/src/hooks/gsapEditOutcome.ts +6 -0
  37. package/src/hooks/gsapResizeIntercept.ts +5 -5
  38. package/src/hooks/gsapRuntimeBridge.test.ts +11 -0
  39. package/src/hooks/gsapRuntimeBridge.ts +2 -1
  40. package/src/hooks/gsapRuntimeKeyframes.test.ts +7 -0
  41. package/src/hooks/gsapRuntimeKeyframes.ts +34 -21
  42. package/src/hooks/timelineTimingSync.ts +40 -4
  43. package/src/hooks/useAnimatedPropertyCommit.test.tsx +40 -1
  44. package/src/hooks/useAnimatedPropertyCommit.ts +11 -13
  45. package/src/hooks/useCommitPreflightCapabilities.ts +3 -11
  46. package/src/hooks/useCropOverlay.ts +8 -2
  47. package/src/hooks/useDomEditCommits.test.tsx +6 -1
  48. package/src/hooks/useDomEditCommits.ts +1 -0
  49. package/src/hooks/useDomEditPositionPatchCommit.ts +2 -2
  50. package/src/hooks/useDomEditSession.ts +2 -0
  51. package/src/hooks/useDomGeometryCommit.ts +1 -0
  52. package/src/hooks/useDomGeometryCommits.test.tsx +112 -0
  53. package/src/hooks/useDomGeometryCommits.ts +5 -0
  54. package/src/hooks/useElementLifecycleOps.multiDelete.test.tsx +3 -1
  55. package/src/hooks/useGsapAnimationFetchFallback.test.ts +44 -3
  56. package/src/hooks/useGsapAnimationFetchFallback.ts +11 -35
  57. package/src/hooks/useGsapAwareEditing.test.tsx +101 -5
  58. package/src/hooks/useGsapAwareEditing.ts +37 -23
  59. package/src/hooks/useGsapTweenCache.element.test.tsx +23 -1
  60. package/src/hooks/useGsapTweenCache.ts +0 -24
  61. package/src/hooks/useTimelineEditing.test.tsx +190 -135
  62. package/src/hooks/useTimelineEditing.ts +13 -13
  63. package/src/hooks/useTimelineGroupEditing.ts +13 -9
  64. package/src/index.ts +4 -1
  65. package/src/player/components/ClipContextMenu.test.tsx +182 -0
  66. package/src/player/components/ClipContextMenu.tsx +52 -10
  67. package/src/player/components/TimelineOverlays.test.ts +30 -7
  68. package/src/player/components/TimelineOverlays.tsx +7 -1
  69. package/src/player/components/TimelineProvider.tsx +1 -0
  70. package/src/player/components/TimelineTypes.ts +10 -0
  71. package/src/player/components/menuKeyboardNav.ts +12 -5
  72. package/src/player/components/useTimelineProviderState.tsx +2 -0
  73. package/src/player/components/useTimelineVirtualRows.test.tsx +30 -1
  74. package/src/utils/rounding.ts +5 -0
  75. package/src/utils/sdkEditTransaction.ts +10 -3
@@ -1,11 +1,13 @@
1
1
  // @vitest-environment happy-dom
2
2
 
3
3
  import React, { act } from "react";
4
- import { afterEach, describe, expect, it, vi } from "vitest";
4
+ import { afterEach, describe, expect, it, vi, type Mock } from "vitest";
5
5
  import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
6
6
  import type { DomEditSelection } from "../components/editor/domEditingTypes";
7
7
  import type { DomEditGroupPathOffsetCommit } from "../components/editor/DomEditOverlay";
8
- import { mountReactHarness } from "./domSelectionTestHarness";
8
+ import { mountReactHarness, withInlineLayoutBox } from "./domSelectionTestHarness";
9
+ import { DomEditCropHandles } from "../components/editor/DomEditCropHandles";
10
+ import { applyStudioBoxSizeDraft } from "../components/editor/manualEdits";
9
11
 
10
12
  (globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
11
13
 
@@ -56,6 +58,7 @@ function mountResizeHandler(
56
58
  const anchorRollback = vi.fn();
57
59
  const elementOffset = vi.fn(() => ({ save: anchorSave, rollback: anchorRollback }));
58
60
  const commitMutation = vi.fn().mockResolvedValue(undefined);
61
+ const commitPatch = vi.fn().mockResolvedValue(undefined);
59
62
  let resize:
60
63
  | ((
61
64
  selection: DomEditSelection,
@@ -64,8 +67,9 @@ function mountResizeHandler(
64
67
  restore?: () => void,
65
68
  ) => Promise<void>)
66
69
  | null = null;
70
+ let property: ReturnType<typeof useGsapAwareEditing>["commitAnimatedProperty"] | null = null;
67
71
  function Harness() {
68
- resize = useGsapAwareEditing({
72
+ const editing = useGsapAwareEditing({
69
73
  domEditSelection: selection,
70
74
  selectedGsapAnimations: animations,
71
75
  gsapCommitMutation: commitMutation,
@@ -76,11 +80,14 @@ function mountResizeHandler(
76
80
  trackGsapInteractionFailure: vi.fn(),
77
81
  stageElementPositionOffset: elementOffset,
78
82
  handleDomBoxSizeCommit: fallback,
83
+ commitPositionPatchToHtml: commitPatch,
79
84
  addGsapAnimation: vi.fn(),
80
85
  convertToKeyframes: vi.fn(),
81
86
  setArcPath: vi.fn(),
82
87
  updateArcSegment: vi.fn(),
83
- }).handleGsapAwareBoxSizeCommit;
88
+ });
89
+ resize = editing.handleGsapAwareBoxSizeCommit;
90
+ property = editing.commitAnimatedProperty;
84
91
  return null;
85
92
  }
86
93
  const root = mountReactHarness(<Harness />);
@@ -91,7 +98,9 @@ function mountResizeHandler(
91
98
  anchorSave,
92
99
  anchorRollback,
93
100
  commitMutation,
101
+ commitPatch,
94
102
  resize: resize!,
103
+ property: property!,
95
104
  root,
96
105
  };
97
106
  }
@@ -122,6 +131,7 @@ function mountGroupHandler({
122
131
  trackGsapInteractionFailure,
123
132
  stageElementPositionOffset,
124
133
  handleDomBoxSizeCommit: vi.fn(),
134
+ commitPositionPatchToHtml: vi.fn(),
125
135
  addGsapAnimation: vi.fn(),
126
136
  convertToKeyframes: vi.fn(),
127
137
  setArcPath: vi.fn(),
@@ -257,6 +267,92 @@ describe("useGsapAwareEditing anchored resize", () => {
257
267
  act(() => h.root.unmount());
258
268
  });
259
269
 
270
+ function mountCropped() {
271
+ const h = mountResizeHandler([]);
272
+ const el = withInlineLayoutBox(h.selection.element);
273
+ el.id = "clip";
274
+ el.style.cssText = "width: 300px; height: 200px; clip-path: inset(0px 60px 0px 0px)";
275
+ document.body.append(el);
276
+ const rect = { left: 0, top: 0, width: 300, height: 200, editScaleX: 1, editScaleY: 1 };
277
+ const crop = mountReactHarness(
278
+ <DomEditCropHandles selection={h.selection} overlayRect={rect} onStyleCommit={vi.fn()} />,
279
+ );
280
+ return { ...h, el, crop };
281
+ }
282
+
283
+ function resizeLandsWhen(where: "intercept" | "dispatch", h: { commitMutation: Mock }) {
284
+ const land = (el: HTMLElement) => {
285
+ el.style.width = "450px";
286
+ el.style.height = "300px";
287
+ };
288
+ mocks.resize.mockImplementation(async (selection, _next, _a, _i, commit) => {
289
+ if (where === "intercept") land(selection.element);
290
+ await commit(selection, { type: "size" }, { label: "Resize", softReload: true });
291
+ return { status: "persisted" };
292
+ });
293
+ if (where === "dispatch") h.commitMutation.mockImplementation(async (s) => land(s.element));
294
+ }
295
+
296
+ const scaled = "inset(0px 90px 0px 0px)";
297
+
298
+ it("saves the crop scaled with a size that lands only when saved (the W/H fields)", async () => {
299
+ const h = mountCropped();
300
+ resizeLandsWhen("dispatch", h);
301
+ await act(() => h.resize(h.selection, { width: 450, height: 300 }));
302
+
303
+ expect(h.commitPatch).toHaveBeenCalledWith(
304
+ h.selection,
305
+ [{ type: "inline-style", property: "clip-path", value: scaled }],
306
+ expect.objectContaining({ coalesceKey: h.commitMutation.mock.calls[0]![2].coalesceKey }),
307
+ );
308
+ act(() => h.crop.unmount());
309
+ expect(h.el.style.getPropertyValue("clip-path")).toBe(scaled);
310
+ act(() => h.root.unmount());
311
+ });
312
+
313
+ it("scales from the box before a draft its caller applied (the agent tool)", async () => {
314
+ const h = mountCropped();
315
+ resizeLandsWhen("intercept", h);
316
+ applyStudioBoxSizeDraft(h.el, { width: 450, height: 300 });
317
+ await act(() => h.resize(h.selection, { width: 450, height: 300 }));
318
+
319
+ expect(h.commitPatch.mock.calls[0]![1]).toEqual([
320
+ { type: "inline-style", property: "clip-path", value: scaled },
321
+ ]);
322
+ act(() => h.crop.unmount());
323
+ act(() => h.root.unmount());
324
+ });
325
+
326
+ it("a W field on an animated element saves the scaled crop in the same undo step", async () => {
327
+ const h = mountCropped();
328
+ mocks.commitAnimatedProperties.mockImplementation(async (selection, _props, keyed) => {
329
+ await (keyed ?? h.commitMutation)(selection, { type: "set" }, { label: "Edit width" });
330
+ selection.element.style.width = "450px";
331
+ });
332
+ await act(() => h.property(h.selection, "width", 450));
333
+
334
+ const key = h.commitMutation.mock.calls[0]![2].coalesceKey;
335
+ expect(key).toBeTruthy();
336
+ expect(h.commitPatch).toHaveBeenCalledWith(
337
+ h.selection,
338
+ [{ type: "inline-style", property: "clip-path", value: "inset(0px 90px 0px 0px)" }],
339
+ expect.objectContaining({ coalesceKey: key }),
340
+ );
341
+ act(() => h.crop.unmount());
342
+ act(() => h.root.unmount());
343
+ });
344
+
345
+ it("shows the scaled crop when deselected before the save lands", async () => {
346
+ const h = mountCropped();
347
+ resizeLandsWhen("intercept", h);
348
+ const saved = h.resize(h.selection, { width: 450, height: 300 });
349
+ act(() => h.crop.unmount());
350
+ await act(() => saved);
351
+
352
+ expect(h.el.style.getPropertyValue("clip-path")).toBe(scaled);
353
+ act(() => h.root.unmount());
354
+ });
355
+
260
356
  it("does not keep the anchor move when the size save fails", async () => {
261
357
  resizeWritesSize();
262
358
  const h = mountResizeHandler([]);
@@ -288,7 +384,7 @@ describe("useGsapAwareEditing anchored resize", () => {
288
384
  commit = h.resize(h.selection, { width: 300, height: 200 }, { x: -50.2, y: -25.6 });
289
385
  });
290
386
 
291
- expect(mocks.setPosition).toHaveBeenCalledWith(h.selection.element, 70, 55);
387
+ expect(mocks.setPosition).toHaveBeenCalledWith(h.selection.element, 70.2, 54.6);
292
388
  expect(mocks.setPosition.mock.invocationCallOrder[0]).toBeLessThan(
293
389
  mocks.resize.mock.invocationCallOrder[0]!,
294
390
  );
@@ -34,14 +34,14 @@ import { logResize, logResizeSettle } from "../utils/resizeDebug";
34
34
  import type { DomEditGroupPathOffsetCommit } from "../components/editor/DomEditOverlay";
35
35
  import { runGestureTransaction } from "./gestureTransaction";
36
36
  import { hasNonHoldTweenForElement } from "./gsapRuntimeKeyframes";
37
- import { assertGsapEditPersisted, type GsapEditOutcome } from "./gsapEditOutcome";
37
+ import { assertGsapEditPersisted, saveMove } from "./gsapEditOutcome";
38
38
  import type { GsapAnimationFetchOptions } from "./useGsapAnimationFetchFallback";
39
-
40
- /** A move only a shared tween positions is saved on the element itself; a blocked one throws. */
41
- async function saveMove(outcome: GsapEditOutcome, saveOnElement: () => Promise<void>) {
42
- if (outcome.status === "element-offset") return saveOnElement();
43
- assertGsapEditPersisted(outcome);
44
- }
39
+ import type { ElementOffsetStagerDeps } from "./elementOffsetStager";
40
+ import {
41
+ prepareCropResize,
42
+ saveCropResize,
43
+ writeSizeWithCrop,
44
+ } from "../components/editor/cropResize";
45
45
 
46
46
  // Distinct coalesceKey per group drag so consecutive group drags don't fold
47
47
  // into one another's undo entry (module-local counter, not Date.now()).
@@ -87,6 +87,7 @@ export interface UseGsapAwareEditingParams {
87
87
  next: { width: number; height: number },
88
88
  offset?: { x: number; y: number },
89
89
  ) => Promise<void>;
90
+ commitPositionPatchToHtml: ElementOffsetStagerDeps["commitPositionPatchToHtml"];
90
91
  // GSAP script commit ops (from useGsapScriptCommits)
91
92
  addGsapAnimation: (
92
93
  sel: DomEditSelection,
@@ -130,6 +131,7 @@ export function useGsapAwareEditing({
130
131
  trackGsapInteractionFailure,
131
132
  stageElementPositionOffset,
132
133
  handleDomBoxSizeCommit,
134
+ commitPositionPatchToHtml,
133
135
  addGsapAnimation,
134
136
  convertToKeyframes,
135
137
  setArcPath,
@@ -353,6 +355,8 @@ export function useGsapAwareEditing({
353
355
  animGroups: targetAnimations.map((a) => `${a.propertyGroup}:${a.method}`),
354
356
  });
355
357
  let anchorMove: ReturnType<typeof stageElementPositionOffset> | null = null;
358
+ const stageCrop = prepareCropResize(selection.element);
359
+ let cropUndoKey: string | null = null;
356
360
  return runGestureTransaction({
357
361
  element: selection.element,
358
362
  label: "Resize layer",
@@ -381,6 +385,7 @@ export function useGsapAwareEditing({
381
385
  makeFetchFallback(selection),
382
386
  );
383
387
  assertGsapEditPersisted(outcome);
388
+ cropUndoKey = coalesceKey;
384
389
  // What the resize actually did, not what its animations suggest
385
390
  // it would do. An element whose scale is an instant hold has a
386
391
  // scale-group tween and still commits width/height, so guessing
@@ -424,7 +429,13 @@ export function useGsapAwareEditing({
424
429
  logResizeSettle(selection.element, "dom-route");
425
430
  await handleDomBoxSizeCommit(selection, next, offset);
426
431
  },
427
- afterBufferedCommitsSaved: () => anchorMove?.save() ?? Promise.resolve(),
432
+ afterBufferedCommitsSaved: async () => {
433
+ await anchorMove?.save();
434
+ // Only now is the size live for every caller, drag or not.
435
+ if (cropUndoKey) {
436
+ await saveCropResize(stageCrop, selection, commitPositionPatchToHtml, cropUndoKey);
437
+ }
438
+ },
428
439
  restore: () => {
429
440
  anchorMove?.rollback();
430
441
  restore();
@@ -434,6 +445,7 @@ export function useGsapAwareEditing({
434
445
  },
435
446
  [
436
447
  handleDomBoxSizeCommit,
448
+ commitPositionPatchToHtml,
437
449
  stageElementPositionOffset,
438
450
  gsapCommitMutation,
439
451
  previewIframeRef,
@@ -481,10 +493,7 @@ export function useGsapAwareEditing({
481
493
 
482
494
  // ── Animated property commit ──
483
495
 
484
- const {
485
- commitAnimatedProperty: commitAnimatedPropertyRaw,
486
- commitAnimatedProperties: commitAnimatedPropertiesRaw,
487
- } = useAnimatedPropertyCommit({
496
+ const { commitAnimatedProperties: commitAnimatedPropertiesRaw } = useAnimatedPropertyCommit({
488
497
  selectedGsapAnimations,
489
498
  gsapCommitMutation,
490
499
  addGsapAnimation: (sel, method, time) => addGsapAnimation(sel, method, time),
@@ -496,25 +505,30 @@ export function useGsapAwareEditing({
496
505
  const commitAnimatedProperties = useCallback(
497
506
  async (selection: DomEditSelection, properties: Record<string, number | string>) => {
498
507
  try {
499
- await commitAnimatedPropertiesRaw(selection, properties);
508
+ await writeSizeWithCrop(
509
+ selection,
510
+ properties,
511
+ gsapCommitMutation,
512
+ commitPositionPatchToHtml,
513
+ (keyed) => commitAnimatedPropertiesRaw(selection, properties, keyed),
514
+ );
500
515
  } catch (error) {
501
516
  trackGsapInteractionFailure(error, selection, "property", "Edit animated property");
502
517
  throw error;
503
518
  }
504
519
  },
505
- [commitAnimatedPropertiesRaw, trackGsapInteractionFailure],
520
+ [
521
+ commitAnimatedPropertiesRaw,
522
+ commitPositionPatchToHtml,
523
+ gsapCommitMutation,
524
+ trackGsapInteractionFailure,
525
+ ],
506
526
  );
507
527
 
508
528
  const commitAnimatedProperty = useCallback(
509
- async (selection: DomEditSelection, property: string, value: number | string) => {
510
- try {
511
- await commitAnimatedPropertyRaw(selection, property, value);
512
- } catch (error) {
513
- trackGsapInteractionFailure(error, selection, "property", "Edit animated property");
514
- throw error;
515
- }
516
- },
517
- [commitAnimatedPropertyRaw, trackGsapInteractionFailure],
529
+ (selection: DomEditSelection, property: string, value: number | string) =>
530
+ commitAnimatedProperties(selection, { [property]: value }),
531
+ [commitAnimatedProperties],
518
532
  );
519
533
 
520
534
  // ── Arc path wrappers ──
@@ -16,9 +16,10 @@ const dotTween = {
16
16
  } as GsapAnimation;
17
17
  vi.mock("./keyframeCacheAstLoad", async (importOriginal) => ({
18
18
  ...(await importOriginal<typeof import("./keyframeCacheAstLoad")>()),
19
- fetchParsedAnimations: async () => ({ animations: [dotTween] }),
19
+ fetchParsedAnimations: vi.fn(async () => ({ animations: [dotTween] })),
20
20
  }));
21
21
  const { useGsapAnimationsForElement } = await import("./useGsapTweenCache");
22
+ const { fetchParsedAnimations } = await import("./keyframeCacheAstLoad");
22
23
 
23
24
  it("attributes a class tween by the selected element, not an earlier same-id copy in a sub-composition", async () => {
24
25
  document.body.innerHTML =
@@ -37,3 +38,24 @@ it("attributes a class tween by the selected element, not an earlier same-id cop
37
38
  expect(animations.map((animation) => animation.id)).toEqual(["dots"]);
38
39
  act(() => root.unmount());
39
40
  });
41
+
42
+ it("reads a file with no tweens once: the parse endpoint answers from the file on disk", async () => {
43
+ vi.useFakeTimers();
44
+ vi.mocked(fetchParsedAnimations)
45
+ .mockClear()
46
+ .mockResolvedValue({ animations: [] } as never);
47
+ const selection = { id: "card", sourceFile: "index.html" } as DomEditSelection;
48
+ function Harness() {
49
+ useGsapAnimationsForElement("p", "index.html", selection, 0);
50
+ return null;
51
+ }
52
+ const root = mountReactHarness(<Harness />);
53
+ try {
54
+ await act(async () => {});
55
+ await act(async () => vi.advanceTimersByTime(2000));
56
+ expect(fetchParsedAnimations).toHaveBeenCalledTimes(1);
57
+ } finally {
58
+ act(() => root.unmount());
59
+ vi.useRealTimers();
60
+ }
61
+ });
@@ -41,7 +41,6 @@ export function useGsapAnimationsForElement(
41
41
  const [multipleTimelines, setMultipleTimelines] = useState(false);
42
42
  const [unsupportedTimelinePattern, setUnsupportedTimelinePattern] = useState(false);
43
43
  const lastFetchKeyRef = useRef("");
44
- const retryTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
45
44
  // Re-run the per-element cache populate when sub-comp DOM children appear, so a
46
45
  // sub-comp element gets its host-relative keyframe percentages (not elDuration=1).
47
46
  const domClipChildrenKey = usePlayerStore((s) =>
@@ -54,11 +53,6 @@ export function useGsapAnimationsForElement(
54
53
  if (fetchKey === lastFetchKeyRef.current) return;
55
54
  lastFetchKeyRef.current = fetchKey;
56
55
 
57
- if (retryTimerRef.current) {
58
- clearTimeout(retryTimerRef.current);
59
- retryTimerRef.current = null;
60
- }
61
-
62
56
  if (!projectId) {
63
57
  setAllAnimations([]);
64
58
  setMultipleTimelines(false);
@@ -80,28 +74,10 @@ export function useGsapAnimationsForElement(
80
74
  setAllAnimations(parsed.animations);
81
75
  setMultipleTimelines(parsed.multipleTimelines === true);
82
76
  setUnsupportedTimelinePattern(parsed.unsupportedTimelinePattern === true);
83
-
84
- // Retry once if initial fetch returned 0 animations — handles
85
- // cold-load race where the sourceFile isn't resolved yet.
86
- if (parsed.animations.length === 0 && targetKey) {
87
- retryTimerRef.current = setTimeout(() => {
88
- if (cancelled) return;
89
- fetchParsedAnimations(projectId, sourceFile).then((retryParsed) => {
90
- if (cancelled) return;
91
- if (retryParsed && retryParsed.animations.length > 0) {
92
- setAllAnimations(retryParsed.animations);
93
- }
94
- });
95
- }, 800);
96
- }
97
77
  });
98
78
 
99
79
  return () => {
100
80
  cancelled = true;
101
- if (retryTimerRef.current) {
102
- clearTimeout(retryTimerRef.current);
103
- retryTimerRef.current = null;
104
- }
105
81
  };
106
82
  }, [projectId, sourceFile, version, target?.id, target?.selector]);
107
83