@hyperframes/studio 0.6.0 → 0.6.2

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 (58) hide show
  1. package/dist/assets/hyperframes-player-CzwFysqv.js +418 -0
  2. package/dist/assets/index-hYc4aP7M.js +117 -0
  3. package/dist/index.html +1 -1
  4. package/package.json +4 -4
  5. package/src/App.tsx +2 -13
  6. package/src/captions/components/CaptionOverlay.tsx +13 -246
  7. package/src/captions/components/CaptionOverlayUtils.ts +221 -0
  8. package/src/components/StudioPreviewArea.tsx +6 -2
  9. package/src/components/editor/DomEditOverlay.tsx +88 -1007
  10. package/src/components/editor/EaseCurveEditor.tsx +221 -0
  11. package/src/components/editor/FileTree.tsx +13 -621
  12. package/src/components/editor/FileTreeIcons.tsx +128 -0
  13. package/src/components/editor/FileTreeNodes.tsx +496 -0
  14. package/src/components/editor/MotionPanel.tsx +16 -390
  15. package/src/components/editor/MotionPanelFields.tsx +185 -0
  16. package/src/components/editor/domEditOverlayGeometry.ts +211 -0
  17. package/src/components/editor/domEditOverlayGestures.ts +138 -0
  18. package/src/components/editor/domEditOverlayStartGesture.ts +155 -0
  19. package/src/components/editor/domEditing.ts +44 -1150
  20. package/src/components/editor/domEditingAgentPrompt.ts +97 -0
  21. package/src/components/editor/domEditingDom.ts +266 -0
  22. package/src/components/editor/domEditingElement.ts +329 -0
  23. package/src/components/editor/domEditingLayers.ts +460 -0
  24. package/src/components/editor/domEditingTypes.ts +125 -0
  25. package/src/components/editor/manualEdits.ts +84 -1081
  26. package/src/components/editor/manualEditsDom.ts +436 -0
  27. package/src/components/editor/manualEditsParsing.ts +280 -0
  28. package/src/components/editor/manualEditsSnapshot.ts +333 -0
  29. package/src/components/editor/manualEditsTypes.ts +141 -0
  30. package/src/components/editor/studioMotion.ts +47 -434
  31. package/src/components/editor/studioMotionOps.ts +299 -0
  32. package/src/components/editor/studioMotionTypes.ts +168 -0
  33. package/src/components/editor/useDomEditOverlayGestures.ts +393 -0
  34. package/src/components/editor/useDomEditOverlayRects.ts +207 -0
  35. package/src/components/nle/NLELayout.tsx +60 -144
  36. package/src/components/nle/useCompositionStack.ts +126 -0
  37. package/src/hooks/useToast.ts +20 -0
  38. package/src/player/components/Timeline.tsx +189 -1418
  39. package/src/player/components/TimelineCanvas.tsx +434 -0
  40. package/src/player/components/TimelineEmptyState.tsx +102 -0
  41. package/src/player/components/TimelineRuler.tsx +90 -0
  42. package/src/player/components/timelineIcons.tsx +49 -0
  43. package/src/player/components/timelineLayout.ts +215 -0
  44. package/src/player/components/timelineUtils.ts +211 -0
  45. package/src/player/components/useTimelineClipDrag.ts +388 -0
  46. package/src/player/components/useTimelinePlayhead.ts +200 -0
  47. package/src/player/components/useTimelineRangeSelection.ts +135 -0
  48. package/src/player/hooks/usePlaybackKeyboard.ts +171 -0
  49. package/src/player/hooks/useTimelinePlayer.ts +69 -1372
  50. package/src/player/hooks/useTimelineSyncCallbacks.ts +288 -0
  51. package/src/player/lib/playbackAdapter.ts +145 -0
  52. package/src/player/lib/playbackShortcuts.ts +68 -0
  53. package/src/player/lib/playbackTypes.ts +60 -0
  54. package/src/player/lib/timelineDOM.ts +373 -0
  55. package/src/player/lib/timelineElementHelpers.ts +303 -0
  56. package/src/player/lib/timelineIframeHelpers.ts +269 -0
  57. package/dist/assets/hyperframes-player-DOFETgjy.js +0 -418
  58. package/dist/assets/index-DUqUmaoH.js +0 -117
@@ -0,0 +1,393 @@
1
+ /**
2
+ * Gesture handling for DomEditOverlay.
3
+ * Owns: onPointerMove, onPointerUp, clearPointerState.
4
+ * startGesture and startGroupDrag live in domEditOverlayStartGesture.ts.
5
+ */
6
+ import type { RefObject } from "react";
7
+ import { type DomEditSelection } from "./domEditing";
8
+ import {
9
+ applyManualOffsetDragCommit,
10
+ applyManualOffsetDragDraft,
11
+ endManualOffsetDragMembers,
12
+ restoreManualOffsetDragMembers,
13
+ } from "./manualOffsetDrag";
14
+ import {
15
+ applyStudioBoxSize,
16
+ applyStudioBoxSizeDraft,
17
+ applyStudioRotation,
18
+ applyStudioRotationDraft,
19
+ endStudioManualEditGesture,
20
+ isStudioManualEditGestureCurrent,
21
+ readStudioBoxSize,
22
+ restoreStudioBoxSize,
23
+ restoreStudioPathOffset,
24
+ restoreStudioRotation,
25
+ } from "./manualEdits";
26
+ import {
27
+ type GroupOverlayItem,
28
+ type OverlayRect,
29
+ groupOverlayItemsEqual,
30
+ rectsEqual,
31
+ } from "./domEditOverlayGeometry";
32
+ import {
33
+ BLOCKED_MOVE_THRESHOLD_PX,
34
+ type BlockedMoveState,
35
+ type GestureKind,
36
+ type GestureState,
37
+ type GroupGestureState,
38
+ hasDomEditRotationChanged,
39
+ resolveDomEditResizeGesture,
40
+ resolveDomEditRotationGesture,
41
+ } from "./domEditOverlayGestures";
42
+ import type { DomEditGroupPathOffsetCommit } from "./DomEditOverlay";
43
+ import {
44
+ startGesture as _startGesture,
45
+ startGroupDrag as _startGroupDrag,
46
+ } from "./domEditOverlayStartGesture";
47
+
48
+ // Refs are stable across renders; values are read via .current.
49
+ export type UseDomEditOverlayGesturesOptions = {
50
+ overlayRef: RefObject<HTMLDivElement | null>;
51
+ boxRef: RefObject<HTMLDivElement | null>;
52
+ selectionRef: RefObject<DomEditSelection | null>;
53
+ overlayRectRef: RefObject<OverlayRect | null>;
54
+ groupOverlayItemsRef: RefObject<GroupOverlayItem[]>;
55
+ gestureRef: RefObject<GestureState | null>;
56
+ groupGestureRef: RefObject<GroupGestureState | null>;
57
+ blockedMoveRef: RefObject<BlockedMoveState | null>;
58
+ rafPausedRef: RefObject<boolean>;
59
+ suppressNextBoxClickRef: RefObject<boolean>;
60
+ setOverlayRect: (next: OverlayRect | null) => void;
61
+ setGroupOverlayItems: (next: GroupOverlayItem[]) => void;
62
+ onBlockedMoveRef: RefObject<(selection: DomEditSelection) => void>;
63
+ onManualDragStartRef: RefObject<(() => void) | undefined>;
64
+ onPathOffsetCommitRef: RefObject<
65
+ (s: DomEditSelection, n: { x: number; y: number }) => Promise<void> | void
66
+ >;
67
+ onGroupPathOffsetCommitRef: RefObject<
68
+ (updates: DomEditGroupPathOffsetCommit[]) => Promise<void> | void
69
+ >;
70
+ onBoxSizeCommitRef: RefObject<
71
+ (s: DomEditSelection, n: { width: number; height: number }) => Promise<void> | void
72
+ >;
73
+ onRotationCommitRef: RefObject<
74
+ (s: DomEditSelection, n: { angle: number }) => Promise<void> | void
75
+ >;
76
+ onCanvasPointerMoveRef: RefObject<
77
+ (
78
+ e: React.PointerEvent<HTMLDivElement>,
79
+ o?: { preferClipAncestor?: boolean },
80
+ ) => DomEditSelection | null
81
+ >;
82
+ onCanvasMouseDown: (
83
+ e: React.MouseEvent<HTMLDivElement>,
84
+ o?: { preferClipAncestor?: boolean },
85
+ ) => void;
86
+ };
87
+
88
+ export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGesturesOptions) {
89
+ const setDraftOverlayRect = (next: OverlayRect) => {
90
+ if (rectsEqual(opts.overlayRectRef.current, next)) return;
91
+ opts.overlayRectRef.current = next;
92
+ opts.setOverlayRect(next);
93
+ };
94
+ const restoreGestureOverlayRect = (g: GestureState) => {
95
+ setDraftOverlayRect({
96
+ left: g.originLeft,
97
+ top: g.originTop,
98
+ width: g.originWidth,
99
+ height: g.originHeight,
100
+ editScaleX: g.editScaleX,
101
+ editScaleY: g.editScaleY,
102
+ });
103
+ };
104
+ const setDraftGroupOverlayItems = (next: GroupOverlayItem[]) => {
105
+ if (groupOverlayItemsEqual(opts.groupOverlayItemsRef.current, next)) return;
106
+ opts.groupOverlayItemsRef.current = next;
107
+ opts.setGroupOverlayItems(next);
108
+ };
109
+
110
+ const restoreGroupPathOffsets = (g: GroupGestureState) => {
111
+ restoreManualOffsetDragMembers(g.members);
112
+ setDraftGroupOverlayItems(g.originItems);
113
+ };
114
+
115
+ const startGroupDrag = (e: React.PointerEvent<HTMLElement>) => _startGroupDrag(e, opts);
116
+ const startGesture = (
117
+ kind: GestureKind,
118
+ e: React.PointerEvent<HTMLElement>,
119
+ options?: { selection?: DomEditSelection; rect?: OverlayRect | null },
120
+ ) => _startGesture(kind, e, opts, options);
121
+
122
+ const onPointerMove = (e: React.PointerEvent<HTMLDivElement>) => {
123
+ const g = opts.gestureRef.current;
124
+ const groupG = opts.groupGestureRef.current;
125
+ const sel = g?.selection ?? opts.selectionRef.current;
126
+ const box = opts.boxRef.current;
127
+ const blockedMove = opts.blockedMoveRef.current;
128
+ if (!blockedMove && !g && !groupG) {
129
+ opts.onCanvasPointerMoveRef.current(e, { preferClipAncestor: false });
130
+ }
131
+
132
+ if (blockedMove && sel) {
133
+ const dx = e.clientX - blockedMove.startX;
134
+ const dy = e.clientY - blockedMove.startY;
135
+ if (!blockedMove.notified && Math.hypot(dx, dy) >= BLOCKED_MOVE_THRESHOLD_PX) {
136
+ blockedMove.notified = true;
137
+ opts.suppressNextBoxClickRef.current = true;
138
+ opts.onBlockedMoveRef.current(sel);
139
+ }
140
+ return;
141
+ }
142
+
143
+ if (groupG) {
144
+ const dx = e.clientX - groupG.startX;
145
+ const dy = e.clientY - groupG.startY;
146
+ setDraftGroupOverlayItems(
147
+ groupG.originItems.map((item) => ({
148
+ ...item,
149
+ rect: { ...item.rect, left: item.rect.left + dx, top: item.rect.top + dy },
150
+ })),
151
+ );
152
+ for (const member of groupG.members) applyManualOffsetDragDraft(member, dx, dy);
153
+ return;
154
+ }
155
+
156
+ if (!g || !sel) return;
157
+ const dx = e.clientX - g.startX;
158
+ const dy = e.clientY - g.startY;
159
+
160
+ if (g.kind === "rotate") {
161
+ applyStudioRotationDraft(
162
+ sel.element,
163
+ resolveDomEditRotationGesture({
164
+ centerX: g.centerX,
165
+ centerY: g.centerY,
166
+ startX: g.startX,
167
+ startY: g.startY,
168
+ currentX: e.clientX,
169
+ currentY: e.clientY,
170
+ actualAngle: g.actualRotation,
171
+ snap: e.shiftKey,
172
+ }),
173
+ );
174
+ return;
175
+ }
176
+
177
+ if (g.kind === "drag") {
178
+ const nextBoxLeft = g.originLeft + dx;
179
+ const nextBoxTop = g.originTop + dy;
180
+ setDraftOverlayRect({
181
+ left: nextBoxLeft,
182
+ top: nextBoxTop,
183
+ width: g.originWidth,
184
+ height: g.originHeight,
185
+ editScaleX: g.editScaleX,
186
+ editScaleY: g.editScaleY,
187
+ });
188
+ if (box) {
189
+ box.style.left = `${nextBoxLeft}px`;
190
+ box.style.top = `${nextBoxTop}px`;
191
+ }
192
+ if (g.pathOffsetMember) applyManualOffsetDragDraft(g.pathOffsetMember, dx, dy);
193
+ } else {
194
+ if (!box) return;
195
+ const nextSize = resolveDomEditResizeGesture({
196
+ originWidth: g.originWidth,
197
+ originHeight: g.originHeight,
198
+ actualWidth: g.actualWidth,
199
+ actualHeight: g.actualHeight,
200
+ scaleX: g.editScaleX,
201
+ scaleY: g.editScaleY,
202
+ dx,
203
+ dy,
204
+ uniform: e.shiftKey,
205
+ });
206
+ setDraftOverlayRect({
207
+ left: g.originLeft,
208
+ top: g.originTop,
209
+ width: nextSize.overlayWidth,
210
+ height: nextSize.overlayHeight,
211
+ editScaleX: g.editScaleX,
212
+ editScaleY: g.editScaleY,
213
+ });
214
+ box.style.width = `${nextSize.overlayWidth}px`;
215
+ box.style.height = `${nextSize.overlayHeight}px`;
216
+ applyStudioBoxSizeDraft(sel.element, nextSize);
217
+ }
218
+ };
219
+
220
+ const onPointerUp = (e: React.PointerEvent<HTMLDivElement>) => {
221
+ const g = opts.gestureRef.current;
222
+ const groupG = opts.groupGestureRef.current;
223
+ const sel = g?.selection ?? opts.selectionRef.current;
224
+ const box = opts.boxRef.current;
225
+ opts.blockedMoveRef.current = null;
226
+
227
+ if (groupG) {
228
+ opts.groupGestureRef.current = null;
229
+ opts.rafPausedRef.current = false;
230
+ const dx = e.clientX - groupG.startX;
231
+ const dy = e.clientY - groupG.startY;
232
+ if (Math.hypot(dx, dy) < BLOCKED_MOVE_THRESHOLD_PX) {
233
+ restoreGroupPathOffsets(groupG);
234
+ opts.suppressNextBoxClickRef.current = true;
235
+ return;
236
+ }
237
+ setDraftGroupOverlayItems(
238
+ groupG.originItems.map((item) => ({
239
+ ...item,
240
+ rect: { ...item.rect, left: item.rect.left + dx, top: item.rect.top + dy },
241
+ })),
242
+ );
243
+ const updates = groupG.members.map((member) => ({
244
+ selection: member.selection,
245
+ next: applyManualOffsetDragCommit(member, dx, dy),
246
+ }));
247
+ void Promise.resolve(opts.onGroupPathOffsetCommitRef.current(updates))
248
+ .catch(() => {
249
+ for (const member of groupG.members) {
250
+ if (
251
+ member.gestureToken &&
252
+ isStudioManualEditGestureCurrent(member.element, member.gestureToken)
253
+ )
254
+ restoreStudioPathOffset(member.element, member.initialPathOffset);
255
+ }
256
+ })
257
+ .finally(() => endManualOffsetDragMembers(groupG.members));
258
+ return;
259
+ }
260
+
261
+ if (!g || !sel) {
262
+ opts.gestureRef.current = null;
263
+ opts.rafPausedRef.current = false;
264
+ return;
265
+ }
266
+ opts.gestureRef.current = null;
267
+ opts.rafPausedRef.current = false;
268
+ const movedDistance = Math.hypot(e.clientX - g.startX, e.clientY - g.startY);
269
+
270
+ if (g.kind === "drag" && movedDistance < BLOCKED_MOVE_THRESHOLD_PX) {
271
+ restoreStudioPathOffset(sel.element, g.initialPathOffset);
272
+ endStudioManualEditGesture(sel.element, g.manualEditDragToken);
273
+ if (box) {
274
+ box.style.left = `${g.originLeft}px`;
275
+ box.style.top = `${g.originTop}px`;
276
+ }
277
+ restoreGestureOverlayRect(g);
278
+ opts.suppressNextBoxClickRef.current = true;
279
+ opts.onCanvasMouseDown(e as unknown as React.MouseEvent<HTMLDivElement>, {
280
+ preferClipAncestor: false,
281
+ });
282
+ return;
283
+ }
284
+
285
+ if (g.kind === "resize" && movedDistance < BLOCKED_MOVE_THRESHOLD_PX) {
286
+ restoreStudioBoxSize(sel.element, g.initialBoxSize);
287
+ endStudioManualEditGesture(sel.element, g.manualEditDragToken);
288
+ if (box) {
289
+ box.style.width = `${g.originWidth}px`;
290
+ box.style.height = `${g.originHeight}px`;
291
+ }
292
+ restoreGestureOverlayRect(g);
293
+ return;
294
+ }
295
+
296
+ if (g.kind === "rotate") {
297
+ const finalRotation = resolveDomEditRotationGesture({
298
+ centerX: g.centerX,
299
+ centerY: g.centerY,
300
+ startX: g.startX,
301
+ startY: g.startY,
302
+ currentX: e.clientX,
303
+ currentY: e.clientY,
304
+ actualAngle: g.actualRotation,
305
+ snap: e.shiftKey,
306
+ });
307
+ if (!hasDomEditRotationChanged(g.actualRotation, finalRotation.angle)) {
308
+ restoreStudioRotation(sel.element, g.initialRotation);
309
+ endStudioManualEditGesture(sel.element, g.manualEditDragToken);
310
+ return;
311
+ }
312
+ applyStudioRotation(sel.element, finalRotation);
313
+ void Promise.resolve(opts.onRotationCommitRef.current(sel, finalRotation))
314
+ .catch(() => {
315
+ if (
316
+ g.manualEditDragToken &&
317
+ isStudioManualEditGestureCurrent(sel.element, g.manualEditDragToken)
318
+ )
319
+ restoreStudioRotation(sel.element, g.initialRotation);
320
+ })
321
+ .finally(() => endStudioManualEditGesture(sel.element, g.manualEditDragToken));
322
+ } else if (g.kind === "drag") {
323
+ const dx = e.clientX - g.startX;
324
+ const dy = e.clientY - g.startY;
325
+ if (!g.pathOffsetMember) return;
326
+ const finalOffset = applyManualOffsetDragCommit(g.pathOffsetMember, dx, dy);
327
+ const nextBoxLeft = g.originLeft + dx;
328
+ const nextBoxTop = g.originTop + dy;
329
+ setDraftOverlayRect({
330
+ left: nextBoxLeft,
331
+ top: nextBoxTop,
332
+ width: g.originWidth,
333
+ height: g.originHeight,
334
+ editScaleX: g.editScaleX,
335
+ editScaleY: g.editScaleY,
336
+ });
337
+ if (box) {
338
+ box.style.left = `${nextBoxLeft}px`;
339
+ box.style.top = `${nextBoxTop}px`;
340
+ }
341
+ void Promise.resolve(opts.onPathOffsetCommitRef.current(sel, finalOffset))
342
+ .catch(() => {
343
+ if (
344
+ g.pathOffsetMember?.gestureToken &&
345
+ isStudioManualEditGestureCurrent(sel.element, g.pathOffsetMember.gestureToken)
346
+ )
347
+ restoreStudioPathOffset(sel.element, g.initialPathOffset);
348
+ })
349
+ .finally(() => {
350
+ if (g.pathOffsetMember) endManualOffsetDragMembers([g.pathOffsetMember]);
351
+ });
352
+ } else {
353
+ const finalSize = readStudioBoxSize(sel.element);
354
+ applyStudioBoxSize(sel.element, finalSize);
355
+ void Promise.resolve(opts.onBoxSizeCommitRef.current(sel, finalSize))
356
+ .catch(() => {
357
+ if (
358
+ g.manualEditDragToken &&
359
+ isStudioManualEditGestureCurrent(sel.element, g.manualEditDragToken)
360
+ )
361
+ restoreStudioBoxSize(sel.element, g.initialBoxSize);
362
+ })
363
+ .finally(() => endStudioManualEditGesture(sel.element, g.manualEditDragToken));
364
+ }
365
+ };
366
+
367
+ const clearPointerState = (selectionRef: RefObject<DomEditSelection | null>) => {
368
+ const groupG = opts.groupGestureRef.current;
369
+ if (groupG) restoreGroupPathOffsets(groupG);
370
+ const g = opts.gestureRef.current;
371
+ const sel = g?.selection ?? selectionRef.current;
372
+ if (g?.mode === "path-offset" && sel) {
373
+ restoreStudioPathOffset(sel.element, g.initialPathOffset);
374
+ endStudioManualEditGesture(sel.element, g.manualEditDragToken);
375
+ restoreGestureOverlayRect(g);
376
+ }
377
+ if (g?.mode === "box-size" && sel) {
378
+ restoreStudioBoxSize(sel.element, g.initialBoxSize);
379
+ endStudioManualEditGesture(sel.element, g.manualEditDragToken);
380
+ restoreGestureOverlayRect(g);
381
+ }
382
+ if (g?.mode === "rotation" && sel) {
383
+ restoreStudioRotation(sel.element, g.initialRotation);
384
+ endStudioManualEditGesture(sel.element, g.manualEditDragToken);
385
+ }
386
+ opts.blockedMoveRef.current = null;
387
+ opts.groupGestureRef.current = null;
388
+ opts.gestureRef.current = null;
389
+ opts.rafPausedRef.current = false;
390
+ };
391
+
392
+ return { startGesture, startGroupDrag, onPointerMove, onPointerUp, clearPointerState };
393
+ }
@@ -0,0 +1,207 @@
1
+ /**
2
+ * RAF-driven hook that tracks overlay, hover, and group rects from the iframe DOM.
3
+ * Runs a requestAnimationFrame loop and writes React state only when rects change.
4
+ */
5
+ import { useRef, useState, type RefObject } from "react";
6
+ import { useMountEffect } from "../../hooks/useMountEffect";
7
+ import { type DomEditSelection, findElementForSelection } from "./domEditing";
8
+ import {
9
+ type GroupOverlayItem,
10
+ type OverlayRect,
11
+ type ResolvedElementRef,
12
+ groupOverlayItemsEqual,
13
+ isElementVisibleForOverlay,
14
+ rectsEqual,
15
+ resolveElementForOverlay,
16
+ selectionCacheKey,
17
+ toOverlayRect,
18
+ } from "./domEditOverlayGeometry";
19
+
20
+ interface UseDomEditOverlayRectsOptions {
21
+ iframeRef: RefObject<HTMLIFrameElement | null>;
22
+ overlayRef: RefObject<HTMLDivElement | null>;
23
+ selectionRef: RefObject<DomEditSelection | null>;
24
+ activeCompositionPathRef: RefObject<string | null>;
25
+ groupSelectionsRef: RefObject<DomEditSelection[]>;
26
+ hoverSelectionRef: RefObject<DomEditSelection | null>;
27
+ rafPausedRef: RefObject<boolean>;
28
+ }
29
+
30
+ interface UseDomEditOverlayRectsResult {
31
+ overlayRect: OverlayRect | null;
32
+ overlayRectRef: RefObject<OverlayRect | null>;
33
+ setOverlayRect: (next: OverlayRect | null) => void;
34
+ hoverRect: OverlayRect | null;
35
+ hoverRectRef: RefObject<OverlayRect | null>;
36
+ setHoverRect: (next: OverlayRect | null) => void;
37
+ groupOverlayItems: GroupOverlayItem[];
38
+ groupOverlayItemsRef: RefObject<GroupOverlayItem[]>;
39
+ setGroupOverlayItems: (next: GroupOverlayItem[]) => void;
40
+ }
41
+
42
+ export function useDomEditOverlayRects({
43
+ iframeRef,
44
+ overlayRef,
45
+ selectionRef,
46
+ activeCompositionPathRef,
47
+ groupSelectionsRef,
48
+ hoverSelectionRef,
49
+ rafPausedRef,
50
+ }: UseDomEditOverlayRectsOptions): UseDomEditOverlayRectsResult {
51
+ const [overlayRect, setOverlayRectState] = useState<OverlayRect | null>(null);
52
+ const [hoverRect, setHoverRectState] = useState<OverlayRect | null>(null);
53
+ const [groupOverlayItems, setGroupOverlayItemsState] = useState<GroupOverlayItem[]>([]);
54
+
55
+ const overlayRectRef = useRef<OverlayRect | null>(null);
56
+ const hoverRectRef = useRef<OverlayRect | null>(null);
57
+ const groupOverlayItemsRef = useRef<GroupOverlayItem[]>([]);
58
+ const resolvedElementRef = useRef<{ key: string; element: HTMLElement } | null>(null);
59
+ const resolvedHoverElementRef = useRef<{ key: string; element: HTMLElement } | null>(null);
60
+ const resolvedGroupElementRef = useRef<Map<string, HTMLElement>>(new Map());
61
+
62
+ const setOverlayRect = (next: OverlayRect | null) => {
63
+ if (rectsEqual(overlayRectRef.current, next)) return;
64
+ overlayRectRef.current = next;
65
+ setOverlayRectState(next);
66
+ };
67
+
68
+ const setHoverRect = (next: OverlayRect | null) => {
69
+ if (rectsEqual(hoverRectRef.current, next)) return;
70
+ hoverRectRef.current = next;
71
+ setHoverRectState(next);
72
+ };
73
+
74
+ const setGroupOverlayItems = (next: GroupOverlayItem[]) => {
75
+ if (groupOverlayItemsEqual(groupOverlayItemsRef.current, next)) return;
76
+ groupOverlayItemsRef.current = next;
77
+ setGroupOverlayItemsState(next);
78
+ };
79
+
80
+ const resolveGroupElement = (doc: Document, sel: DomEditSelection) => {
81
+ const key = selectionCacheKey(sel);
82
+ const cached = resolvedGroupElementRef.current.get(key);
83
+ if (cached?.isConnected && cached.ownerDocument === doc) return cached;
84
+
85
+ const next = findElementForSelection(doc, sel, activeCompositionPathRef.current);
86
+ if (next) {
87
+ resolvedGroupElementRef.current.set(key, next);
88
+ } else {
89
+ resolvedGroupElementRef.current.delete(key);
90
+ }
91
+ return next;
92
+ };
93
+
94
+ useMountEffect(() => {
95
+ let frame = 0;
96
+
97
+ const clearAll = () => {
98
+ setOverlayRect(null);
99
+ setHoverRect(null);
100
+ setGroupOverlayItems([]);
101
+ };
102
+
103
+ const update = () => {
104
+ frame = requestAnimationFrame(update);
105
+ if (rafPausedRef.current) return;
106
+
107
+ const sel = selectionRef.current;
108
+ const iframe = iframeRef.current;
109
+ const overlayEl = overlayRef.current;
110
+ if (!iframe || !overlayEl) {
111
+ resolvedElementRef.current = null;
112
+ resolvedHoverElementRef.current = null;
113
+ resolvedGroupElementRef.current.clear();
114
+ clearAll();
115
+ return;
116
+ }
117
+
118
+ const doc = iframe.contentDocument;
119
+ if (!doc) {
120
+ resolvedElementRef.current = null;
121
+ resolvedHoverElementRef.current = null;
122
+ resolvedGroupElementRef.current.clear();
123
+ clearAll();
124
+ return;
125
+ }
126
+
127
+ if (sel) {
128
+ const el = resolveElementForOverlay(
129
+ doc,
130
+ sel,
131
+ activeCompositionPathRef.current,
132
+ resolvedElementRef as ResolvedElementRef,
133
+ );
134
+ if (el && isElementVisibleForOverlay(el)) {
135
+ setOverlayRect(toOverlayRect(overlayEl, iframe, el));
136
+ } else {
137
+ setOverlayRect(null);
138
+ }
139
+ } else {
140
+ resolvedElementRef.current = null;
141
+ setOverlayRect(null);
142
+ }
143
+
144
+ const group = groupSelectionsRef.current;
145
+ if (group.length > 0) {
146
+ const nextGroupItems: GroupOverlayItem[] = [];
147
+ const liveGroupKeys = new Set<string>();
148
+ for (const groupSelection of group) {
149
+ const key = selectionCacheKey(groupSelection);
150
+ liveGroupKeys.add(key);
151
+ const el = resolveGroupElement(doc, groupSelection);
152
+ const rect = el ? toOverlayRect(overlayEl, iframe, el) : null;
153
+ if (el && rect)
154
+ nextGroupItems.push({ key, selection: groupSelection, element: el, rect });
155
+ }
156
+ for (const key of resolvedGroupElementRef.current.keys()) {
157
+ if (!liveGroupKeys.has(key)) resolvedGroupElementRef.current.delete(key);
158
+ }
159
+ setGroupOverlayItems(nextGroupItems);
160
+ } else {
161
+ resolvedGroupElementRef.current.clear();
162
+ setGroupOverlayItems([]);
163
+ }
164
+
165
+ const hoverSel = hoverSelectionRef.current;
166
+ const hoverMatchesSelection = Boolean(
167
+ sel && hoverSel && selectionCacheKey(sel) === selectionCacheKey(hoverSel),
168
+ );
169
+ const hoverMatchesGroup = Boolean(
170
+ hoverSel && group.some((entry) => selectionCacheKey(entry) === selectionCacheKey(hoverSel)),
171
+ );
172
+ if (!hoverSel || hoverMatchesSelection || hoverMatchesGroup) {
173
+ resolvedHoverElementRef.current = null;
174
+ setHoverRect(null);
175
+ return;
176
+ }
177
+
178
+ const hoverEl = resolveElementForOverlay(
179
+ doc,
180
+ hoverSel,
181
+ activeCompositionPathRef.current,
182
+ resolvedHoverElementRef as ResolvedElementRef,
183
+ );
184
+ if (!hoverEl) {
185
+ setHoverRect(null);
186
+ return;
187
+ }
188
+
189
+ setHoverRect(toOverlayRect(overlayEl, iframe, hoverEl));
190
+ };
191
+
192
+ frame = requestAnimationFrame(update);
193
+ return () => cancelAnimationFrame(frame);
194
+ });
195
+
196
+ return {
197
+ overlayRect,
198
+ overlayRectRef,
199
+ setOverlayRect,
200
+ hoverRect,
201
+ hoverRectRef,
202
+ setHoverRect,
203
+ groupOverlayItems,
204
+ groupOverlayItemsRef,
205
+ setGroupOverlayItems,
206
+ };
207
+ }