@hyperframes/core 0.6.109 → 0.6.111

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/dist/compiler/htmlDocument.d.ts.map +1 -1
  2. package/dist/compiler/htmlDocument.js +19 -2
  3. package/dist/compiler/htmlDocument.js.map +1 -1
  4. package/dist/generated/runtime-inline.js +1 -1
  5. package/dist/generated/runtime-inline.js.map +1 -1
  6. package/dist/hyperframe.manifest.json +1 -1
  7. package/dist/hyperframe.runtime.iife.js +24 -24
  8. package/dist/hyperframe.runtime.mjs +24 -24
  9. package/dist/lint/rules/composition.d.ts.map +1 -1
  10. package/dist/lint/rules/composition.js +63 -0
  11. package/dist/lint/rules/composition.js.map +1 -1
  12. package/dist/lint/rules/fonts.d.ts.map +1 -1
  13. package/dist/lint/rules/fonts.js +13 -2
  14. package/dist/lint/rules/fonts.js.map +1 -1
  15. package/dist/parsers/gsapParser.d.ts.map +1 -1
  16. package/dist/parsers/gsapParser.js +7 -54
  17. package/dist/parsers/gsapParser.js.map +1 -1
  18. package/dist/parsers/gsapSerialize.d.ts +26 -0
  19. package/dist/parsers/gsapSerialize.d.ts.map +1 -1
  20. package/dist/parsers/gsapSerialize.js +143 -4
  21. package/dist/parsers/gsapSerialize.js.map +1 -1
  22. package/dist/parsers/gsapWriterAcorn.d.ts +75 -1
  23. package/dist/parsers/gsapWriterAcorn.d.ts.map +1 -1
  24. package/dist/parsers/gsapWriterAcorn.js +1221 -47
  25. package/dist/parsers/gsapWriterAcorn.js.map +1 -1
  26. package/dist/parsers/htmlParser.d.ts.map +1 -1
  27. package/dist/parsers/htmlParser.js +40 -4
  28. package/dist/parsers/htmlParser.js.map +1 -1
  29. package/dist/storyboard/editStoryboard.d.ts +21 -0
  30. package/dist/storyboard/editStoryboard.d.ts.map +1 -0
  31. package/dist/storyboard/editStoryboard.js +95 -0
  32. package/dist/storyboard/editStoryboard.js.map +1 -0
  33. package/dist/storyboard/index.d.ts +4 -0
  34. package/dist/storyboard/index.d.ts.map +1 -0
  35. package/dist/storyboard/index.js +4 -0
  36. package/dist/storyboard/index.js.map +1 -0
  37. package/dist/storyboard/parseStoryboard.d.ts +37 -0
  38. package/dist/storyboard/parseStoryboard.d.ts.map +1 -0
  39. package/dist/storyboard/parseStoryboard.js +275 -0
  40. package/dist/storyboard/parseStoryboard.js.map +1 -0
  41. package/dist/storyboard/types.d.ts +89 -0
  42. package/dist/storyboard/types.d.ts.map +1 -0
  43. package/dist/storyboard/types.js +24 -0
  44. package/dist/storyboard/types.js.map +1 -0
  45. package/dist/studio-api/createStudioApi.d.ts.map +1 -1
  46. package/dist/studio-api/createStudioApi.js +2 -0
  47. package/dist/studio-api/createStudioApi.js.map +1 -1
  48. package/dist/studio-api/routes/storyboard.d.ts +4 -0
  49. package/dist/studio-api/routes/storyboard.d.ts.map +1 -0
  50. package/dist/studio-api/routes/storyboard.js +65 -0
  51. package/dist/studio-api/routes/storyboard.js.map +1 -0
  52. package/package.json +1 -1
@@ -7,8 +7,9 @@
7
7
  * pretty-printer churn. Consumes ParsedGsapAcornForWrite from gsapParserAcorn.ts.
8
8
  */
9
9
  import MagicString from "magic-string";
10
- import { serializeValue, safeJsKey } from "./gsapSerialize.js";
10
+ import { resolveConversionProps, extractArcWaypoints, buildMotionPathObjectCode, } from "./gsapSerialize.js";
11
11
  import { parseGsapScriptAcornForWrite, } from "./gsapParserAcorn.js";
12
+ import { classifyPropertyGroup } from "./gsapConstants.js";
12
13
  import * as acornWalk from "acorn-walk";
13
14
  // ── Code generation helpers ──────────────────────────────────────────────────
14
15
  // Local serializer for the tween-statement path, which may carry boolean/object
@@ -69,6 +70,11 @@ function findPropertyNode(varsArgNode, key) {
69
70
  }
70
71
  return undefined;
71
72
  }
73
+ /** The `keyframes` property's ObjectExpression value, or null when not a keyframe tween. */
74
+ function keyframesObjectNode(varsNode) {
75
+ const kfProp = findPropertyNode(varsNode, "keyframes");
76
+ return kfProp?.value?.type === "ObjectExpression" ? kfProp.value : null;
77
+ }
72
78
  function findEnclosingExpressionStatement(ancestors) {
73
79
  for (let i = ancestors.length - 2; i >= 0; i--) {
74
80
  if (ancestors[i]?.type === "ExpressionStatement")
@@ -118,6 +124,17 @@ function removeProp(ms, propNode, editableProps) {
118
124
  ms.remove(editableProps[idx - 1].end, propNode.end);
119
125
  }
120
126
  }
127
+ /** Serialize a vars record to an object-literal source: `{ k: v, ... }`. */
128
+ function buildVarsObjectCode(record) {
129
+ const entries = Object.entries(record).map(([k, v]) => `${safeKey(k)}: ${valueToCode(v)}`);
130
+ return entries.length > 0 ? `{ ${entries.join(", ")} }` : "{}";
131
+ }
132
+ /** Overwrite a tween call's vars ObjectExpression with freshly-built source. */
133
+ function overwriteVarsArg(ms, call, objCode) {
134
+ if (!call.varsArg)
135
+ return;
136
+ ms.overwrite(call.varsArg.start, call.varsArg.end, objCode);
137
+ }
121
138
  /**
122
139
  * Update a property value if it exists, or append a new key: val before the
123
140
  * closing `}`. Call with the full ObjectExpression node.
@@ -134,6 +151,84 @@ function upsertProp(ms, objNode, key, value) {
134
151
  ms.appendLeft(objNode.end - 1, `${sep}${safeKey(key)}: ${valueToCode(value)}`);
135
152
  }
136
153
  }
154
+ /**
155
+ * Vars keys that are NOT editable transform/style props: builtins
156
+ * (duration/ease/delay), dropped callbacks, and extras (stagger/yoyo/repeat/…).
157
+ * The exact union of recast's BUILTIN_VAR_KEYS + DROPPED_VAR_KEYS + EXTRAS_KEYS,
158
+ * so both writers classify vars keys identically. (Distinct from the keyframe-
159
+ * conversion NON_EDITABLE_VAR_KEYS below, which intentionally omits `ease`
160
+ * because that path re-emits ease separately.)
161
+ */
162
+ const NON_EDITABLE_PROP_KEYS = new Set([
163
+ "duration",
164
+ "ease",
165
+ "delay",
166
+ "onComplete",
167
+ "onStart",
168
+ "onUpdate",
169
+ "onRepeat",
170
+ "stagger",
171
+ "yoyo",
172
+ "repeat",
173
+ "repeatDelay",
174
+ "snap",
175
+ "overwrite",
176
+ "immediateRender",
177
+ ]);
178
+ /**
179
+ * Editable transform/style key test: anything NOT a builtin, dropped callback, or
180
+ * extras key. Mirrors recast's isEditablePropertyKey so both writers classify
181
+ * vars keys identically.
182
+ */
183
+ function isEditableVarKey(key) {
184
+ return !NON_EDITABLE_PROP_KEYS.has(key);
185
+ }
186
+ /**
187
+ * Collect verbatim `key: value` entries to PRESERVE from a vars/keyframe
188
+ * ObjectExpression: every property whose key `drop` does not reject, sliced from
189
+ * source — except keys present in `overrides`, whose value is replaced. Returns
190
+ * the entries plus the set of keys it kept, so callers can append new keys.
191
+ */
192
+ function preservedEntries(objNode, source, drop, overrides) {
193
+ const entries = [];
194
+ const keys = new Set();
195
+ for (const prop of objNode.properties ?? []) {
196
+ if (!isObjectProperty(prop))
197
+ continue;
198
+ const key = propKeyName(prop);
199
+ if (typeof key !== "string" || drop(key))
200
+ continue;
201
+ keys.add(key);
202
+ const code = key in overrides
203
+ ? valueToCode(overrides[key])
204
+ : source.slice(prop.value.start, prop.value.end);
205
+ entries.push(`${safeKey(key)}: ${code}`);
206
+ }
207
+ return { entries, keys };
208
+ }
209
+ /**
210
+ * Replace the editable-property keys on a vars ObjectExpression with exactly
211
+ * `newProps`, leaving non-editable keys (duration/ease/stagger/callbacks/…)
212
+ * untouched unless overridden in `nonEditableOverrides`. Mirrors recast's
213
+ * reconcileEditableProperties: editable keys absent from `newProps` are DROPPED,
214
+ * not merged. Rebuilt in a single ms.overwrite so the splice can never overlap a
215
+ * sibling edit — non-editable updates that also target this node (duration/ease/
216
+ * extras) are folded into the same rebuild rather than spliced separately.
217
+ */
218
+ function reconcileEditableProps(ms, objNode, source, newProps, nonEditableOverrides) {
219
+ if (objNode?.type !== "ObjectExpression")
220
+ return;
221
+ const overrides = nonEditableOverrides ?? {};
222
+ const { entries, keys } = preservedEntries(objNode, source, isEditableVarKey, overrides);
223
+ for (const [key, value] of Object.entries(overrides)) {
224
+ if (!keys.has(key))
225
+ entries.push(`${safeKey(key)}: ${valueToCode(value)}`);
226
+ }
227
+ for (const [key, value] of Object.entries(newProps)) {
228
+ entries.push(`${safeKey(key)}: ${valueToCode(value)}`);
229
+ }
230
+ ms.overwrite(objNode.start, objNode.end, `{ ${entries.join(", ")} }`);
231
+ }
137
232
  // ── Insertion helpers ─────────────────────────────────────────────────────────
138
233
  /** Traverse callee.object chain to check if a call ultimately roots at timelineVar. */
139
234
  function isTimelineRooted(node, timelineVar) {
@@ -149,8 +244,9 @@ function isTimelineRooted(node, timelineVar) {
149
244
  * not emit `tl.xxx()` calls in that case as `tl` would be undefined at render.
150
245
  */
151
246
  function findInsertionPoint(parsed) {
152
- if (parsed.located.length > 0) {
153
- const lastCall = parsed.located[parsed.located.length - 1].call;
247
+ const lastLocated = parsed.located[parsed.located.length - 1];
248
+ if (lastLocated) {
249
+ const lastCall = lastLocated.call;
154
250
  const exprStmt = findEnclosingExpressionStatement(lastCall.ancestors);
155
251
  return exprStmt?.end ?? lastCall.node.end;
156
252
  }
@@ -172,38 +268,116 @@ export function updateAnimationInScript(script, animationId, updates) {
172
268
  return script;
173
269
  const ms = new MagicString(script);
174
270
  const { call } = target;
175
- if (updates.duration !== undefined) {
176
- upsertProp(ms, call.varsArg, "duration", updates.duration);
177
- }
178
- if (updates.ease !== undefined) {
179
- upsertProp(ms, call.varsArg, "ease", updates.ease);
180
- }
271
+ // When `properties` is present we REPLACE the editable set (recast parity:
272
+ // editable keys absent from the update are dropped). Fold any concurrent
273
+ // non-editable updates (duration/ease/extras) into the single varsArg rebuild
274
+ // so their splices can't overlap the rebuild's overwrite of the whole node.
181
275
  if (updates.properties) {
182
- for (const [key, value] of Object.entries(updates.properties)) {
183
- upsertProp(ms, call.varsArg, key, value);
276
+ const overrides = {};
277
+ if (updates.duration !== undefined)
278
+ overrides.duration = updates.duration;
279
+ if (updates.ease !== undefined)
280
+ overrides.ease = updates.ease;
281
+ if (updates.extras)
282
+ Object.assign(overrides, updates.extras);
283
+ reconcileEditableProps(ms, call.varsArg, script, updates.properties, overrides);
284
+ }
285
+ else {
286
+ if (updates.duration !== undefined) {
287
+ upsertProp(ms, call.varsArg, "duration", updates.duration);
288
+ }
289
+ if (updates.ease !== undefined) {
290
+ // For a keyframe tween, easing lives at keyframes.easeEach (per-keyframe),
291
+ // not a top-level ease. Writing top-level ease would leave the per-keyframe
292
+ // easing unchanged — the user's edit would silently do nothing.
293
+ const kfNode = keyframesObjectNode(call.varsArg);
294
+ if (kfNode)
295
+ upsertProp(ms, kfNode, "easeEach", updates.ease);
296
+ else
297
+ upsertProp(ms, call.varsArg, "ease", updates.ease);
298
+ }
299
+ if (updates.extras) {
300
+ for (const [key, value] of Object.entries(updates.extras)) {
301
+ upsertProp(ms, call.varsArg, key, value);
302
+ }
184
303
  }
185
304
  }
186
305
  if (updates.fromProperties && call.method === "fromTo" && call.fromArg) {
187
- for (const [key, value] of Object.entries(updates.fromProperties)) {
188
- upsertProp(ms, call.fromArg, key, value);
189
- }
306
+ // fromTo's from-vars carry only editable props — REPLACE them too (recast
307
+ // parity). fromArg is a distinct node from varsArg, so this rebuild never
308
+ // overlaps the varsArg edits above.
309
+ reconcileEditableProps(ms, call.fromArg, script, updates.fromProperties);
190
310
  }
191
311
  if (updates.position !== undefined) {
192
- const posIdx = call.method === "fromTo" ? 3 : 2;
193
- const posArgNode = call.node.arguments?.[posIdx];
194
- if (posArgNode) {
195
- ms.overwrite(posArgNode.start, posArgNode.end, valueToCode(updates.position));
196
- }
197
- else {
198
- ms.appendLeft(call.node.end - 1, `, ${valueToCode(updates.position)}`);
199
- }
312
+ overwritePosition(ms, call, updates.position);
313
+ }
314
+ return ms.toString();
315
+ }
316
+ /**
317
+ * Overwrite a tween call's numeric position argument (the positionArg the parser
318
+ * located: 3rd arg for fromTo, else 2nd), or append one when the call has no
319
+ * explicit position. Shared by updateAnimationInScript and the
320
+ * shift/scalePositionsInScript timeline ops.
321
+ */
322
+ function overwritePosition(ms, call, position) {
323
+ if (call.positionArg) {
324
+ ms.overwrite(call.positionArg.start, call.positionArg.end, valueToCode(position));
325
+ }
326
+ else {
327
+ ms.appendLeft(call.node.end - 1, `, ${valueToCode(position)}`);
328
+ }
329
+ }
330
+ /**
331
+ * Shift every tween targeting `targetSelector` by `delta` seconds (clamped ≥0),
332
+ * rewriting each call's position argument. Mirrors recast's shiftPositionsInScript
333
+ * (used by timeline clip-move to keep GSAP positions in sync with the clip start).
334
+ */
335
+ export function shiftPositionsInScript(script, targetSelector, delta) {
336
+ const parsed = parseGsapScriptAcornForWrite(script);
337
+ if (!parsed)
338
+ return script;
339
+ const ms = new MagicString(script);
340
+ let changed = false;
341
+ for (const entry of parsed.located) {
342
+ if (entry.animation.targetSelector !== targetSelector)
343
+ continue;
344
+ if (typeof entry.animation.position !== "number")
345
+ continue;
346
+ const newPos = Math.max(0, Math.round((entry.animation.position + delta) * 1000) / 1000);
347
+ overwritePosition(ms, entry.call, newPos);
348
+ changed = true;
200
349
  }
201
- if (updates.extras) {
202
- for (const [key, value] of Object.entries(updates.extras)) {
203
- upsertProp(ms, call.varsArg, key, value);
350
+ return changed ? ms.toString() : script;
351
+ }
352
+ /**
353
+ * Linearly remap every tween targeting `targetSelector` from the old clip
354
+ * [oldStart, oldDuration] onto the new [newStart, newDuration] (position and,
355
+ * when present, duration scaled by the duration ratio). Mirrors recast's
356
+ * scalePositionsInScript (used by timeline clip-resize).
357
+ */
358
+ export function scalePositionsInScript(script, targetSelector, oldStart, oldDuration, newStart, newDuration) {
359
+ if (oldDuration <= 0 || newDuration <= 0)
360
+ return script;
361
+ const ratio = newDuration / oldDuration;
362
+ const parsed = parseGsapScriptAcornForWrite(script);
363
+ if (!parsed)
364
+ return script;
365
+ const ms = new MagicString(script);
366
+ let changed = false;
367
+ for (const entry of parsed.located) {
368
+ if (entry.animation.targetSelector !== targetSelector)
369
+ continue;
370
+ if (typeof entry.animation.position !== "number")
371
+ continue;
372
+ const newPos = Math.max(0, Math.round((newStart + (entry.animation.position - oldStart) * ratio) * 1000) / 1000);
373
+ overwritePosition(ms, entry.call, newPos);
374
+ if (typeof entry.animation.duration === "number" && entry.animation.duration > 0) {
375
+ const newDur = Math.max(0.001, Math.round(entry.animation.duration * ratio * 1000) / 1000);
376
+ upsertProp(ms, entry.call.varsArg, "duration", newDur);
204
377
  }
378
+ changed = true;
205
379
  }
206
- return ms.toString();
380
+ return changed ? ms.toString() : script;
207
381
  }
208
382
  export function addAnimationToScript(script, animation) {
209
383
  const parsed = parseGsapScriptAcornForWrite(script);
@@ -372,7 +546,7 @@ function percentageFromKey(key) {
372
546
  }
373
547
  /** Serialize a final keyframe property record (number|string values) to code. */
374
548
  function recordToCode(record) {
375
- const entries = Object.entries(record).map(([k, v]) => `${safeJsKey(k)}: ${serializeValue(v)}`);
549
+ const entries = Object.entries(record).map(([k, v]) => `${safeKey(k)}: ${valueToCode(v)}`);
376
550
  return `{ ${entries.join(", ")} }`;
377
551
  }
378
552
  /** Percentage-keyed property nodes of a keyframes ObjectExpression, in source order. */
@@ -456,17 +630,28 @@ function autoEndpointOverwrites(kfNode, source, percentage, properties) {
456
630
  return result;
457
631
  }
458
632
  function findKfPropByPct(kfNode, percentage) {
633
+ // Match the CLOSEST keyframe within tolerance, not the first one within range.
634
+ // Keyframes at e.g. 0/49/50/100 are all valid (the SDK dedups to a unique
635
+ // match at TOLERANCE=0.001 upstream); picking the first-within-PCT_TOLERANCE=2
636
+ // would hit 49% when the caller meant 50%. Tie-break on the earliest index so
637
+ // the choice stays deterministic.
459
638
  const props = kfNode.properties ?? [];
639
+ let best = null;
640
+ let bestDist = Number.POSITIVE_INFINITY;
460
641
  for (let i = 0; i < props.length; i++) {
461
642
  const prop = props[i];
462
643
  if (!isObjectProperty(prop))
463
644
  continue;
464
645
  const key = propKeyName(prop);
465
- if (typeof key === "string" && Math.abs(percentageFromKey(key) - percentage) <= PCT_TOLERANCE) {
466
- return { prop, idx: i };
646
+ if (typeof key !== "string")
647
+ continue;
648
+ const dist = Math.abs(percentageFromKey(key) - percentage);
649
+ if (dist <= PCT_TOLERANCE && dist < bestDist) {
650
+ best = { prop, idx: i };
651
+ bestDist = dist;
467
652
  }
468
653
  }
469
- return null;
654
+ return best;
470
655
  }
471
656
  export function updateKeyframeInScript(script, animationId, percentage, properties, ease) {
472
657
  const parsed = parseGsapScriptAcornForWrite(script);
@@ -603,7 +788,19 @@ export function addKeyframeToScript(script, animationId, percentage, properties,
603
788
  // Emit exactly one overwrite per changed node, plus one insert for a new key.
604
789
  const ms = new MagicString(src);
605
790
  if (existing) {
606
- ms.overwrite(existing.prop.value.start, existing.prop.value.end, recordToCode(targetRecord));
791
+ // Merge into the existing keyframe at this percentage, preserving sibling
792
+ // properties — overwrite only the given keys. (A whole-value overwrite here
793
+ // would silently drop other properties already keyframed at this percent.)
794
+ if (existing.prop.value?.type === "ObjectExpression") {
795
+ for (const [k, v] of Object.entries(properties)) {
796
+ upsertProp(ms, existing.prop.value, k, v);
797
+ }
798
+ if (ease !== undefined)
799
+ upsertProp(ms, existing.prop.value, "ease", ease);
800
+ }
801
+ else {
802
+ ms.overwrite(existing.prop.value.start, existing.prop.value.end, recordToCode(targetRecord));
803
+ }
607
804
  }
608
805
  else {
609
806
  insertNewKeyframe(ms, kfNode, percentage, `${percentage}%`, recordToCode(targetRecord));
@@ -632,6 +829,25 @@ function insertNewKeyframe(ms, kfNode, percentage, pctKey, valueCode) {
632
829
  ms.appendLeft(kfNode.end - 1, `${sep}${JSON.stringify(pctKey)}: ${valueCode}`);
633
830
  }
634
831
  }
832
+ /**
833
+ * Rebuild a vars ObjectExpression that has just dropped below two keyframes,
834
+ * collapsing `keyframes: {…}` back to a flat tween. Mirrors recast's
835
+ * collapseKeyframesToFlat: drop the `keyframes` + `easeEach` keys, preserve every
836
+ * other vars key verbatim, and splice the remaining keyframe's properties (minus
837
+ * its per-keyframe `ease`) in as flat vars keys. Single ms.overwrite of the whole
838
+ * vars node so the splice can't overlap the keyframe removal.
839
+ */
840
+ function collapseKeyframesToFlat(ms, varsNode, source, remainingRecord) {
841
+ if (varsNode?.type !== "ObjectExpression")
842
+ return;
843
+ const dropKeyframeKeys = (key) => key === "keyframes" || key === "easeEach";
844
+ const { entries } = preservedEntries(varsNode, source, dropKeyframeKeys, {});
845
+ for (const [k, v] of Object.entries(remainingRecord)) {
846
+ if (k !== "ease")
847
+ entries.push(`${safeKey(k)}: ${valueToCode(v)}`);
848
+ }
849
+ ms.overwrite(varsNode.start, varsNode.end, `{ ${entries.join(", ")} }`);
850
+ }
635
851
  export function removeKeyframeFromScript(script, animationId, percentage) {
636
852
  const parsed = parseGsapScriptAcornForWrite(script);
637
853
  if (!parsed)
@@ -646,16 +862,373 @@ export function removeKeyframeFromScript(script, animationId, percentage) {
646
862
  const match = findKfPropByPct(kfNode, percentage);
647
863
  if (!match)
648
864
  return script;
649
- const allProps = (kfNode.properties ?? []).filter((p) => isObjectProperty(p));
650
865
  const ms = new MagicString(script);
866
+ // If removing this keyframe leaves fewer than two, collapse the keyframes
867
+ // object back to a flat tween (recast parity) instead of leaving a lone
868
+ // keyframe. We rebuild the whole vars node, so we never also splice the kf
869
+ // node — the two edits would overlap.
870
+ const remaining = percentagePropsOf(kfNode).filter((p) => p !== match.prop);
871
+ if (remaining.length < 2) {
872
+ const sole = remaining[0];
873
+ const record = sole ? valueNodeToRecord(sole.value, script) : {};
874
+ collapseKeyframesToFlat(ms, target.call.varsArg, script, record);
875
+ return ms.toString();
876
+ }
877
+ const allProps = (kfNode.properties ?? []).filter((p) => isObjectProperty(p));
651
878
  removeProp(ms, match.prop, allProps);
652
879
  return ms.toString();
653
880
  }
881
+ export function removePropertyFromAnimation(script, animationId, property, from = false) {
882
+ const parsed = parseGsapScriptAcornForWrite(script);
883
+ if (!parsed)
884
+ return script;
885
+ const target = parsed.located.find((l) => l.id === animationId);
886
+ if (!target)
887
+ return script;
888
+ const { call } = target;
889
+ const objNode = from ? (call.method === "fromTo" ? call.fromArg : null) : call.varsArg;
890
+ if (!objNode)
891
+ return script;
892
+ const propNode = findPropertyNode(objNode, property);
893
+ if (!propNode)
894
+ return script;
895
+ const allProps = (objNode.properties ?? []).filter((p) => isObjectProperty(p));
896
+ const ms = new MagicString(script);
897
+ removeProp(ms, propNode, allProps);
898
+ return ms.toString();
899
+ }
900
+ /**
901
+ * Remove all keyframes from a tween, collapsing to a flat tween with one
902
+ * keyframe's properties: the first for `from()`, the last otherwise (the
903
+ * destination = the visible resting state).
904
+ */
905
+ export function removeAllKeyframesFromScript(script, animationId) {
906
+ const parsed = parseGsapScriptAcornForWrite(script);
907
+ if (!parsed)
908
+ return script;
909
+ const target = parsed.located.find((l) => l.id === animationId);
910
+ if (!target)
911
+ return script;
912
+ const kfs = target.animation.keyframes?.keyframes;
913
+ if (!kfs || kfs.length === 0)
914
+ return script;
915
+ const sorted = [...kfs].sort((a, b) => a.percentage - b.percentage);
916
+ const collapse = target.call.method === "from" ? sorted[0] : sorted[sorted.length - 1];
917
+ if (!collapse)
918
+ return script;
919
+ const ms = new MagicString(script);
920
+ overwriteVarsArg(ms, target.call, buildVarsObjectCode(buildCollapsedFlatVars(target.animation, collapse)));
921
+ return ms.toString();
922
+ }
923
+ // Flat vars for a tween collapsing its keyframes onto one stop: existing
924
+ // top-level props, then the collapse keyframe's props (skip per-keyframe
925
+ // `ease`), then duration/ease/extras. Drops keyframes + easeEach by omission.
926
+ function buildCollapsedFlatVars(animation, collapse) {
927
+ const flat = { ...animation.properties };
928
+ for (const [k, v] of Object.entries(collapse.properties)) {
929
+ if (k !== "ease")
930
+ flat[k] = v;
931
+ }
932
+ if (animation.duration !== undefined)
933
+ flat.duration = animation.duration;
934
+ if (animation.ease)
935
+ flat.ease = animation.ease;
936
+ for (const [k, v] of Object.entries(animation.extras ?? {})) {
937
+ if (typeof v === "number" || typeof v === "string")
938
+ flat[k] = v;
939
+ }
940
+ return flat;
941
+ }
942
+ /** Build the full replacement vars object for a tween being converted to keyframes. */
943
+ function buildKeyframesVarsCode(animation, fromProps, toProps, varsNode, source) {
944
+ const fromEntries = Object.entries(fromProps).map(([k, v]) => `${safeKey(k)}: ${valueToCode(v)}`);
945
+ const toEntries = Object.entries(toProps).map(([k, v]) => `${safeKey(k)}: ${valueToCode(v)}`);
946
+ const easeEntry = animation.ease ? `, easeEach: ${JSON.stringify(animation.ease)}` : "";
947
+ const kfCode = `{ "0%": { ${fromEntries.join(", ")} }, "100%": { ${toEntries.join(", ")} }${easeEntry} }`;
948
+ // Preserve every non-editable key (duration/delay/callbacks/stagger/yoyo/…)
949
+ // verbatim from source — rebuilding from the animation object alone dropped
950
+ // `delay` (not a GsapAnimation field), shifting the tween's start time.
951
+ const parts = [`keyframes: ${kfCode}`, ...preservedVarsEntries(varsNode, source)];
952
+ if (animation.ease)
953
+ parts.push(`ease: "none"`);
954
+ return `{ ${parts.join(", ")} }`;
955
+ }
956
+ /**
957
+ * Convert a flat tween (to/from/fromTo) to percentage-keyframes format.
958
+ * `resolvedFromValues` supplies the current DOM state: overrides the 0% endpoint
959
+ * for `to()`, the 100% endpoint for `from()`, or merges into toProps for `fromTo()`.
960
+ */
961
+ export function convertToKeyframesFromScript(script, animationId, resolvedFromValues) {
962
+ const parsed = parseGsapScriptAcornForWrite(script);
963
+ if (!parsed)
964
+ return script;
965
+ const target = parsed.located.find((l) => l.id === animationId);
966
+ if (!target)
967
+ return script;
968
+ const { animation, call } = target;
969
+ if (animation.keyframes || call.method === "set")
970
+ return script;
971
+ const { fromProps, toProps } = resolveConversionProps(animation, resolvedFromValues);
972
+ const ms = new MagicString(script);
973
+ if (call.method === "from" || call.method === "fromTo") {
974
+ ms.overwrite(call.node.callee.property.start, call.node.callee.property.end, "to");
975
+ }
976
+ if (call.method === "fromTo" && call.fromArg) {
977
+ ms.remove(call.fromArg.start, call.varsArg.start);
978
+ }
979
+ overwriteVarsArg(ms, call, buildKeyframesVarsCode(animation, fromProps, toProps, call.varsArg, script));
980
+ return ms.toString();
981
+ }
982
+ // ── Keyframe-object code builder ─────────────────────────────────────────────
983
+ /** Build a percentage-keyframes object literal: `{ "0%": { x: 0 }, "100%": { x: 100 } }`. */
984
+ function buildKeyframeObjectCode(keyframes, easeEach) {
985
+ const entries = keyframes.map((kf) => {
986
+ const props = Object.entries(kf.properties).map(([k, v]) => `${safeKey(k)}: ${valueToCode(v)}`);
987
+ if (kf.ease)
988
+ props.push(`ease: ${JSON.stringify(kf.ease)}`);
989
+ return `${JSON.stringify(`${kf.percentage}%`)}: { ${props.join(", ")} }`;
990
+ });
991
+ if (easeEach)
992
+ entries.push(`easeEach: ${JSON.stringify(easeEach)}`);
993
+ return `{ ${entries.join(", ")} }`;
994
+ }
995
+ // ── Materialize keyframes ────────────────────────────────────────────────────
996
+ /**
997
+ * Replace a dynamic or static keyframes expression with a fully-resolved
998
+ * percentage-keyframes object. Called when a user first edits a dynamically-
999
+ * generated keyframe in the studio so it becomes statically editable.
1000
+ */
1001
+ export function materializeKeyframesFromScript(script, animationId, keyframes, easeEach, resolvedSelector) {
1002
+ // An empty keyframe list has no materialized form — rebuilding vars with an
1003
+ // empty keyframes object would empty the animation. No-op instead.
1004
+ if (keyframes.length === 0)
1005
+ return script;
1006
+ const parsed = parseGsapScriptAcornForWrite(script);
1007
+ if (!parsed)
1008
+ return script;
1009
+ const target = parsed.located.find((l) => l.id === animationId);
1010
+ if (!target)
1011
+ return script;
1012
+ const { call } = target;
1013
+ const sorted = [...keyframes].sort((a, b) => a.percentage - b.percentage);
1014
+ const kfObjCode = buildKeyframeObjectCode(sorted, easeEach);
1015
+ const ms = new MagicString(script);
1016
+ if (resolvedSelector) {
1017
+ const selectorArg = call.node.arguments[0];
1018
+ if (selectorArg)
1019
+ ms.overwrite(selectorArg.start, selectorArg.end, JSON.stringify(resolvedSelector));
1020
+ }
1021
+ const kfProp = findPropertyNode(call.varsArg, "keyframes");
1022
+ if (kfProp) {
1023
+ ms.overwrite(kfProp.value.start, kfProp.value.end, kfObjCode);
1024
+ }
1025
+ else if (call.varsArg?.type === "ObjectExpression") {
1026
+ const vars = call.varsArg;
1027
+ if (vars.properties.length > 0) {
1028
+ ms.prependLeft(vars.properties[0].start, `keyframes: ${kfObjCode}, `);
1029
+ }
1030
+ else {
1031
+ ms.appendLeft(vars.end - 1, `keyframes: ${kfObjCode}`);
1032
+ }
1033
+ }
1034
+ const eachProp = findPropertyNode(call.varsArg, "easeEach");
1035
+ if (eachProp) {
1036
+ const allProps = (call.varsArg.properties ?? []).filter((p) => isObjectProperty(p));
1037
+ removeProp(ms, eachProp, allProps);
1038
+ }
1039
+ return ms.toString();
1040
+ }
1041
+ // ── Add animation with keyframes ──────────────────────────────────────────────
1042
+ /** Insert a new keyframed `to()` call and return the new animation ID. */
1043
+ export function addAnimationWithKeyframesToScript(script, targetSelector, position, duration, keyframes, ease) {
1044
+ const parsed = parseGsapScriptAcornForWrite(script);
1045
+ if (!parsed)
1046
+ return { script, id: "" };
1047
+ const insertionPoint = findInsertionPoint(parsed);
1048
+ if (insertionPoint === null)
1049
+ return { script, id: "" };
1050
+ const sorted = [...keyframes].sort((a, b) => a.percentage - b.percentage);
1051
+ const kfObjCode = buildKeyframeObjectCode(sorted);
1052
+ const varParts = [`keyframes: ${kfObjCode}`, `duration: ${valueToCode(duration)}`];
1053
+ if (ease)
1054
+ varParts.push(`ease: ${JSON.stringify(ease)}`);
1055
+ const stmtCode = `${parsed.timelineVar}.to(${JSON.stringify(targetSelector)}, { ${varParts.join(", ")} }, ${valueToCode(position)});`;
1056
+ const ms = new MagicString(script);
1057
+ ms.appendLeft(insertionPoint, "\n" + stmtCode);
1058
+ const result = ms.toString();
1059
+ const reParsed = parseGsapScriptAcornForWrite(result);
1060
+ const newId = reParsed?.located[reParsed.located.length - 1]?.id ?? "";
1061
+ return { script: result, id: newId };
1062
+ }
1063
+ // ── Split into property groups ────────────────────────────────────────────────
1064
+ function collectPropertyKeys(anim) {
1065
+ const keys = new Set();
1066
+ if (anim.keyframes) {
1067
+ for (const kf of anim.keyframes.keyframes) {
1068
+ for (const k of Object.keys(kf.properties))
1069
+ keys.add(k);
1070
+ }
1071
+ }
1072
+ else {
1073
+ for (const k of Object.keys(anim.properties))
1074
+ keys.add(k);
1075
+ }
1076
+ return keys;
1077
+ }
1078
+ function partitionPropertyGroups(keys) {
1079
+ const groups = new Map();
1080
+ for (const key of keys) {
1081
+ if (key === "transformOrigin")
1082
+ continue;
1083
+ const group = classifyPropertyGroup(key);
1084
+ let arr = groups.get(group);
1085
+ if (!arr) {
1086
+ arr = [];
1087
+ groups.set(group, arr);
1088
+ }
1089
+ arr.push(key);
1090
+ }
1091
+ return groups;
1092
+ }
1093
+ function assignTransformOrigin(groupProps) {
1094
+ let largestGroup;
1095
+ let largestCount = 0;
1096
+ for (const [group, props] of groupProps) {
1097
+ if (props.length > largestCount) {
1098
+ largestCount = props.length;
1099
+ largestGroup = group;
1100
+ }
1101
+ }
1102
+ const largest = largestGroup ? groupProps.get(largestGroup) : undefined;
1103
+ if (largest)
1104
+ largest.push("transformOrigin");
1105
+ }
1106
+ function filterGroupKeyframes(kfs, propSet) {
1107
+ const result = [];
1108
+ for (const kf of kfs) {
1109
+ const filtered = {};
1110
+ for (const [k, v] of Object.entries(kf.properties)) {
1111
+ if (propSet.has(k))
1112
+ filtered[k] = v;
1113
+ }
1114
+ if (Object.keys(filtered).length > 0) {
1115
+ result.push({
1116
+ percentage: kf.percentage,
1117
+ properties: filtered,
1118
+ ...(kf.ease ? { ease: kf.ease } : {}),
1119
+ });
1120
+ }
1121
+ }
1122
+ return result;
1123
+ }
1124
+ function filterGroupProperties(properties, propSet) {
1125
+ const result = {};
1126
+ for (const [k, v] of Object.entries(properties)) {
1127
+ if (propSet.has(k))
1128
+ result[k] = v;
1129
+ }
1130
+ return result;
1131
+ }
1132
+ function addGroupAnimToScript(script, anim, propSet) {
1133
+ if (anim.keyframes) {
1134
+ const groupKeyframes = filterGroupKeyframes(anim.keyframes.keyframes, propSet);
1135
+ if (groupKeyframes.length === 0)
1136
+ return { script, id: "" };
1137
+ const pos = typeof anim.position === "number" ? anim.position : 0;
1138
+ return addAnimationWithKeyframesToScript(script, anim.targetSelector, pos, anim.duration ?? 0.5, groupKeyframes, anim.keyframes.easeEach ?? anim.ease);
1139
+ }
1140
+ const groupProperties = filterGroupProperties(anim.properties, propSet);
1141
+ if (Object.keys(groupProperties).length === 0)
1142
+ return { script, id: "" };
1143
+ const fromProperties = anim.method === "fromTo" && anim.fromProperties
1144
+ ? filterGroupProperties(anim.fromProperties, propSet)
1145
+ : undefined;
1146
+ return addAnimationToScript(script, {
1147
+ targetSelector: anim.targetSelector,
1148
+ method: anim.method,
1149
+ position: anim.position,
1150
+ duration: anim.duration,
1151
+ ease: anim.ease,
1152
+ properties: groupProperties,
1153
+ fromProperties,
1154
+ extras: anim.extras,
1155
+ });
1156
+ }
1157
+ /**
1158
+ * Split a mixed-property tween into one tween per property group (position,
1159
+ * scale, visual, etc.) so each group can be edited independently.
1160
+ * Returns the updated script and the IDs of the newly-created tweens.
1161
+ */
1162
+ export function splitIntoPropertyGroupsFromScript(script, animationId) {
1163
+ const parsed = parseGsapScriptAcornForWrite(script);
1164
+ if (!parsed)
1165
+ return { script, ids: [animationId] };
1166
+ const target = parsed.located.find((l) => l.id === animationId);
1167
+ if (!target)
1168
+ return { script, ids: [animationId] };
1169
+ const { animation } = target;
1170
+ const allPropKeys = collectPropertyKeys(animation);
1171
+ const groupProps = partitionPropertyGroups(allPropKeys);
1172
+ if (groupProps.size <= 1)
1173
+ return { script, ids: [animationId] };
1174
+ if (allPropKeys.has("transformOrigin"))
1175
+ assignTransformOrigin(groupProps);
1176
+ let result = removeAnimationFromScript(script, animationId);
1177
+ for (const [, props] of groupProps) {
1178
+ const { script: next, id } = addGroupAnimToScript(result, animation, new Set(props));
1179
+ if (id)
1180
+ result = next;
1181
+ }
1182
+ const reParsed = parseGsapScriptAcornForWrite(result);
1183
+ const newIds = (reParsed?.located ?? [])
1184
+ .filter((l) => l.animation.targetSelector === animation.targetSelector)
1185
+ .map((l) => l.id);
1186
+ return { script: result, ids: newIds };
1187
+ }
654
1188
  // ── Label write ops ───────────────────────────────────────────────────────────
1189
+ /** True when `expr` is `tl.<method>(…)` rooted at the timeline var. */
1190
+ function isTimelineMethodCall(expr, timelineVar, method) {
1191
+ return (expr?.type === "CallExpression" &&
1192
+ expr.callee?.type === "MemberExpression" &&
1193
+ isTimelineRooted(expr.callee.object, timelineVar) &&
1194
+ expr.callee.property?.name === method);
1195
+ }
1196
+ /** True when `expr` is `tl.addLabel("<name>", …)` rooted at the timeline var. */
1197
+ function isAddLabelCall(expr, timelineVar, name) {
1198
+ const firstArg = expr?.arguments?.[0];
1199
+ return (isTimelineMethodCall(expr, timelineVar, "addLabel") &&
1200
+ firstArg?.type === "Literal" &&
1201
+ firstArg.value === name);
1202
+ }
1203
+ /** Every `tl.addLabel("<name>", …)` ExpressionStatement in the script. */
1204
+ function findLabelStatements(parsed, name) {
1205
+ const targets = [];
1206
+ acornWalk.simple(parsed.ast, {
1207
+ ExpressionStatement(node) {
1208
+ if (isAddLabelCall(node.expression, parsed.timelineVar, name))
1209
+ targets.push(node);
1210
+ },
1211
+ });
1212
+ return targets;
1213
+ }
655
1214
  export function addLabelToScript(script, name, position) {
656
1215
  const parsed = parseGsapScriptAcornForWrite(script);
657
1216
  if (!parsed)
658
1217
  return script;
1218
+ // If the label already exists, MOVE it (overwrite its position) rather than
1219
+ // appending a duplicate. Two same-named addLabel statements make removeLabel
1220
+ // over-remove — it deletes every match, including a pre-existing label the
1221
+ // user never touched.
1222
+ const existing = findLabelStatements(parsed, name)[0];
1223
+ if (existing) {
1224
+ const ms = new MagicString(script);
1225
+ const posArg = existing.expression.arguments?.[1];
1226
+ if (posArg)
1227
+ ms.overwrite(posArg.start, posArg.end, valueToCode(position));
1228
+ else
1229
+ ms.appendLeft(existing.expression.end - 1, `, ${valueToCode(position)}`);
1230
+ return ms.toString();
1231
+ }
659
1232
  const insertionPoint = findInsertionPoint(parsed);
660
1233
  if (insertionPoint === null)
661
1234
  return script;
@@ -668,21 +1241,7 @@ export function removeLabelFromScript(script, name) {
668
1241
  const parsed = parseGsapScriptAcornForWrite(script);
669
1242
  if (!parsed)
670
1243
  return script;
671
- const targets = [];
672
- acornWalk.simple(parsed.ast, {
673
- // fallow-ignore-next-line complexity
674
- ExpressionStatement(node) {
675
- const expr = node.expression;
676
- if (expr?.type === "CallExpression" &&
677
- expr.callee?.type === "MemberExpression" &&
678
- isTimelineRooted(expr.callee.object, parsed.timelineVar) &&
679
- expr.callee.property?.name === "addLabel" &&
680
- expr.arguments?.[0]?.type === "Literal" &&
681
- expr.arguments[0].value === name) {
682
- targets.push(node);
683
- }
684
- },
685
- });
1244
+ const targets = findLabelStatements(parsed, name);
686
1245
  if (!targets.length)
687
1246
  return script;
688
1247
  const ms = new MagicString(script);
@@ -692,4 +1251,619 @@ export function removeLabelFromScript(script, name) {
692
1251
  }
693
1252
  return ms.toString();
694
1253
  }
1254
+ // ── Arc path helpers ─────────────────────────────────────────────────────────
1255
+ /**
1256
+ * Remove a set of properties from an ObjectExpression in a single pass.
1257
+ * Groups consecutive marked props into blocks to avoid overlapping remove ranges.
1258
+ */
1259
+ function removePropsByKey(ms, objNode, keys) {
1260
+ if (objNode?.type !== "ObjectExpression")
1261
+ return;
1262
+ const allProps = (objNode.properties ?? []).filter(isObjectProperty);
1263
+ const marked = allProps.map((p) => keys.has(propKeyName(p) ?? ""));
1264
+ let i = 0;
1265
+ while (i < allProps.length) {
1266
+ if (!marked[i]) {
1267
+ i++;
1268
+ continue;
1269
+ }
1270
+ const blockStart = i;
1271
+ while (i < allProps.length && marked[i])
1272
+ i++;
1273
+ ms.remove(...blockRemoveRange(allProps, blockStart, i));
1274
+ }
1275
+ }
1276
+ function blockRemoveRange(allProps, blockStart, blockEnd) {
1277
+ if (blockStart === 0 && blockEnd === allProps.length)
1278
+ return [allProps[0].start, allProps[allProps.length - 1].end];
1279
+ if (blockStart === 0)
1280
+ return [allProps[0].start, allProps[blockEnd].start];
1281
+ return [allProps[blockStart - 1].end, allProps[blockEnd - 1].end];
1282
+ }
1283
+ // fallow-ignore-next-line complexity
1284
+ function readLastWaypointXY(mpVal) {
1285
+ if (mpVal?.type !== "ObjectExpression")
1286
+ return { x: null, y: null };
1287
+ const pathProp = findPropertyNode(mpVal, "path");
1288
+ if (pathProp?.value?.type !== "ArrayExpression")
1289
+ return { x: null, y: null };
1290
+ const elems = pathProp.value.elements ?? [];
1291
+ const last = elems[elems.length - 1];
1292
+ if (last?.type !== "ObjectExpression")
1293
+ return { x: null, y: null };
1294
+ return {
1295
+ x: readNumericLiteralNode(findPropertyNode(last, "x")?.value),
1296
+ y: readNumericLiteralNode(findPropertyNode(last, "y")?.value),
1297
+ };
1298
+ }
1299
+ /**
1300
+ * Read a numeric value node — a plain numeric literal or a unary-minus negative
1301
+ * literal (e.g. `-120`). Returns null for anything non-numeric. Without the
1302
+ * UnaryExpression branch, negative waypoint coords (parsed as a UnaryExpression
1303
+ * with no `.value`) would be lost when disabling an arc path.
1304
+ */
1305
+ function readNumericLiteralNode(v) {
1306
+ if (LITERAL_NODE_TYPES.has(v?.type) && typeof v.value === "number")
1307
+ return v.value;
1308
+ if (v?.type === "UnaryExpression" &&
1309
+ v.operator === "-" &&
1310
+ typeof v.argument?.value === "number") {
1311
+ return -v.argument.value;
1312
+ }
1313
+ return null;
1314
+ }
1315
+ function disableArcPath(ms, call) {
1316
+ const mpProp = findPropertyNode(call.varsArg, "motionPath");
1317
+ if (!mpProp)
1318
+ return false;
1319
+ const { x, y } = readLastWaypointXY(mpProp.value);
1320
+ if (x === null && y === null) {
1321
+ const allProps = (call.varsArg.properties ?? []).filter(isObjectProperty);
1322
+ removeProp(ms, mpProp, allProps);
1323
+ return true;
1324
+ }
1325
+ // Overwrite the entire motionPath property with the recovered x/y pair — avoids
1326
+ // the appendLeft+remove range-boundary issue in MagicString.
1327
+ const parts = [];
1328
+ if (x !== null)
1329
+ parts.push(`x: ${x}`);
1330
+ if (y !== null)
1331
+ parts.push(`y: ${y}`);
1332
+ ms.overwrite(mpProp.start, mpProp.end, parts.join(", "));
1333
+ return true;
1334
+ }
1335
+ function stripXYFromKeyframes(ms, kfPropNode) {
1336
+ if (kfPropNode?.value?.type !== "ObjectExpression")
1337
+ return;
1338
+ const xyKeys = new Set(["x", "y"]);
1339
+ for (const pctProp of (kfPropNode.value.properties ?? []).filter(isObjectProperty)) {
1340
+ const k = propKeyName(pctProp);
1341
+ if (typeof k === "string" && k.endsWith("%") && pctProp.value?.type === "ObjectExpression") {
1342
+ removePropsByKey(ms, pctProp.value, xyKeys);
1343
+ }
1344
+ }
1345
+ }
1346
+ function enableArcPath(ms, call, animation, config) {
1347
+ const waypoints = extractArcWaypoints(animation);
1348
+ if (waypoints.length < 2)
1349
+ return false;
1350
+ const segments = config.segments.length === waypoints.length - 1
1351
+ ? config.segments
1352
+ : Array.from({ length: waypoints.length - 1 }, () => ({ curviness: 1 }));
1353
+ const motionPathCode = buildMotionPathObjectCode({
1354
+ waypoints,
1355
+ segments,
1356
+ autoRotate: config.autoRotate,
1357
+ });
1358
+ const vars = call.varsArg;
1359
+ if (vars?.type !== "ObjectExpression")
1360
+ return false;
1361
+ // Insert motionPath right after the opening `{` (appendRight at start+1) so the
1362
+ // insertion point can never coincide with the end boundary of the x/y removal
1363
+ // range. upsertProp would appendLeft at `end - 1`, which collides with a
1364
+ // remove-range that ends at the same offset when x/y are the only props —
1365
+ // MagicString then discards the append and the output loses everything.
1366
+ const editable = (vars.properties ?? []).filter(isObjectProperty);
1367
+ const survivesRemoval = editable.some((p) => {
1368
+ const k = propKeyName(p);
1369
+ return k !== "x" && k !== "y";
1370
+ });
1371
+ const sep = survivesRemoval ? ", " : "";
1372
+ ms.appendRight(vars.start + 1, ` motionPath: ${motionPathCode}${sep}`);
1373
+ stripXYFromKeyframes(ms, findPropertyNode(call.varsArg, "keyframes"));
1374
+ removePropsByKey(ms, call.varsArg, new Set(["x", "y"]));
1375
+ return true;
1376
+ }
1377
+ export function setArcPathInScript(script, animationId, config) {
1378
+ const parsed = parseGsapScriptAcornForWrite(script);
1379
+ if (!parsed)
1380
+ return script;
1381
+ const target = parsed.located.find((l) => l.id === animationId);
1382
+ if (!target)
1383
+ return script;
1384
+ const ms = new MagicString(script);
1385
+ const handled = config.enabled
1386
+ ? enableArcPath(ms, target.call, target.animation, config)
1387
+ : disableArcPath(ms, target.call);
1388
+ return handled ? ms.toString() : script;
1389
+ }
1390
+ export function updateArcSegmentInScript(script, animationId, segmentIndex, update) {
1391
+ const parsed = parseGsapScriptAcornForWrite(script);
1392
+ if (!parsed)
1393
+ return script;
1394
+ const target = parsed.located.find((l) => l.id === animationId);
1395
+ if (!target)
1396
+ return script;
1397
+ const { call, animation } = target;
1398
+ if (!animation.arcPath?.enabled)
1399
+ return script;
1400
+ const segments = [...animation.arcPath.segments];
1401
+ const existingSeg = segments[segmentIndex];
1402
+ if (segmentIndex < 0 || segmentIndex >= segments.length || !existingSeg)
1403
+ return script;
1404
+ segments[segmentIndex] = { ...existingSeg, ...update };
1405
+ const waypoints = extractArcWaypoints(animation);
1406
+ if (waypoints.length < 2)
1407
+ return script;
1408
+ const motionPathCode = buildMotionPathObjectCode({
1409
+ waypoints,
1410
+ segments,
1411
+ autoRotate: animation.arcPath.autoRotate,
1412
+ });
1413
+ const mpProp = findPropertyNode(call.varsArg, "motionPath");
1414
+ if (!mpProp)
1415
+ return script;
1416
+ const ms = new MagicString(script);
1417
+ ms.overwrite(mpProp.value.start, mpProp.value.end, motionPathCode);
1418
+ return ms.toString();
1419
+ }
1420
+ export function removeArcPathFromScript(script, animationId) {
1421
+ return setArcPathInScript(script, animationId, {
1422
+ enabled: false,
1423
+ autoRotate: false,
1424
+ segments: [],
1425
+ });
1426
+ }
1427
+ // ── splitAnimationsInScript helpers ──────────────────────────────────────────
1428
+ /** Overwrite the selector (first arg) of a tween call. */
1429
+ function updateAnimationSelectorInScript(script, animationId, newSelector) {
1430
+ const parsed = parseGsapScriptAcornForWrite(script);
1431
+ if (!parsed)
1432
+ return script;
1433
+ const target = parsed.located.find((l) => l.id === animationId);
1434
+ if (!target)
1435
+ return script;
1436
+ const selectorArg = target.call.node.arguments?.[0];
1437
+ if (!selectorArg)
1438
+ return script;
1439
+ const ms = new MagicString(script);
1440
+ ms.overwrite(selectorArg.start, selectorArg.end, JSON.stringify(newSelector));
1441
+ return ms.toString();
1442
+ }
1443
+ /**
1444
+ * Insert a `tl.set()` call immediately after the timeline declaration
1445
+ * (before existing tweens) to establish inherited state on a new element.
1446
+ */
1447
+ function insertInheritedStateSetInScript(script, selector, position, properties) {
1448
+ const parsed = parseGsapScriptAcornForWrite(script);
1449
+ if (!parsed)
1450
+ return script;
1451
+ const props = Object.entries(properties)
1452
+ .map(([k, v]) => `${safeKey(k)}: ${valueToCode(v)}`)
1453
+ .join(", ");
1454
+ const code = `${parsed.timelineVar}.set(${JSON.stringify(selector)}, { ${props} }, ${position});`;
1455
+ const ms = new MagicString(script);
1456
+ const tlDecl = findTimelineDeclarationStatement(parsed.ast, parsed.timelineVar);
1457
+ const firstLocated = parsed.located[0];
1458
+ if (tlDecl) {
1459
+ ms.appendLeft(tlDecl.end, "\n" + code);
1460
+ }
1461
+ else if (firstLocated) {
1462
+ const firstCall = firstLocated.call;
1463
+ const exprStmt = findEnclosingExpressionStatement(firstCall.ancestors);
1464
+ const insertAt = exprStmt?.start ?? firstCall.node.start;
1465
+ ms.prependLeft(insertAt, code + "\n");
1466
+ }
1467
+ else {
1468
+ ms.append("\n" + code);
1469
+ }
1470
+ return ms.toString();
1471
+ }
1472
+ /**
1473
+ * Compute, in forward (timeline) order, the inherited-props baseline available
1474
+ * BEFORE each matching tween, plus the final cumulative state at the split point.
1475
+ * A tween contributes to later baselines when it ends at/before the split (full
1476
+ * props or last keyframe), spans the split via keyframes (kfs at/before split),
1477
+ * or spans the split as a flat tween (its interpolated midpoint). Decoupled from
1478
+ * the reverse write loop so the spanning-tween midpoint reads earlier tweens.
1479
+ */
1480
+ // fallow-ignore-next-line complexity
1481
+ function computeForwardBaselines(matching, splitTime) {
1482
+ const before = [];
1483
+ const acc = {};
1484
+ for (const anim of matching) {
1485
+ before.push({ ...acc });
1486
+ const pos = typeof anim.position === "number" ? anim.position : 0;
1487
+ const dur = anim.duration ?? 0;
1488
+ const animEnd = pos + dur;
1489
+ if (anim.keyframes) {
1490
+ const kfs = anim.keyframes.keyframes;
1491
+ if (pos >= splitTime) {
1492
+ // Moves wholly to the new element — contributes nothing to the baseline.
1493
+ }
1494
+ else if (animEnd > splitTime) {
1495
+ for (const kf of kfs) {
1496
+ const kfTime = pos + (kf.percentage / 100) * dur;
1497
+ if (kfTime <= splitTime) {
1498
+ for (const [k, v] of Object.entries(kf.properties))
1499
+ acc[k] = v;
1500
+ }
1501
+ }
1502
+ }
1503
+ else {
1504
+ const lastKf = kfs[kfs.length - 1];
1505
+ if (lastKf) {
1506
+ for (const [k, v] of Object.entries(lastKf.properties))
1507
+ acc[k] = v;
1508
+ }
1509
+ }
1510
+ continue;
1511
+ }
1512
+ if (animEnd <= splitTime) {
1513
+ for (const [k, v] of Object.entries(anim.properties))
1514
+ acc[k] = v;
1515
+ continue;
1516
+ }
1517
+ if (pos >= splitTime)
1518
+ continue;
1519
+ // Flat tween spanning the split — its midpoint becomes the inherited value.
1520
+ const progress = dur > 0 ? (splitTime - pos) / dur : 0;
1521
+ const fromSource = anim.fromProperties ?? acc;
1522
+ for (const [k, v] of Object.entries(anim.properties)) {
1523
+ if (typeof v !== "number") {
1524
+ acc[k] = v;
1525
+ continue;
1526
+ }
1527
+ const fromVal = typeof fromSource[k] === "number" ? fromSource[k] : 0;
1528
+ acc[k] = fromVal + (v - fromVal) * progress;
1529
+ }
1530
+ }
1531
+ return { before, final: { ...acc } };
1532
+ }
1533
+ // Split one tween that straddles the split point: trim the original to the
1534
+ // first half (interpolated midpoint as its new end) and add a fromTo for the
1535
+ // second half on the new element. `fromSource` is the forward baseline.
1536
+ function buildSpanningSplit(result, anim, pos, dur, fromSource, ctx) {
1537
+ const progress = dur > 0 ? (ctx.splitTime - pos) / dur : 0;
1538
+ const midProps = {};
1539
+ for (const [k, v] of Object.entries(anim.properties)) {
1540
+ if (typeof v !== "number") {
1541
+ midProps[k] = v;
1542
+ continue;
1543
+ }
1544
+ const fromVal = typeof fromSource[k] === "number" ? fromSource[k] : 0;
1545
+ midProps[k] = fromVal + (v - fromVal) * progress;
1546
+ }
1547
+ const trimmed = updateAnimationInScript(result, anim.id, {
1548
+ duration: ctx.splitTime - pos,
1549
+ properties: midProps,
1550
+ });
1551
+ return addAnimationToScript(trimmed, {
1552
+ targetSelector: ctx.newSelector,
1553
+ method: "fromTo",
1554
+ position: ctx.newElementStart,
1555
+ duration: pos + dur - ctx.splitTime,
1556
+ properties: { ...anim.properties },
1557
+ fromProperties: { ...midProps },
1558
+ ease: anim.ease,
1559
+ extras: anim.extras,
1560
+ }).script;
1561
+ }
1562
+ // Decide what one matching tween does at the split point: move to the new
1563
+ // element (wholly after), stay (wholly before / keyframes before), get skipped
1564
+ // (keyframes spanning), or get interpolated in half (spanning). Returns the
1565
+ // updated script; pushes any skip reason into `skippedSelectors`.
1566
+ function applyTweenSplit(result, anim, baselineBefore, ctx, skippedSelectors) {
1567
+ const pos = typeof anim.position === "number" ? anim.position : 0;
1568
+ const dur = anim.duration ?? 0;
1569
+ const animEnd = pos + dur;
1570
+ if (anim.keyframes) {
1571
+ if (pos >= ctx.splitTime)
1572
+ return updateAnimationSelectorInScript(result, anim.id, ctx.newSelector);
1573
+ if (animEnd > ctx.splitTime) {
1574
+ skippedSelectors.push(`${ctx.originalSelector} (keyframes spanning split)`);
1575
+ }
1576
+ // Inherited-state for kf tweens is handled by computeForwardBaselines.
1577
+ return result;
1578
+ }
1579
+ // Wholly before the split — kept on the original element.
1580
+ if (animEnd <= ctx.splitTime)
1581
+ return result;
1582
+ // Wholly after — move to the new element.
1583
+ if (pos >= ctx.splitTime)
1584
+ return updateAnimationSelectorInScript(result, anim.id, ctx.newSelector);
1585
+ // Spans the split — interpolate the midpoint from the FORWARD baseline.
1586
+ const fromSource = anim.fromProperties ?? baselineBefore;
1587
+ return buildSpanningSplit(result, anim, pos, dur, fromSource, ctx);
1588
+ }
1589
+ export function splitAnimationsInScript(script, opts) {
1590
+ const parsed = parseGsapScriptAcornForWrite(script);
1591
+ if (!parsed)
1592
+ return { script, skippedSelectors: [] };
1593
+ const originalSelector = `#${opts.originalId}`;
1594
+ const newSelector = `#${opts.newId}`;
1595
+ const animations = parsed.located.map((l) => l.animation);
1596
+ const skippedSelectors = [];
1597
+ for (const a of animations) {
1598
+ if (a.targetSelector !== originalSelector && a.targetSelector.includes(opts.originalId)) {
1599
+ skippedSelectors.push(a.targetSelector);
1600
+ }
1601
+ }
1602
+ const matching = animations.filter((a) => a.targetSelector === originalSelector);
1603
+ if (matching.length === 0)
1604
+ return { script, skippedSelectors };
1605
+ let result = script;
1606
+ const newElementStart = opts.splitTime;
1607
+ // Forward pre-pass: compute the inherited-props baseline available BEFORE each
1608
+ // matching tween, in source/timeline order. The write loop below runs in
1609
+ // REVERSE (so updateAnimationSelectorInScript's selector edits can't shift the
1610
+ // count-based IDs of not-yet-processed tweens), but the spanning-tween midpoint
1611
+ // interpolation needs the baseline from EARLIER tweens — which a reverse
1612
+ // accumulator hasn't seen yet. Decoupling the two fixes the wrong midpoint.
1613
+ const { before: baselineBefore, final: finalInheritedProps } = computeForwardBaselines(matching, opts.splitTime);
1614
+ // Reverse iteration: updateAnimationSelectorInScript mutates selectors which
1615
+ // can shift count-based ID suffixes for later animations.
1616
+ const ctx = { splitTime: opts.splitTime, originalSelector, newSelector, newElementStart };
1617
+ for (let i = matching.length - 1; i >= 0; i--) {
1618
+ const anim = matching[i];
1619
+ if (!anim)
1620
+ continue;
1621
+ result = applyTweenSplit(result, anim, baselineBefore[i] ?? {}, ctx, skippedSelectors);
1622
+ }
1623
+ if (Object.keys(finalInheritedProps).length > 0) {
1624
+ result = insertInheritedStateSetInScript(result, newSelector, newElementStart, finalInheritedProps);
1625
+ }
1626
+ return { script: result, skippedSelectors };
1627
+ }
1628
+ // ── Unroll dynamic animations ────────────────────────────────────────────────
1629
+ function isLoopNode(node) {
1630
+ const t = node?.type;
1631
+ return (t === "ForStatement" ||
1632
+ t === "ForInStatement" ||
1633
+ t === "ForOfStatement" ||
1634
+ t === "WhileStatement");
1635
+ }
1636
+ function isForEachStatement(node) {
1637
+ return (node?.type === "ExpressionStatement" &&
1638
+ node.expression?.type === "CallExpression" &&
1639
+ node.expression.callee?.property?.name === "forEach");
1640
+ }
1641
+ /** The nearest enclosing loop / forEach AST node (not just its byte range). */
1642
+ function findEnclosingLoopNode(ancestors) {
1643
+ for (let i = ancestors.length - 2; i >= 0; i--) {
1644
+ const node = ancestors[i];
1645
+ if (isLoopNode(node) || isForEachStatement(node))
1646
+ return node;
1647
+ }
1648
+ return null;
1649
+ }
1650
+ /** Statements making up a loop's body block, or null when not a simple block. */
1651
+ function loopBodyStatements(loopNode) {
1652
+ let body;
1653
+ if (loopNode?.type === "ExpressionStatement") {
1654
+ // forEach(cb): body is the callback's block.
1655
+ const cb = loopNode.expression?.arguments?.[0];
1656
+ body = cb?.body;
1657
+ }
1658
+ else {
1659
+ body = loopNode?.body;
1660
+ }
1661
+ if (body?.type !== "BlockStatement")
1662
+ return null;
1663
+ return (body.body ?? []).filter((s) => s?.type === "ExpressionStatement");
1664
+ }
1665
+ /** The loop's index identifier name (`for (let i …)`), used for per-iteration substitution. */
1666
+ function loopIndexVarName(loopNode) {
1667
+ if (loopNode?.type === "ForStatement") {
1668
+ const decl = loopNode.init?.declarations?.[0];
1669
+ return typeof decl?.id?.name === "string" ? decl.id.name : null;
1670
+ }
1671
+ return null;
1672
+ }
1673
+ /**
1674
+ * Rewrite one body statement's source for iteration `idx`: replace USES of the
1675
+ * loop index variable (AST Identifier nodes) with the literal index. AST-based,
1676
+ * not a text regex, so the index name appearing inside a string literal (e.g. a
1677
+ * selector ".row-i") or as a non-computed member/key (`obj.i`, `{ i: … }`) is
1678
+ * left untouched — only real references to the variable are substituted.
1679
+ */
1680
+ // An identifier in "binding position" is a name, not a value reference: a
1681
+ // non-computed member property (`obj.i`) or object-literal key (`{ i: … }`).
1682
+ // Those must NOT be substituted with the iteration index.
1683
+ function isIndexBindingPosition(node, parent) {
1684
+ if (parent?.type === "MemberExpression")
1685
+ return parent.property === node && !parent.computed;
1686
+ if (parent?.type === "Property" || parent?.type === "ObjectProperty") {
1687
+ return parent.key === node && !parent.computed;
1688
+ }
1689
+ return false;
1690
+ }
1691
+ function substituteLoopIndex(stmt, indexVar, idx, script) {
1692
+ const base = stmt.start;
1693
+ const src = script.slice(base, stmt.end);
1694
+ const ranges = [];
1695
+ acornWalk.ancestor(stmt, {
1696
+ Identifier(node, _state, ancestors) {
1697
+ if (node.name !== indexVar)
1698
+ return;
1699
+ if (isIndexBindingPosition(node, ancestors[ancestors.length - 2]))
1700
+ return;
1701
+ ranges.push([node.start - base, node.end - base]);
1702
+ },
1703
+ });
1704
+ if (ranges.length === 0)
1705
+ return src;
1706
+ ranges.sort((a, b) => b[0] - a[0]);
1707
+ let out = src;
1708
+ for (const [s, e] of ranges)
1709
+ out = out.slice(0, s) + String(idx) + out.slice(e);
1710
+ return out;
1711
+ }
1712
+ function buildUnrollReplacement(timelineVar, animation, elements) {
1713
+ const duration = typeof animation.duration === "number" ? animation.duration : 8;
1714
+ const ease = typeof animation.ease === "string" ? animation.ease : "none";
1715
+ const pos = animation.position ?? 0;
1716
+ const posCode = typeof pos === "number" ? String(pos) : JSON.stringify(pos);
1717
+ const calls = elements.map((el) => {
1718
+ const sorted = [...el.keyframes].sort((a, b) => a.percentage - b.percentage);
1719
+ const kfCode = buildKeyframeObjectCode(sorted, el.easeEach);
1720
+ return `${timelineVar}.to(${JSON.stringify(el.selector)}, { keyframes: ${kfCode}, duration: ${duration}, ease: ${JSON.stringify(ease)} }, ${posCode});`;
1721
+ });
1722
+ return calls.join("\n ");
1723
+ }
1724
+ /** Build one element's unrolled `tl.to(...)` call from the target animation. */
1725
+ function buildUnrollCallForElement(timelineVar, animation, el) {
1726
+ const duration = typeof animation.duration === "number" ? animation.duration : 8;
1727
+ const ease = typeof animation.ease === "string" ? animation.ease : "none";
1728
+ const pos = animation.position ?? 0;
1729
+ const posCode = typeof pos === "number" ? String(pos) : JSON.stringify(pos);
1730
+ const sorted = [...el.keyframes].sort((a, b) => a.percentage - b.percentage);
1731
+ const kfCode = buildKeyframeObjectCode(sorted, el.easeEach);
1732
+ return `${timelineVar}.to(${JSON.stringify(el.selector)}, { keyframes: ${kfCode}, duration: ${duration}, ease: ${JSON.stringify(ease)} }, ${posCode});`;
1733
+ }
1734
+ /** Sentinel: the unroll cannot safely reproduce the loop body — caller no-ops. */
1735
+ const REFUSE_UNROLL = Symbol("refuse-unroll");
1736
+ /** Every statement in a loop's body block (unfiltered), or [] when not a block. */
1737
+ function loopBodyRawStatements(loopNode) {
1738
+ const body = loopNode?.type === "ExpressionStatement"
1739
+ ? loopNode.expression?.arguments?.[0]?.body
1740
+ : loopNode?.body;
1741
+ return body?.type === "BlockStatement" ? (body.body ?? []) : [];
1742
+ }
1743
+ /** A node that re-binds `indexVar`: a re-declaration or a function param. */
1744
+ function rebindsIndex(node, indexVar) {
1745
+ if (node.type === "VariableDeclarator")
1746
+ return node.id?.name === indexVar;
1747
+ if (node.type === "FunctionExpression" ||
1748
+ node.type === "FunctionDeclaration" ||
1749
+ node.type === "ArrowFunctionExpression") {
1750
+ return (node.params ?? []).some((p) => p?.name === indexVar);
1751
+ }
1752
+ return false;
1753
+ }
1754
+ /** Object shorthand `{ i }` — substituting the value would yield invalid `{ 0 }`. */
1755
+ function isShorthandIndexUse(node, indexVar) {
1756
+ return ((node.type === "Property" || node.type === "ObjectProperty") &&
1757
+ node.shorthand === true &&
1758
+ propKeyName(node) === indexVar);
1759
+ }
1760
+ /**
1761
+ * A sibling statement can't be safely index-substituted when it re-binds the
1762
+ * loop index (shadowing — a nested `for (let i …)`, a callback param `i`) or
1763
+ * uses it in object shorthand (`{ i }`, which would splice to the invalid
1764
+ * `{ 0 }`). substituteLoopIndex has no scope analysis, so in these cases it
1765
+ * would emit broken or wrong code — the unroll must refuse instead.
1766
+ */
1767
+ function hasUnsafeLoopIndexUse(stmt, indexVar) {
1768
+ let unsafe = false;
1769
+ acornWalk.full(stmt, (node) => {
1770
+ if (!unsafe && (isShorthandIndexUse(node, indexVar) || rebindsIndex(node, indexVar))) {
1771
+ unsafe = true;
1772
+ }
1773
+ });
1774
+ return unsafe;
1775
+ }
1776
+ /** How to handle the loop body's non-target siblings when unrolling. */
1777
+ function unrollSiblingStrategy(loopNode, targetStmt, stmts, indexVar) {
1778
+ const siblings = stmts.filter((s) => s !== targetStmt);
1779
+ // A sibling the filtered statement list doesn't model (non-ExpressionStatement)
1780
+ // would be silently lost by either path — refuse if any exists.
1781
+ const hasUnmodeledSibling = loopBodyRawStatements(loopNode).some((s) => s !== targetStmt && !stmts.includes(s));
1782
+ if (siblings.length === 0 && !hasUnmodeledSibling)
1783
+ return "blanket";
1784
+ if (hasUnmodeledSibling || !indexVar)
1785
+ return "refuse";
1786
+ return siblings.some((s) => hasUnsafeLoopIndexUse(s, indexVar)) ? "refuse" : "preserve";
1787
+ }
1788
+ /** Emit the per-iteration unrolled lines (target → static tl.to, siblings → index-substituted). */
1789
+ function emitUnrolledLines(stmts, targetStmt, elements, timelineVar, animation, indexVar, script) {
1790
+ const lines = [];
1791
+ for (let idx = 0; idx < elements.length; idx++) {
1792
+ const el = elements[idx];
1793
+ if (!el)
1794
+ continue;
1795
+ for (const stmt of stmts) {
1796
+ lines.push(stmt === targetStmt
1797
+ ? buildUnrollCallForElement(timelineVar, animation, el)
1798
+ : substituteLoopIndex(stmt, indexVar, idx, script));
1799
+ }
1800
+ }
1801
+ return lines.join("\n ");
1802
+ }
1803
+ /**
1804
+ * Unroll the loop body, preserving every statement that is NOT the target tween.
1805
+ * For each iteration, emit each non-target statement with the loop index
1806
+ * substituted (e.g. `tl.set(items[i], …)` → `tl.set(items[0], …)`), and replace
1807
+ * the target tween statement with that element's static `tl.to()` call.
1808
+ *
1809
+ * Returns null when a blanket overwrite is lossless (no sibling statements), and
1810
+ * REFUSE_UNROLL when siblings exist but can't be safely reproduced — a non-`for`
1811
+ * loop (no numeric index to splice), a statement we don't model, or an unsafe
1812
+ * index use (shadowing / shorthand). Refusing no-ops the unroll, which is safe:
1813
+ * the dynamic loop keeps rendering correctly, just un-flattened.
1814
+ */
1815
+ function buildLoopUnrollPreserving(script, timelineVar, animation, elements, loopNode, targetStmt) {
1816
+ const stmts = loopBodyStatements(loopNode);
1817
+ if (!stmts || !stmts.includes(targetStmt))
1818
+ return null;
1819
+ const indexVar = loopIndexVarName(loopNode);
1820
+ const strategy = unrollSiblingStrategy(loopNode, targetStmt, stmts, indexVar);
1821
+ if (strategy === "blanket")
1822
+ return null;
1823
+ if (strategy === "refuse" || !indexVar)
1824
+ return REFUSE_UNROLL;
1825
+ return emitUnrolledLines(stmts, targetStmt, elements, timelineVar, animation, indexVar, script);
1826
+ }
1827
+ /**
1828
+ * Replace a dynamic loop that generates multiple tween calls with individual
1829
+ * static `tl.to()` calls — one per element. Finds the loop containing the
1830
+ * animation and replaces the loop with unrolled static calls, preserving every
1831
+ * non-target statement in the loop body per iteration.
1832
+ */
1833
+ export function unrollDynamicAnimations(script, animationId, elements) {
1834
+ // An empty element list has no unrolled form — replacing the loop/statement
1835
+ // with zero calls would silently delete the animation. No-op instead.
1836
+ if (elements.length === 0)
1837
+ return script;
1838
+ const parsed = parseGsapScriptAcornForWrite(script);
1839
+ if (!parsed)
1840
+ return script;
1841
+ const target = parsed.located.find((l) => l.id === animationId);
1842
+ if (!target)
1843
+ return script;
1844
+ const ms = new MagicString(script);
1845
+ const loopNode = findEnclosingLoopNode(target.call.ancestors);
1846
+ if (loopNode) {
1847
+ const targetStmt = findEnclosingExpressionStatement(target.call.ancestors);
1848
+ const preserving = targetStmt
1849
+ ? buildLoopUnrollPreserving(script, parsed.timelineVar, target.animation, elements, loopNode, targetStmt)
1850
+ : null;
1851
+ // Siblings exist but can't be safely reproduced — leave the loop untouched
1852
+ // rather than drop or corrupt them. The op no-ops (before === after).
1853
+ if (preserving === REFUSE_UNROLL)
1854
+ return script;
1855
+ // Fall back to the simple whole-body replacement when the body isn't a plain
1856
+ // block of statements we can preserve.
1857
+ const replacement = preserving ?? buildUnrollReplacement(parsed.timelineVar, target.animation, elements);
1858
+ ms.overwrite(loopNode.start, loopNode.end, replacement);
1859
+ }
1860
+ else {
1861
+ const stmt = findEnclosingExpressionStatement(target.call.ancestors);
1862
+ if (!stmt)
1863
+ return script;
1864
+ const replacement = buildUnrollReplacement(parsed.timelineVar, target.animation, elements);
1865
+ ms.overwrite(stmt.start, stmt.end, replacement);
1866
+ }
1867
+ return ms.toString();
1868
+ }
695
1869
  //# sourceMappingURL=gsapWriterAcorn.js.map