hyperframes 0.7.24 → 0.7.26

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.
@@ -1305,6 +1305,8 @@ var usePlayerStore = create((set, get) => ({
1305
1305
  setMotionPathArmed: (armed) => set({ motionPathArmed: armed }),
1306
1306
  motionPathCreateAvailable: false,
1307
1307
  setMotionPathCreateAvailable: (available) => set({ motionPathCreateAvailable: available }),
1308
+ autoKeyframeEnabled: true,
1309
+ setAutoKeyframeEnabled: (enabled) => set({ autoKeyframeEnabled: enabled }),
1308
1310
  selectedElementIds: /* @__PURE__ */ new Set(),
1309
1311
  toggleSelectedElementId: (id) => set((s) => {
1310
1312
  const next = new Set(s.selectedElementIds);
@@ -4902,10 +4904,18 @@ var TimelineClipDiamonds = memo6(function TimelineClipDiamonds2({
4902
4904
  onClickKeyframe,
4903
4905
  onShiftClickKeyframe,
4904
4906
  onContextMenuKeyframe,
4905
- onMoveKeyframe
4907
+ onMoveKeyframe,
4908
+ suppressClickRef
4906
4909
  }) {
4907
4910
  const dragRef = useRef11(null);
4908
4911
  const [preview, setPreview] = useState10(null);
4912
+ const suppressNextClick = () => {
4913
+ if (!suppressClickRef) return;
4914
+ suppressClickRef.current = true;
4915
+ requestAnimationFrame(() => {
4916
+ suppressClickRef.current = false;
4917
+ });
4918
+ };
4909
4919
  if (clipWidthPx < 20) return null;
4910
4920
  const diamondSize = Math.round(clipHeightPx * (beatsActive ? 0.45 : DIAMOND_RATIO));
4911
4921
  const half = diamondSize / 2;
@@ -4915,151 +4925,171 @@ var TimelineClipDiamonds = memo6(function TimelineClipDiamonds2({
4915
4925
  const baseColor = isSelected ? accentColor : "#a3a3a3";
4916
4926
  const baseOpacity = isSelected ? 0.4 : 0.25;
4917
4927
  const canDrag = isSelected && !!onMoveKeyframe;
4918
- return /* @__PURE__ */ jsxs11("div", { className: "absolute inset-0", style: { zIndex: 3, pointerEvents: "none" }, children: [
4919
- sorted.map((kf, i) => {
4920
- if (i === 0) return null;
4921
- const prev = sorted[i - 1];
4922
- const x1 = Math.max(0, Math.min(clipWidthPx, prev.percentage / 100 * clipWidthPx));
4923
- const x2 = Math.max(0, Math.min(clipWidthPx, kf.percentage / 100 * clipWidthPx));
4924
- if (x2 - x1 < 1) return null;
4925
- return /* @__PURE__ */ jsx14(
4926
- "div",
4927
- {
4928
- className: "absolute",
4929
- style: {
4930
- left: x1,
4931
- top: centerY,
4932
- width: x2 - x1,
4933
- height: 2,
4934
- transform: "translateY(-1px)",
4935
- background: baseColor,
4936
- opacity: baseOpacity,
4937
- borderRadius: 1
4938
- }
4939
- },
4940
- `line-${i}-${prev.percentage}-${kf.percentage}`
4941
- );
4942
- }),
4943
- sorted.map((kf, i) => {
4944
- const kfKey = `${elementId}:${kf.percentage}`;
4945
- const renderPct = preview?.kfKey === kfKey ? preview.clipPct : kf.percentage;
4946
- const leftPx = renderPct / 100 * clipWidthPx - half;
4947
- const isKfSelected = selectedKeyframes.has(kfKey);
4948
- const atPlayhead = isSelected && Math.abs(kf.percentage - currentPercentage) < 0.5;
4949
- const isHighlighted = isKfSelected || atPlayhead;
4950
- const color = isHighlighted ? accentColor : "#a3a3a3";
4951
- const onPointerDown = (e) => {
4952
- if (e.button !== 0) return;
4953
- e.stopPropagation();
4954
- if (canDrag) {
4955
- e.currentTarget.setPointerCapture?.(e.pointerId);
4956
- dragRef.current = {
4957
- kfKey,
4958
- startX: e.clientX,
4959
- fromClipPct: kf.percentage,
4960
- moved: false
4928
+ return /* @__PURE__ */ jsxs11(
4929
+ "div",
4930
+ {
4931
+ className: "absolute inset-0",
4932
+ style: {
4933
+ // Above the clip's trim-handle strips (TimelineClip.tsx, z-index 4) so
4934
+ // a keyframe sitting in the first/last ~14px of the clip stays
4935
+ // clickable instead of being covered by the resize handle. This div
4936
+ // establishes its own stacking context (position + z-index), so the
4937
+ // diamonds' own z-index (1/2) can't escape it on their own — the bump
4938
+ // has to happen here.
4939
+ zIndex: 5,
4940
+ pointerEvents: "none"
4941
+ },
4942
+ children: [
4943
+ sorted.map((kf, i) => {
4944
+ if (i === 0) return null;
4945
+ const prev = sorted[i - 1];
4946
+ const x1 = Math.max(0, Math.min(clipWidthPx, prev.percentage / 100 * clipWidthPx));
4947
+ const x2 = Math.max(0, Math.min(clipWidthPx, kf.percentage / 100 * clipWidthPx));
4948
+ if (x2 - x1 < 1) return null;
4949
+ return /* @__PURE__ */ jsx14(
4950
+ "div",
4951
+ {
4952
+ className: "absolute",
4953
+ style: {
4954
+ left: x1,
4955
+ top: centerY,
4956
+ width: x2 - x1,
4957
+ height: 2,
4958
+ transform: "translateY(-1px)",
4959
+ background: baseColor,
4960
+ opacity: baseOpacity,
4961
+ borderRadius: 1
4962
+ }
4963
+ },
4964
+ `line-${i}-${prev.percentage}-${kf.percentage}`
4965
+ );
4966
+ }),
4967
+ sorted.map((kf, i) => {
4968
+ const kfKey = `${elementId}:${kf.percentage}`;
4969
+ const renderPct = preview?.kfKey === kfKey ? preview.clipPct : kf.percentage;
4970
+ const leftPx = renderPct / 100 * clipWidthPx - half;
4971
+ const isKfSelected = selectedKeyframes.has(kfKey);
4972
+ const atPlayhead = isSelected && Math.abs(kf.percentage - currentPercentage) < 0.5;
4973
+ const isHighlighted = isKfSelected || atPlayhead;
4974
+ const color = isHighlighted ? accentColor : "#a3a3a3";
4975
+ const onPointerDown = (e) => {
4976
+ if (e.button !== 0) return;
4977
+ e.stopPropagation();
4978
+ if (canDrag) {
4979
+ e.currentTarget.setPointerCapture?.(e.pointerId);
4980
+ dragRef.current = {
4981
+ kfKey,
4982
+ startX: e.clientX,
4983
+ fromClipPct: kf.percentage,
4984
+ moved: false
4985
+ };
4986
+ }
4961
4987
  };
4962
- }
4963
- };
4964
- const onPointerMove = (e) => {
4965
- const d = dragRef.current;
4966
- if (!d || d.kfKey !== kfKey) return;
4967
- if (!d.moved && Math.abs(e.clientX - d.startX) >= KEYFRAME_DRAG_THRESHOLD_PX) {
4968
- d.moved = true;
4969
- }
4970
- if (d.moved) {
4971
- setPreview({
4972
- kfKey,
4973
- clipPct: previewClipPct({
4988
+ const onPointerMove = (e) => {
4989
+ const d = dragRef.current;
4990
+ if (!d || d.kfKey !== kfKey) return;
4991
+ if (!d.moved && Math.abs(e.clientX - d.startX) >= KEYFRAME_DRAG_THRESHOLD_PX) {
4992
+ d.moved = true;
4993
+ }
4994
+ if (d.moved) {
4995
+ setPreview({
4996
+ kfKey,
4997
+ clipPct: previewClipPct({
4998
+ pointerDownX: d.startX,
4999
+ pointerMoveX: e.clientX,
5000
+ clipWidthPx,
5001
+ draggedClipPct: d.fromClipPct,
5002
+ draggedIndex: i,
5003
+ sortedClipPcts
5004
+ })
5005
+ });
5006
+ }
5007
+ };
5008
+ const onPointerUp = (e) => {
5009
+ const d = dragRef.current;
5010
+ if (!d || d.kfKey !== kfKey) {
5011
+ if (e.button !== 0) return;
5012
+ suppressNextClick();
5013
+ if (e.shiftKey) onShiftClickKeyframe?.(elementId, kf.percentage);
5014
+ else onClickKeyframe?.(kf.percentage);
5015
+ return;
5016
+ }
5017
+ e.stopPropagation();
5018
+ dragRef.current = null;
5019
+ setPreview(null);
5020
+ e.currentTarget.releasePointerCapture?.(e.pointerId);
5021
+ suppressNextClick();
5022
+ const res = resolveKeyframeDrag({
4974
5023
  pointerDownX: d.startX,
4975
- pointerMoveX: e.clientX,
5024
+ pointerUpX: e.clientX,
4976
5025
  clipWidthPx,
4977
5026
  draggedClipPct: d.fromClipPct,
4978
5027
  draggedIndex: i,
4979
5028
  sortedClipPcts
4980
- })
4981
- });
4982
- }
4983
- };
4984
- const onPointerUp = (e) => {
4985
- const d = dragRef.current;
4986
- if (!d || d.kfKey !== kfKey) {
4987
- if (e.shiftKey) onShiftClickKeyframe?.(elementId, kf.percentage);
4988
- else onClickKeyframe?.(kf.percentage);
4989
- return;
4990
- }
4991
- e.stopPropagation();
4992
- dragRef.current = null;
4993
- setPreview(null);
4994
- e.currentTarget.releasePointerCapture?.(e.pointerId);
4995
- const res = resolveKeyframeDrag({
4996
- pointerDownX: d.startX,
4997
- pointerUpX: e.clientX,
4998
- clipWidthPx,
4999
- draggedClipPct: d.fromClipPct,
5000
- draggedIndex: i,
5001
- sortedClipPcts
5002
- });
5003
- if (res.kind === "click") {
5004
- if (e.shiftKey) onShiftClickKeyframe?.(elementId, kf.percentage);
5005
- else onClickKeyframe?.(kf.percentage);
5006
- } else if (res.kind === "move" && res.toClipPct != null) {
5007
- onMoveKeyframe?.(elementId, d.fromClipPct, res.toClipPct);
5008
- }
5009
- };
5010
- return /* @__PURE__ */ jsx14(
5011
- "button",
5012
- {
5013
- type: "button",
5014
- className: "absolute",
5015
- style: {
5016
- left: leftPx,
5017
- top: centerY,
5018
- transform: "translateY(-50%)",
5019
- width: diamondSize,
5020
- height: diamondSize,
5021
- zIndex: isHighlighted ? 2 : 1,
5022
- pointerEvents: "auto",
5023
- background: "none",
5024
- border: "none",
5025
- cursor: canDrag ? "ew-resize" : "pointer",
5026
- padding: 0,
5027
- touchAction: "none"
5028
- },
5029
- onPointerDown,
5030
- onPointerMove,
5031
- onPointerUp,
5032
- onContextMenu: (e) => {
5033
- e.preventDefault();
5034
- e.stopPropagation();
5035
- onContextMenuKeyframe?.(e, elementId, kf.percentage);
5036
- },
5037
- title: `${kf.percentage}%`,
5038
- children: /* @__PURE__ */ jsxs11("svg", { width: diamondSize, height: diamondSize, viewBox: "0 0 10 10", children: [
5039
- isKfSelected && /* @__PURE__ */ jsx14(
5040
- "path",
5041
- {
5042
- d: "M5 0L10 5L5 10L0 5Z",
5043
- fill: "none",
5044
- stroke: accentColor,
5045
- strokeWidth: "0.8",
5046
- opacity: 0.5
5047
- }
5048
- ),
5049
- /* @__PURE__ */ jsx14(
5050
- "path",
5051
- {
5052
- d: "M5 1L9 5L5 9L1 5Z",
5053
- fill: color,
5054
- opacity: isKfSelected || atPlayhead ? 1 : 0.55
5055
- }
5056
- )
5057
- ] })
5058
- },
5059
- `${i}-${kf.percentage}`
5060
- );
5061
- })
5062
- ] });
5029
+ });
5030
+ if (res.kind === "click" || res.kind === "noop") {
5031
+ if (e.shiftKey) onShiftClickKeyframe?.(elementId, kf.percentage);
5032
+ else onClickKeyframe?.(kf.percentage);
5033
+ } else if (res.kind === "move" && res.toClipPct != null) {
5034
+ onMoveKeyframe?.(elementId, d.fromClipPct, res.toClipPct);
5035
+ onClickKeyframe?.(res.toClipPct);
5036
+ }
5037
+ };
5038
+ return /* @__PURE__ */ jsx14(
5039
+ "button",
5040
+ {
5041
+ type: "button",
5042
+ className: "absolute",
5043
+ style: {
5044
+ left: leftPx,
5045
+ top: centerY,
5046
+ transform: "translateY(-50%)",
5047
+ width: diamondSize,
5048
+ height: diamondSize,
5049
+ zIndex: isHighlighted ? 2 : 1,
5050
+ pointerEvents: "auto",
5051
+ background: "none",
5052
+ border: "none",
5053
+ cursor: canDrag ? "ew-resize" : "pointer",
5054
+ padding: 0,
5055
+ touchAction: "none"
5056
+ },
5057
+ onPointerDown,
5058
+ onPointerMove,
5059
+ onPointerUp,
5060
+ onContextMenu: (e) => {
5061
+ e.preventDefault();
5062
+ e.stopPropagation();
5063
+ onContextMenuKeyframe?.(e, elementId, kf.percentage);
5064
+ },
5065
+ title: `${kf.percentage}%`,
5066
+ children: /* @__PURE__ */ jsxs11("svg", { width: diamondSize, height: diamondSize, viewBox: "0 0 10 10", children: [
5067
+ isKfSelected && /* @__PURE__ */ jsx14(
5068
+ "path",
5069
+ {
5070
+ d: "M5 0L10 5L5 10L0 5Z",
5071
+ fill: "none",
5072
+ stroke: accentColor,
5073
+ strokeWidth: "0.8",
5074
+ opacity: 0.5
5075
+ }
5076
+ ),
5077
+ /* @__PURE__ */ jsx14(
5078
+ "path",
5079
+ {
5080
+ d: "M5 1L9 5L5 9L1 5Z",
5081
+ fill: color,
5082
+ opacity: isKfSelected || atPlayhead ? 1 : 0.55
5083
+ }
5084
+ )
5085
+ ] })
5086
+ },
5087
+ `${i}-${kf.percentage}`
5088
+ );
5089
+ })
5090
+ ]
5091
+ }
5092
+ );
5063
5093
  });
5064
5094
 
5065
5095
  // src/player/components/TimelineRuler.tsx
@@ -6184,7 +6214,8 @@ var TimelineCanvas = memo8(function TimelineCanvas2({
6184
6214
  onClickKeyframe: (pct) => onClickKeyframe?.(previewElement, pct),
6185
6215
  onShiftClickKeyframe,
6186
6216
  onContextMenuKeyframe,
6187
- onMoveKeyframe
6217
+ onMoveKeyframe,
6218
+ suppressClickRef
6188
6219
  }
6189
6220
  )
6190
6221
  ]
@@ -7214,8 +7245,6 @@ var Timeline = memo11(function Timeline2({
7214
7245
  if (el) {
7215
7246
  setSelectedElementId(elId);
7216
7247
  onSelectElement?.(el);
7217
- const absTime = el.start + pct / 100 * el.duration;
7218
- onSeek?.(absTime);
7219
7248
  }
7220
7249
  const kfData = keyframeCache.get(elId);
7221
7250
  const kf = kfData?.keyframes.find((k) => Math.abs(k.percentage - pct) < 0.2);
@@ -9813,7 +9842,7 @@ function verifyTimelinesPopulated(win, targetKeys) {
9813
9842
  }
9814
9843
  return Object.keys(timelines).filter((k) => k !== "__proxied").length > 0;
9815
9844
  }
9816
- function applySoftReload(iframe, scriptText, onAsyncFailure) {
9845
+ function applySoftReload(iframe, scriptText, onAsyncFailure, currentTimeOverride) {
9817
9846
  if (!iframe || !scriptText) return "cannot-soft-reload";
9818
9847
  const win = iframe.contentWindow;
9819
9848
  const doc = iframe.contentDocument;
@@ -9830,7 +9859,7 @@ function applySoftReload(iframe, scriptText, onAsyncFailure) {
9830
9859
  })
9831
9860
  );
9832
9861
  if (gsapScripts.length > 1 && staleScripts.length === 0) return "cannot-soft-reload";
9833
- const currentTime = win.__player?.getTime?.() ?? 0;
9862
+ const currentTime = currentTimeOverride ?? win.__player?.getTime?.() ?? 0;
9834
9863
  let deferredToAsync = false;
9835
9864
  const doReload = () => {
9836
9865
  const timelines = win.__timelines;
@@ -9889,8 +9918,8 @@ function applySoftReload(iframe, scriptText, onAsyncFailure) {
9889
9918
  s.textContent = `(function(){${scriptText}
9890
9919
  })();`;
9891
9920
  doc.body.appendChild(s);
9892
- win.__hfForceTimelineRebind?.();
9893
9921
  win.__player?.seek?.(currentTime);
9922
+ win.__hfForceTimelineRebind?.();
9894
9923
  win.__hfStudioManualEditsApply?.();
9895
9924
  };
9896
9925
  const needsMotionPath = /motionPath\s*[:{]/.test(scriptText);
@@ -26987,7 +27016,8 @@ async function mutateGsapScript(projectId, sourceFile, mutation) {
26987
27016
  return result;
26988
27017
  }
26989
27018
  function softReloadOrEscalate(iframe, scriptText, reloadPreview, origin) {
26990
- const result = applySoftReload(iframe, scriptText, reloadPreview);
27019
+ const currentTime = usePlayerStore.getState().currentTime;
27020
+ const result = applySoftReload(iframe, scriptText, reloadPreview, currentTime);
26991
27021
  if (result === "applied") return;
26992
27022
  trackStudioEvent("gsap_soft_reload_outcome", {
26993
27023
  origin,
@@ -27950,6 +27980,102 @@ async function commitGsapPositionFromDrag(selection, anim, studioOffset, gsapPos
27950
27980
  }
27951
27981
  }
27952
27982
 
27983
+ // src/hooks/gsapTweenSynth.ts
27984
+ function deduplicateKeyframes(keyframes) {
27985
+ const byPct = /* @__PURE__ */ new Map();
27986
+ for (const kf of keyframes) {
27987
+ const existing = byPct.get(kf.percentage);
27988
+ if (existing) {
27989
+ existing.properties = { ...existing.properties, ...kf.properties };
27990
+ if (kf.ease) existing.ease = kf.ease;
27991
+ } else {
27992
+ byPct.set(kf.percentage, { ...kf, properties: { ...kf.properties } });
27993
+ }
27994
+ }
27995
+ return Array.from(byPct.values()).sort((a, b) => a.percentage - b.percentage);
27996
+ }
27997
+ function synthesizeFlatTweenKeyframes(anim) {
27998
+ const hasImmediateRenderHold = anim.extras?.immediateRender === "__raw:true";
27999
+ if (anim.method === "set" || anim.duration === 0 && hasImmediateRenderHold) {
28000
+ return null;
28001
+ }
28002
+ const toProps = anim.properties;
28003
+ const fromProps = anim.fromProperties;
28004
+ if (!toProps || Object.keys(toProps).length === 0) return null;
28005
+ const startProps = {};
28006
+ const endProps = {};
28007
+ if (anim.method === "from") {
28008
+ for (const [k, v] of Object.entries(toProps)) {
28009
+ startProps[k] = v;
28010
+ endProps[k] = PROPERTY_DEFAULTS[k] ?? 0;
28011
+ }
28012
+ } else if (anim.method === "fromTo" && fromProps) {
28013
+ Object.assign(startProps, fromProps);
28014
+ Object.assign(endProps, toProps);
28015
+ } else {
28016
+ for (const [k, v] of Object.entries(toProps)) {
28017
+ startProps[k] = PROPERTY_DEFAULTS[k] ?? 0;
28018
+ endProps[k] = v;
28019
+ }
28020
+ }
28021
+ return {
28022
+ format: "percentage",
28023
+ keyframes: [
28024
+ { percentage: 0, properties: startProps },
28025
+ { percentage: 100, properties: endProps }
28026
+ ],
28027
+ ...anim.ease ? { ease: anim.ease } : {}
28028
+ };
28029
+ }
28030
+
28031
+ // src/hooks/gsapWholePropertyOffsetCommit.ts
28032
+ async function commitWholePropertyOffset(selection, anim, newValues, currentPct, iframe, callbacks, label) {
28033
+ let effectiveAnim = anim;
28034
+ if (anim.keyframes) {
28035
+ const newId = await materializeIfDynamic(anim, iframe, callbacks.commitMutation, selection);
28036
+ if (newId) effectiveAnim = { ...anim, id: newId };
28037
+ }
28038
+ const ts = resolveTweenStart(effectiveAnim);
28039
+ const td = resolveTweenDuration(effectiveAnim);
28040
+ const ease = effectiveAnim.keyframes?.easeEach ?? effectiveAnim.ease;
28041
+ const keys = Object.keys(newValues);
28042
+ const at = (props, key) => typeof props[key] === "number" ? props[key] : PROPERTY_DEFAULTS[key] ?? 0;
28043
+ const kfs = effectiveAnim.keyframes?.keyframes ?? synthesizeFlatTweenKeyframes(effectiveAnim)?.keyframes;
28044
+ if (!kfs || kfs.length === 0) {
28045
+ await callbacks.commitMutation(
28046
+ selection,
28047
+ { type: "update-properties", animationId: effectiveAnim.id, properties: newValues },
28048
+ { label, softReload: true }
28049
+ );
28050
+ return;
28051
+ }
28052
+ const nearest = kfs.reduce(
28053
+ (best, kf) => Math.abs(kf.percentage - currentPct) < Math.abs(best.percentage - currentPct) ? kf : best
28054
+ );
28055
+ const shifted = kfs.map((kf) => {
28056
+ const properties = { ...kf.properties };
28057
+ for (const key of keys) {
28058
+ properties[key] = roundTo3(
28059
+ at(properties, key) + (newValues[key] - at(nearest.properties, key))
28060
+ );
28061
+ }
28062
+ return { percentage: kf.percentage, properties, ...kf.ease ? { ease: kf.ease } : {} };
28063
+ });
28064
+ await callbacks.commitMutation(
28065
+ selection,
28066
+ {
28067
+ type: "replace-with-keyframes",
28068
+ animationId: effectiveAnim.id,
28069
+ targetSelector: effectiveAnim.targetSelector,
28070
+ position: roundTo3(ts ?? 0),
28071
+ duration: roundTo3(td || 1),
28072
+ keyframes: shifted,
28073
+ ease
28074
+ },
28075
+ { label, softReload: true }
28076
+ );
28077
+ }
28078
+
27953
28079
  // src/hooks/gsapPositionDetection.ts
27954
28080
  function readGsapPositionFromIframe(iframe, elementSelector) {
27955
28081
  const gsap2 = getIframeGsap(iframe);
@@ -28110,7 +28236,8 @@ async function tryGsapDragIntercept(selection, offset, animations, iframe, commi
28110
28236
  }
28111
28237
  }
28112
28238
  const cbs = { commitMutation, fetchAnimations: fetchFallbackAnimations };
28113
- if (options?.altKey) {
28239
+ const autoKeyframeEnabled = usePlayerStore.getState().autoKeyframeEnabled;
28240
+ if (options?.altKey || !autoKeyframeEnabled) {
28114
28241
  await commitWholePathOffset(selection, posAnim, offset, gsapPos, iframe, selector, cbs);
28115
28242
  } else {
28116
28243
  await commitGsapPositionFromDrag(selection, posAnim, offset, gsapPos, iframe, selector, cbs);
@@ -28182,6 +28309,19 @@ async function tryGsapResizeIntercept(selection, size, animations, iframe, commi
28182
28309
  height: Math.round(size.height)
28183
28310
  };
28184
28311
  }
28312
+ if (!usePlayerStore.getState().autoKeyframeEnabled) {
28313
+ if (activeKeyframePct != null) setActiveKeyframePct(null);
28314
+ await commitWholePropertyOffset(
28315
+ selection,
28316
+ anim,
28317
+ resizeProps,
28318
+ pct,
28319
+ iframe,
28320
+ { commitMutation, fetchAnimations: fetchFallbackAnimations },
28321
+ "Resize animation"
28322
+ );
28323
+ return true;
28324
+ }
28185
28325
  const ct = usePlayerStore.getState().currentTime;
28186
28326
  const ts = resolveTweenStart(anim);
28187
28327
  const td = resolveTweenDuration(anim);
@@ -28296,6 +28436,18 @@ async function tryGsapRotationIntercept(selection, angle, animations, iframe, co
28296
28436
  return true;
28297
28437
  }
28298
28438
  const pct = computeCurrentPercentage(selection, anim);
28439
+ if (!usePlayerStore.getState().autoKeyframeEnabled) {
28440
+ await commitWholePropertyOffset(
28441
+ selection,
28442
+ anim,
28443
+ { rotation: newRotation },
28444
+ pct,
28445
+ iframe,
28446
+ { commitMutation, fetchAnimations: fetchFallbackAnimations },
28447
+ "Rotate animation"
28448
+ );
28449
+ return true;
28450
+ }
28299
28451
  if (anim.hasUnresolvedKeyframes || anim.hasUnresolvedSelector) {
28300
28452
  const newId = await materializeIfDynamic(anim, iframe, commitMutation, selection);
28301
28453
  if (newId) anim = { ...anim, id: newId };
@@ -28327,53 +28479,6 @@ async function tryGsapRotationIntercept(selection, angle, animations, iframe, co
28327
28479
  return true;
28328
28480
  }
28329
28481
 
28330
- // src/hooks/gsapTweenSynth.ts
28331
- function deduplicateKeyframes(keyframes) {
28332
- const byPct = /* @__PURE__ */ new Map();
28333
- for (const kf of keyframes) {
28334
- const existing = byPct.get(kf.percentage);
28335
- if (existing) {
28336
- existing.properties = { ...existing.properties, ...kf.properties };
28337
- if (kf.ease) existing.ease = kf.ease;
28338
- } else {
28339
- byPct.set(kf.percentage, { ...kf, properties: { ...kf.properties } });
28340
- }
28341
- }
28342
- return Array.from(byPct.values()).sort((a, b) => a.percentage - b.percentage);
28343
- }
28344
- function synthesizeFlatTweenKeyframes(anim) {
28345
- if (anim.method === "set") {
28346
- return null;
28347
- }
28348
- const toProps = anim.properties;
28349
- const fromProps = anim.fromProperties;
28350
- if (!toProps || Object.keys(toProps).length === 0) return null;
28351
- const startProps = {};
28352
- const endProps = {};
28353
- if (anim.method === "from") {
28354
- for (const [k, v] of Object.entries(toProps)) {
28355
- startProps[k] = v;
28356
- endProps[k] = PROPERTY_DEFAULTS[k] ?? 0;
28357
- }
28358
- } else if (anim.method === "fromTo" && fromProps) {
28359
- Object.assign(startProps, fromProps);
28360
- Object.assign(endProps, toProps);
28361
- } else {
28362
- for (const [k, v] of Object.entries(toProps)) {
28363
- startProps[k] = PROPERTY_DEFAULTS[k] ?? 0;
28364
- endProps[k] = v;
28365
- }
28366
- }
28367
- return {
28368
- format: "percentage",
28369
- keyframes: [
28370
- { percentage: 0, properties: startProps },
28371
- { percentage: 100, properties: endProps }
28372
- ],
28373
- ...anim.ease ? { ease: anim.ease } : {}
28374
- };
28375
- }
28376
-
28377
28482
  // src/hooks/useGsapTweenCache.ts
28378
28483
  function extractIdFromSelector(selector) {
28379
28484
  const match = selector.match(/^#([\w-]+)/);
@@ -29450,6 +29555,25 @@ function useAnimatedPropertyCommit(deps) {
29450
29555
  const elementHasKeyframes = selectedGsapAnimations.some((a) => !!a.keyframes);
29451
29556
  try {
29452
29557
  if (elementHasKeyframes && anim) {
29558
+ if (!usePlayerStore.getState().autoKeyframeEnabled) {
29559
+ const pct = computeElementPercentage(
29560
+ usePlayerStore.getState().currentTime,
29561
+ selection,
29562
+ anim
29563
+ );
29564
+ await commitWholePropertyOffset(
29565
+ selection,
29566
+ anim,
29567
+ Object.fromEntries(
29568
+ propEntries.filter((e) => typeof e[1] === "number")
29569
+ ),
29570
+ pct,
29571
+ iframe,
29572
+ { commitMutation: gsapCommitMutation },
29573
+ `Edit ${primaryProp} (whole animation)`
29574
+ );
29575
+ return;
29576
+ }
29453
29577
  await commitKeyframeProps(
29454
29578
  selection,
29455
29579
  anim,
@@ -30731,16 +30855,15 @@ function dispatchPlainKey(event, key, cb) {
30731
30855
  return;
30732
30856
  }
30733
30857
  if (event.key === "s" && !event.altKey) {
30858
+ event.preventDefault();
30734
30859
  const { selectedElementId, elements, currentTime } = usePlayerStore.getState();
30735
30860
  if (selectedElementId) {
30736
30861
  const el = elements.find((e) => (e.key ?? e.id) === selectedElementId);
30737
30862
  if (el && canSplitElement(el) && currentTime > el.start && currentTime < el.start + el.duration) {
30738
- event.preventDefault();
30739
30863
  void cb.handleTimelineElementSplit(el, currentTime);
30740
30864
  return;
30741
30865
  }
30742
30866
  if (!el && selectedElementId.includes("#")) {
30743
- event.preventDefault();
30744
30867
  cb.showToast("Use the razor tool (B) to split clips inside a sub-composition", "info");
30745
30868
  return;
30746
30869
  }
@@ -31812,6 +31935,9 @@ function useRenderClipContent({
31812
31935
  duration: el.duration
31813
31936
  });
31814
31937
  }
31938
+ if (el.tag === "audio") {
31939
+ return renderAudioClip(el, pid, style.label);
31940
+ }
31815
31941
  if (activePreviewUrl && el.duration > 0) {
31816
31942
  return createElement(CompositionThumbnail, {
31817
31943
  previewUrl: activePreviewUrl,
@@ -31824,9 +31950,6 @@ function useRenderClipContent({
31824
31950
  });
31825
31951
  }
31826
31952
  const htmlPreviewEligible = el.duration > 0 && effectiveTimelineDuration > 0 && el.duration < effectiveTimelineDuration * 0.92 && !/(backdrop|background|overlay|scrim|mask)/i.test(el.id);
31827
- if (el.tag === "audio") {
31828
- return renderAudioClip(el, pid, style.label);
31829
- }
31830
31953
  if ((el.tag === "video" || el.tag === "img") && el.src) {
31831
31954
  const mediaSrc = el.src.startsWith("http") ? el.src : `/api/projects/${pid}/preview/${el.src}`;
31832
31955
  return createElement(VideoThumbnail, {
@@ -39725,18 +39848,30 @@ var MotionPathOverlay = memo36(function MotionPathOverlay2({
39725
39848
  clearTimeout(parkTimerRef.current);
39726
39849
  if (e.detail < 2) {
39727
39850
  parkTimerRef.current = setTimeout(() => {
39728
- const anim = selectedGsapAnimations?.find((a) => a.id === animId);
39729
- if (anim) parkPlayheadOnKeyframe(anim, ref.pct);
39851
+ const anim2 = selectedGsapAnimations?.find((a) => a.id === animId);
39852
+ if (anim2) parkPlayheadOnKeyframe(anim2, ref.pct);
39730
39853
  }, 250);
39731
39854
  }
39732
39855
  }
39733
39856
  return;
39734
39857
  }
39735
39858
  if (x === Math.round(d.initX) && y === Math.round(d.initY)) return;
39736
- void commitNode(d.ref, x, y, animId, commitMutation);
39737
- if (d.ref.type === "keyframe") {
39738
- const anim = selectedGsapAnimations?.find((a) => a.id === animId);
39739
- if (anim) parkPlayheadOnKeyframe(anim, d.ref.pct);
39859
+ const anim = d.ref.type === "keyframe" ? selectedGsapAnimations?.find((a) => a.id === animId) : void 0;
39860
+ if (d.ref.type === "keyframe" && anim && selection && !usePlayerStore.getState().autoKeyframeEnabled) {
39861
+ void commitWholePropertyOffset(
39862
+ selection,
39863
+ anim,
39864
+ { x, y },
39865
+ d.ref.pct,
39866
+ iframeRef.current,
39867
+ { commitMutation: (_sel, mutation, options) => commitMutation(mutation, options) },
39868
+ "Move animation path"
39869
+ );
39870
+ } else {
39871
+ void commitNode(d.ref, x, y, animId, commitMutation);
39872
+ }
39873
+ if (d.ref.type === "keyframe" && anim) {
39874
+ parkPlayheadOnKeyframe(anim, d.ref.pct);
39740
39875
  }
39741
39876
  };
39742
39877
  const onPathHover = (e) => {
@@ -39935,6 +40070,7 @@ var SnapToolbar = memo37(function SnapToolbar2({ onSnapChange }) {
39935
40070
  }, [prefs.gridVisible, updatePrefs]);
39936
40071
  useEffect56(() => {
39937
40072
  const handleKeyDown = (e) => {
40073
+ if (e.defaultPrevented) return;
39938
40074
  const t = e.target;
39939
40075
  if (t instanceof HTMLInputElement || t instanceof HTMLTextAreaElement) return;
39940
40076
  if (t instanceof HTMLElement && t.isContentEditable) return;
@@ -43831,7 +43967,7 @@ function useKeyframeKeyboard({
43831
43967
  }
43832
43968
 
43833
43969
  // src/components/TimelineToolbar.tsx
43834
- import { Fragment as Fragment34, jsx as jsx101, jsxs as jsxs86 } from "react/jsx-runtime";
43970
+ import { jsx as jsx101, jsxs as jsxs86 } from "react/jsx-runtime";
43835
43971
  function useKeyframeToggle(session) {
43836
43972
  const currentTime = usePlayerStore((s) => s.currentTime);
43837
43973
  const sessionRef = useRef88(session);
@@ -43863,6 +43999,8 @@ function TimelineToolbar({
43863
43999
  }) {
43864
44000
  const activeTool = usePlayerStore((s) => s.activeTool);
43865
44001
  const setActiveTool = usePlayerStore((s) => s.setActiveTool);
44002
+ const autoKeyframeEnabled = usePlayerStore((s) => s.autoKeyframeEnabled);
44003
+ const setAutoKeyframeEnabled = usePlayerStore((s) => s.setAutoKeyframeEnabled);
43866
44004
  const currentTime = usePlayerStore((s) => s.currentTime);
43867
44005
  const beatAnalysisReady = usePlayerStore((s) => s.beatAnalysis !== null);
43868
44006
  const { zoomMode, manualZoomPercent, setZoomMode, setManualZoomPercent } = useTimelineZoom();
@@ -43899,10 +44037,10 @@ function TimelineToolbar({
43899
44037
  }
43900
44038
  ) })
43901
44039
  ] }),
43902
- STUDIO_KEYFRAMES_ENABLED && onToggleKeyframe && /* @__PURE__ */ jsx101(Fragment34, { children: /* @__PURE__ */ jsx101(
44040
+ STUDIO_KEYFRAMES_ENABLED && onToggleKeyframe && /* @__PURE__ */ jsx101(
43903
44041
  Tooltip,
43904
44042
  {
43905
- label: keyframeState === "active" ? "Remove keyframe at playhead (K)" : keyframeState === "inactive" ? keyframeWillExtend ? "Add keyframe at playhead \u2014 extends animation (K)" : "Add keyframe at playhead (K)" : "Add keyframe (K)",
44043
+ label: keyframeState === "active" ? "Remove keyframe at playhead (K)" : keyframeState === "inactive" ? keyframeWillExtend ? "Add keyframe at playhead, extends animation (K)" : "Add keyframe at playhead (K)" : "Add keyframe (K)",
43906
44044
  children: /* @__PURE__ */ jsx101(
43907
44045
  "button",
43908
44046
  {
@@ -43921,7 +44059,36 @@ function TimelineToolbar({
43921
44059
  }
43922
44060
  )
43923
44061
  }
43924
- ) }),
44062
+ ),
44063
+ STUDIO_KEYFRAMES_ENABLED && /* @__PURE__ */ jsx101(
44064
+ Tooltip,
44065
+ {
44066
+ label: autoKeyframeEnabled ? "Auto-record manual edits as keyframes (click to turn off)" : "Manual edits will not be recorded as keyframes (click to turn on)",
44067
+ children: /* @__PURE__ */ jsx101(
44068
+ "button",
44069
+ {
44070
+ type: "button",
44071
+ onClick: () => setAutoKeyframeEnabled(!autoKeyframeEnabled),
44072
+ "aria-pressed": autoKeyframeEnabled,
44073
+ className: `flex h-7 w-7 items-center justify-center rounded transition-colors ${autoKeyframeEnabled ? "text-red-400 hover:text-red-300" : "text-neutral-600 hover:text-neutral-400"}`,
44074
+ children: /* @__PURE__ */ jsxs86("svg", { width: "18", height: "18", viewBox: "0 0 10 10", fill: "none", children: [
44075
+ /* @__PURE__ */ jsx101("path", { d: "M5 0.7L9.3 5L5 9.3L0.7 5Z", stroke: "currentColor", strokeWidth: "1" }),
44076
+ /* @__PURE__ */ jsx101(
44077
+ "circle",
44078
+ {
44079
+ cx: "5",
44080
+ cy: "5",
44081
+ r: "1.8",
44082
+ fill: autoKeyframeEnabled ? "currentColor" : "none",
44083
+ stroke: "currentColor",
44084
+ strokeWidth: "1"
44085
+ }
44086
+ )
44087
+ ] })
44088
+ }
44089
+ )
44090
+ }
44091
+ ),
43925
44092
  onSplitElement && (() => {
43926
44093
  const { selectedElementId, elements, currentTime: currentTime2 } = usePlayerStore.getState();
43927
44094
  const el = selectedElementId ? elements.find((e) => (e.key ?? e.id) === selectedElementId) : null;