@hyperframes/studio 0.6.97 → 0.6.98

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 (120) hide show
  1. package/dist/assets/hyperframes-player-DgsMQSvV.js +418 -0
  2. package/dist/assets/index-B62bDCQv.css +1 -0
  3. package/dist/assets/index-Ce3pBm_I.js +252 -0
  4. package/dist/assets/{index-HveJ0MuV.js → index-D-ET9M0b.js} +1 -1
  5. package/dist/assets/index-D-bS9Dxx.js +1 -0
  6. package/dist/index.html +2 -2
  7. package/package.json +7 -5
  8. package/src/App.tsx +182 -177
  9. package/src/captions/store.ts +11 -11
  10. package/src/components/StudioHeader.tsx +4 -4
  11. package/src/components/StudioLeftSidebar.tsx +2 -2
  12. package/src/components/StudioPreviewArea.tsx +225 -183
  13. package/src/components/StudioRightPanel.tsx +3 -3
  14. package/src/components/TimelineToolbar.tsx +25 -0
  15. package/src/components/editor/DomEditOverlay.tsx +2 -5
  16. package/src/components/editor/EaseCurveSection.tsx +2 -3
  17. package/src/components/editor/GestureTrailOverlay.tsx +4 -3
  18. package/src/components/editor/LayersPanel.tsx +3 -9
  19. package/src/components/editor/PropertyPanel.tsx +20 -61
  20. package/src/components/editor/colorValue.ts +3 -1
  21. package/src/components/editor/domEditOverlayGestures.ts +54 -1
  22. package/src/components/editor/domEditOverlayStartGesture.ts +5 -2
  23. package/src/components/editor/gradientValue.ts +3 -3
  24. package/src/components/editor/keyframeMove.test.ts +101 -0
  25. package/src/components/editor/keyframeMove.ts +151 -0
  26. package/src/components/editor/manualEditsDom.ts +0 -12
  27. package/src/components/editor/propertyPanelHelpers.ts +10 -38
  28. package/src/components/editor/propertyPanelMediaSection.tsx +1 -5
  29. package/src/components/editor/propertyPanelTimingSection.tsx +1 -6
  30. package/src/components/editor/propertyPanelTransformCommit.ts +129 -0
  31. package/src/components/editor/studioMotionOps.test.ts +1 -1
  32. package/src/components/editor/studioMotionOps.ts +2 -1
  33. package/src/components/editor/useDomEditOverlayGestures.ts +1 -46
  34. package/src/components/nle/NLELayout.tsx +1 -24
  35. package/src/components/sidebar/BlocksTab.tsx +2 -2
  36. package/src/contexts/DomEditContext.tsx +134 -31
  37. package/src/contexts/StudioContext.tsx +90 -40
  38. package/src/contexts/TimelineEditContext.tsx +47 -0
  39. package/src/hooks/domEditCommitTypes.ts +14 -0
  40. package/src/hooks/gsapDragCommit.ts +9 -24
  41. package/src/hooks/gsapKeyframeCacheHelpers.ts +2 -1
  42. package/src/hooks/gsapKeyframeCommit.ts +5 -15
  43. package/src/hooks/gsapRuntimeBridge.ts +18 -52
  44. package/src/hooks/gsapRuntimeKeyframes.ts +8 -57
  45. package/src/hooks/gsapRuntimeReaders.ts +19 -26
  46. package/src/hooks/gsapScriptCommitHelpers.ts +1 -11
  47. package/src/hooks/gsapScriptCommitTypes.ts +58 -0
  48. package/src/hooks/gsapShared.ts +157 -0
  49. package/src/hooks/timelineEditingHelpers.ts +63 -2
  50. package/src/hooks/useAnimatedPropertyCommit.ts +3 -25
  51. package/src/hooks/useAppHotkeys.ts +299 -377
  52. package/src/hooks/useConsoleErrorCapture.ts +33 -5
  53. package/src/hooks/useDomEditCommits.ts +35 -293
  54. package/src/hooks/useDomEditPositionPatchCommit.ts +1 -1
  55. package/src/hooks/useDomEditSession.ts +78 -249
  56. package/src/hooks/useDomEditTextCommits.ts +1 -1
  57. package/src/hooks/useDomEditWiring.ts +255 -0
  58. package/src/hooks/useDomGeometryCommits.ts +181 -0
  59. package/src/hooks/useDomSelection.ts +10 -27
  60. package/src/hooks/useEditorSave.ts +82 -0
  61. package/src/hooks/useElementLifecycleOps.ts +177 -0
  62. package/src/hooks/useEnableKeyframes.ts +10 -15
  63. package/src/hooks/useFileManager.ts +32 -114
  64. package/src/hooks/useFileTree.ts +80 -0
  65. package/src/hooks/useGestureCommit.ts +7 -5
  66. package/src/hooks/useGestureRecording.ts +1 -1
  67. package/src/hooks/useGsapAnimationOps.ts +122 -0
  68. package/src/hooks/useGsapArcPathOps.ts +61 -0
  69. package/src/hooks/useGsapAwareEditing.ts +242 -0
  70. package/src/hooks/useGsapKeyframeOps.ts +167 -0
  71. package/src/hooks/useGsapPropertyDebounce.ts +135 -0
  72. package/src/hooks/useGsapScriptCommits.ts +58 -570
  73. package/src/hooks/useGsapSelectionHandlers.ts +22 -9
  74. package/src/hooks/useGsapTweenCache.ts +35 -29
  75. package/src/hooks/useLintModal.ts +7 -0
  76. package/src/hooks/useMusicBeatAnalysis.ts +152 -0
  77. package/src/hooks/useRazorSplit.ts +1 -1
  78. package/src/hooks/useRenderClipContent.ts +46 -21
  79. package/src/hooks/useTimelineEditing.ts +48 -4
  80. package/src/player/components/AudioWaveform.tsx +29 -4
  81. package/src/player/components/BeatStrip.tsx +166 -0
  82. package/src/player/components/Timeline.tsx +39 -18
  83. package/src/player/components/TimelineCanvas.tsx +52 -12
  84. package/src/player/components/TimelineClipDiamonds.tsx +130 -20
  85. package/src/player/components/TimelinePropertyRows.tsx +8 -2
  86. package/src/player/components/TimelineRuler.tsx +36 -2
  87. package/src/player/components/timelineEditing.ts +30 -5
  88. package/src/player/components/useTimelineClipDrag.ts +155 -4
  89. package/src/player/components/useTimelinePlayhead.ts +30 -1
  90. package/src/player/hooks/useTimelinePlayer.ts +47 -45
  91. package/src/player/lib/mediaProbe.ts +46 -3
  92. package/src/player/lib/playbackScrub.ts +16 -0
  93. package/src/player/lib/timelineDOM.ts +10 -2
  94. package/src/player/lib/timelineIframeHelpers.ts +89 -0
  95. package/src/player/store/playerStore.ts +92 -33
  96. package/src/utils/beatEditActions.ts +109 -0
  97. package/src/utils/beatEditing.ts +136 -0
  98. package/src/utils/clipboardPayload.ts +3 -2
  99. package/src/utils/compositionPatterns.ts +2 -0
  100. package/src/utils/keyframeSelection.test.ts +45 -0
  101. package/src/utils/keyframeSelection.ts +29 -0
  102. package/src/utils/rounding.ts +9 -0
  103. package/src/utils/studioHelpers.ts +5 -2
  104. package/src/utils/studioUrlState.ts +2 -1
  105. package/src/utils/timelineAssetDrop.ts +6 -5
  106. package/src/utils/timelineInspector.ts +15 -100
  107. package/dist/assets/hyperframes-player-Daj5djxa.js +0 -418
  108. package/dist/assets/index-B0twsRu0.css +0 -1
  109. package/dist/assets/index-Cfye9xzo.js +0 -251
  110. package/src/components/editor/DopesheetStrip.tsx +0 -141
  111. package/src/components/editor/StaggerControls.tsx +0 -61
  112. package/src/components/editor/TimelineLayerPanel.test.ts +0 -42
  113. package/src/components/editor/TimelineLayerPanel.tsx +0 -15
  114. package/src/components/nle/TimelineEditorNotice.tsx +0 -133
  115. package/src/hooks/gsapRuntimePreview.ts +0 -19
  116. package/src/player/components/timelineUtils.ts +0 -211
  117. package/src/utils/audioBeatDetection.ts +0 -58
  118. package/src/utils/keyframeSnapping.test.ts +0 -74
  119. package/src/utils/keyframeSnapping.ts +0 -63
  120. package/src/utils/timelineInspector.test.ts +0 -79
@@ -0,0 +1,129 @@
1
+ import type { DomEditSelection } from "./domEditingTypes";
2
+ import { readStudioBoxSize, readStudioPathOffset } from "./manualEdits";
3
+ import { parsePxMetricValue, type PropertyPanelProps } from "./propertyPanelHelpers";
4
+
5
+ interface TransformCommitDeps {
6
+ element: DomEditSelection;
7
+ styles: Record<string, string>;
8
+ hasGsapAnimation: boolean;
9
+ gsapAnimId: string | null;
10
+ gsapKeyframes: unknown;
11
+ currentPct: number;
12
+ onCommitAnimatedProperty: PropertyPanelProps["onCommitAnimatedProperty"];
13
+ onAddKeyframe: PropertyPanelProps["onAddKeyframe"];
14
+ onSetManualOffset: PropertyPanelProps["onSetManualOffset"];
15
+ onSetManualSize: PropertyPanelProps["onSetManualSize"];
16
+ onSetManualRotation: PropertyPanelProps["onSetManualRotation"];
17
+ showToast?: (message: string, tone?: "error" | "info") => void;
18
+ }
19
+
20
+ /**
21
+ * Build the X/Y, W/H and rotation field commit handlers for the property panel.
22
+ * Each handler routes the value into the GSAP animation when the element is
23
+ * animated (matching the drag gesture and keyframe buttons), and otherwise
24
+ * falls through to the manual transform setter.
25
+ */
26
+ // fallow-ignore-next-line unit-size
27
+ export function createTransformCommitHandlers({
28
+ element,
29
+ styles,
30
+ hasGsapAnimation,
31
+ gsapAnimId,
32
+ gsapKeyframes,
33
+ currentPct,
34
+ onCommitAnimatedProperty,
35
+ onAddKeyframe,
36
+ onSetManualOffset,
37
+ onSetManualSize,
38
+ onSetManualRotation,
39
+ showToast,
40
+ }: TransformCommitDeps) {
41
+ // Route a transform value into the GSAP animation (or a new keyframe) when the
42
+ // element is animated. Returns true when handled, so callers fall through to
43
+ // the manual-transform path only for non-animated elements.
44
+ const commitAnimatedTransformValue = (
45
+ property: string,
46
+ value: number,
47
+ noCallbacksMessage: string,
48
+ ): boolean => {
49
+ if (onCommitAnimatedProperty && hasGsapAnimation) {
50
+ void onCommitAnimatedProperty(element, property, value);
51
+ return true;
52
+ }
53
+ if (gsapKeyframes && gsapAnimId && onAddKeyframe) {
54
+ const pct = Math.max(0, Math.min(100, Math.round(currentPct * 10) / 10));
55
+ onAddKeyframe(gsapAnimId, pct, property, value);
56
+ return true;
57
+ }
58
+ if (hasGsapAnimation) {
59
+ showToast?.(noCallbacksMessage);
60
+ return true;
61
+ }
62
+ return false;
63
+ };
64
+
65
+ const commitManualOffset = (axis: "x" | "y", nextValue: string) => {
66
+ const parsed = parsePxMetricValue(nextValue);
67
+ if (parsed == null) return;
68
+ if (
69
+ commitAnimatedTransformValue(
70
+ axis,
71
+ parsed,
72
+ "Cannot edit position — animation callbacks not available",
73
+ )
74
+ )
75
+ return;
76
+ const current = readStudioPathOffset(element.element);
77
+ void Promise.resolve(
78
+ onSetManualOffset(element, {
79
+ x: axis === "x" ? parsed : current.x,
80
+ y: axis === "y" ? parsed : current.y,
81
+ }),
82
+ ).catch(() => undefined);
83
+ };
84
+
85
+ // fallow-ignore-next-line complexity
86
+ const commitManualSize = (axis: "width" | "height", nextValue: string) => {
87
+ const parsed = parsePxMetricValue(nextValue);
88
+ if (parsed == null || parsed <= 0) return;
89
+ if (onCommitAnimatedProperty && hasGsapAnimation) {
90
+ void onCommitAnimatedProperty(element, axis, parsed);
91
+ return;
92
+ }
93
+ if (hasGsapAnimation) {
94
+ showToast?.("Cannot edit size — animation callbacks not available");
95
+ return;
96
+ }
97
+ const current = readStudioBoxSize(element.element);
98
+ const width =
99
+ current.width > 0
100
+ ? current.width
101
+ : (parsePxMetricValue(styles.width ?? "") ?? element.boundingBox.width);
102
+ const height =
103
+ current.height > 0
104
+ ? current.height
105
+ : (parsePxMetricValue(styles.height ?? "") ?? element.boundingBox.height);
106
+ void Promise.resolve(
107
+ onSetManualSize(element, {
108
+ width: axis === "width" ? parsed : width,
109
+ height: axis === "height" ? parsed : height,
110
+ }),
111
+ ).catch(() => undefined);
112
+ };
113
+
114
+ const commitManualRotation = (nextValue: string) => {
115
+ const parsed = Number.parseFloat(nextValue);
116
+ if (!Number.isFinite(parsed)) return;
117
+ if (
118
+ commitAnimatedTransformValue(
119
+ "rotation",
120
+ parsed,
121
+ "Cannot edit rotation — animation callbacks not available",
122
+ )
123
+ )
124
+ return;
125
+ void Promise.resolve(onSetManualRotation(element, { angle: parsed })).catch(() => undefined);
126
+ };
127
+
128
+ return { commitManualOffset, commitManualSize, commitManualRotation };
129
+ }
@@ -11,7 +11,7 @@ import {
11
11
  STUDIO_MOTION_ORIGINAL_OPACITY_ATTR,
12
12
  STUDIO_MOTION_ORIGINAL_VISIBILITY_ATTR,
13
13
  } from "./studioMotionTypes";
14
- import { buildMotionPatches, buildClearMotionPatches } from "./manualEditsDom";
14
+ import { buildMotionPatches, buildClearMotionPatches } from "./manualEditsDomPatches";
15
15
  import { applyPatchByTarget, readAttributeByTarget } from "../../utils/sourcePatcher";
16
16
 
17
17
  function createElement(markup: string): HTMLElement {
@@ -18,6 +18,7 @@ import {
18
18
  type StudioMotionManifest,
19
19
  type StudioMotionTarget,
20
20
  } from "./studioMotionTypes";
21
+ import { roundTo3 } from "../../utils/rounding";
21
22
 
22
23
  // ── Private helpers ──
23
24
 
@@ -34,7 +35,7 @@ function sanitizeEase(value: string): string {
34
35
  }
35
36
 
36
37
  function roundEaseNumber(value: number): number {
37
- return Math.round(value * 1000) / 1000;
38
+ return roundTo3(value);
38
39
  }
39
40
 
40
41
  function clampRange(value: number, min: number, max: number, fallback: number): number {
@@ -33,15 +33,14 @@ import {
33
33
  } from "./domEditOverlayGeometry";
34
34
  import {
35
35
  BLOCKED_MOVE_THRESHOLD_PX,
36
- type BlockedMoveState,
37
36
  type GestureKind,
38
37
  type GestureState,
39
38
  type GroupGestureState,
39
+ type UseDomEditOverlayGesturesOptions,
40
40
  hasDomEditRotationChanged,
41
41
  resolveDomEditResizeGesture,
42
42
  resolveDomEditRotationGesture,
43
43
  } from "./domEditOverlayGestures";
44
- import type { DomEditGroupPathOffsetCommit } from "./DomEditOverlay";
45
44
  import {
46
45
  startGesture as _startGesture,
47
46
  startGroupDrag as _startGroupDrag,
@@ -52,50 +51,6 @@ import {
52
51
  resolveEquidistanceGuides,
53
52
  SNAP_THRESHOLD_PX,
54
53
  } from "./snapEngine";
55
- import type { SnapGuidesState } from "./SnapGuideOverlay";
56
-
57
- // Refs are stable across renders; values are read via .current.
58
- export type UseDomEditOverlayGesturesOptions = {
59
- overlayRef: RefObject<HTMLDivElement | null>;
60
- iframeRef: RefObject<HTMLIFrameElement | null>;
61
- boxRef: RefObject<HTMLDivElement | null>;
62
- selectionRef: RefObject<DomEditSelection | null>;
63
- overlayRectRef: RefObject<OverlayRect | null>;
64
- groupOverlayItemsRef: RefObject<GroupOverlayItem[]>;
65
- gestureRef: RefObject<GestureState | null>;
66
- groupGestureRef: RefObject<GroupGestureState | null>;
67
- blockedMoveRef: RefObject<BlockedMoveState | null>;
68
- rafPausedRef: RefObject<boolean>;
69
- suppressNextBoxClickRef: RefObject<boolean>;
70
- setOverlayRect: (next: OverlayRect | null) => void;
71
- setGroupOverlayItems: (next: GroupOverlayItem[]) => void;
72
- onBlockedMoveRef: RefObject<(selection: DomEditSelection) => void>;
73
- onManualDragStartRef: RefObject<(() => void) | undefined>;
74
- onPathOffsetCommitRef: RefObject<
75
- (s: DomEditSelection, n: { x: number; y: number }) => Promise<void> | void
76
- >;
77
- onGroupPathOffsetCommitRef: RefObject<
78
- (updates: DomEditGroupPathOffsetCommit[]) => Promise<void> | void
79
- >;
80
- onBoxSizeCommitRef: RefObject<
81
- (s: DomEditSelection, n: { width: number; height: number }) => Promise<void> | void
82
- >;
83
- onRotationCommitRef: RefObject<
84
- (s: DomEditSelection, n: { angle: number }) => Promise<void> | void
85
- >;
86
- onCanvasPointerMoveRef: RefObject<
87
- (
88
- e: React.PointerEvent<HTMLDivElement>,
89
- o?: { preferClipAncestor?: boolean },
90
- ) => Promise<DomEditSelection | null>
91
- >;
92
- onCanvasMouseDown: (
93
- e: React.MouseEvent<HTMLDivElement>,
94
- o?: { preferClipAncestor?: boolean },
95
- ) => void;
96
- snapGuidesRef: RefObject<SnapGuidesState | null>;
97
- };
98
-
99
54
  export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGesturesOptions) {
100
55
  const setDraftOverlayRect = (next: OverlayRect) => {
101
56
  opts.setOverlayRect(next);
@@ -10,7 +10,6 @@ import {
10
10
  import { useMountEffect } from "../../hooks/useMountEffect";
11
11
  import { useTimelinePlayer, PlayerControls, Timeline, usePlayerStore } from "../../player";
12
12
  import type { TimelineElement } from "../../player";
13
- import type { TimelineEditCallbacks } from "../../player/components/timelineCallbacks";
14
13
  import { NLEPreview } from "./NLEPreview";
15
14
  import { CompositionBreadcrumb } from "./CompositionBreadcrumb";
16
15
  import { usePreviewBlockDrop } from "./usePreviewBlockDrop";
@@ -20,7 +19,7 @@ import {
20
19
  getTimelineToggleTitle,
21
20
  } from "../../utils/timelineDiscovery";
22
21
 
23
- interface NLELayoutProps extends TimelineEditCallbacks {
22
+ interface NLELayoutProps {
24
23
  projectId: string;
25
24
  portrait?: boolean;
26
25
  /** Slot for overlays rendered on top of the preview (cursors, highlights, etc.) */
@@ -104,18 +103,7 @@ export const NLELayout = memo(function NLELayout({
104
103
  onAssetDrop,
105
104
  onBlockDrop,
106
105
  onPreviewBlockDrop,
107
- onMoveElement,
108
- onResizeElement,
109
- onBlockedEditAttempt,
110
- onSplitElement,
111
- onRazorSplit,
112
- onRazorSplitAll,
113
106
  onSelectTimelineElement,
114
- onDeleteKeyframe,
115
- onDeleteAllKeyframes,
116
- onChangeKeyframeEase,
117
- onMoveKeyframe,
118
- onToggleKeyframeAtPlayhead,
119
107
  onCompIdToSrcChange,
120
108
  timelineVisible,
121
109
  onToggleTimeline,
@@ -444,18 +432,7 @@ export const NLELayout = memo(function NLELayout({
444
432
  onDeleteElement={onDeleteElement}
445
433
  onAssetDrop={onAssetDrop}
446
434
  onBlockDrop={onBlockDrop}
447
- onMoveElement={onMoveElement}
448
- onResizeElement={onResizeElement}
449
- onBlockedEditAttempt={onBlockedEditAttempt}
450
- onSplitElement={onSplitElement}
451
- onRazorSplit={onRazorSplit}
452
- onRazorSplitAll={onRazorSplitAll}
453
435
  onSelectElement={onSelectTimelineElement}
454
- onDeleteKeyframe={onDeleteKeyframe}
455
- onDeleteAllKeyframes={onDeleteAllKeyframes}
456
- onChangeKeyframeEase={onChangeKeyframeEase}
457
- onMoveKeyframe={onMoveKeyframe}
458
- onToggleKeyframeAtPlayhead={onToggleKeyframeAtPlayhead}
459
436
  />
460
437
  </div>
461
438
  {timelineFooter && <div className="flex-shrink-0">{timelineFooter}</div>}
@@ -8,7 +8,7 @@ import {
8
8
  } from "../../utils/blockCategories";
9
9
  import { usePlayerStore } from "../../player";
10
10
  import { formatTime } from "../../player/lib/time";
11
- import { useStudioContext } from "../../contexts/StudioContext";
11
+ import { useStudioShellContext } from "../../contexts/StudioContext";
12
12
  export interface BlockPreviewInfo {
13
13
  videoUrl?: string;
14
14
  posterUrl?: string;
@@ -345,7 +345,7 @@ function BlockCard({
345
345
  [onAdd, adding],
346
346
  );
347
347
 
348
- const { activeCompPath, compositionDimensions } = useStudioContext();
348
+ const { activeCompPath, compositionDimensions } = useStudioShellContext();
349
349
 
350
350
  const handleShowPrompt = useCallback(
351
351
  (e: React.MouseEvent) => {
@@ -1,17 +1,101 @@
1
1
  // fallow-ignore-file code-duplication
2
- import { createContext, useContext, useMemo, type ReactNode } from "react";
2
+ import { createContext, useCallback, useContext, useMemo, useRef, type ReactNode } from "react";
3
3
  import type { useDomEditSession } from "../hooks/useDomEditSession";
4
4
 
5
5
  type DomEditValue = ReturnType<typeof useDomEditSession>;
6
6
 
7
- const DomEditContext = createContext<DomEditValue | null>(null);
7
+ export interface DomEditActionsValue extends Pick<
8
+ DomEditValue,
9
+ | "handleTimelineElementSelect"
10
+ | "handlePreviewCanvasMouseDown"
11
+ | "handlePreviewCanvasPointerMove"
12
+ | "handlePreviewCanvasPointerLeave"
13
+ | "applyDomSelection"
14
+ | "clearDomSelection"
15
+ | "handleDomStyleCommit"
16
+ | "handleDomAttributeCommit"
17
+ | "handleDomHtmlAttributeCommit"
18
+ | "handleDomPathOffsetCommit"
19
+ | "handleDomGroupPathOffsetCommit"
20
+ | "handleDomZIndexReorderCommit"
21
+ | "handleDomBoxSizeCommit"
22
+ | "handleDomRotationCommit"
23
+ | "handleDomManualEditsReset"
24
+ | "handleDomTextCommit"
25
+ | "handleDomTextFieldStyleCommit"
26
+ | "handleDomAddTextField"
27
+ | "handleDomRemoveTextField"
28
+ | "handleAskAgent"
29
+ | "handleAgentModalSubmit"
30
+ | "handleBlockedDomMove"
31
+ | "handleDomManualDragStart"
32
+ | "handleDomEditElementDelete"
33
+ | "buildDomSelectionFromTarget"
34
+ | "buildDomSelectionForTimelineElement"
35
+ | "updateDomEditHoverSelection"
36
+ | "resolveImportedFontAsset"
37
+ | "setAgentModalOpen"
38
+ | "setAgentPromptSelectionContext"
39
+ | "setAgentModalAnchorPoint"
40
+ | "handleGsapUpdateProperty"
41
+ | "handleGsapUpdateMeta"
42
+ | "handleGsapDeleteAnimation"
43
+ | "handleGsapDeleteAllForElement"
44
+ | "handleGsapAddAnimation"
45
+ | "handleGsapAddProperty"
46
+ | "handleGsapRemoveProperty"
47
+ | "handleGsapUpdateFromProperty"
48
+ | "handleGsapAddFromProperty"
49
+ | "handleGsapRemoveFromProperty"
50
+ | "handleGsapAddKeyframe"
51
+ | "handleGsapAddKeyframeBatch"
52
+ | "handleGsapRemoveKeyframe"
53
+ | "handleGsapConvertToKeyframes"
54
+ | "handleGsapRemoveAllKeyframes"
55
+ | "handleResetSelectedElementKeyframes"
56
+ | "commitAnimatedProperty"
57
+ | "handleSetArcPath"
58
+ | "handleUpdateArcSegment"
59
+ | "invalidateGsapCache"
60
+ | "previewIframeRef"
61
+ | "commitMutation"
62
+ > {}
8
63
 
9
- export function useDomEditContext(): DomEditValue {
10
- const ctx = useContext(DomEditContext);
11
- if (!ctx) throw new Error("useDomEditContext must be used within DomEditProvider");
64
+ export interface DomEditSelectionValue extends Pick<
65
+ DomEditValue,
66
+ | "domEditSelection"
67
+ | "domEditGroupSelections"
68
+ | "domEditHoverSelection"
69
+ | "domEditSelectionRef"
70
+ | "selectedGsapAnimations"
71
+ | "gsapMultipleTimelines"
72
+ | "gsapUnsupportedTimelinePattern"
73
+ | "agentModalOpen"
74
+ | "agentModalAnchorPoint"
75
+ | "copiedAgentPrompt"
76
+ | "agentPromptSelectionContext"
77
+ > {}
78
+
79
+ const DomEditActionsContext = createContext<DomEditActionsValue | null>(null);
80
+ const DomEditSelectionContext = createContext<DomEditSelectionValue | null>(null);
81
+
82
+ export function useDomEditActionsContext(): DomEditActionsValue {
83
+ const ctx = useContext(DomEditActionsContext);
84
+ if (!ctx) throw new Error("useDomEditActionsContext must be used within DomEditProvider");
85
+ return ctx;
86
+ }
87
+
88
+ export function useDomEditSelectionContext(): DomEditSelectionValue {
89
+ const ctx = useContext(DomEditSelectionContext);
90
+ if (!ctx) throw new Error("useDomEditSelectionContext must be used within DomEditProvider");
12
91
  return ctx;
13
92
  }
14
93
 
94
+ /** @deprecated Prefer useDomEditActionsContext or useDomEditSelectionContext. */
95
+ export function useDomEditContext(): DomEditValue {
96
+ return { ...useDomEditActionsContext(), ...useDomEditSelectionContext() };
97
+ }
98
+
15
99
  export function DomEditProvider({
16
100
  value: {
17
101
  domEditSelection,
@@ -85,16 +169,16 @@ export function DomEditProvider({
85
169
  value: DomEditValue;
86
170
  children: ReactNode;
87
171
  }) {
88
- const stable = useMemo<DomEditValue>(
172
+ const commitMutationRef = useRef(commitMutation);
173
+ commitMutationRef.current = commitMutation;
174
+
175
+ const stableCommitMutation = useCallback<DomEditActionsValue["commitMutation"]>(
176
+ (mutation, options) => commitMutationRef.current(mutation, options),
177
+ [],
178
+ );
179
+
180
+ const actions = useMemo<DomEditActionsValue>(
89
181
  () => ({
90
- domEditSelection,
91
- domEditGroupSelections,
92
- domEditHoverSelection,
93
- agentModalOpen,
94
- agentModalAnchorPoint,
95
- copiedAgentPrompt,
96
- agentPromptSelectionContext,
97
- domEditSelectionRef,
98
182
  handleTimelineElementSelect,
99
183
  handlePreviewCanvasMouseDown,
100
184
  handlePreviewCanvasPointerMove,
@@ -126,9 +210,6 @@ export function DomEditProvider({
126
210
  setAgentModalOpen,
127
211
  setAgentPromptSelectionContext,
128
212
  setAgentModalAnchorPoint,
129
- selectedGsapAnimations,
130
- gsapMultipleTimelines,
131
- gsapUnsupportedTimelinePattern,
132
213
  handleGsapUpdateProperty,
133
214
  handleGsapUpdateMeta,
134
215
  handleGsapDeleteAnimation,
@@ -150,17 +231,9 @@ export function DomEditProvider({
150
231
  handleUpdateArcSegment,
151
232
  invalidateGsapCache,
152
233
  previewIframeRef,
153
- commitMutation,
234
+ commitMutation: stableCommitMutation,
154
235
  }),
155
236
  [
156
- domEditSelection,
157
- domEditGroupSelections,
158
- domEditHoverSelection,
159
- agentModalOpen,
160
- agentModalAnchorPoint,
161
- copiedAgentPrompt,
162
- agentPromptSelectionContext,
163
- domEditSelectionRef,
164
237
  handleTimelineElementSelect,
165
238
  handlePreviewCanvasMouseDown,
166
239
  handlePreviewCanvasPointerMove,
@@ -192,9 +265,6 @@ export function DomEditProvider({
192
265
  setAgentModalOpen,
193
266
  setAgentPromptSelectionContext,
194
267
  setAgentModalAnchorPoint,
195
- selectedGsapAnimations,
196
- gsapMultipleTimelines,
197
- gsapUnsupportedTimelinePattern,
198
268
  handleGsapUpdateProperty,
199
269
  handleGsapUpdateMeta,
200
270
  handleGsapDeleteAnimation,
@@ -216,8 +286,41 @@ export function DomEditProvider({
216
286
  handleUpdateArcSegment,
217
287
  invalidateGsapCache,
218
288
  previewIframeRef,
219
- commitMutation,
289
+ stableCommitMutation,
220
290
  ],
221
291
  );
222
- return <DomEditContext value={stable}>{children}</DomEditContext>;
292
+
293
+ const selection = useMemo<DomEditSelectionValue>(
294
+ () => ({
295
+ domEditSelection,
296
+ domEditGroupSelections,
297
+ domEditHoverSelection,
298
+ domEditSelectionRef,
299
+ selectedGsapAnimations,
300
+ gsapMultipleTimelines,
301
+ gsapUnsupportedTimelinePattern,
302
+ agentModalOpen,
303
+ agentModalAnchorPoint,
304
+ copiedAgentPrompt,
305
+ agentPromptSelectionContext,
306
+ }),
307
+ [
308
+ domEditSelection,
309
+ domEditGroupSelections,
310
+ domEditHoverSelection,
311
+ domEditSelectionRef,
312
+ selectedGsapAnimations,
313
+ gsapMultipleTimelines,
314
+ gsapUnsupportedTimelinePattern,
315
+ agentModalOpen,
316
+ agentModalAnchorPoint,
317
+ copiedAgentPrompt,
318
+ agentPromptSelectionContext,
319
+ ],
320
+ );
321
+ return (
322
+ <DomEditActionsContext value={actions}>
323
+ <DomEditSelectionContext value={selection}>{children}</DomEditSelectionContext>
324
+ </DomEditActionsContext>
325
+ );
223
326
  }