hyperframes 0.7.52 → 0.7.54

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.
@@ -20,7 +20,7 @@ import {
20
20
  resolveDomEditSelection,
21
21
  serializeDomEditTextFields,
22
22
  setCompositionSourceMap
23
- } from "./chunk-BA66NM4L.js";
23
+ } from "./chunk-SOTCF4DF.js";
24
24
 
25
25
  // src/components/nle/NLELayout.tsx
26
26
  import {
@@ -12319,6 +12319,19 @@ function getTimelineToggleTitle(timelineVisible) {
12319
12319
  return `${timelineVisible ? "Hide" : "Show"} timeline editor (${TIMELINE_TOGGLE_SHORTCUT_LABEL})`;
12320
12320
  }
12321
12321
 
12322
+ // src/utils/gsapSoftReload.ts
12323
+ import { COLOR_GRADING_SOURCE_HIDDEN_ATTR } from "@hyperframes/core/color-grading";
12324
+
12325
+ // src/utils/authoredOpacity.ts
12326
+ import { COLOR_GRADING_AUTHORED_OPACITY_ATTR } from "@hyperframes/core/color-grading";
12327
+ function readStampedAuthoredOpacity(element) {
12328
+ return element.getAttribute(COLOR_GRADING_AUTHORED_OPACITY_ATTR);
12329
+ }
12330
+ function applyAuthoredInlineOpacity(style, authored) {
12331
+ if (authored === "") style.removeProperty("opacity");
12332
+ else style.setProperty("opacity", authored);
12333
+ }
12334
+
12322
12335
  // src/utils/gsapSoftReload.ts
12323
12336
  var MOTION_PATH_PLUGIN_CDN = "https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/MotionPathPlugin.min.js";
12324
12337
  function ensureMotionPathPluginLoaded(iframe) {
@@ -12379,7 +12392,8 @@ function verifyTimelinesPopulated(win, targetKeys) {
12379
12392
  }
12380
12393
  return Object.keys(timelines).filter((k) => k !== "__proxied").length > 0;
12381
12394
  }
12382
- function applySoftReload(iframe, scriptText, onAsyncFailure, currentTimeOverride) {
12395
+ function applySoftReload(iframe, scriptText, options = {}) {
12396
+ const { onAsyncFailure, currentTimeOverride, authoredHtml } = options;
12383
12397
  if (!iframe || !scriptText) return "cannot-soft-reload";
12384
12398
  const win = iframe.contentWindow;
12385
12399
  const doc = iframe.contentDocument;
@@ -12398,6 +12412,25 @@ function applySoftReload(iframe, scriptText, onAsyncFailure, currentTimeOverride
12398
12412
  if (gsapScripts.length > 1 && staleScripts.length === 0) return "cannot-soft-reload";
12399
12413
  const currentTime = currentTimeOverride ?? win.__player?.getTime?.() ?? 0;
12400
12414
  let deferredToAsync = false;
12415
+ let authoredDoc;
12416
+ const findAuthoredSource = (el) => {
12417
+ if (authoredDoc === void 0) {
12418
+ try {
12419
+ authoredDoc = authoredHtml ? new DOMParser().parseFromString(authoredHtml, "text/html") : null;
12420
+ } catch {
12421
+ authoredDoc = null;
12422
+ }
12423
+ }
12424
+ if (!authoredDoc) return null;
12425
+ const hfId = el.getAttribute("data-hf-id");
12426
+ if (hfId) return authoredDoc.querySelector(`[data-hf-id="${hfId}"]`);
12427
+ return el.id ? authoredDoc.getElementById(el.id) : null;
12428
+ };
12429
+ const readAuthoredOpacity = (el) => {
12430
+ const source = findAuthoredSource(el);
12431
+ if (source instanceof HTMLElement) return source.style.opacity;
12432
+ return readStampedAuthoredOpacity(el);
12433
+ };
12401
12434
  const doReload = () => {
12402
12435
  const timelines = win.__timelines;
12403
12436
  const allTargets = [];
@@ -12432,8 +12465,8 @@ function applySoftReload(iframe, scriptText, onAsyncFailure, currentTimeOverride
12432
12465
  if (allTargets.length > 0 && win.gsap?.set) {
12433
12466
  const saved = [];
12434
12467
  for (const el of allTargets) {
12435
- const s = el.style;
12436
- if (s?.cssText != null) saved.push([el, s.cssText]);
12468
+ const styled = el;
12469
+ if (styled.style?.cssText != null) saved.push([styled, styled.style.cssText]);
12437
12470
  }
12438
12471
  try {
12439
12472
  win.gsap.set(allTargets, { clearProps: "all" });
@@ -12441,9 +12474,14 @@ function applySoftReload(iframe, scriptText, onAsyncFailure, currentTimeOverride
12441
12474
  }
12442
12475
  for (const [el, css2] of saved) {
12443
12476
  const s = el.style;
12444
- if (!s) continue;
12445
12477
  s.cssText = css2;
12446
12478
  s.removeProperty("transform");
12479
+ const authored = readAuthoredOpacity(el);
12480
+ if (authored !== null) {
12481
+ applyAuthoredInlineOpacity(s, authored);
12482
+ } else if (el.hasAttribute(COLOR_GRADING_SOURCE_HIDDEN_ATTR) && s.getPropertyValue("opacity") === "0" && s.getPropertyPriority("opacity") === "important") {
12483
+ s.removeProperty("opacity");
12484
+ }
12447
12485
  }
12448
12486
  }
12449
12487
  for (const script of staleScripts) script.remove();
@@ -31263,6 +31301,22 @@ function seekToCurrent(iframe, timeline) {
31263
31301
  const currentTime = typeof player?.getTime === "function" ? player.getTime() : typeof timeline.time === "function" ? timeline.time() : 0;
31264
31302
  player?.seek?.(Number.isFinite(currentTime) ? currentTime : 0);
31265
31303
  }
31304
+ function changeTouchesOpacity(change) {
31305
+ if (change.kind === "set" || change.kind === "global-set")
31306
+ return change.props.opacity !== void 0;
31307
+ if (change.kind === "keyframes") return change.keyframes.some((step) => "opacity" in step);
31308
+ return "opacity" in change.props;
31309
+ }
31310
+ function restoreAuthoredOpacityForCapture(tween) {
31311
+ const targets = typeof tween.targets === "function" ? tween.targets() : [];
31312
+ for (const target of targets ?? []) {
31313
+ const el = target;
31314
+ if (!el?.style || typeof el.getAttribute !== "function") continue;
31315
+ const authored = readStampedAuthoredOpacity(el);
31316
+ if (authored === null) continue;
31317
+ applyAuthoredInlineOpacity(el.style, authored);
31318
+ }
31319
+ }
31266
31320
  function applyChange(tween, change) {
31267
31321
  if (change.kind === "set") return patchSet(tween, change.props);
31268
31322
  if (change.kind === "keyframes") return patchKeyframes(tween, change.keyframes);
@@ -31283,10 +31337,10 @@ function patchRuntimeTweenInPlace(iframe, selector, change, compositionId) {
31283
31337
  );
31284
31338
  if (!resolved) return false;
31285
31339
  const { tween, timeline } = resolved;
31340
+ if (changeTouchesOpacity(change)) restoreAuthoredOpacityForCapture(tween);
31286
31341
  if (!applyChange(tween, change)) return false;
31287
31342
  if (change.kind !== "keyframe-rebuild") {
31288
31343
  tween.invalidate?.();
31289
- timeline.invalidate?.();
31290
31344
  }
31291
31345
  seekToCurrent(iframe, timeline);
31292
31346
  return true;
@@ -32167,9 +32221,13 @@ async function mutateGsapScript(projectId, sourceFile, mutation) {
32167
32221
  if (!result.ok) throw new Error(`Failed to update GSAP in ${sourceFile}`);
32168
32222
  return result;
32169
32223
  }
32170
- function softReloadOrEscalate(iframe, scriptText, reloadPreview, origin) {
32224
+ function softReloadOrEscalate(iframe, scriptText, reloadPreview, origin, authoredHtml) {
32171
32225
  const currentTime = usePlayerStore.getState().currentTime;
32172
- const result = applySoftReload(iframe, scriptText, reloadPreview, currentTime);
32226
+ const result = applySoftReload(iframe, scriptText, {
32227
+ onAsyncFailure: reloadPreview,
32228
+ currentTimeOverride: currentTime,
32229
+ authoredHtml
32230
+ });
32173
32231
  if (result === "applied") return;
32174
32232
  trackStudioEvent("gsap_soft_reload_outcome", {
32175
32233
  origin,
@@ -32189,7 +32247,13 @@ function applyPreviewSync(iframe, result, options, reloadPreview) {
32189
32247
  trackStudioEvent("gsap_instant_patch_fallback", { selector: options.instantPatch.selector });
32190
32248
  }
32191
32249
  if (options.softReload && result.scriptText) {
32192
- softReloadOrEscalate(iframe, result.scriptText, reloadPreview, "preview_sync");
32250
+ softReloadOrEscalate(
32251
+ iframe,
32252
+ result.scriptText,
32253
+ reloadPreview,
32254
+ "preview_sync",
32255
+ result.after ?? void 0
32256
+ );
32193
32257
  } else {
32194
32258
  reloadPreview();
32195
32259
  }
@@ -32214,7 +32278,12 @@ function useGsapScriptCommits({ projectIdRef, activeCompPath, previewIframeRef,
32214
32278
  if (options.skipReload) return;
32215
32279
  throw error;
32216
32280
  }
32217
- if (result.changed === false) return;
32281
+ if (result.changed === false) {
32282
+ if (!options.skipReload && options.instantPatch) {
32283
+ applyPreviewSync(previewIframeRef.current, result, options, reloadPreview);
32284
+ }
32285
+ return;
32286
+ }
32218
32287
  domEditSaveTimestampRef.current = Date.now();
32219
32288
  if (result.before != null && result.after != null) {
32220
32289
  await editHistory.recordEdit({ label: options.label, kind: "manual", coalesceKey: options.coalesceKey, files: { [targetPath]: { before: result.before, after: result.after } } });
@@ -32241,7 +32310,7 @@ function useGsapScriptCommits({ projectIdRef, activeCompPath, previewIframeRef,
32241
32310
  (after) => {
32242
32311
  const script = extractGsapScriptText(after);
32243
32312
  if (script) {
32244
- softReloadOrEscalate(previewIframeRef.current, script, reloadPreview, "sdk_refresh");
32313
+ softReloadOrEscalate(previewIframeRef.current, script, reloadPreview, "sdk_refresh", after);
32245
32314
  } else {
32246
32315
  reloadPreview();
32247
32316
  }
@@ -32319,6 +32388,21 @@ import { isStudioHoldSet } from "@hyperframes/core/gsap-parser";
32319
32388
 
32320
32389
  // src/hooks/gsapRuntimeReaders.ts
32321
32390
  import { classifyPropertyGroup as classifyPropertyGroup2 } from "@hyperframes/core/gsap-parser";
32391
+ import {
32392
+ COLOR_GRADING_SOURCE_HIDDEN_ATTR as COLOR_GRADING_SOURCE_HIDDEN_ATTR2,
32393
+ HF_COLOR_GRADING_CANVAS_ID_PREFIX
32394
+ } from "@hyperframes/core/color-grading";
32395
+ function readLiveGsapValue(gsap2, el, prop) {
32396
+ if (prop === "opacity" && el.getAttribute(COLOR_GRADING_SOURCE_HIDDEN_ATTR2) != null && el.id) {
32397
+ const canvas = el.ownerDocument.getElementById(HF_COLOR_GRADING_CANVAS_ID_PREFIX + el.id);
32398
+ const win = el.ownerDocument.defaultView;
32399
+ if (canvas && win) {
32400
+ const val = Number(win.getComputedStyle(canvas).opacity);
32401
+ if (Number.isFinite(val)) return val;
32402
+ }
32403
+ }
32404
+ return Number(gsap2.getProperty(el, prop));
32405
+ }
32322
32406
  function readGsapProperty(iframe, selector, prop) {
32323
32407
  if (!selector) return null;
32324
32408
  const gsap2 = getIframeGsap(iframe);
@@ -32326,7 +32410,7 @@ function readGsapProperty(iframe, selector, prop) {
32326
32410
  const el = queryIframeElement(iframe, selector);
32327
32411
  if (!el) return null;
32328
32412
  try {
32329
- const val = Number(gsap2.getProperty(el, prop));
32413
+ const val = readLiveGsapValue(gsap2, el, prop);
32330
32414
  if (!Number.isFinite(val)) return null;
32331
32415
  return POSITION_PROPS.has(prop) ? Math.round(val) : roundTo3(val);
32332
32416
  } catch {
@@ -32376,13 +32460,10 @@ function readAllAnimatedProperties(iframe, selector, anim, group) {
32376
32460
  } else {
32377
32461
  for (const p of Object.keys(anim.properties)) propKeys.add(p);
32378
32462
  }
32379
- if (group) {
32380
- for (const p of propKeys) {
32381
- if (classifyPropertyGroup2(p) !== group) propKeys.delete(p);
32382
- }
32383
- }
32384
- for (const prop of propKeys) {
32385
- const val = Number(gsap2.getProperty(el, prop));
32463
+ const inGroup = (p) => !group || classifyPropertyGroup2(p) === group;
32464
+ const groupedPropKeys = new Set([...propKeys].filter(inGroup));
32465
+ for (const prop of groupedPropKeys) {
32466
+ const val = readLiveGsapValue(gsap2, el, prop);
32386
32467
  if (Number.isFinite(val)) {
32387
32468
  result[prop] = POSITION_PROPS.has(prop) ? Math.round(val) : roundTo3(val);
32388
32469
  }
@@ -32402,14 +32483,14 @@ function readAllAnimatedProperties(iframe, selector, anim, group) {
32402
32483
  const vars = child.vars;
32403
32484
  if (!vars) continue;
32404
32485
  for (const k of Object.keys(vars)) {
32405
- if (!GSAP_CONFIG_KEYS.has(k)) otherTweenProps.add(k);
32486
+ if (!GSAP_CONFIG_KEYS.has(k) && inGroup(k)) otherTweenProps.add(k);
32406
32487
  }
32407
32488
  }
32408
32489
  }
32409
32490
  }
32410
32491
  } catch {
32411
32492
  }
32412
- for (const p of propKeys) otherTweenProps.delete(p);
32493
+ for (const p of groupedPropKeys) otherTweenProps.delete(p);
32413
32494
  const UNIVERSAL_BASELINE = {
32414
32495
  opacity: 1,
32415
32496
  scale: 1,
@@ -32434,11 +32515,11 @@ function readAllAnimatedProperties(iframe, selector, anim, group) {
32434
32515
  sepia: 0,
32435
32516
  invert: 0
32436
32517
  };
32437
- const allTweenedProps = /* @__PURE__ */ new Set([...propKeys, ...otherTweenProps]);
32518
+ const allTweenedProps = /* @__PURE__ */ new Set([...groupedPropKeys, ...otherTweenProps]);
32438
32519
  for (const [prop, defaultVal] of Object.entries(UNIVERSAL_BASELINE)) {
32439
32520
  if (prop in result) continue;
32440
32521
  if (!allTweenedProps.has(prop)) continue;
32441
- const val = Number(gsap2.getProperty(el, prop));
32522
+ const val = readLiveGsapValue(gsap2, el, prop);
32442
32523
  if (Number.isFinite(val) && Math.round(val * 1e3) !== Math.round(defaultVal * 1e3)) {
32443
32524
  result[prop] = roundTo3(val);
32444
32525
  }
@@ -32467,6 +32548,7 @@ function readAllAnimatedProperties(iframe, selector, anim, group) {
32467
32548
  }
32468
32549
  for (const prop of COMPUTED_BASELINE) {
32469
32550
  if (prop in result) continue;
32551
+ if (!inGroup(prop)) continue;
32470
32552
  if (otherTweenProps.has(prop)) continue;
32471
32553
  const gsapVal = Number(gsap2.getProperty(el, prop));
32472
32554
  if (!Number.isFinite(gsapVal)) continue;
@@ -33437,169 +33519,6 @@ async function tryGsapDragIntercept(selection, offset, animations, iframe, commi
33437
33519
  }
33438
33520
  return true;
33439
33521
  }
33440
- var IDENTITY_ONE_PROPS = /* @__PURE__ */ new Set(["opacity", "autoAlpha", "scale", "scaleX", "scaleY"]);
33441
- function synthesizeIdentityProps(source) {
33442
- const id = {};
33443
- for (const [k, v] of Object.entries(source)) {
33444
- if (typeof v === "number") id[k] = IDENTITY_ONE_PROPS.has(k) ? 1 : 0;
33445
- else id[k] = v;
33446
- }
33447
- return id;
33448
- }
33449
- async function tryGsapResizeIntercept(selection, size, animations, iframe, commitMutation, fetchFallbackAnimations) {
33450
- const hasScaleGroup = animations.some((a) => a.propertyGroup === "scale");
33451
- const resizeGroup = hasScaleGroup ? "scale" : "size";
33452
- const resolved = await resolveGroupTween(
33453
- resizeGroup,
33454
- animations,
33455
- selection,
33456
- commitMutation,
33457
- fetchFallbackAnimations
33458
- );
33459
- let anim = resolved?.anim ?? null;
33460
- if (!anim || anim.method === "set") {
33461
- const sel = selectorFromSelection(selection);
33462
- if (!sel) return false;
33463
- const sizeSet = anim?.method === "set" ? anim : findSizeSetAnimation(animations, sel);
33464
- if (resizeGroup === "size") {
33465
- const animatedTween = pickClosestToPlayhead(
33466
- animations.filter((a) => a.method !== "set" && resolveTweenDuration(a) > 0)
33467
- );
33468
- if (animatedTween) {
33469
- const handled = await commitKeyframedSizeFromResize(
33470
- selection,
33471
- size,
33472
- sel,
33473
- sizeSet,
33474
- animatedTween,
33475
- { commitMutation, fetchAnimations: fetchFallbackAnimations }
33476
- );
33477
- if (handled) return true;
33478
- }
33479
- }
33480
- await commitStaticGsapSize(selection, size, sel, sizeSet, {
33481
- commitMutation,
33482
- fetchAnimations: fetchFallbackAnimations
33483
- });
33484
- return true;
33485
- }
33486
- const { activeKeyframePct, setActiveKeyframePct } = usePlayerStore.getState();
33487
- const pct = activeKeyframePct ?? computeCurrentPercentage(selection, anim);
33488
- if (activeKeyframePct != null) setActiveKeyframePct(null);
33489
- const coalesceKey = `gsap:resize:${anim.id}`;
33490
- const selector = selectorFromSelection(selection);
33491
- const runtimeProps = selector ? readAllAnimatedProperties(iframe, selector, anim) : {};
33492
- let resizeProps;
33493
- if (resizeGroup === "scale") {
33494
- const el = iframe?.contentDocument?.querySelector(selector ?? "");
33495
- const origW = Number.parseFloat(el?.getAttribute("data-hf-studio-original-width") ?? "");
33496
- const cssW = Number.isFinite(origW) && origW > 0 ? origW : 200;
33497
- const newScale = roundTo3(size.width / cssW);
33498
- resizeProps = { scale: newScale };
33499
- } else {
33500
- resizeProps = {
33501
- width: Math.round(size.width),
33502
- height: Math.round(size.height)
33503
- };
33504
- }
33505
- if (!usePlayerStore.getState().autoKeyframeEnabled) {
33506
- if (activeKeyframePct != null) setActiveKeyframePct(null);
33507
- await commitWholePropertyOffset(
33508
- selection,
33509
- anim,
33510
- resizeProps,
33511
- pct,
33512
- iframe,
33513
- { commitMutation, fetchAnimations: fetchFallbackAnimations },
33514
- "Resize animation"
33515
- );
33516
- return true;
33517
- }
33518
- const ct = usePlayerStore.getState().currentTime;
33519
- const ts = resolveTweenStart(anim);
33520
- const td = resolveTweenDuration(anim);
33521
- const outsideRange = ts !== null && td > 0 && (ct < ts - 0.01 || ct > ts + td + 0.01);
33522
- if (!outsideRange) {
33523
- if (anim.hasUnresolvedKeyframes || anim.hasUnresolvedSelector) {
33524
- const newId = await materializeIfDynamic(anim, iframe, commitMutation, selection);
33525
- if (newId) anim = { ...anim, id: newId };
33526
- } else if (!anim.keyframes) {
33527
- const resolvedFromValues = selector ? readAllAnimatedProperties(iframe, selector, anim) : void 0;
33528
- await commitMutation(
33529
- selection,
33530
- { type: "convert-to-keyframes", animationId: anim.id, resolvedFromValues },
33531
- { label: "Convert to keyframes for resize", skipReload: true, coalesceKey }
33532
- );
33533
- if (fetchFallbackAnimations) {
33534
- const fresh = await fetchFallbackAnimations();
33535
- const refreshed = fresh.find(
33536
- (a) => a.targetSelector === anim.targetSelector && a.keyframes
33537
- );
33538
- if (refreshed) anim = refreshed;
33539
- }
33540
- }
33541
- }
33542
- if (outsideRange && ts !== null) {
33543
- const kfs = anim.keyframes?.keyframes ?? (() => {
33544
- const fromProps = anim.method === "from" || anim.method === "fromTo" ? { ...anim.properties } : synthesizeIdentityProps(anim.properties);
33545
- const toProps = anim.method === "from" ? synthesizeIdentityProps(anim.properties) : { ...anim.properties };
33546
- return [
33547
- { percentage: 0, properties: fromProps },
33548
- { percentage: 100, properties: toProps }
33549
- ];
33550
- })();
33551
- const newStart = Math.min(ct, ts);
33552
- const newEnd = Math.max(ct, ts + td);
33553
- const newDuration = Math.max(0.01, newEnd - newStart);
33554
- const existingKfs = kfs;
33555
- const remapped = [];
33556
- for (const kf of existingKfs) {
33557
- const absTime = ts + kf.percentage / 100 * td;
33558
- const newPct = Math.round((absTime - newStart) / newDuration * 1e3) / 10;
33559
- const props = { ...kf.properties };
33560
- for (const k of Object.keys(resizeProps)) {
33561
- if (k in props) continue;
33562
- if (k === "width" || k === "height") continue;
33563
- props[k] = IDENTITY_ONE_PROPS.has(k) ? 1 : 0;
33564
- }
33565
- remapped.push({ percentage: newPct, properties: props });
33566
- }
33567
- const targetPct = Math.round((ct - newStart) / newDuration * 1e3) / 10;
33568
- remapped.push({ percentage: targetPct, properties: resizeProps });
33569
- remapped.sort((a, b) => a.percentage - b.percentage);
33570
- await commitMutation(
33571
- selection,
33572
- {
33573
- type: "replace-with-keyframes",
33574
- animationId: anim.id,
33575
- targetSelector: anim.targetSelector,
33576
- position: roundTo3(newStart),
33577
- duration: roundTo3(newDuration),
33578
- keyframes: remapped
33579
- },
33580
- { label: `Resize (extended to ${ct.toFixed(2)}s)`, softReload: true, coalesceKey }
33581
- );
33582
- return true;
33583
- }
33584
- const SIZE_PROPS = /* @__PURE__ */ new Set(["width", "height"]);
33585
- const backfillDefaults = {};
33586
- for (const k of Object.keys(runtimeProps)) {
33587
- if (SIZE_PROPS.has(k)) continue;
33588
- backfillDefaults[k] = IDENTITY_ONE_PROPS.has(k) ? 1 : 0;
33589
- }
33590
- await commitMutation(
33591
- selection,
33592
- {
33593
- type: "add-keyframe",
33594
- animationId: anim.id,
33595
- percentage: pct,
33596
- properties: resizeProps,
33597
- backfillDefaults
33598
- },
33599
- { label: `Resize (keyframe ${pct}%)`, softReload: true, coalesceKey }
33600
- );
33601
- return true;
33602
- }
33603
33522
  async function tryGsapRotationIntercept(selection, angle, animations, iframe, commitMutation, fetchFallbackAnimations) {
33604
33523
  const selector = selectorFromSelection(selection);
33605
33524
  if (!selector) return false;
@@ -34558,6 +34477,256 @@ function useDomEditWiring({
34558
34477
  // src/hooks/useGsapAwareEditing.ts
34559
34478
  import { useCallback as useCallback72 } from "react";
34560
34479
 
34480
+ // src/utils/elementGsap.ts
34481
+ function gsapOf(element) {
34482
+ return element.ownerDocument.defaultView?.gsap;
34483
+ }
34484
+ function setElementGsapPosition(element, x, y) {
34485
+ const gsap2 = gsapOf(element);
34486
+ if (!gsap2?.set) return false;
34487
+ gsap2.set(element, { x, y });
34488
+ return true;
34489
+ }
34490
+ function readElementGsapNumber(element, prop) {
34491
+ const value = Number(gsapOf(element)?.getProperty?.(element, prop));
34492
+ return Number.isFinite(value) ? value : null;
34493
+ }
34494
+
34495
+ // src/hooks/gsapResizeIntercept.ts
34496
+ var IDENTITY_ONE_PROPS = /* @__PURE__ */ new Set(["opacity", "autoAlpha", "scale", "scaleX", "scaleY"]);
34497
+ function synthesizeIdentityProps(source) {
34498
+ const id = {};
34499
+ for (const [k, v] of Object.entries(source)) {
34500
+ if (typeof v === "number") id[k] = IDENTITY_ONE_PROPS.has(k) ? 1 : 0;
34501
+ else id[k] = v;
34502
+ }
34503
+ return id;
34504
+ }
34505
+ async function tryGsapResizeIntercept(selection, size, animations, iframe, commitMutation, fetchFallbackAnimations) {
34506
+ const hasScaleGroup = animations.some((a) => a.propertyGroup === "scale");
34507
+ const resizeGroup = hasScaleGroup ? "scale" : "size";
34508
+ const resolved = await resolveGroupTween(
34509
+ resizeGroup,
34510
+ animations,
34511
+ selection,
34512
+ commitMutation,
34513
+ fetchFallbackAnimations
34514
+ );
34515
+ let anim = resolved?.anim ?? null;
34516
+ if (!anim || anim.method === "set") {
34517
+ const sel = selectorFromSelection(selection);
34518
+ if (!sel) return false;
34519
+ const sizeSet = anim?.method === "set" ? anim : findSizeSetAnimation(animations, sel);
34520
+ if (resizeGroup === "size") {
34521
+ const animatedTween = pickClosestToPlayhead(
34522
+ animations.filter((a) => a.method !== "set" && resolveTweenDuration(a) > 0)
34523
+ );
34524
+ if (animatedTween) {
34525
+ const handled = await commitKeyframedSizeFromResize(
34526
+ selection,
34527
+ size,
34528
+ sel,
34529
+ sizeSet,
34530
+ animatedTween,
34531
+ { commitMutation, fetchAnimations: fetchFallbackAnimations }
34532
+ );
34533
+ if (handled) return true;
34534
+ }
34535
+ }
34536
+ await commitStaticGsapSize(selection, size, sel, sizeSet, {
34537
+ commitMutation,
34538
+ fetchAnimations: fetchFallbackAnimations
34539
+ });
34540
+ return true;
34541
+ }
34542
+ const { activeKeyframePct, setActiveKeyframePct } = usePlayerStore.getState();
34543
+ const pct = activeKeyframePct ?? computeCurrentPercentage(selection, anim);
34544
+ if (activeKeyframePct != null) setActiveKeyframePct(null);
34545
+ const coalesceKey = `gsap:resize:${anim.id}`;
34546
+ const selector = selectorFromSelection(selection);
34547
+ const runtimeProps = selector ? readAllAnimatedProperties(iframe, selector, anim, resizeGroup) : {};
34548
+ let resizeProps;
34549
+ let scaleDraftEl = null;
34550
+ let scaleDraftDropPoint = null;
34551
+ let nonUniformScale = false;
34552
+ if (resizeGroup === "scale") {
34553
+ const el = iframe?.contentDocument?.querySelector(selector ?? "");
34554
+ const origW = Number.parseFloat(el?.getAttribute("data-hf-studio-original-width") ?? "");
34555
+ const origH = Number.parseFloat(el?.getAttribute("data-hf-studio-original-height") ?? "");
34556
+ const cssW = Number.isFinite(origW) && origW > 0 ? origW : 200;
34557
+ const cssH = Number.isFinite(origH) && origH > 0 ? origH : cssW;
34558
+ const rawLiveScaleX = readGsapProperty(iframe, selector ?? null, "scaleX") ?? 1;
34559
+ const rawLiveScaleY = readGsapProperty(iframe, selector ?? null, "scaleY") ?? 1;
34560
+ const liveScaleX = rawLiveScaleX > 0 ? rawLiveScaleX : 1;
34561
+ const liveScaleY = rawLiveScaleY > 0 ? rawLiveScaleY : 1;
34562
+ const newScaleX = roundTo3(size.width * liveScaleX / cssW);
34563
+ const newScaleY = roundTo3(size.height * liveScaleY / cssH);
34564
+ nonUniformScale = Math.abs(newScaleX - newScaleY) > 0.01;
34565
+ resizeProps = nonUniformScale ? { scaleX: newScaleX, scaleY: newScaleY } : { scale: newScaleX };
34566
+ scaleDraftEl = el;
34567
+ if (el) {
34568
+ const dropRect = el.getBoundingClientRect();
34569
+ scaleDraftDropPoint = { x: dropRect.x, y: dropRect.y };
34570
+ }
34571
+ } else {
34572
+ resizeProps = {
34573
+ width: Math.round(size.width),
34574
+ height: Math.round(size.height)
34575
+ };
34576
+ }
34577
+ const finalizeScaleResizeCommit = async () => {
34578
+ if (!scaleDraftEl) return;
34579
+ clearStudioBoxSize(scaleDraftEl);
34580
+ if (!scaleDraftDropPoint || !selector) return;
34581
+ const hasLivePositionTween = hasNonHoldTweenForElement(
34582
+ iframe,
34583
+ selector,
34584
+ void 0,
34585
+ POSITION_CHANNELS
34586
+ );
34587
+ if (hasLivePositionTween) {
34588
+ return;
34589
+ }
34590
+ const post = scaleDraftEl.getBoundingClientRect();
34591
+ const residual = { x: scaleDraftDropPoint.x - post.x, y: scaleDraftDropPoint.y - post.y };
34592
+ if (!Number.isFinite(residual.x) || !Number.isFinite(residual.y)) return;
34593
+ if (Math.abs(residual.x) < 0.5 && Math.abs(residual.y) < 0.5) return;
34594
+ const gsapPos = readGsapPositionFromIframe(iframe, selector) ?? { x: 0, y: 0 };
34595
+ const corrected = {
34596
+ x: Math.round(gsapPos.x + residual.x),
34597
+ y: Math.round(gsapPos.y + residual.y)
34598
+ };
34599
+ setElementGsapPosition(scaleDraftEl, corrected.x, corrected.y);
34600
+ const currentAnimations = fetchFallbackAnimations ? await fetchFallbackAnimations() : resolved?.animations ?? animations;
34601
+ const existingSet = findExistingPositionWrite(currentAnimations, selector);
34602
+ await commitStaticGsapPosition(
34603
+ selection,
34604
+ { x: corrected.x - gsapPos.x, y: corrected.y - gsapPos.y },
34605
+ gsapPos,
34606
+ selector,
34607
+ existingSet,
34608
+ {
34609
+ commitMutation,
34610
+ fetchAnimations: fetchFallbackAnimations
34611
+ }
34612
+ );
34613
+ };
34614
+ if (!usePlayerStore.getState().autoKeyframeEnabled) {
34615
+ if (activeKeyframePct != null) setActiveKeyframePct(null);
34616
+ await commitWholePropertyOffset(
34617
+ selection,
34618
+ anim,
34619
+ resizeProps,
34620
+ pct,
34621
+ iframe,
34622
+ { commitMutation, fetchAnimations: fetchFallbackAnimations },
34623
+ "Resize animation"
34624
+ );
34625
+ await finalizeScaleResizeCommit();
34626
+ return true;
34627
+ }
34628
+ const ct = usePlayerStore.getState().currentTime;
34629
+ const ts = resolveTweenStart(anim);
34630
+ const td = resolveTweenDuration(anim);
34631
+ const outsideRange = ts !== null && td > 0 && (ct < ts - 0.01 || ct > ts + td + 0.01);
34632
+ if (!outsideRange) {
34633
+ if (anim.hasUnresolvedKeyframes || anim.hasUnresolvedSelector) {
34634
+ const newId = await materializeIfDynamic(anim, iframe, commitMutation, selection);
34635
+ if (newId) anim = { ...anim, id: newId };
34636
+ } else if (!anim.keyframes) {
34637
+ const resolvedFromValues = selector ? readAllAnimatedProperties(iframe, selector, anim, resizeGroup) : void 0;
34638
+ await commitMutation(
34639
+ selection,
34640
+ { type: "convert-to-keyframes", animationId: anim.id, resolvedFromValues },
34641
+ { label: "Convert to keyframes for resize", skipReload: true, coalesceKey }
34642
+ );
34643
+ if (fetchFallbackAnimations) {
34644
+ const fresh = await fetchFallbackAnimations();
34645
+ const refreshed = fresh.find(
34646
+ (a) => a.targetSelector === anim.targetSelector && a.keyframes
34647
+ );
34648
+ if (refreshed) anim = refreshed;
34649
+ }
34650
+ }
34651
+ }
34652
+ if ((outsideRange || nonUniformScale) && ts !== null) {
34653
+ const kfs = anim.keyframes?.keyframes ?? (() => {
34654
+ const fromProps = anim.method === "from" || anim.method === "fromTo" ? { ...anim.properties } : synthesizeIdentityProps(anim.properties);
34655
+ const toProps = anim.method === "from" ? synthesizeIdentityProps(anim.properties) : { ...anim.properties };
34656
+ return [
34657
+ { percentage: 0, properties: fromProps },
34658
+ { percentage: 100, properties: toProps }
34659
+ ];
34660
+ })();
34661
+ const newStart = Math.min(ct, ts);
34662
+ const newEnd = Math.max(ct, ts + td);
34663
+ const newDuration = Math.max(0.01, newEnd - newStart);
34664
+ const existingKfs = kfs;
34665
+ const remapped = [];
34666
+ for (const kf of existingKfs) {
34667
+ const absTime = ts + kf.percentage / 100 * td;
34668
+ const newPct = Math.round((absTime - newStart) / newDuration * 1e3) / 10;
34669
+ const props = { ...kf.properties };
34670
+ if (nonUniformScale && "scale" in props) {
34671
+ const uniform = props.scale;
34672
+ if (typeof uniform === "number") {
34673
+ props.scaleX = uniform;
34674
+ props.scaleY = uniform;
34675
+ }
34676
+ delete props.scale;
34677
+ }
34678
+ for (const k of Object.keys(resizeProps)) {
34679
+ if (k in props) continue;
34680
+ if (k === "width" || k === "height") continue;
34681
+ props[k] = IDENTITY_ONE_PROPS.has(k) ? 1 : 0;
34682
+ }
34683
+ remapped.push({ percentage: newPct, properties: props });
34684
+ }
34685
+ const targetPct = Math.round((ct - newStart) / newDuration * 1e3) / 10;
34686
+ const collidingKf = remapped.find((kf) => Math.abs(kf.percentage - targetPct) < 0.05);
34687
+ if (collidingKf) Object.assign(collidingKf.properties, resizeProps);
34688
+ else remapped.push({ percentage: targetPct, properties: resizeProps });
34689
+ remapped.sort((a, b) => a.percentage - b.percentage);
34690
+ await commitMutation(
34691
+ selection,
34692
+ {
34693
+ type: "replace-with-keyframes",
34694
+ animationId: anim.id,
34695
+ targetSelector: anim.targetSelector,
34696
+ position: roundTo3(newStart),
34697
+ duration: roundTo3(newDuration),
34698
+ keyframes: remapped
34699
+ },
34700
+ {
34701
+ label: outsideRange ? `Resize (extended to ${ct.toFixed(2)}s)` : `Resize (keyframe ${Math.round((ct - newStart) / newDuration * 1e3) / 10}%)`,
34702
+ softReload: true,
34703
+ coalesceKey
34704
+ }
34705
+ );
34706
+ await finalizeScaleResizeCommit();
34707
+ return true;
34708
+ }
34709
+ const SIZE_PROPS = /* @__PURE__ */ new Set(["width", "height"]);
34710
+ const backfillDefaults = {};
34711
+ for (const k of Object.keys(runtimeProps)) {
34712
+ if (SIZE_PROPS.has(k)) continue;
34713
+ backfillDefaults[k] = IDENTITY_ONE_PROPS.has(k) ? 1 : 0;
34714
+ }
34715
+ await commitMutation(
34716
+ selection,
34717
+ {
34718
+ type: "add-keyframe",
34719
+ animationId: anim.id,
34720
+ percentage: pct,
34721
+ properties: resizeProps,
34722
+ backfillDefaults
34723
+ },
34724
+ { label: `Resize (keyframe ${pct}%)`, softReload: true, coalesceKey }
34725
+ );
34726
+ await finalizeScaleResizeCommit();
34727
+ return true;
34728
+ }
34729
+
34561
34730
  // src/hooks/useAnimatedPropertyCommit.ts
34562
34731
  import { useCallback as useCallback71 } from "react";
34563
34732
  import { classifyPropertyGroup as classifyPropertyGroup3 } from "@hyperframes/core/gsap-parser";
@@ -34593,6 +34762,19 @@ async function maybeAutoKeyframeSet(selection, setAnim, animations, commit) {
34593
34762
  { label: "Keyframe 3D transform", softReload: true }
34594
34763
  );
34595
34764
  }
34765
+ var STATIC_SET_LABELS = {
34766
+ position: "Move layer",
34767
+ scale: "Resize layer",
34768
+ size: "Resize layer",
34769
+ rotation: "Rotate layer",
34770
+ visual: "Set opacity",
34771
+ other: "Set 3D transform"
34772
+ };
34773
+ function staticSetLabel(propEntries) {
34774
+ const groups = new Set(propEntries.map(([k]) => classifyPropertyGroup3(k)));
34775
+ const only = groups.size === 1 ? [...groups][0] : void 0;
34776
+ return only && STATIC_SET_LABELS[only] || "Set properties";
34777
+ }
34596
34778
  async function commitSetProps(selection, setAnim, propEntries, selector, animations, commit) {
34597
34779
  const properties = Object.fromEntries(propEntries);
34598
34780
  const numericProps = {};
@@ -34609,19 +34791,47 @@ async function commitSetProps(selection, setAnim, propEntries, selector, animati
34609
34791
  await commit(
34610
34792
  selection,
34611
34793
  { type: "update-properties", animationId: setAnim.id, properties },
34612
- { label: "Set 3D transform", softReload: true, ...instantPatch ? { instantPatch } : {} }
34794
+ {
34795
+ label: staticSetLabel(propEntries),
34796
+ softReload: true,
34797
+ ...instantPatch ? { instantPatch } : {}
34798
+ }
34613
34799
  );
34614
34800
  await maybeAutoKeyframeSet(selection, setAnim, animations, commit);
34615
34801
  }
34616
34802
  async function commitStaticSet(selection, propEntries, selector, animations, commit) {
34617
34803
  if (!selector) return;
34618
- const existingSet = animations.find((a) => a.method === "set" && a.targetSelector === selector);
34619
- if (existingSet) {
34620
- await commitSetProps(selection, existingSet, propEntries, selector, animations, commit);
34621
- return;
34804
+ const byGroup = /* @__PURE__ */ new Map();
34805
+ for (const entry of propEntries) {
34806
+ const group = classifyPropertyGroup3(entry[0]);
34807
+ const batch = byGroup.get(group) ?? [];
34808
+ batch.push(entry);
34809
+ byGroup.set(group, batch);
34810
+ }
34811
+ const sets = animations.filter((a) => a.method === "set" && a.targetSelector === selector);
34812
+ const byTargetSet = /* @__PURE__ */ new Map();
34813
+ const newSetBatches = [];
34814
+ for (const [group, batch] of byGroup) {
34815
+ const existingSet = findGroupOwningSet(sets, group);
34816
+ if (existingSet) {
34817
+ byTargetSet.set(existingSet, [...byTargetSet.get(existingSet) ?? [], ...batch]);
34818
+ } else {
34819
+ newSetBatches.push(batch);
34820
+ }
34821
+ }
34822
+ for (const [targetSet, batch] of byTargetSet) {
34823
+ await commitSetProps(selection, targetSet, batch, selector, animations, commit);
34622
34824
  }
34825
+ for (const batch of newSetBatches) {
34826
+ await addGlobalStaticSet(selection, batch, selector, commit);
34827
+ }
34828
+ }
34829
+ function findGroupOwningSet(sets, group) {
34830
+ return sets.find((a) => a.propertyGroup === group) ?? sets.find((a) => Object.keys(a.properties).some((k) => classifyPropertyGroup3(k) === group));
34831
+ }
34832
+ async function addGlobalStaticSet(selection, batch, selector, commit) {
34623
34833
  const numericProps = {};
34624
- for (const [k, v] of propEntries) {
34834
+ for (const [k, v] of batch) {
34625
34835
  if (typeof v === "number") numericProps[k] = v;
34626
34836
  }
34627
34837
  await commit(
@@ -34631,11 +34841,11 @@ async function commitStaticSet(selection, propEntries, selector, animations, com
34631
34841
  targetSelector: selector,
34632
34842
  method: "set",
34633
34843
  position: 0,
34634
- properties: Object.fromEntries(propEntries),
34844
+ properties: Object.fromEntries(batch),
34635
34845
  global: true
34636
34846
  },
34637
34847
  {
34638
- label: "Set 3D transform",
34848
+ label: staticSetLabel(batch),
34639
34849
  softReload: true,
34640
34850
  ...Object.keys(numericProps).length > 0 ? {
34641
34851
  instantPatch: {
@@ -42135,8 +42345,8 @@ function cropRectFromInsets(rect, insets, scaleX, scaleY) {
42135
42345
  function readElementCropInsets(element) {
42136
42346
  const inline = element.style.getPropertyValue("clip-path").trim();
42137
42347
  const value = inline || element.ownerDocument.defaultView?.getComputedStyle(element).clipPath.trim() || "";
42138
- const parsed = parseInsetClipPathSides(value === "none" ? "" : value);
42139
- return parsed ?? { top: 0, right: 0, bottom: 0, left: 0, radius: 0 };
42348
+ if (!value || value === "none") return { top: 0, right: 0, bottom: 0, left: 0, radius: 0 };
42349
+ return parseInsetClipPathSides(value);
42140
42350
  }
42141
42351
  function clampInset(value, max) {
42142
42352
  if (!Number.isFinite(value)) return 0;
@@ -42180,9 +42390,62 @@ function resolveCropInsetFromMoveDrag(input) {
42180
42390
  }
42181
42391
  function hugRectForElement(rect, element) {
42182
42392
  const insets = readElementCropInsets(element);
42183
- if (insets.top <= 0 && insets.right <= 0 && insets.bottom <= 0 && insets.left <= 0) return rect;
42393
+ if (!insets || insets.top <= 0 && insets.right <= 0 && insets.bottom <= 0 && insets.left <= 0)
42394
+ return rect;
42184
42395
  return cropRectFromInsets(rect, insets, rect.editScaleX, rect.editScaleY);
42185
42396
  }
42397
+ function readElementCropFrame(element, overlayRect) {
42398
+ const editX = overlayRect.editScaleX > 0 ? overlayRect.editScaleX : 1;
42399
+ const editY = overlayRect.editScaleY > 0 ? overlayRect.editScaleY : 1;
42400
+ const aabb = {
42401
+ angleDeg: 0,
42402
+ left: overlayRect.left,
42403
+ top: overlayRect.top,
42404
+ width: overlayRect.width,
42405
+ height: overlayRect.height,
42406
+ scaleX: editX,
42407
+ scaleY: editY
42408
+ };
42409
+ let transform = "";
42410
+ try {
42411
+ transform = element.ownerDocument.defaultView?.getComputedStyle(element).transform ?? "";
42412
+ } catch {
42413
+ return aabb;
42414
+ }
42415
+ if (!transform || transform === "none") return aabb;
42416
+ const m = /^matrix\(([^)]+)\)$/.exec(transform);
42417
+ if (!m) return aabb;
42418
+ const [a, b, c, d] = m[1].split(",").map((v) => Number.parseFloat(v));
42419
+ if (![a, b, c, d].every(Number.isFinite)) return aabb;
42420
+ const elScaleX = Math.hypot(a, b);
42421
+ const det = a * d - b * c;
42422
+ const elScaleY = elScaleX !== 0 ? det / elScaleX : 1;
42423
+ if (elScaleX <= 0 || elScaleY <= 0) return aabb;
42424
+ const angleDeg = Math.atan2(b, a) * 180 / Math.PI;
42425
+ const scaleX = elScaleX * editX;
42426
+ const scaleY = elScaleY * editY;
42427
+ const width = element.offsetWidth * scaleX;
42428
+ const height = element.offsetHeight * scaleY;
42429
+ if (!(width > 0) || !(height > 0)) return aabb;
42430
+ const cx = overlayRect.left + overlayRect.width / 2;
42431
+ const cy = overlayRect.top + overlayRect.height / 2;
42432
+ return {
42433
+ angleDeg,
42434
+ left: cx - width / 2,
42435
+ top: cy - height / 2,
42436
+ width,
42437
+ height,
42438
+ scaleX,
42439
+ scaleY
42440
+ };
42441
+ }
42442
+ function rotateDeltaIntoFrame(deltaX, deltaY, angleDeg) {
42443
+ if (angleDeg === 0) return { deltaX, deltaY };
42444
+ const rad = -angleDeg * Math.PI / 180;
42445
+ const cos = Math.cos(rad);
42446
+ const sin = Math.sin(rad);
42447
+ return { deltaX: deltaX * cos - deltaY * sin, deltaY: deltaX * sin + deltaY * cos };
42448
+ }
42186
42449
 
42187
42450
  // src/components/editor/domEditOverlayGeometry.ts
42188
42451
  function isElementVisibleForOverlay(el) {
@@ -42501,14 +42764,16 @@ function focusDomEditOverlayElement(element) {
42501
42764
  function resolveDomEditResizeGesture(input) {
42502
42765
  const scaleX = input.scaleX > 0 ? input.scaleX : 1;
42503
42766
  const scaleY = input.scaleY > 0 ? input.scaleY : 1;
42767
+ const contentScaleX = input.contentScaleX !== void 0 && input.contentScaleX > 0 ? input.contentScaleX : 1;
42768
+ const contentScaleY = input.contentScaleY !== void 0 && input.contentScaleY > 0 ? input.contentScaleY : 1;
42504
42769
  if (input.uniform) {
42505
- const deltaX = input.dx / scaleX;
42506
- const deltaY = input.dy / scaleY;
42770
+ const deltaX = input.dx / (scaleX * contentScaleX);
42771
+ const deltaY = input.dy / (scaleY * contentScaleY);
42507
42772
  const delta = Math.abs(deltaX) >= Math.abs(deltaY) ? deltaX : deltaY;
42508
42773
  const side = Math.max(1, Math.max(input.actualWidth, input.actualHeight) + delta);
42509
42774
  return {
42510
- overlayWidth: Math.max(MIN_RESIZE_EDGE_PX, side * scaleX),
42511
- overlayHeight: Math.max(MIN_RESIZE_EDGE_PX, side * scaleY),
42775
+ overlayWidth: Math.max(MIN_RESIZE_EDGE_PX, side * scaleX * contentScaleX),
42776
+ overlayHeight: Math.max(MIN_RESIZE_EDGE_PX, side * scaleY * contentScaleY),
42512
42777
  width: side,
42513
42778
  height: side
42514
42779
  };
@@ -42516,8 +42781,8 @@ function resolveDomEditResizeGesture(input) {
42516
42781
  return {
42517
42782
  overlayWidth: Math.max(MIN_RESIZE_EDGE_PX, input.originWidth + input.dx),
42518
42783
  overlayHeight: Math.max(MIN_RESIZE_EDGE_PX, input.originHeight + input.dy),
42519
- width: Math.max(1, input.actualWidth + input.dx / scaleX),
42520
- height: Math.max(1, input.actualHeight + input.dy / scaleY)
42784
+ width: Math.max(1, input.actualWidth + input.dx / (scaleX * contentScaleX)),
42785
+ height: Math.max(1, input.actualHeight + input.dy / (scaleY * contentScaleY))
42521
42786
  };
42522
42787
  }
42523
42788
  function pointerAngleDegrees(centerX, centerY, x, y) {
@@ -42769,7 +43034,7 @@ function OffCanvasIndicators({
42769
43034
  const selectOffCanvas = async () => {
42770
43035
  const el = elements.current.get(r.key);
42771
43036
  if (!el) return;
42772
- const { resolveDomEditSelection: resolveDomEditSelection2 } = await import("./domEditingLayers-H7LDFIJ7.js");
43037
+ const { resolveDomEditSelection: resolveDomEditSelection2 } = await import("./domEditingLayers-2ECJK24D.js");
42773
43038
  const acp = activeCompositionPathRef.current ?? "index.html";
42774
43039
  const sel = await resolveDomEditSelection2(el, {
42775
43040
  activeCompositionPath: acp,
@@ -43584,8 +43849,26 @@ function startGesture(kind, e, opts, options) {
43584
43849
  return false;
43585
43850
  const size = readStudioBoxSize(sel.element);
43586
43851
  const rotation = { angle: readGsapRotation(sel.element) + readStudioRotation(sel.element).angle };
43587
- const actualWidth = size.width > 0 ? size.width : rect.width / rect.editScaleX;
43588
- const actualHeight = size.height > 0 ? size.height : rect.height / rect.editScaleY;
43852
+ const layoutWidth = sel.element.offsetWidth;
43853
+ const layoutHeight = sel.element.offsetHeight;
43854
+ const actualWidth = size.width > 0 ? size.width : layoutWidth > 0 ? layoutWidth : rect.width / rect.editScaleX;
43855
+ const actualHeight = size.height > 0 ? size.height : layoutHeight > 0 ? layoutHeight : rect.height / rect.editScaleY;
43856
+ const rawContentScaleX = rect.width / (rect.editScaleX * actualWidth);
43857
+ const rawContentScaleY = rect.height / (rect.editScaleY * actualHeight);
43858
+ const contentScaleX = Number.isFinite(rawContentScaleX) && rawContentScaleX > 0 ? rawContentScaleX : 1;
43859
+ const contentScaleY = Number.isFinite(rawContentScaleY) && rawContentScaleY > 0 ? rawContentScaleY : 1;
43860
+ let resizeAnchor;
43861
+ if (kind === "resize") {
43862
+ const startBcr = sel.element.getBoundingClientRect();
43863
+ resizeAnchor = {
43864
+ anchorX: startBcr.x,
43865
+ anchorY: startBcr.y,
43866
+ baseGsapX: readElementGsapNumber(sel.element, "x") ?? 0,
43867
+ baseGsapY: readElementGsapNumber(sel.element, "y") ?? 0,
43868
+ pinX: 0,
43869
+ pinY: 0
43870
+ };
43871
+ }
43589
43872
  let initialPathOffset = captureStudioPathOffset(sel.element);
43590
43873
  let manualEditDragToken;
43591
43874
  let pathOffsetMember;
@@ -43642,6 +43925,9 @@ function startGesture(kind, e, opts, options) {
43642
43925
  actualRotation: rotation.angle,
43643
43926
  editScaleX: rect.editScaleX,
43644
43927
  editScaleY: rect.editScaleY,
43928
+ contentScaleX,
43929
+ contentScaleY,
43930
+ resizeAnchor,
43645
43931
  manualEditDragToken,
43646
43932
  snapContext
43647
43933
  };
@@ -43649,6 +43935,11 @@ function startGesture(kind, e, opts, options) {
43649
43935
  }
43650
43936
 
43651
43937
  // src/components/editor/useDomEditOverlayGestures.ts
43938
+ function restoreResizeAnchorPin(element, g) {
43939
+ const anchor = g.resizeAnchor;
43940
+ if (!anchor || anchor.pinX === 0 && anchor.pinY === 0) return;
43941
+ setElementGsapPosition(element, anchor.baseGsapX, anchor.baseGsapY);
43942
+ }
43652
43943
  function createDomEditOverlayGestureHandlers(opts) {
43653
43944
  const setDraftOverlayRect = (next) => {
43654
43945
  opts.setOverlayRect(next);
@@ -43751,7 +44042,8 @@ function createDomEditOverlayGestureHandlers(opts) {
43751
44042
  actualAngle: g.actualRotation,
43752
44043
  snap: e.shiftKey
43753
44044
  });
43754
- if (!applyRotationDraftViaGsap(sel.element, rotated.angle)) {
44045
+ const draftViaGsap = applyRotationDraftViaGsap(sel.element, rotated.angle);
44046
+ if (!draftViaGsap) {
43755
44047
  applyStudioRotationDraft(sel.element, rotated);
43756
44048
  }
43757
44049
  return;
@@ -43843,11 +44135,27 @@ function createDomEditOverlayGestureHandlers(opts) {
43843
44135
  actualHeight: g.actualHeight,
43844
44136
  scaleX: g.editScaleX,
43845
44137
  scaleY: g.editScaleY,
44138
+ contentScaleX: g.contentScaleX,
44139
+ contentScaleY: g.contentScaleY,
43846
44140
  dx,
43847
44141
  dy,
43848
44142
  uniform: e.shiftKey
43849
44143
  });
43850
44144
  applyStudioBoxSizeDraft(sel.element, nextSize);
44145
+ const anchor = g.resizeAnchor;
44146
+ if (anchor) {
44147
+ const pinned = sel.element.getBoundingClientRect();
44148
+ const nextPinX = anchor.pinX + (anchor.anchorX - pinned.x);
44149
+ const nextPinY = anchor.pinY + (anchor.anchorY - pinned.y);
44150
+ if (setElementGsapPosition(
44151
+ sel.element,
44152
+ anchor.baseGsapX + nextPinX,
44153
+ anchor.baseGsapY + nextPinY
44154
+ )) {
44155
+ anchor.pinX = nextPinX;
44156
+ anchor.pinY = nextPinY;
44157
+ }
44158
+ }
43851
44159
  const overlayEl = opts.overlayRef.current;
43852
44160
  const iframe = opts.iframeRef.current;
43853
44161
  const refreshed = overlayEl && iframe ? toOverlayRect(overlayEl, iframe, sel.element) : null;
@@ -43931,6 +44239,7 @@ function createDomEditOverlayGestureHandlers(opts) {
43931
44239
  return;
43932
44240
  }
43933
44241
  if (g.kind === "resize" && movedDistance < BLOCKED_MOVE_THRESHOLD_PX) {
44242
+ restoreResizeAnchorPin(sel.element, g);
43934
44243
  restoreStudioBoxSize(sel.element, g.initialBoxSize);
43935
44244
  endStudioManualEditGesture(sel.element, g.manualEditDragToken);
43936
44245
  if (box) {
@@ -43957,7 +44266,8 @@ function createDomEditOverlayGestureHandlers(opts) {
43957
44266
  restoreStudioRotation(sel.element, g.initialRotation);
43958
44267
  }
43959
44268
  };
43960
- if (!hasDomEditRotationChanged(g.actualRotation, finalRotation.angle)) {
44269
+ const rotationChanged = hasDomEditRotationChanged(g.actualRotation, finalRotation.angle);
44270
+ if (!rotationChanged) {
43961
44271
  restoreRotation();
43962
44272
  endStudioManualEditGesture(sel.element, g.manualEditDragToken);
43963
44273
  return;
@@ -43965,10 +44275,13 @@ function createDomEditOverlayGestureHandlers(opts) {
43965
44275
  if (!applyRotationDraftViaGsap(sel.element, finalRotation.angle)) {
43966
44276
  applyStudioRotation(sel.element, finalRotation);
43967
44277
  }
43968
- void Promise.resolve(opts.onRotationCommitRef.current(sel, finalRotation)).catch(() => {
44278
+ void Promise.resolve(opts.onRotationCommitRef.current(sel, finalRotation)).catch((error) => {
44279
+ console.error("rotate commit failed", error);
43969
44280
  if (g.manualEditDragToken && isStudioManualEditGestureCurrent(sel.element, g.manualEditDragToken))
43970
44281
  restoreRotation();
43971
- }).finally(() => endStudioManualEditGesture(sel.element, g.manualEditDragToken));
44282
+ }).finally(() => {
44283
+ endStudioManualEditGesture(sel.element, g.manualEditDragToken);
44284
+ });
43972
44285
  } else if (g.kind === "drag") {
43973
44286
  const dx = g.lastSnappedDx ?? e.clientX - g.startX;
43974
44287
  const dy = g.lastSnappedDy ?? e.clientY - g.startY;
@@ -44002,9 +44315,12 @@ function createDomEditOverlayGestureHandlers(opts) {
44002
44315
  opts.suppressNextBoxClickRef.current = true;
44003
44316
  const finalSize = readStudioBoxSize(sel.element);
44004
44317
  applyStudioBoxSize(sel.element, finalSize);
44005
- void Promise.resolve(opts.onBoxSizeCommitRef.current(sel, finalSize)).catch(() => {
44006
- if (g.manualEditDragToken && isStudioManualEditGestureCurrent(sel.element, g.manualEditDragToken))
44318
+ void Promise.resolve(opts.onBoxSizeCommitRef.current(sel, finalSize)).catch((error) => {
44319
+ console.error("resize commit failed", error);
44320
+ if (g.manualEditDragToken && isStudioManualEditGestureCurrent(sel.element, g.manualEditDragToken)) {
44321
+ restoreResizeAnchorPin(sel.element, g);
44007
44322
  restoreStudioBoxSize(sel.element, g.initialBoxSize);
44323
+ }
44008
44324
  }).finally(() => endStudioManualEditGesture(sel.element, g.manualEditDragToken));
44009
44325
  }
44010
44326
  };
@@ -44021,6 +44337,7 @@ function createDomEditOverlayGestureHandlers(opts) {
44021
44337
  restoreGestureOverlayRect(g);
44022
44338
  }
44023
44339
  if (g?.mode === "box-size" && sel) {
44340
+ restoreResizeAnchorPin(sel.element, g);
44024
44341
  restoreStudioBoxSize(sel.element, g.initialBoxSize);
44025
44342
  endStudioManualEditGesture(sel.element, g.manualEditDragToken);
44026
44343
  restoreGestureOverlayRect(g);
@@ -44248,51 +44565,43 @@ function DomEditCropHandles({
44248
44565
  }) {
44249
44566
  const gestureRef = useRef89(null);
44250
44567
  const [dragging, setDragging] = useState79(false);
44251
- const [state, setState] = useState79(() => {
44252
- const parsed = readElementCropInsets(selection.element);
44253
- return {
44254
- element: selection.element,
44255
- insets: {
44256
- top: parsed.top,
44257
- right: parsed.right,
44258
- bottom: parsed.bottom,
44259
- left: parsed.left
44260
- },
44261
- radius: parsed.radius
44262
- };
44263
- });
44568
+ const cropStateFor = (element) => {
44569
+ const parsed = readElementCropInsets(element);
44570
+ const { radius, ...insets } = parsed ?? { top: 0, right: 0, bottom: 0, left: 0, radius: 0 };
44571
+ return { element, croppable: parsed !== null, insets, radius };
44572
+ };
44573
+ const [state, setState] = useState79(() => cropStateFor(selection.element));
44264
44574
  if (state.element !== selection.element) {
44265
- const liveInsets = readElementCropInsets(selection.element);
44266
- setState({
44267
- element: selection.element,
44268
- insets: {
44269
- top: liveInsets.top,
44270
- right: liveInsets.right,
44271
- bottom: liveInsets.bottom,
44272
- left: liveInsets.left
44273
- },
44274
- radius: liveInsets.radius
44275
- });
44575
+ setState(cropStateFor(selection.element));
44276
44576
  }
44277
44577
  const hasCrop = state.insets.top > 0 || state.insets.right > 0 || state.insets.bottom > 0 || state.insets.left > 0;
44278
- const committedRef = useRef89(null);
44279
- committedRef.current = hasCrop ? buildInsetClipPathSides(state.insets, state.radius) : null;
44280
44578
  const liftedRef = useRef89(false);
44579
+ const preLiftInlineClipRef = useRef89("");
44580
+ const committedClipRef = useRef89(null);
44281
44581
  useEffect62(() => {
44282
44582
  const el = selection.element;
44583
+ if (readElementCropInsets(el) === null) return;
44584
+ preLiftInlineClipRef.current = el.style.getPropertyValue("clip-path");
44585
+ committedClipRef.current = null;
44283
44586
  el.style.setProperty("clip-path", "none");
44284
44587
  liftedRef.current = true;
44285
44588
  return () => {
44286
44589
  liftedRef.current = false;
44287
- if (committedRef.current) el.style.setProperty("clip-path", committedRef.current);
44590
+ const committed = committedClipRef.current;
44591
+ const restore = committed !== null ? committed || null : preLiftInlineClipRef.current || null;
44592
+ if (restore) el.style.setProperty("clip-path", restore);
44288
44593
  else el.style.removeProperty("clip-path");
44289
44594
  };
44290
44595
  }, [selection.element]);
44291
- const scaleX = overlayRect.editScaleX > 0 ? overlayRect.editScaleX : 1;
44292
- const scaleY = overlayRect.editScaleY > 0 ? overlayRect.editScaleY : 1;
44293
- const width = overlayRect.width / scaleX;
44294
- const height = overlayRect.height / scaleY;
44295
- const cropRect = cropRectFromInsets(overlayRect, state.insets, scaleX, scaleY);
44596
+ const frame = readElementCropFrame(selection.element, overlayRect);
44597
+ const width = frame.width / frame.scaleX;
44598
+ const height = frame.height / frame.scaleY;
44599
+ const cropRect = cropRectFromInsets(
44600
+ { left: 0, top: 0, width: frame.width, height: frame.height },
44601
+ state.insets,
44602
+ frame.scaleX,
44603
+ frame.scaleY
44604
+ );
44296
44605
  const startCropGesture = (edge, event) => {
44297
44606
  if (!onStyleCommit) return;
44298
44607
  event.preventDefault();
@@ -44304,7 +44613,10 @@ function DomEditCropHandles({
44304
44613
  startX: event.clientX,
44305
44614
  startY: event.clientY,
44306
44615
  startInsets: state.insets,
44307
- didMove: false
44616
+ didMove: false,
44617
+ angleDeg: frame.angleDeg,
44618
+ scaleX: frame.scaleX,
44619
+ scaleY: frame.scaleY
44308
44620
  };
44309
44621
  setDragging(true);
44310
44622
  };
@@ -44313,12 +44625,17 @@ function DomEditCropHandles({
44313
44625
  if (!gesture || gesture.pointerId !== event.pointerId) return;
44314
44626
  event.preventDefault();
44315
44627
  event.stopPropagation();
44628
+ const local = rotateDeltaIntoFrame(
44629
+ event.clientX - gesture.startX,
44630
+ event.clientY - gesture.startY,
44631
+ gesture.angleDeg
44632
+ );
44316
44633
  const drag = {
44317
44634
  startInsets: gesture.startInsets,
44318
- deltaX: event.clientX - gesture.startX,
44319
- deltaY: event.clientY - gesture.startY,
44320
- scaleX,
44321
- scaleY
44635
+ deltaX: local.deltaX,
44636
+ deltaY: local.deltaY,
44637
+ scaleX: gesture.scaleX,
44638
+ scaleY: gesture.scaleY
44322
44639
  };
44323
44640
  const nextInsets = gesture.edge === "move" ? resolveCropInsetFromMoveDrag(drag) : resolveCropInsetFromEdgeDrag({ ...drag, edge: gesture.edge, width, height });
44324
44641
  gesture.didMove = true;
@@ -44336,9 +44653,12 @@ function DomEditCropHandles({
44336
44653
  const reLift = () => {
44337
44654
  if (liftedRef.current) el.style.setProperty("clip-path", "none");
44338
44655
  };
44339
- void Promise.resolve(
44340
- onStyleCommit?.("clip-path", buildInsetClipPathSides(state.insets, state.radius))
44341
- ).then(reLift, reLift);
44656
+ const committedValue = buildInsetClipPathSides(state.insets, state.radius);
44657
+ const cropped = state.insets.top > 0 || state.insets.right > 0 || state.insets.bottom > 0 || state.insets.left > 0;
44658
+ void Promise.resolve(onStyleCommit?.("clip-path", committedValue)).then(() => {
44659
+ committedClipRef.current = cropped ? committedValue : "";
44660
+ reLift();
44661
+ }, reLift);
44342
44662
  };
44343
44663
  const cancelCropGesture = (event) => {
44344
44664
  const gesture = gestureRef.current;
@@ -44349,101 +44669,104 @@ function DomEditCropHandles({
44349
44669
  setDragging(false);
44350
44670
  setState((prev) => ({ ...prev, insets: gesture.startInsets }));
44351
44671
  };
44352
- return /* @__PURE__ */ jsxs81(Fragment30, { children: [
44353
- hasCrop && /* @__PURE__ */ jsx97(
44354
- "div",
44355
- {
44356
- className: "pointer-events-none absolute overflow-hidden",
44357
- style: {
44358
- left: overlayRect.left,
44359
- top: overlayRect.top,
44360
- width: overlayRect.width,
44361
- height: overlayRect.height
44362
- },
44363
- children: /* @__PURE__ */ jsx97(
44672
+ if (!state.croppable) return null;
44673
+ return /* @__PURE__ */ jsxs81(
44674
+ "div",
44675
+ {
44676
+ "data-dom-edit-crop-frame": "true",
44677
+ className: "pointer-events-none absolute",
44678
+ style: {
44679
+ left: frame.left,
44680
+ top: frame.top,
44681
+ width: frame.width,
44682
+ height: frame.height,
44683
+ transform: frame.angleDeg !== 0 ? `rotate(${frame.angleDeg}deg)` : void 0
44684
+ },
44685
+ children: [
44686
+ hasCrop && /* @__PURE__ */ jsx97("div", { className: "pointer-events-none absolute inset-0 overflow-hidden", children: /* @__PURE__ */ jsx97(
44364
44687
  "div",
44365
44688
  {
44366
44689
  className: "absolute",
44367
44690
  style: {
44368
- left: cropRect.left - overlayRect.left,
44369
- top: cropRect.top - overlayRect.top,
44691
+ left: cropRect.left,
44692
+ top: cropRect.top,
44370
44693
  width: cropRect.width,
44371
44694
  height: cropRect.height,
44372
44695
  boxShadow: "0 0 0 100000px rgba(8, 8, 12, 0.6)"
44373
44696
  }
44374
44697
  }
44375
- )
44376
- }
44377
- ),
44378
- /* @__PURE__ */ jsx97(
44379
- "div",
44380
- {
44381
- "aria-hidden": "true",
44382
- className: "pointer-events-none absolute border border-dashed border-studio-accent",
44383
- style: {
44384
- left: cropRect.left,
44385
- top: cropRect.top,
44386
- width: cropRect.width,
44387
- height: cropRect.height
44388
- },
44389
- children: dragging && /* @__PURE__ */ jsxs81(Fragment30, { children: [
44390
- /* @__PURE__ */ jsx97("div", { className: "absolute inset-y-0 left-1/3 w-px bg-studio-accent/40" }),
44391
- /* @__PURE__ */ jsx97("div", { className: "absolute inset-y-0 left-2/3 w-px bg-studio-accent/40" }),
44392
- /* @__PURE__ */ jsx97("div", { className: "absolute inset-x-0 top-1/3 h-px bg-studio-accent/40" }),
44393
- /* @__PURE__ */ jsx97("div", { className: "absolute inset-x-0 top-2/3 h-px bg-studio-accent/40" })
44394
- ] })
44395
- }
44396
- ),
44397
- hasCrop && /* @__PURE__ */ jsx97(
44398
- "button",
44399
- {
44400
- type: "button",
44401
- "aria-label": "Reposition crop",
44402
- "data-dom-edit-crop-handle": "true",
44403
- className: "pointer-events-auto absolute rounded-full border-2 border-studio-accent bg-studio-accent/30 shadow-[0_0_0_1px_rgba(0,0,0,0.4)]",
44404
- style: {
44405
- left: cropRect.left + cropRect.width / 2,
44406
- top: cropRect.top + cropRect.height / 2,
44407
- width: 22,
44408
- height: 22,
44409
- transform: "translate(-50%, -50%)",
44410
- cursor: "move",
44411
- touchAction: "none"
44412
- },
44413
- onPointerDown: (event) => startCropGesture("move", event),
44414
- onPointerMove: updateCropGesture,
44415
- onPointerUp: finishCropGesture,
44416
- onPointerCancel: cancelCropGesture
44417
- }
44418
- ),
44419
- EDGES.map((edge) => {
44420
- const vertical = edge === "left" || edge === "right";
44421
- const place = edgeHandlePlacement(edge, cropRect);
44422
- return /* @__PURE__ */ jsx97(
44423
- "button",
44424
- {
44425
- type: "button",
44426
- "aria-label": `Crop ${edge}`,
44427
- "data-dom-edit-crop-handle": "true",
44428
- className: "pointer-events-auto absolute rounded-full bg-studio-accent shadow-[0_0_0_1px_rgba(0,0,0,0.4)]",
44429
- style: {
44430
- left: place.left,
44431
- top: place.top,
44432
- width: vertical ? 5 : 26,
44433
- height: vertical ? 26 : 5,
44434
- transform: place.transform,
44435
- cursor: vertical ? "ew-resize" : "ns-resize",
44436
- touchAction: "none"
44437
- },
44438
- onPointerDown: (event) => startCropGesture(edge, event),
44439
- onPointerMove: updateCropGesture,
44440
- onPointerUp: finishCropGesture,
44441
- onPointerCancel: cancelCropGesture
44442
- },
44443
- edge
44444
- );
44445
- })
44446
- ] });
44698
+ ) }),
44699
+ /* @__PURE__ */ jsx97(
44700
+ "div",
44701
+ {
44702
+ "aria-hidden": "true",
44703
+ className: "pointer-events-none absolute border border-dashed border-studio-accent",
44704
+ style: {
44705
+ left: cropRect.left,
44706
+ top: cropRect.top,
44707
+ width: cropRect.width,
44708
+ height: cropRect.height
44709
+ },
44710
+ children: dragging && /* @__PURE__ */ jsxs81(Fragment30, { children: [
44711
+ /* @__PURE__ */ jsx97("div", { className: "absolute inset-y-0 left-1/3 w-px bg-studio-accent/40" }),
44712
+ /* @__PURE__ */ jsx97("div", { className: "absolute inset-y-0 left-2/3 w-px bg-studio-accent/40" }),
44713
+ /* @__PURE__ */ jsx97("div", { className: "absolute inset-x-0 top-1/3 h-px bg-studio-accent/40" }),
44714
+ /* @__PURE__ */ jsx97("div", { className: "absolute inset-x-0 top-2/3 h-px bg-studio-accent/40" })
44715
+ ] })
44716
+ }
44717
+ ),
44718
+ hasCrop && /* @__PURE__ */ jsx97(
44719
+ "button",
44720
+ {
44721
+ type: "button",
44722
+ "aria-label": "Reposition crop",
44723
+ "data-dom-edit-crop-handle": "true",
44724
+ className: "pointer-events-auto absolute rounded-full border-2 border-studio-accent bg-studio-accent/30 shadow-[0_0_0_1px_rgba(0,0,0,0.4)]",
44725
+ style: {
44726
+ left: cropRect.left + cropRect.width / 2,
44727
+ top: cropRect.top + cropRect.height / 2,
44728
+ width: 22,
44729
+ height: 22,
44730
+ transform: "translate(-50%, -50%)",
44731
+ cursor: "move",
44732
+ touchAction: "none"
44733
+ },
44734
+ onPointerDown: (event) => startCropGesture("move", event),
44735
+ onPointerMove: updateCropGesture,
44736
+ onPointerUp: finishCropGesture,
44737
+ onPointerCancel: cancelCropGesture
44738
+ }
44739
+ ),
44740
+ EDGES.map((edge) => {
44741
+ const vertical = edge === "left" || edge === "right";
44742
+ const place = edgeHandlePlacement(edge, cropRect);
44743
+ return /* @__PURE__ */ jsx97(
44744
+ "button",
44745
+ {
44746
+ type: "button",
44747
+ "aria-label": `Crop ${edge}`,
44748
+ "data-dom-edit-crop-handle": "true",
44749
+ className: "pointer-events-auto absolute rounded-full bg-studio-accent shadow-[0_0_0_1px_rgba(0,0,0,0.4)]",
44750
+ style: {
44751
+ left: place.left,
44752
+ top: place.top,
44753
+ width: vertical ? 5 : 26,
44754
+ height: vertical ? 26 : 5,
44755
+ transform: place.transform,
44756
+ cursor: vertical ? "ew-resize" : "ns-resize",
44757
+ touchAction: "none"
44758
+ },
44759
+ onPointerDown: (event) => startCropGesture(edge, event),
44760
+ onPointerMove: updateCropGesture,
44761
+ onPointerUp: finishCropGesture,
44762
+ onPointerCancel: cancelCropGesture
44763
+ },
44764
+ edge
44765
+ );
44766
+ })
44767
+ ]
44768
+ }
44769
+ );
44447
44770
  }
44448
44771
 
44449
44772
  // src/components/editor/DomEditRotateHandle.tsx
@@ -44891,6 +45214,14 @@ var DomEditOverlay = memo35(function DomEditOverlay2({
44891
45214
  const target = event.target;
44892
45215
  if (target?.closest('[data-dom-edit-selection-box="true"]')) return;
44893
45216
  if (!hoverSelectionRef.current && onMarqueeSelectRef.current && compRect.width > 0) {
45217
+ const iframe = iframeRef.current;
45218
+ const freshTarget = iframe ? getPreviewTargetFromPointer(
45219
+ iframe,
45220
+ event.clientX,
45221
+ event.clientY,
45222
+ activeCompositionPathRef.current
45223
+ ) : null;
45224
+ if (freshTarget) return;
44894
45225
  const overlayEl = overlayRef.current;
44895
45226
  if (overlayEl) {
44896
45227
  const oRect = overlayEl.getBoundingClientRect();