@hyperframes/parsers 0.7.15 → 0.7.16
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.
- package/dist/gsapParser.js +76 -25
- package/dist/gsapParser.js.map +1 -1
- package/dist/gsapParserAcorn.js +67 -27
- package/dist/gsapParserAcorn.js.map +1 -1
- package/dist/gsapParserExports.js +60 -22
- package/dist/gsapParserExports.js.map +1 -1
- package/dist/gsapWriterAcorn.js +70 -26
- package/dist/gsapWriterAcorn.js.map +1 -1
- package/dist/index.js +64 -25
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/types.ts","../src/gsapConstants.ts","../src/gsapSerialize.ts","../src/gsapParser.ts","../src/springEase.ts","../src/gsapParserAcorn.ts","../src/gsapInline.ts","../src/hfIds.ts","../src/utils/cssSelector.ts","../src/gsapWriterAcorn.ts","../src/htmlParser.ts","../src/gsapUnroll.ts"],"sourcesContent":["// ── Composition data types ───────────────────────────────────────────────────\n// Moved from @hyperframes/core/core.types in the parsers extraction refactor.\n// These are the types produced and consumed by the parser pipeline.\n\nexport interface Asset {\n id: string;\n url: string;\n type: string;\n is_reference?: boolean;\n /** Duration in seconds for video/audio assets */\n duration?: number;\n}\n\n// ── Timeline types ──────────────────────────────────────────────────────────\n\nexport type TimelineElementType = \"video\" | \"image\" | \"text\" | \"audio\" | \"composition\";\nexport type MediaElementType = \"video\" | \"image\" | \"audio\";\n\nexport const CANVAS_DIMENSIONS = {\n landscape: { width: 1920, height: 1080 },\n portrait: { width: 1080, height: 1920 },\n \"landscape-4k\": { width: 3840, height: 2160 },\n \"portrait-4k\": { width: 2160, height: 3840 },\n square: { width: 1080, height: 1080 },\n \"square-4k\": { width: 2160, height: 2160 },\n} as const;\n\n// Single source of truth: derive the type from the table so adding a preset\n// extends the union automatically. Avoids the prior `as readonly CanvasResolution[]`\n// cast on `VALID_CANVAS_RESOLUTIONS` quietly drifting if the table grew but\n// the union didn't.\nexport type CanvasResolution = keyof typeof CANVAS_DIMENSIONS;\n\n// `Object.keys` ordering matches insertion order in `CANVAS_DIMENSIONS` on\n// every supported JS engine; tests pin the order in `index.test.ts`. Reorder\n// the table above with care.\nexport const VALID_CANVAS_RESOLUTIONS = Object.keys(\n CANVAS_DIMENSIONS,\n) as readonly CanvasResolution[];\n\nconst RESOLUTION_ALIASES: Record<string, CanvasResolution> = {\n \"1080p\": \"landscape\",\n hd: \"landscape\",\n \"1080p-portrait\": \"portrait\",\n \"portrait-1080p\": \"portrait\",\n \"4k\": \"landscape-4k\",\n uhd: \"landscape-4k\",\n \"4k-portrait\": \"portrait-4k\",\n \"1080p-square\": \"square\",\n \"square-1080p\": \"square\",\n \"4k-square\": \"square-4k\",\n};\n\n/**\n * Map a user-facing resolution string (canonical name or alias) to a\n * `CanvasResolution`. Returns undefined for unknown values so callers\n * can produce their own \"invalid\" UX (CLI exit, route validation, etc.).\n */\nexport function normalizeResolutionFlag(input: string | undefined): CanvasResolution | undefined {\n if (!input) return undefined;\n const lowered = input.toLowerCase();\n if ((VALID_CANVAS_RESOLUTIONS as readonly string[]).includes(lowered)) {\n return lowered as CanvasResolution;\n }\n return RESOLUTION_ALIASES[lowered];\n}\n\nexport interface TimelineElementBase {\n id: string;\n type: TimelineElementType;\n name: string;\n startTime: number;\n duration: number;\n zIndex: number;\n x?: number;\n y?: number;\n scale?: number;\n opacity?: number;\n}\n\nexport interface TimelineMediaElement extends TimelineElementBase {\n type: MediaElementType;\n src: string;\n mediaStartTime?: number;\n sourceDuration?: number;\n isAroll?: boolean;\n sourceWidth?: number;\n sourceHeight?: number;\n volume?: number; // 0-1 (0% to 100%), default 1.0\n hasAudio?: boolean; // For videos - indicates if video has audio track\n}\n\nexport interface WaveformData {\n peaks: number[];\n duration: number;\n sampleRate?: number;\n}\n\nexport interface TimelineTextElement extends TimelineElementBase {\n type: \"text\";\n content: string;\n color?: string;\n fontSize?: number;\n textShadow?: boolean;\n fontFamily?: string;\n fontWeight?: number;\n textOutline?: boolean;\n textOutlineColor?: string;\n textOutlineWidth?: number;\n textHighlight?: boolean;\n textHighlightColor?: string;\n textHighlightPadding?: number;\n textHighlightRadius?: number;\n}\n\nexport interface TimelineCompositionElement extends TimelineElementBase {\n type: \"composition\";\n src: string;\n compositionId: string;\n scale?: number;\n sourceDuration?: number;\n variableValues?: Record<string, string | number | boolean>;\n sourceWidth?: number;\n sourceHeight?: number;\n}\n\n// Composition Variable Types\nexport type CompositionVariableType =\n | \"string\"\n | \"number\"\n | \"color\"\n | \"boolean\"\n | \"enum\"\n | \"font\"\n | \"image\";\n\n/**\n * Runtime list of every valid `CompositionVariableType`. Use this anywhere\n * a Set/array of valid type strings is needed (lint rules, validators).\n * The `satisfies` guard turns adding a new variant to the union without\n * also adding it here into a compile error.\n */\nexport const COMPOSITION_VARIABLE_TYPES = [\n \"string\",\n \"number\",\n \"color\",\n \"boolean\",\n \"enum\",\n \"font\",\n \"image\",\n] as const satisfies readonly CompositionVariableType[];\n\nexport interface CompositionVariableBase {\n id: string;\n type: CompositionVariableType;\n label: string;\n description?: string;\n}\n\nexport interface StringVariable extends CompositionVariableBase {\n type: \"string\";\n default: string;\n placeholder?: string;\n maxLength?: number;\n}\n\nexport interface NumberVariable extends CompositionVariableBase {\n type: \"number\";\n default: number;\n min?: number;\n max?: number;\n step?: number;\n unit?: string;\n}\n\nexport interface ColorVariable extends CompositionVariableBase {\n type: \"color\";\n default: string;\n /** Brand role identifier, e.g. \"color:primary\". */\n brandRole?: string;\n}\n\nexport interface BooleanVariable extends CompositionVariableBase {\n type: \"boolean\";\n default: boolean;\n}\n\nexport interface EnumVariable extends CompositionVariableBase {\n type: \"enum\";\n default: string;\n options: { value: string; label: string }[];\n}\n\n/**\n * Font variable — value is a `{name, source}` object (object-valued; LOCKED §7).\n * `default` is the fallback font-family name string.\n * `source` is the font stylesheet URL (e.g. Google Fonts CSS).\n * `default_name` / `default_source` are the CSS-level fallbacks when the\n * brand font is absent.\n */\nexport interface FontVariable extends CompositionVariableBase {\n type: \"font\";\n /** Fallback font-family name, e.g. \"Inter\". */\n default: string;\n /** Font stylesheet URL (e.g. Google Fonts CSS link). */\n source?: string;\n /** CSS font-family name to use when source is unavailable, e.g. \"sans-serif\". */\n default_name?: string;\n /** Fallback font stylesheet URL (empty string = system font). */\n default_source?: string;\n}\n\n/**\n * Image variable — value is a `{url, …}` object (object-valued; LOCKED §7).\n * `default` is the fallback image URL string.\n * `brandRole` is an optional semantic label, e.g. \"logo:primary\".\n */\nexport interface ImageVariable extends CompositionVariableBase {\n type: \"image\";\n /** Fallback image URL. */\n default: string;\n /** Brand role identifier, e.g. \"logo:primary\". */\n brandRole?: string;\n}\n\nexport type CompositionVariable =\n | StringVariable\n | NumberVariable\n | ColorVariable\n | BooleanVariable\n | EnumVariable\n | FontVariable\n | ImageVariable;\n\nexport interface CompositionSpec {\n id: string;\n duration: number;\n variables: CompositionVariable[];\n}\n\nexport type TimelineElement =\n | TimelineMediaElement\n | TimelineTextElement\n | TimelineCompositionElement;\n\nexport function isTextElement(el: TimelineElement): el is TimelineTextElement {\n return el.type === \"text\";\n}\n\nexport function isMediaElement(el: TimelineElement): el is TimelineMediaElement {\n return el.type === \"video\" || el.type === \"image\" || el.type === \"audio\";\n}\n\nexport function isCompositionElement(el: TimelineElement): el is TimelineCompositionElement {\n return el.type === \"composition\";\n}\n\nexport interface MediaFile {\n id: string;\n name: string;\n type: TimelineElementType;\n src: string;\n file?: File;\n duration?: number;\n compositionId?: string;\n sourceWidth?: number; // Intrinsic width for compositions\n sourceHeight?: number; // Intrinsic height for compositions\n}\n\nexport const TIMELINE_COLORS: Record<TimelineElementType, string> = {\n video: \"#ec4899\",\n image: \"#3b82f6\",\n text: \"#06b6d4\",\n audio: \"#10b981\",\n composition: \"#f97316\",\n};\n\nexport const DEFAULT_DURATIONS: Record<TimelineElementType, number> = {\n video: 5,\n image: 5,\n text: 2,\n audio: 5,\n composition: 5,\n};\n\nexport interface CompositionAPI {\n id: string;\n duration: number;\n seek(time: number): void;\n getTime(): number;\n getDuration(): number;\n}\n\n// ── Player API types (used by runtime) ────────────────────────────────────\n\nexport interface PlayerAPI {\n play(): void;\n pause(): void;\n seek(time: number, options?: { keepPlaying?: boolean }): void;\n getTime(): number;\n getDuration(): number;\n isPlaying(): boolean;\n getMainTimeline(): unknown;\n getElementBounds(elementId: string): void;\n getElementsAtPoint(x: number, y: number): void;\n setElementPosition(elementId: string, x: number, y: number): void;\n previewElementPosition(elementId: string, x: number, y: number): void;\n setElementKeyframes(\n elementId: string,\n keyframes: Array<{\n id: string;\n time: number;\n properties: { x?: number; y?: number };\n }> | null,\n ): void;\n setElementScale(elementId: string, scale: number): void;\n setElementFontSize(elementId: string, fontSize: number): void;\n setElementTextContent(elementId: string, content: string): void;\n setElementTextColor(elementId: string, color: string): void;\n setElementTextShadow(elementId: string, enabled: boolean): void;\n setElementTextFontWeight(elementId: string, weight: number): void;\n setElementTextFontFamily(elementId: string, fontFamily: string): void;\n setElementTextOutline(elementId: string, enabled: boolean, color?: string, width?: number): void;\n setElementTextHighlight(\n elementId: string,\n enabled: boolean,\n color?: string,\n padding?: number,\n radius?: number,\n ): void;\n setElementVolume(elementId: string, volume: number): void;\n setStageZoom(scale: number, focusX: number, focusY: number): void;\n getStageZoom(): { scale: number; focusX: number; focusY: number };\n setStageZoomKeyframes(\n keyframes: Array<{\n id: string;\n time: number;\n zoom: { scale: number; focusX: number; focusY: number };\n ease?: string;\n }> | null,\n ): void;\n getStageZoomKeyframes(): Array<{\n id: string;\n time: number;\n zoom: { scale: number; focusX: number; focusY: number };\n ease?: string;\n }>;\n addElement(data: AddElementData): boolean;\n removeElement(elementId: string): boolean;\n updateElementTiming(elementId: string, start?: number, end?: number): boolean;\n setElementTiming(\n elementId: string,\n startTime: number,\n duration: number,\n mediaStartTime?: number,\n ): void;\n updateElementSrc(elementId: string, src: string): boolean;\n updateElementLayer(elementId: string, zIndex: number): boolean;\n updateElementBasePosition(elementId: string, x?: number, y?: number, scale?: number): boolean;\n markTimelineDirty(): void;\n isTimelineDirty(): boolean;\n rebuildTimeline(): void;\n ensureTimeline(): void;\n enableRenderMode(): void;\n disableRenderMode(): void;\n renderSeek(time: number): void;\n getElementVisibility(elementId: string): { visible: boolean; opacity?: number };\n getVisibleElements(): Array<{ id: string; tagName: string; start: number; end: number }>;\n getRenderState(): {\n time: number;\n duration: number;\n isPlaying: boolean;\n renderMode: boolean;\n timelineDirty: boolean;\n };\n}\n\nexport interface AddElementData {\n id: string;\n type: \"video\" | \"image\" | \"text\" | \"audio\" | \"composition\";\n name?: string;\n src?: string;\n content?: string;\n start: number;\n end: number;\n zIndex?: number;\n x?: number;\n y?: number;\n scale?: number;\n fontSize?: number;\n color?: string;\n textShadow?: boolean;\n fontWeight?: number;\n textOutline?: boolean;\n textOutlineColor?: string;\n textOutlineWidth?: number;\n textHighlight?: boolean;\n textHighlightColor?: string;\n textHighlightPadding?: number;\n textHighlightRadius?: number;\n compositionId?: string;\n sourceWidth?: number;\n sourceHeight?: number;\n isAroll?: boolean;\n}\n\nexport interface ValidationResult {\n valid: boolean;\n errors: string[];\n warnings: string[];\n}\n\nexport interface CompositionAsset {\n id: string;\n name: string;\n type: \"composition\";\n src: string;\n duration: number;\n compositionId: string;\n thumbnail?: string;\n}\n\nexport interface Keyframe {\n id: string;\n time: number;\n properties: Partial<KeyframeProperties>;\n ease?: string;\n}\n\nexport interface KeyframeProperties {\n x: number;\n y: number;\n opacity: number;\n scale: number;\n scaleX: number;\n scaleY: number;\n rotation: number;\n width: number;\n height: number;\n}\n\nexport interface ElementKeyframes {\n elementId: string;\n keyframes: Keyframe[];\n}\n\nexport interface StageZoom {\n scale: number;\n focusX: number;\n focusY: number;\n}\n\nexport interface StageZoomKeyframe {\n id: string;\n time: number;\n zoom: StageZoom;\n ease?: string;\n}\n\nexport function getDefaultStageZoom(resolution: CanvasResolution): StageZoom {\n const { width, height } = CANVAS_DIMENSIONS[resolution];\n return {\n scale: 1,\n focusX: width / 2,\n focusY: height / 2,\n };\n}\n","/**\n * GSAP property and ease constants.\n *\n * Extracted into a standalone module so browser code can import them\n * without pulling in gsapParser (which depends on recast / @babel/parser).\n */\n\nexport const SUPPORTED_PROPS = [\n // 2D Transforms\n \"x\",\n \"y\",\n \"scale\",\n \"scaleX\",\n \"scaleY\",\n \"rotation\",\n \"skewX\",\n \"skewY\",\n // 3D Transforms\n \"z\",\n \"rotationX\",\n \"rotationY\",\n \"rotationZ\",\n \"perspective\",\n \"transformPerspective\",\n \"transformOrigin\",\n // Visibility\n \"opacity\",\n \"visibility\",\n \"autoAlpha\",\n // Dimensions\n \"width\",\n \"height\",\n // Colors\n \"color\",\n \"backgroundColor\",\n \"borderColor\",\n // Box model\n \"borderRadius\",\n // Typography\n \"fontSize\",\n \"letterSpacing\",\n // Filter & Clipping\n \"filter\",\n \"clipPath\",\n // DOM content (number counters, text roll-ups)\n \"innerText\",\n];\n\n// ── Property Groups ─────────────────────────────────────────────────────────\n// Each group maps to an independent GSAP tween so editing one property\n// (e.g. drag → x/y) never contaminates another (e.g. scale, rotation).\n\nexport type PropertyGroupName = \"position\" | \"scale\" | \"size\" | \"rotation\" | \"visual\" | \"other\";\n\nexport const PROPERTY_GROUPS: Record<PropertyGroupName, ReadonlySet<string>> = {\n position: new Set([\"x\", \"y\", \"xPercent\", \"yPercent\"]),\n scale: new Set([\"scale\", \"scaleX\", \"scaleY\"]),\n size: new Set([\"width\", \"height\"]),\n rotation: new Set([\"rotation\", \"skewX\", \"skewY\"]),\n visual: new Set([\"opacity\", \"autoAlpha\"]),\n other: new Set<string>(),\n};\n\nconst PROP_TO_GROUP = new Map<string, PropertyGroupName>();\nfor (const [group, props] of Object.entries(PROPERTY_GROUPS) as [\n PropertyGroupName,\n ReadonlySet<string>,\n][]) {\n for (const p of props) PROP_TO_GROUP.set(p, group);\n}\n\nexport function classifyPropertyGroup(prop: string): PropertyGroupName {\n return PROP_TO_GROUP.get(prop) ?? \"other\";\n}\n\nexport function classifyTweenPropertyGroup(\n properties: Record<string, unknown>,\n): PropertyGroupName | undefined {\n const groups = new Set<PropertyGroupName>();\n for (const key of Object.keys(properties)) {\n // transformOrigin is a modifier; `_auto` is Studio's internal endpoint marker;\n // `data` is GSAP-reserved (carries the Studio hold-set tag). None is an animated\n // property, so none should affect the group.\n if (key === \"transformOrigin\" || key === \"_auto\" || key === \"data\") continue;\n const g = classifyPropertyGroup(key);\n groups.add(g);\n }\n if (groups.size === 1) return groups.values().next().value;\n return undefined;\n}\n\nexport const SUPPORTED_EASES = [\n \"none\",\n \"power1.in\",\n \"power1.out\",\n \"power1.inOut\",\n \"power2.in\",\n \"power2.out\",\n \"power2.inOut\",\n \"power3.in\",\n \"power3.out\",\n \"power3.inOut\",\n \"power4.in\",\n \"power4.out\",\n \"power4.inOut\",\n \"back.in\",\n \"back.out\",\n \"back.inOut\",\n \"elastic.in\",\n \"elastic.out\",\n \"elastic.inOut\",\n \"bounce.in\",\n \"bounce.out\",\n \"bounce.inOut\",\n \"expo.in\",\n \"expo.out\",\n \"expo.inOut\",\n \"spring-gentle\",\n \"spring-bouncy\",\n \"spring-stiff\",\n \"spring-wobbly\",\n \"spring-heavy\",\n \"steps(1)\",\n];\n","/**\n * Recast-free GSAP helpers: serialization, keyframe<->animation conversion,\n * validation, and shared types.\n *\n * This module MUST NOT import recast / @babel/parser. It is part of the\n * isomorphic core layer that the barrel and browser code depend on. AST\n * parsing of GSAP source lives in the Node-only `./gsapParser` module.\n */\nimport type { Keyframe, KeyframeProperties, ValidationResult } from \"./types.js\";\nimport type { PropertyGroupName } from \"./gsapConstants\";\n\nexport type GsapMethod = \"set\" | \"to\" | \"from\" | \"fromTo\";\n\n/** How a tween was constructed in source — drives display classification and editability. */\nexport type GsapProvenanceKind = \"literal\" | \"helper\" | \"loop\" | \"runtime-dynamic\";\n\n/**\n * Origin of a parsed tween. `literal` tweens map 1:1 to a source call and edit\n * directly; `helper`/`loop` tweens are expanded from a reused construct (unroll\n * to edit); `runtime-dynamic` tweens come from live introspection (override to\n * edit). Absent provenance is treated as `literal`.\n */\nexport interface GsapProvenance {\n kind: GsapProvenanceKind;\n /** Helper function name (kind === \"helper\"). */\n fn?: string;\n /** 1-based ordinal of the originating call site / loop construct in source order. */\n callSite?: number;\n /** 0-based iteration index (kind === \"loop\"). */\n iteration?: number;\n /** Source offset [start, end] of the originating call/loop, when known. */\n sourceRange?: [number, number];\n}\n\n/** How a tween's keyframes can be edited, derived from its provenance. */\nexport type KeyframeEditability = \"direct\" | \"unroll\" | \"source\";\n\n/**\n * Map provenance to an editing strategy:\n * - `direct` — literal tween, maps 1:1 to source; edit in place.\n * - `unroll` — helper/loop expansion; unroll to literal tweens, then edit.\n * - `source` — runtime-dynamic value; not statically editable, edit the code.\n */\nexport function editabilityForProvenance(provenance?: GsapProvenance): KeyframeEditability {\n if (!provenance || provenance.kind === \"literal\") return \"direct\";\n if (provenance.kind === \"runtime-dynamic\") return \"source\";\n return \"unroll\";\n}\n\nexport interface GsapAnimation {\n id: string;\n targetSelector: string;\n method: GsapMethod;\n position: number | string;\n properties: Record<string, number | string>;\n fromProperties?: Record<string, number | string>;\n duration?: number;\n ease?: string;\n /** Non-editable GSAP config (stagger, yoyo, repeat, etc.) preserved for round-trips. */\n extras?: Record<string, unknown>;\n /** Native GSAP keyframes data — present when the tween uses keyframes: { ... }. */\n keyframes?: GsapKeyframesData;\n /** Arc motion path config — present when the tween uses motionPath for curved position interpolation. */\n arcPath?: ArcPathConfig;\n /** True when the tween has a `keyframes` property that couldn't be statically resolved (dynamic). */\n hasUnresolvedKeyframes?: boolean;\n /** True when the tween's target selector couldn't be statically resolved (dynamic). */\n hasUnresolvedSelector?: boolean;\n /** Absolute start time computed by walking the timeline chain (handles +=, -=, <, >, labels). */\n resolvedStart?: number;\n /** True when no position arg was authored — the tween is sequentially placed by GSAP. */\n implicitPosition?: boolean;\n /** Which property group this tween belongs to (position, scale, size, rotation, visual, other).\n * Undefined for legacy mixed tweens that bundle multiple groups. */\n propertyGroup?: PropertyGroupName;\n /** True for a base `gsap.set(...)` (a static hold that runs immediately, OFF the\n * timeline) rather than `tl.set(...)`. Carries no timeline position and shows no\n * keyframe marker — used to persist a static value (e.g. a 3D transform) without\n * introducing a 0% keyframe. */\n global?: boolean;\n /** How this tween was constructed in source. Absent ⇒ literal. */\n provenance?: GsapProvenance;\n}\n\nexport interface GsapPercentageKeyframe {\n percentage: number;\n properties: Record<string, number | string>;\n ease?: string;\n}\n\nexport type GsapKeyframeFormat = \"percentage\" | \"object-array\" | \"simple-array\";\n\nexport interface GsapKeyframesData {\n format: GsapKeyframeFormat;\n keyframes: GsapPercentageKeyframe[];\n ease?: string;\n easeEach?: string;\n}\n\nexport interface ArcPathSegment {\n curviness: number;\n cp1?: { x: number; y: number };\n cp2?: { x: number; y: number };\n}\n\nexport interface ArcPathConfig {\n enabled: boolean;\n autoRotate: boolean | number;\n segments: ArcPathSegment[];\n}\n\nexport interface MotionPathShape {\n arcPath: ArcPathConfig;\n waypoints: Array<{ x: number; y: number }>;\n}\n\n/**\n * Build arcPath segments + waypoints from resolved path coordinates. Shared by\n * the AST parser (coords from literal nodes) and the runtime scanner (coords\n * from a live `vars.motionPath`), so both produce identical arc config.\n */\nexport function buildArcPath(\n coords: Array<{ x: number; y: number }>,\n curviness: number,\n autoRotate: boolean | number,\n isCubic: boolean,\n): MotionPathShape | undefined {\n const first = coords[0];\n if (coords.length < 2 || !first) return undefined;\n const segments: ArcPathSegment[] = [];\n let waypoints: Array<{ x: number; y: number }>;\n if (isCubic && coords.length >= 4) {\n // coords are [anchor, cp1, cp2, anchor, cp1, cp2, anchor, ...].\n waypoints = [first];\n for (let i = 1; i + 2 < coords.length; i += 3) {\n const cp1 = coords[i];\n const cp2 = coords[i + 1];\n const anchor = coords[i + 2];\n if (!cp1 || !cp2 || !anchor) continue;\n waypoints.push(anchor);\n segments.push({ curviness, cp1, cp2 });\n }\n } else {\n waypoints = coords;\n for (let i = 0; i < waypoints.length - 1; i++) segments.push({ curviness });\n }\n return { arcPath: { enabled: true, autoRotate, segments }, waypoints };\n}\n\nexport interface ParsedGsap {\n animations: GsapAnimation[];\n timelineVar: string;\n preamble: string;\n postamble: string;\n multipleTimelines?: boolean;\n unsupportedTimelinePattern?: boolean;\n}\n\nexport { SUPPORTED_PROPS, SUPPORTED_EASES } from \"./gsapConstants\";\n\n// ── Split-animation types (used by gsapWriterAcorn) ─────────────────────────\n\nexport interface SplitAnimationsOptions {\n originalId: string;\n newId: string;\n splitTime: number;\n elementStart: number;\n elementDuration: number;\n}\n\nexport interface SplitAnimationsResult {\n script: string;\n /** Non-ID-selector animations that the engine cannot safely retarget. */\n skippedSelectors: string[];\n}\n\n// ── Serialization ───────────────────────────────────────────────────────────\n\nexport function serializeGsapAnimations(\n animations: GsapAnimation[],\n timelineVar = \"tl\",\n options?: { includeMediaSync?: boolean; preamble?: string; postamble?: string },\n): string {\n const sorted = [...animations].sort((a, b) => {\n const aNum =\n a.resolvedStart ?? (typeof a.position === \"number\" ? a.position : Number.MAX_SAFE_INTEGER);\n const bNum =\n b.resolvedStart ?? (typeof b.position === \"number\" ? b.position : Number.MAX_SAFE_INTEGER);\n return aNum - bNum;\n });\n // fallow-ignore-next-line complexity\n const lines = sorted.map((anim) => {\n const selector = `\"${anim.targetSelector}\"`;\n const props: Record<string, number | string> = { ...anim.properties };\n if (anim.duration !== undefined) props.duration = anim.duration;\n if (anim.ease) props.ease = anim.ease;\n let propsStr = serializeObject(props);\n if (anim.extras && Object.keys(anim.extras).length > 0) {\n const extrasStr = serializeExtras(anim.extras);\n if (Object.keys(props).length === 0) {\n propsStr = `{ ${extrasStr} }`;\n } else {\n // Insert extras before the closing brace\n propsStr = propsStr.slice(0, -2) + `, ${extrasStr} }`;\n }\n }\n const posStr = typeof anim.position === \"string\" ? `\"${anim.position}\"` : anim.position;\n switch (anim.method) {\n case \"set\":\n // A global set is a base `gsap.set` — off the timeline, no position arg.\n return anim.global\n ? ` gsap.set(${selector}, ${propsStr});`\n : ` ${timelineVar}.set(${selector}, ${propsStr}, ${posStr});`;\n case \"to\":\n return ` ${timelineVar}.to(${selector}, ${propsStr}, ${posStr});`;\n case \"from\":\n return ` ${timelineVar}.from(${selector}, ${propsStr}, ${posStr});`;\n case \"fromTo\": {\n const fromStr = serializeObject(anim.fromProperties || {});\n return ` ${timelineVar}.fromTo(${selector}, ${fromStr}, ${propsStr}, ${posStr});`;\n }\n }\n });\n\n let mediaSync = \"\";\n if (options?.includeMediaSync) {\n mediaSync = `\n ${timelineVar}.eventCallback(\"onUpdate\", function() {\n const time = ${timelineVar}.time();\n document.querySelectorAll(\"video[data-start], audio[data-start]\").forEach(function(media) {\n const start = parseFloat(media.dataset.start);\n const end = parseFloat(media.dataset.end) || Infinity;\n const mediaTime = time - start;\n if (time >= start && time < end) {\n if (Math.abs(media.currentTime - mediaTime) > 0.1) {\n media.currentTime = mediaTime;\n }\n if (media.paused && !${timelineVar}.paused()) {\n media.play().catch(function() {});\n }\n } else if (!media.paused) {\n media.pause();\n }\n });\n });`;\n }\n\n const preamble = options?.preamble || `const ${timelineVar} = gsap.timeline({ paused: true });`;\n const postamble = options?.postamble ? `\\n ${options.postamble}` : \"\";\n\n return `\n ${preamble}\n${lines.join(\"\\n\")}${mediaSync}${postamble}\n `;\n}\n\nexport function serializeValue(value: unknown): string {\n if (typeof value === \"string\" && value.startsWith(\"__raw:\")) {\n return value.slice(6);\n }\n if (typeof value === \"string\") return JSON.stringify(value);\n return String(value);\n}\n\nexport function safeJsKey(key: string): string {\n return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key) ? key : JSON.stringify(key);\n}\n\nfunction serializeObject(obj: Record<string, number | string>): string {\n const entries = Object.entries(obj).map(([key, value]) => {\n return `${safeJsKey(key)}: ${serializeValue(value)}`;\n });\n return `{ ${entries.join(\", \")} }`;\n}\n\nfunction serializeExtras(extras: Record<string, unknown>): string {\n return Object.entries(extras)\n .map(([key, value]) => {\n return `${safeJsKey(key)}: ${serializeValue(value)}`;\n })\n .join(\", \");\n}\n\n// ── Element filtering ─────────────────────────────────────────────────────────\n\n/**\n * Filter animations to those targeting `#<elementId>` (id-only match). For the\n * studio panel's id-OR-selector matching, see `getAnimationsForElement` in\n * `useGsapTweenCache.ts` — distinct on purpose, hence the distinct name.\n */\nexport function getAnimationsForElementId(\n animations: GsapAnimation[],\n elementId: string,\n): GsapAnimation[] {\n const selector = `#${elementId}`;\n return animations.filter((a) => a.targetSelector === selector);\n}\n\n// ── Validation (regex-based, no AST needed) ─────────────────────────────────\n\nconst FORBIDDEN_GSAP_PATTERNS: Array<{ pattern: RegExp; message: string }> = [\n { pattern: /\\.call\\s*\\(/, message: \"call() method not allowed\" },\n { pattern: /\\.add\\s*\\(/, message: \"add() method not allowed\" },\n { pattern: /\\.addPause\\s*\\(/, message: \"addPause() method not allowed\" },\n { pattern: /gsap\\.registerEffect\\s*\\(/, message: \"registerEffect() not allowed\" },\n { pattern: /ScrollTrigger/, message: \"ScrollTrigger not allowed\" },\n { pattern: /onComplete\\s*:/, message: \"onComplete callback not allowed\" },\n { pattern: /onUpdate\\s*:/, message: \"onUpdate callback not allowed\" },\n { pattern: /onStart\\s*:/, message: \"onStart callback not allowed\" },\n { pattern: /onRepeat\\s*:/, message: \"onRepeat callback not allowed\" },\n { pattern: /onReverseComplete\\s*:/, message: \"onReverseComplete callback not allowed\" },\n { pattern: /repeat\\s*:\\s*-1/, message: \"Infinite repeat (repeat: -1) not allowed\" },\n { pattern: /Math\\.random\\s*\\(/, message: \"Random values (Math.random) not allowed\" },\n { pattern: /Date\\.now\\s*\\(/, message: \"Date-dependent values (Date.now) not allowed\" },\n { pattern: /new\\s+Date\\s*\\(/, message: \"Date constructor not allowed\" },\n { pattern: /setTimeout\\s*\\(/, message: \"setTimeout not allowed\" },\n { pattern: /setInterval\\s*\\(/, message: \"setInterval not allowed\" },\n { pattern: /requestAnimationFrame\\s*\\(/, message: \"requestAnimationFrame not allowed\" },\n];\n\nexport function validateCompositionGsap(script: string): ValidationResult {\n const errors: string[] = [];\n const warnings: string[] = [];\n for (const { pattern, message } of FORBIDDEN_GSAP_PATTERNS) {\n if (pattern.test(script)) errors.push(message);\n }\n if (/yoyo\\s*:\\s*true/.test(script)) {\n warnings.push(\"yoyo animations may behave unexpectedly when scrubbing\");\n }\n if (/stagger\\s*:/.test(script)) {\n warnings.push(\"stagger animations may not serialize correctly\");\n }\n return { valid: errors.length === 0, errors, warnings };\n}\n\n// ── Keyframe Conversion Helpers ─────────────────────────────────────────────\n\nexport function keyframesToGsapAnimations(\n elementId: string,\n keyframes: Keyframe[],\n elementStartTime: number,\n base?: { x?: number; y?: number; scale?: number },\n): GsapAnimation[] {\n const sorted = [...keyframes].sort((a, b) => a.time - b.time);\n const animations: GsapAnimation[] = [];\n const baseX = base?.x ?? 0;\n const baseY = base?.y ?? 0;\n const baseScale = base?.scale ?? 1;\n\n // fallow-ignore-next-line complexity\n sorted.forEach((kf, i) => {\n const absoluteTime = elementStartTime + kf.time;\n const isFirst = i === 0;\n const prevKf = i > 0 ? sorted[i - 1] : null;\n const duration = prevKf ? kf.time - prevKf.time : undefined;\n const position = prevKf ? elementStartTime + prevKf.time : absoluteTime;\n\n const properties: Record<string, number | string> = {};\n for (const [key, value] of Object.entries(kf.properties)) {\n if (typeof value !== \"number\") continue;\n if (key === \"x\") properties.x = baseX + value;\n else if (key === \"y\") properties.y = baseY + value;\n else if (key === \"scale\") properties.scale = baseScale * value;\n else properties[key] = value;\n }\n\n animations.push({\n id: `${elementId}-kf-${kf.id}`,\n targetSelector: `#${elementId}`,\n method: isFirst ? \"set\" : \"to\",\n position,\n properties,\n duration: isFirst ? undefined : duration,\n ease: kf.ease,\n });\n });\n\n return animations;\n}\n\nexport function gsapAnimationsToKeyframes(\n animations: GsapAnimation[],\n elementStartTime: number,\n options?: {\n baseX?: number;\n baseY?: number;\n baseScale?: number;\n clampTimeToZero?: boolean;\n skipBaseSet?: boolean;\n },\n): Keyframe[] {\n const validMethods: GsapMethod[] = [\"set\", \"to\", \"from\", \"fromTo\"];\n const baseX = options?.baseX ?? 0;\n const baseY = options?.baseY ?? 0;\n const baseScale = options?.baseScale ?? 1;\n const clampTimeToZero = options?.clampTimeToZero ?? true;\n const skipBaseSet = options?.skipBaseSet ?? false;\n const baseTimeEpsilon = 0.001;\n const baseValueEpsilon = 0.00001;\n\n return (\n animations\n .filter(\n (a): a is GsapAnimation & { position: number } =>\n validMethods.includes(a.method) && typeof a.position === \"number\",\n )\n // fallow-ignore-next-line complexity\n .map((a) => {\n const relativeTimeRaw = a.position - elementStartTime;\n const time = clampTimeToZero ? Math.max(0, relativeTimeRaw) : relativeTimeRaw;\n\n const properties: Partial<KeyframeProperties> = {};\n for (const [key, value] of Object.entries(a.properties)) {\n if (typeof value !== \"number\") continue;\n if (key === \"x\") properties.x = value - baseX;\n else if (key === \"y\") properties.y = value - baseY;\n else if (key === \"scale\") {\n properties.scale = baseScale !== 0 ? value / baseScale : value;\n } else {\n (properties as Record<string, number>)[key] = value;\n }\n }\n\n if (\n skipBaseSet &&\n a.method === \"set\" &&\n time < baseTimeEpsilon &&\n Object.values(properties).every(\n (v) => typeof v === \"number\" && Math.abs(v) < baseValueEpsilon,\n )\n ) {\n return null;\n }\n\n return {\n id: a.id.replace(/^.*-kf-/, \"\"),\n time,\n properties: properties as KeyframeProperties,\n ease: a.ease,\n };\n })\n .filter((kf): kf is NonNullable<typeof kf> => kf !== null)\n );\n}\n\n// ── Keyframe-conversion transforms (pure; shared by recast + acorn writers) ────\n\n/**\n * CSS identity values for properties whose \"rest\" state isn't 0 — used to\n * synthesize the missing endpoint when converting a flat tween to keyframes.\n */\nconst CSS_IDENTITY: Record<string, number> = {\n opacity: 1,\n autoAlpha: 1,\n scale: 1,\n scaleX: 1,\n scaleY: 1,\n};\n\nfunction cssIdentityValue(prop: string): number {\n return CSS_IDENTITY[prop] ?? 0;\n}\n\n/** Build the identity-endpoint map for a flat tween's properties. */\nfunction buildIdentityMap(props: Record<string, number | string>): Record<string, number | string> {\n const identity: Record<string, number | string> = {};\n for (const [key, val] of Object.entries(props)) {\n if (val != null) identity[key] = typeof val === \"number\" ? cssIdentityValue(key) : val;\n }\n return identity;\n}\n\n/**\n * Resolve the 0% (from) and 100% (to) property maps for a tween being\n * converted to percentage keyframes.\n *\n * @param resolvedFromValues — Despite the \"from\" in the name (historical), these\n * are runtime-captured DOM values that override the conversion endpoint:\n * - For to(): overrides fromProps (the 0% state / where the element is now).\n * - For from(): overrides toProps (the 100% state / where the element rests).\n * - For fromTo(): merges into toProps (the 100% endpoint the user is editing).\n */\nexport function resolveConversionProps(\n anim: GsapAnimation,\n resolvedFromValues?: Record<string, number | string>,\n): { fromProps: Record<string, number | string>; toProps: Record<string, number | string> } {\n if (anim.method === \"set\") {\n // A static hold becomes a keyframed `to` whose 0% and 100% both start at the\n // set's value — the visual is unchanged until the user edits a keyframe to\n // animate it. (The caller flips the call from `set` to `to` + adds a duration.)\n return { fromProps: { ...anim.properties }, toProps: { ...anim.properties } };\n }\n if (anim.method === \"to\") {\n const identity = buildIdentityMap(anim.properties);\n const fromProps = resolvedFromValues ? { ...identity, ...resolvedFromValues } : identity;\n return { fromProps, toProps: { ...anim.properties } };\n }\n if (anim.method === \"from\") {\n const identity = buildIdentityMap(anim.properties);\n const toProps = resolvedFromValues ? { ...identity, ...resolvedFromValues } : identity;\n return { fromProps: { ...anim.properties }, toProps };\n }\n // fromTo(fromVars, toVars): anim.fromProperties = fromVars (0% state),\n // anim.properties = toVars (100% state). resolvedFromValues contains the\n // current DOM position from a drag — it represents the NEW destination, so\n // it merges into toProps (the 100% endpoint the user is editing), NOT into\n // fromProps. This is intentional and not inverted.\n const toProps = resolvedFromValues\n ? { ...anim.properties, ...resolvedFromValues }\n : { ...anim.properties };\n return { fromProps: { ...(anim.fromProperties ?? {}) }, toProps };\n}\n\n// ── Arc path serialization helpers (shared by recast + acorn writers) ─────────\n\nfunction numericXY(props: Record<string, number | string>): { x: number; y: number } | null {\n const vx = props.x;\n const vy = props.y;\n return typeof vx === \"number\" && typeof vy === \"number\" ? { x: vx, y: vy } : null;\n}\n\nexport function extractArcWaypoints(anim: GsapAnimation): Array<{ x: number; y: number }> {\n const keyframeWps = (anim.keyframes?.keyframes ?? [])\n .map((kf) => numericXY(kf.properties))\n .filter((pt): pt is { x: number; y: number } => pt !== null);\n if (keyframeWps.length >= 2) return keyframeWps;\n const propX = anim.properties.x;\n const propY = anim.properties.y;\n if (typeof propX !== \"number\" && typeof propY !== \"number\") return keyframeWps;\n const destX = typeof propX === \"number\" ? propX : 0;\n const destY = typeof propY === \"number\" ? propY : 0;\n return [\n { x: 0, y: 0 },\n { x: destX, y: destY },\n ];\n}\n\nfunction autoRotateSuffix(autoRotate: boolean | number): string {\n if (autoRotate === true) return \", autoRotate: true\";\n if (typeof autoRotate === \"number\") return `, autoRotate: ${autoRotate}`;\n return \"\";\n}\n\nfunction cubicControlPoints(\n seg: ArcPathSegment,\n wp: { x: number; y: number },\n nextWp: { x: number; y: number },\n): string[] {\n if (seg.cp1 && seg.cp2) {\n return [`{x: ${seg.cp1.x}, y: ${seg.cp1.y}}`, `{x: ${seg.cp2.x}, y: ${seg.cp2.y}}`];\n }\n const dx = nextWp.x - wp.x;\n const dy = nextWp.y - wp.y;\n const c = seg.curviness ?? 1;\n return [\n `{x: ${wp.x + dx * 0.33}, y: ${wp.y + dy * 0.33 - c * Math.abs(dx) * 0.25}}`,\n `{x: ${wp.x + dx * 0.66}, y: ${wp.y + dy * 0.66 - c * Math.abs(dx) * 0.25}}`,\n ];\n}\n\nfunction buildCubicPathEntries(\n waypoints: Array<{ x: number; y: number }>,\n segments: ArcPathSegment[],\n): string[] {\n const first = waypoints[0];\n if (!first) return [];\n const entries = [`{x: ${first.x}, y: ${first.y}}`];\n for (let i = 0; i < segments.length; i++) {\n const seg = segments[i];\n const wp = waypoints[i];\n const nextWp = waypoints[i + 1];\n if (!seg || !wp || !nextWp) continue;\n entries.push(...cubicControlPoints(seg, wp, nextWp));\n entries.push(`{x: ${nextWp.x}, y: ${nextWp.y}}`);\n }\n return entries;\n}\n\nexport function buildMotionPathObjectCode(config: {\n waypoints: Array<{ x: number; y: number }>;\n segments: ArcPathSegment[];\n autoRotate: boolean | number;\n}): string {\n const { waypoints, segments, autoRotate } = config;\n const arSuffix = autoRotateSuffix(autoRotate);\n // GSAP's simple `path` array supports only ONE scalar `curviness` for the whole\n // path, so per-segment curviness can only be expressed in the cubic form (each\n // segment's curviness baked into its control points). Emit cubic when segments\n // carry explicit control points OR when their curviness values differ — the\n // simple branch would otherwise serialize only segments[0].curviness and drop\n // every other segment's curve.\n const hasExplicitCp = segments.some((s) => s.cp1 && s.cp2);\n const curvinessVaries = segments.some(\n (s) => (s.curviness ?? 1) !== (segments[0]?.curviness ?? 1),\n );\n if ((hasExplicitCp || curvinessVaries) && waypoints.length >= 2) {\n const pathStr = buildCubicPathEntries(waypoints, segments).join(\", \");\n return `{ path: [${pathStr}], type: \"cubic\"${arSuffix} }`;\n }\n const pathEntries = waypoints.map((wp) => `{x: ${wp.x}, y: ${wp.y}}`);\n const curviness = segments[0]?.curviness ?? 1;\n const curvPart = curviness !== 1 ? `, curviness: ${curviness}` : \"\";\n return `{ path: [${pathEntries.join(\", \")}]${curvPart}${arSuffix} }`;\n}\n","/**\n * Node-only GSAP AST parser. Depends on recast / @babel/parser, which compile\n * to CommonJS that calls `require(\"fs\")` — so this module must never be in the\n * static import graph of isomorphic/browser code. It is reachable only via the\n * `@hyperframes/core/gsap-parser` subpath (studio-api mutations + the linter).\n *\n * Recast-free helpers (serialization, keyframe conversion, validation, types)\n * live in `./gsapSerialize` and are re-exported here so this subpath exposes the\n * full surface for tests and server-side consumers.\n */\nimport * as recast from \"recast\";\nimport { parse as babelParse } from \"@babel/parser\";\nimport {\n type ArcPathConfig,\n type ArcPathSegment,\n type GsapAnimation,\n type GsapKeyframesData,\n type GsapMethod,\n type GsapPercentageKeyframe,\n type ParsedGsap,\n serializeValue as valueToCode,\n safeJsKey as safeKey,\n resolveConversionProps,\n} from \"./gsapSerialize\";\n\nexport type {\n ArcPathConfig,\n ArcPathSegment,\n GsapAnimation,\n GsapMethod,\n ParsedGsap,\n GsapKeyframesData,\n GsapPercentageKeyframe,\n GsapKeyframeFormat,\n} from \"./gsapSerialize\";\nexport {\n serializeGsapAnimations,\n getAnimationsForElementId,\n validateCompositionGsap,\n keyframesToGsapAnimations,\n gsapAnimationsToKeyframes,\n SUPPORTED_PROPS,\n SUPPORTED_EASES,\n} from \"./gsapSerialize\";\nexport type { PropertyGroupName } from \"./gsapConstants\";\nexport {\n PROPERTY_GROUPS,\n classifyPropertyGroup,\n classifyTweenPropertyGroup,\n} from \"./gsapConstants\";\nimport { classifyPropertyGroup, classifyTweenPropertyGroup } from \"./gsapConstants\";\nimport type { PropertyGroupName } from \"./gsapConstants\";\nexport { generateSpringEaseData, SPRING_PRESETS } from \"./springEase\";\nexport type { SpringPreset } from \"./springEase\";\n\nconst GSAP_METHODS = new Set<string>([\"set\", \"to\", \"from\", \"fromTo\"]);\n\n// ── Recast / Babel AST shape types ────────────────────────────────────────\n//\n// Recast's own typings are loose (`any` everywhere). These local shapes\n// capture the properties we actually access, giving us IDE navigation and\n// catch-at-write-time safety without depending on @babel/types at runtime.\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- recast AST nodes are inherently untyped\ninterface AstNode extends Record<string, any> {\n type: string;\n}\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- recast visitor paths are inherently untyped\ninterface AstPath extends Record<string, any> {\n node: AstNode;\n}\n\n// ── Recast AST Helpers ──────────────────────────────────────────────────────\n\ntype ScopeBindings = ReadonlyMap<string, number | string | boolean>;\n\nfunction parseScript(script: string) {\n return recast.parse(script, {\n parser: {\n parse(source: string) {\n return babelParse(source, { sourceType: \"script\", plugins: [], tokens: true });\n },\n },\n });\n}\n\nfunction collectScopeBindings(ast: AstNode): ScopeBindings {\n const bindings = new Map<string, number | string | boolean>();\n recast.types.visit(ast, {\n visitVariableDeclarator(path: AstPath) {\n const name = path.node.id?.name;\n const init = path.node.init;\n if (name && init) {\n const val = resolveNode(init, bindings);\n if (val !== undefined) bindings.set(name, val);\n }\n this.traverse(path);\n },\n });\n return bindings;\n}\n\nfunction resolveNode(\n node: AstNode | undefined,\n scope: ReadonlyMap<string, number | string | boolean>,\n): number | string | boolean | undefined {\n if (!node) return undefined;\n if (node.type === \"NumericLiteral\" || (node.type === \"Literal\" && typeof node.value === \"number\"))\n return node.value;\n if (node.type === \"StringLiteral\" || (node.type === \"Literal\" && typeof node.value === \"string\"))\n return node.value;\n if (\n node.type === \"BooleanLiteral\" ||\n (node.type === \"Literal\" && typeof node.value === \"boolean\")\n )\n return node.value;\n if (node.type === \"UnaryExpression\" && node.operator === \"-\" && node.argument) {\n const val = resolveNode(node.argument, scope);\n return typeof val === \"number\" ? -val : undefined;\n }\n if (node.type === \"BinaryExpression\") {\n const left = resolveNode(node.left, scope);\n const right = resolveNode(node.right, scope);\n if (typeof left === \"number\" && typeof right === \"number\") {\n switch (node.operator) {\n case \"+\":\n return left + right;\n case \"-\":\n return left - right;\n case \"*\":\n return left * right;\n case \"/\":\n return right !== 0 ? left / right : undefined;\n }\n }\n if (typeof left === \"string\" && node.operator === \"+\") return left + String(right ?? \"\");\n if (typeof right === \"string\" && node.operator === \"+\") return String(left ?? \"\") + right;\n }\n if (node.type === \"Identifier\" && scope.has(node.name)) {\n return scope.get(node.name);\n }\n if (node.type === \"TemplateLiteral\" && node.expressions?.length === 0) {\n return node.quasis?.[0]?.value?.cooked ?? undefined;\n }\n return undefined;\n}\n\nfunction extractLiteralValue(node: AstNode | undefined, scope: ScopeBindings): unknown {\n return resolveNode(node, scope);\n}\n\n// ── Element-target resolution ───────────────────────────────────────────────\n//\n// Real compositions target tweens through element variables resolved from the\n// DOM (`const kicker = root.querySelector(\".kicker\"); tl.to(kicker, …)`), arrays\n// of them (`tl.to([a, b], …)`), `gsap.utils.toArray(\".sel\")`, and per-element\n// loop variables (`items.forEach(el => tl.to(el, …))`) — not inline string\n// selectors. To make those tweens editable we resolve each target back to the\n// CSS selector(s) it addresses. Resolution is lexically scoped: the same\n// variable name can mean different elements in different IIFEs.\n\nconst QUERY_METHODS = new Set([\"querySelector\", \"querySelectorAll\"]);\nconst ITERATION_METHODS = new Set([\"forEach\", \"map\"]);\nconst SCOPE_NODE_TYPES = new Set([\n \"Program\",\n \"FunctionDeclaration\",\n \"FunctionExpression\",\n \"ArrowFunctionExpression\",\n]);\n\n/**\n * If `node` is a DOM lookup call — `x.querySelector(\".sel\")`,\n * `document.querySelectorAll(\".sel\")`, `document.getElementById(\"id\")`, or\n * `gsap.utils.toArray(\".sel\")` — return the CSS selector it resolves to.\n * `getElementById(\"id\")` maps to `#id`. Returns null for anything else.\n */\nfunction selectorFromQueryCall(node: AstNode, scope: ScopeBindings): string | null {\n if (node?.type !== \"CallExpression\") return null;\n const callee = node.callee;\n if (callee?.type !== \"MemberExpression\" || callee.property?.type !== \"Identifier\") return null;\n const method = callee.property.name;\n const argValue = resolveNode(node.arguments?.[0], scope);\n if (typeof argValue !== \"string\" || argValue.length === 0) return null;\n if (QUERY_METHODS.has(method) || method === \"toArray\") return argValue;\n if (method === \"getElementById\") return `#${argValue}`;\n return null;\n}\n\n/** The nearest enclosing function/program node — the binding scope of `path`. */\nfunction enclosingScopeNode(path: AstPath): AstNode | null {\n let p = path?.parentPath;\n while (p) {\n if (SCOPE_NODE_TYPES.has(p.node?.type)) return p.node;\n p = p.parentPath;\n }\n return null;\n}\n\n/** Scope nodes enclosing `path`, innermost first. */\nfunction scopeChainOf(path: AstPath): AstNode[] {\n const chain: AstNode[] = [];\n let p = path;\n while (p) {\n if (SCOPE_NODE_TYPES.has(p.node?.type)) chain.push(p.node);\n p = p.parentPath;\n }\n return chain;\n}\n\n/** Per-scope element bindings: scopeNode → (variable name → selector). */\ntype TargetBindings = Map<any, Map<string, string>>;\n\nfunction addBinding(\n bindings: TargetBindings,\n scopeNode: AstNode,\n name: string,\n selector: string,\n): void {\n let scoped = bindings.get(scopeNode);\n if (!scoped) {\n scoped = new Map();\n bindings.set(scopeNode, scoped);\n }\n if (!scoped.has(name)) scoped.set(name, selector);\n}\n\n/**\n * Build a lexically-scoped index of element variables → selector. Two passes:\n * (1) direct DOM-lookup assignments (`const x = root.querySelector(...)`), then\n * (2) iteration callback params (`coll.forEach(el => …)`), whose element type is\n * the collection's selector — resolved against the pass-1 bindings.\n */\nfunction collectTargetBindings(ast: AstNode, scope: ScopeBindings): TargetBindings {\n const bindings: TargetBindings = new Map();\n\n recast.types.visit(ast, {\n visitVariableDeclarator(path: AstPath) {\n const name = path.node.id?.name;\n const selector = selectorFromQueryCall(path.node.init, scope);\n const scopeNode = enclosingScopeNode(path);\n if (name && selector !== null && scopeNode) addBinding(bindings, scopeNode, name, selector);\n this.traverse(path);\n },\n visitAssignmentExpression(path: AstPath) {\n const left = path.node.left;\n const selector = selectorFromQueryCall(path.node.right, scope);\n const scopeNode = enclosingScopeNode(path);\n if (left?.type === \"Identifier\" && selector !== null && scopeNode) {\n addBinding(bindings, scopeNode, left.name, selector);\n }\n this.traverse(path);\n },\n });\n\n // Pass 2: forEach/map callback params take the collection's selector.\n recast.types.visit(ast, {\n visitCallExpression(path: AstPath) {\n const node = path.node;\n const callee = node.callee;\n if (\n callee?.type === \"MemberExpression\" &&\n callee.property?.type === \"Identifier\" &&\n ITERATION_METHODS.has(callee.property.name)\n ) {\n const collectionSelector = resolveCollectionSelector(callee.object, path, scope, bindings);\n const fn = node.arguments?.[0];\n const param = fn?.params?.[0];\n if (collectionSelector && param?.type === \"Identifier\" && isFunctionNode(fn)) {\n addBinding(bindings, fn, param.name, collectionSelector);\n }\n }\n this.traverse(path);\n },\n });\n\n return bindings;\n}\n\nfunction isFunctionNode(node: AstNode): boolean {\n return (\n node?.type === \"ArrowFunctionExpression\" ||\n node?.type === \"FunctionExpression\" ||\n node?.type === \"FunctionDeclaration\"\n );\n}\n\n/** Resolve the selector a `.forEach`/`.map` is iterating over (variable or inline call). */\nfunction resolveCollectionSelector(\n node: AstNode,\n callPath: AstPath,\n scope: ScopeBindings,\n bindings: TargetBindings,\n): string | null {\n if (node?.type === \"Identifier\") return lookupBinding(node.name, callPath, bindings);\n if (node?.type === \"CallExpression\") return selectorFromQueryCall(node, scope);\n return null;\n}\n\n/** Resolve a variable name to its selector using the lexical scope chain of `path`. */\nfunction lookupBinding(name: string, path: AstPath, bindings: TargetBindings): string | null {\n for (const scopeNode of scopeChainOf(path)) {\n const selector = bindings.get(scopeNode)?.get(name);\n if (selector !== undefined) return selector;\n }\n return null;\n}\n\n/**\n * Resolve a tween's first argument to a CSS selector. Handles inline string\n * literals, element variables (lexically scoped), arrays of elements (joined\n * into a CSS group selector), inline DOM lookup / `toArray` calls, and indexed\n * access (`items[i]`). Returns null when the target can't be resolved\n * statically (e.g. an object-target duration anchor `tl.to({ _: 0 }, …)`, or a\n * runtime-computed selector).\n */\nfunction resolveTargetSelector(\n node: AstNode,\n path: AstPath,\n scope: ScopeBindings,\n bindings: TargetBindings,\n): string | null {\n if (!node) return null;\n if (node.type === \"StringLiteral\" || node.type === \"Literal\") {\n return typeof node.value === \"string\" ? node.value : null;\n }\n if (node.type === \"Identifier\") {\n return lookupBinding(node.name, path, bindings);\n }\n if (node.type === \"CallExpression\") {\n return selectorFromQueryCall(node, scope);\n }\n if (node.type === \"ArrayExpression\") {\n const parts = node.elements\n .map((el: AstNode) => resolveTargetSelector(el, path, scope, bindings))\n .filter((s: string | null): s is string => typeof s === \"string\" && s.length > 0);\n return parts.length > 0 ? parts.join(\", \") : null;\n }\n if (node.type === \"MemberExpression\" && node.object?.type === \"Identifier\") {\n // `items[i]` — the element type is the collection's selector.\n return lookupBinding(node.object.name, path, bindings);\n }\n return null;\n}\n\nfunction objectExpressionToRecord(node: AstNode, scope: ScopeBindings): Record<string, unknown> {\n const result: Record<string, unknown> = {};\n if (node?.type !== \"ObjectExpression\") return result;\n for (const prop of node.properties ?? []) {\n if (prop.type !== \"ObjectProperty\" && prop.type !== \"Property\") continue;\n const key = prop.key?.name ?? prop.key?.value;\n if (!key) continue;\n const resolved = resolveNode(prop.value, scope);\n if (resolved !== undefined) {\n result[key] = resolved;\n } else {\n // Preserve unresolvable values as raw source text so they survive round-trips\n result[key] = `__raw:${recast.print(prop.value).code}`;\n }\n }\n return result;\n}\n\n// ── Timeline Variable Detection ─────────────────────────────────────────────\n\nfunction isGsapTimelineCall(node: AstNode): boolean {\n return (\n node?.type === \"CallExpression\" &&\n node.callee?.type === \"MemberExpression\" &&\n node.callee.object?.name === \"gsap\" &&\n node.callee.property?.name === \"timeline\"\n );\n}\n\ninterface TimelineDefaults {\n ease?: string;\n duration?: number;\n}\n\ninterface TimelineDetection {\n timelineVar: string | null;\n timelineCount: number;\n defaults?: TimelineDefaults;\n}\n\nfunction extractTimelineDefaults(\n callNode: AstNode,\n scope: ScopeBindings,\n): TimelineDefaults | undefined {\n const arg = callNode.arguments?.[0];\n if (!arg || arg.type !== \"ObjectExpression\") return undefined;\n const defaultsProp = arg.properties?.find(\n (p: AstNode) => isObjectProperty(p) && propKeyName(p) === \"defaults\",\n );\n if (!defaultsProp?.value || defaultsProp.value.type !== \"ObjectExpression\") return undefined;\n const record = objectExpressionToRecord(defaultsProp.value, scope);\n const result: TimelineDefaults = {};\n if (typeof record.ease === \"string\") result.ease = record.ease;\n if (typeof record.duration === \"number\") result.duration = record.duration;\n return Object.keys(result).length > 0 ? result : undefined;\n}\n\nfunction findTimelineVar(ast: AstNode, scope?: ScopeBindings): TimelineDetection {\n let timelineVar: string | null = null;\n let timelineCount = 0;\n let defaults: TimelineDefaults | undefined;\n const emptyScope: ScopeBindings = scope ?? new Map();\n recast.types.visit(ast, {\n visitVariableDeclarator(path: AstPath) {\n if (isGsapTimelineCall(path.node.init)) {\n timelineCount += 1;\n if (!timelineVar) {\n timelineVar = path.node.id?.name ?? null;\n defaults = extractTimelineDefaults(path.node.init, emptyScope);\n }\n }\n this.traverse(path);\n },\n visitAssignmentExpression(path: AstPath) {\n if (isGsapTimelineCall(path.node.right)) {\n timelineCount += 1;\n if (!timelineVar) {\n const left = path.node.left;\n if (left?.type === \"Identifier\") timelineVar = left.name;\n defaults = extractTimelineDefaults(path.node.right, emptyScope);\n }\n }\n this.traverse(path);\n },\n });\n return { timelineVar, timelineCount, defaults };\n}\n\n// ── Find All Tween Calls ────────────────────────────────────────────────────\n\ninterface TweenCallInfo {\n path: AstPath;\n node: AstNode;\n method: GsapMethod;\n selector: string;\n varsArg: AstNode;\n fromArg?: AstNode;\n positionArg?: AstNode;\n /** True for a base `gsap.set(...)` (off-timeline) rather than `tl.set(...)`. */\n global?: boolean;\n}\n\n/**\n * True when the member chain of `callNode.callee` is rooted at the timeline\n * variable — `tl.to(...)` and every link of a chain `tl.to(...).to(...)`.\n */\nfunction isTimelineRootedCall(callNode: AstNode, timelineVar: string): boolean {\n let obj = callNode.callee?.object;\n while (obj?.type === \"CallExpression\") {\n obj = obj.callee?.object;\n }\n return obj?.type === \"Identifier\" && obj.name === timelineVar;\n}\n\nfunction findAllTweenCalls(\n ast: AstNode,\n timelineVar: string,\n scope: ScopeBindings,\n targetBindings: TargetBindings,\n): TweenCallInfo[] {\n const results: TweenCallInfo[] = [];\n recast.types.visit(ast, {\n visitCallExpression(path: AstPath) {\n const node = path.node;\n const callee = node.callee;\n // A base `gsap.set(\"#sel\", props)` is an off-timeline static hold (no position,\n // no keyframe marker). Treat it as an editable `set` animation so a static\n // value (e.g. a 3D transform) round-trips and re-edits in place. Restricted to\n // a STRING-LITERAL selector: variable-target `gsap.set(el, ...)` holds stay\n // opaque surrounding source (editing them by selector would be ambiguous).\n const gsapSetArg = node.arguments?.[0];\n const isGlobalSet =\n callee?.type === \"MemberExpression\" &&\n callee.object?.type === \"Identifier\" &&\n callee.object.name === \"gsap\" &&\n callee.property?.type === \"Identifier\" &&\n callee.property.name === \"set\" &&\n (gsapSetArg?.type === \"StringLiteral\" ||\n (gsapSetArg?.type === \"Literal\" && typeof gsapSetArg.value === \"string\"));\n if (\n callee?.type === \"MemberExpression\" &&\n callee.property?.type === \"Identifier\" &&\n (isTimelineRootedCall(node, timelineVar) || isGlobalSet)\n ) {\n const method = callee.property.name;\n if (!GSAP_METHODS.has(method)) {\n this.traverse(path);\n return;\n }\n const args = node.arguments;\n if (args.length < 2) {\n this.traverse(path);\n return;\n }\n const selectorValue =\n resolveTargetSelector(args[0], path, scope, targetBindings) ?? \"__unresolved__\";\n\n if (method === \"fromTo\") {\n results.push({\n path,\n node,\n method: \"fromTo\",\n selector: selectorValue,\n fromArg: args[1],\n varsArg: args[2],\n positionArg: args[3],\n });\n } else {\n results.push({\n path,\n node,\n method: method as GsapMethod,\n selector: selectorValue,\n varsArg: args[1],\n positionArg: args[2],\n ...(isGlobalSet ? { global: true } : {}),\n });\n }\n }\n this.traverse(path);\n },\n });\n return results;\n}\n\n/** Keys that are stored on dedicated GsapAnimation fields (not in properties/extras). */\nconst BUILTIN_VAR_KEYS = new Set([\"duration\", \"ease\", \"delay\"]);\n\n/** Keys that are never preserved (callbacks / advanced patterns). */\nconst DROPPED_VAR_KEYS = new Set([\"onComplete\", \"onStart\", \"onUpdate\", \"onRepeat\"]);\n\n/** Keys that belong in `extras` — non-editable GSAP config that must survive round-trips. */\nconst EXTRAS_KEYS = new Set([\n \"stagger\",\n \"yoyo\",\n \"repeat\",\n \"repeatDelay\",\n \"snap\",\n \"overwrite\",\n \"immediateRender\",\n]);\n\n/**\n * Extract raw source text for a property in an ObjectExpression AST node.\n * Returns the printed source of the value node, suitable for verbatim re-emission.\n */\nfunction extractRawPropertySource(varsArgNode: AstNode, key: string): string | undefined {\n const node = findPropertyNode(varsArgNode, key);\n return node ? recast.print(node).code : undefined;\n}\n\n/** Find the raw AST node for a named property inside an ObjectExpression. */\nfunction findPropertyNode(varsArgNode: AstNode, key: string): AstNode | undefined {\n if (varsArgNode?.type !== \"ObjectExpression\") return undefined;\n for (const prop of varsArgNode.properties ?? []) {\n if (!isObjectProperty(prop)) continue;\n if (propKeyName(prop) === key) return prop.value;\n }\n return undefined;\n}\n\n// ── Native GSAP Keyframes Parsing ──────────────────────────────────────────\n\nconst PERCENTAGE_KEY_RE = /^(\\d+(?:\\.\\d+)?)%$/;\n\n/** Extract a string-valued ease or easeEach from an AST property node. */\nfunction tryResolveStringProp(propValue: AstNode, scope: ScopeBindings): string | undefined {\n const val = resolveNode(propValue, scope);\n return typeof val === \"string\" ? val : undefined;\n}\n\n/**\n * Parse a `keyframes` property value from a tween vars AST node into a\n * normalized `GsapKeyframesData` structure. Handles all three GSAP formats:\n * percentage objects, object arrays, and simple (property-array) objects.\n */\n// fallow-ignore-next-line complexity\nfunction parseKeyframesNode(\n node: AstNode | undefined,\n scope: ScopeBindings,\n): GsapKeyframesData | undefined {\n if (!node) return undefined;\n\n // ── Object array format: keyframes: [ { x: 0, duration: 0.5 }, ... ] ──\n if (node.type === \"ArrayExpression\") {\n return parseObjectArrayKeyframes(node, scope);\n }\n\n if (node.type !== \"ObjectExpression\") return undefined;\n\n // Distinguish percentage vs simple-array by inspecting property keys/values.\n const props = node.properties ?? [];\n let hasPercentageKey = false;\n let hasArrayValue = false;\n\n for (const prop of props) {\n if (prop.type !== \"ObjectProperty\" && prop.type !== \"Property\") continue;\n const key = prop.key?.value ?? prop.key?.name;\n if (typeof key === \"string\" && PERCENTAGE_KEY_RE.test(key)) {\n hasPercentageKey = true;\n break;\n }\n if (prop.value?.type === \"ArrayExpression\") {\n hasArrayValue = true;\n }\n }\n\n if (hasPercentageKey) return parsePercentageKeyframes(node, scope);\n if (hasArrayValue) return parseSimpleArrayKeyframes(node, scope);\n\n return undefined;\n}\n\n// fallow-ignore-next-line complexity\nfunction parsePercentageKeyframes(node: AstNode, scope: ScopeBindings): GsapKeyframesData {\n const keyframes: GsapPercentageKeyframe[] = [];\n let ease: string | undefined;\n let easeEach: string | undefined;\n\n for (const prop of node.properties ?? []) {\n if (prop.type !== \"ObjectProperty\" && prop.type !== \"Property\") continue;\n const key = prop.key?.value ?? prop.key?.name;\n if (typeof key !== \"string\") continue;\n\n const pctMatch = PERCENTAGE_KEY_RE.exec(key);\n if (pctMatch) {\n const percentage = Number.parseFloat(pctMatch[1]!);\n const record = objectExpressionToRecord(prop.value, scope);\n const properties: Record<string, number | string> = {};\n let kfEase: string | undefined;\n for (const [k, v] of Object.entries(record)) {\n if (k === \"ease\" && typeof v === \"string\") {\n kfEase = v;\n } else if (typeof v === \"number\" || typeof v === \"string\") {\n properties[k] = v;\n }\n }\n keyframes.push({ percentage, properties, ...(kfEase ? { ease: kfEase } : {}) });\n } else if (key === \"ease\") {\n ease = tryResolveStringProp(prop.value, scope) ?? ease;\n } else if (key === \"easeEach\") {\n easeEach = tryResolveStringProp(prop.value, scope) ?? easeEach;\n }\n }\n\n keyframes.sort((a, b) => a.percentage - b.percentage);\n\n return {\n format: \"percentage\",\n keyframes,\n ...(ease ? { ease } : {}),\n ...(easeEach ? { easeEach } : {}),\n };\n}\n\nfunction computeKeyframesTotalDuration(\n varsNode: AstNode,\n scope: ScopeBindings,\n): number | undefined {\n const kfNode = (varsNode.properties ?? []).find(\n (p: AstNode) => (p.key?.name ?? p.key?.value) === \"keyframes\",\n )?.value;\n if (!kfNode || kfNode.type !== \"ArrayExpression\") return undefined;\n let total = 0;\n for (const el of kfNode.elements ?? []) {\n if (!el || el.type !== \"ObjectExpression\") continue;\n const r = objectExpressionToRecord(el, scope);\n if (typeof r.duration === \"number\") total += r.duration;\n }\n return total > 0 ? total : undefined;\n}\n\n// fallow-ignore-next-line complexity\nfunction parseObjectArrayKeyframes(node: AstNode, scope: ScopeBindings): GsapKeyframesData {\n const elements = node.elements ?? [];\n const raw: Array<{\n properties: Record<string, number | string>;\n duration?: number;\n ease?: string;\n }> = [];\n\n for (const el of elements) {\n if (!el || (el.type !== \"ObjectExpression\" && el.type !== \"ObjectProperty\")) {\n // Skip non-object elements\n if (el?.type !== \"ObjectExpression\") continue;\n }\n const record = objectExpressionToRecord(el, scope);\n const properties: Record<string, number | string> = {};\n let duration: number | undefined;\n let ease: string | undefined;\n for (const [k, v] of Object.entries(record)) {\n if (k === \"duration\" && typeof v === \"number\") {\n duration = v;\n } else if (k === \"ease\" && typeof v === \"string\") {\n ease = v;\n } else if (typeof v === \"number\" || typeof v === \"string\") {\n properties[k] = v;\n }\n }\n raw.push({ properties, duration, ease });\n }\n\n // Convert durations to percentage positions. If durations are present, use\n // cumulative ratios; otherwise distribute evenly.\n const totalDuration = raw.reduce((sum, r) => sum + (r.duration ?? 0), 0);\n const keyframes: GsapPercentageKeyframe[] = [];\n\n if (totalDuration > 0) {\n let cumulative = 0;\n for (const entry of raw) {\n cumulative += entry.duration ?? 0;\n const percentage = Math.round((cumulative / totalDuration) * 100);\n keyframes.push({\n percentage,\n properties: entry.properties,\n ...(entry.ease ? { ease: entry.ease } : {}),\n });\n }\n } else {\n for (let i = 0; i < raw.length; i++) {\n const entry = raw[i]!;\n const percentage = raw.length > 1 ? Math.round((i / (raw.length - 1)) * 100) : 0;\n keyframes.push({\n percentage,\n properties: entry.properties,\n ...(entry.ease ? { ease: entry.ease } : {}),\n });\n }\n }\n\n return { format: \"object-array\", keyframes };\n}\n\n// fallow-ignore-next-line complexity\nfunction parseSimpleArrayKeyframes(node: AstNode, scope: ScopeBindings): GsapKeyframesData {\n const arrayProps: Map<string, (number | string)[]> = new Map();\n let ease: string | undefined;\n let easeEach: string | undefined;\n\n for (const prop of node.properties ?? []) {\n if (prop.type !== \"ObjectProperty\" && prop.type !== \"Property\") continue;\n const key = prop.key?.name ?? prop.key?.value;\n if (typeof key !== \"string\") continue;\n\n if (prop.value?.type === \"ArrayExpression\") {\n const values: (number | string)[] = [];\n for (const el of prop.value.elements ?? []) {\n const val = resolveNode(el, scope);\n if (typeof val === \"number\" || typeof val === \"string\") {\n values.push(val);\n }\n }\n if (values.length > 0) arrayProps.set(key, values);\n } else if (key === \"ease\") {\n ease = tryResolveStringProp(prop.value, scope) ?? ease;\n } else if (key === \"easeEach\") {\n easeEach = tryResolveStringProp(prop.value, scope) ?? easeEach;\n }\n }\n\n // Zip arrays into percentage keyframes (evenly spaced).\n const maxLen = Math.max(...[...arrayProps.values()].map((a) => a.length), 0);\n const keyframes: GsapPercentageKeyframe[] = [];\n\n for (let i = 0; i < maxLen; i++) {\n const percentage = maxLen > 1 ? Math.round((i / (maxLen - 1)) * 100) : 0;\n const properties: Record<string, number | string> = {};\n for (const [key, values] of arrayProps) {\n if (i < values.length) properties[key] = values[i]!;\n }\n keyframes.push({ percentage, properties });\n }\n\n return {\n format: \"simple-array\",\n keyframes,\n ...(ease ? { ease } : {}),\n ...(easeEach ? { easeEach } : {}),\n };\n}\n\n// ── MotionPath Parsing ────────────────────────────────────────────────────\n\ninterface MotionPathParseResult {\n arcPath: ArcPathConfig;\n waypoints: Array<{ x: number; y: number }>;\n}\n\nfunction parseMotionPathNode(\n node: AstNode | undefined,\n scope: ScopeBindings,\n): MotionPathParseResult | undefined {\n if (!node) return undefined;\n\n let pathNode: AstNode | undefined;\n let autoRotate: boolean | number = false;\n let curviness = 1;\n let isCubic = false;\n\n if (node.type === \"ObjectExpression\") {\n for (const prop of node.properties ?? []) {\n if (!isObjectProperty(prop)) continue;\n const key = propKeyName(prop);\n if (key === \"path\") pathNode = prop.value;\n else if (key === \"autoRotate\") {\n const val = resolveNode(prop.value, scope);\n autoRotate = typeof val === \"number\" ? val : val === true;\n } else if (key === \"curviness\") {\n const val = resolveNode(prop.value, scope);\n if (typeof val === \"number\") curviness = val;\n } else if (key === \"type\") {\n const val = resolveNode(prop.value, scope);\n if (val === \"cubic\") isCubic = true;\n }\n }\n } else if (node.type === \"ArrayExpression\") {\n pathNode = node;\n }\n\n if (!pathNode || pathNode.type !== \"ArrayExpression\") return undefined;\n\n const elements = pathNode.elements ?? [];\n const coords: Array<{ x: number; y: number }> = [];\n for (const elem of elements) {\n if (!elem || elem.type !== \"ObjectExpression\") continue;\n const rec = objectExpressionToRecord(elem, scope);\n const x = typeof rec.x === \"number\" ? rec.x : undefined;\n const y = typeof rec.y === \"number\" ? rec.y : undefined;\n if (x !== undefined && y !== undefined) coords.push({ x, y });\n }\n\n if (coords.length < 2) return undefined;\n\n let waypoints: Array<{ x: number; y: number }>;\n const segments: ArcPathSegment[] = [];\n\n if (isCubic && coords.length >= 4) {\n // type: \"cubic\" — coords are [anchor, cp1, cp2, anchor, cp1, cp2, anchor, ...]\n // Every 3rd coord starting from 0 is an anchor, the two between are control points.\n waypoints = [];\n waypoints.push(coords[0]!);\n for (let i = 1; i + 2 < coords.length; i += 3) {\n const cp1 = coords[i]!;\n const cp2 = coords[i + 1]!;\n const anchor = coords[i + 2]!;\n waypoints.push(anchor);\n segments.push({ curviness, cp1, cp2 });\n }\n } else {\n // Waypoint array with global curviness\n waypoints = coords;\n for (let i = 0; i < waypoints.length - 1; i++) {\n segments.push({ curviness });\n }\n }\n\n return {\n arcPath: { enabled: true, autoRotate, segments },\n waypoints,\n };\n}\n\n// fallow-ignore-next-line complexity\nfunction tweenCallToAnimation(\n call: TweenCallInfo,\n scope: ScopeBindings,\n): Omit<GsapAnimation, \"id\"> {\n const vars = objectExpressionToRecord(call.varsArg, scope);\n const properties: Record<string, number | string> = {};\n const extras: Record<string, unknown> = {};\n let keyframesData: GsapKeyframesData | undefined;\n let hasUnresolvedKeyframes = false;\n let motionPathResult: MotionPathParseResult | undefined;\n\n for (const [key, val] of Object.entries(vars)) {\n if (BUILTIN_VAR_KEYS.has(key)) continue;\n if (DROPPED_VAR_KEYS.has(key)) continue;\n\n if (key === \"keyframes\") {\n const kfNode = findPropertyNode(call.varsArg, \"keyframes\");\n keyframesData = parseKeyframesNode(kfNode, scope);\n if (!keyframesData && kfNode) hasUnresolvedKeyframes = true;\n continue;\n }\n\n if (key === \"motionPath\") {\n const mpNode = findPropertyNode(call.varsArg, \"motionPath\");\n motionPathResult = parseMotionPathNode(mpNode, scope);\n continue;\n }\n\n if (key === \"easeEach\") {\n // easeEach is only meaningful alongside keyframes — handled below.\n continue;\n }\n\n if (EXTRAS_KEYS.has(key)) {\n // For extras, prefer the raw AST source so complex objects like\n // `stagger: { each: 0.15, from: \"start\" }` survive verbatim.\n const rawSource = extractRawPropertySource(call.varsArg, key);\n if (rawSource !== undefined) {\n extras[key] = `__raw:${rawSource}`;\n } else if (val !== undefined) {\n extras[key] = val;\n }\n continue;\n }\n\n if (typeof val === \"number\" || typeof val === \"string\") {\n properties[key] = val;\n }\n }\n\n // Apply tween-level easeEach to keyframes data.\n if (keyframesData && typeof vars.easeEach === \"string\") {\n keyframesData.easeEach = vars.easeEach as string;\n }\n\n // When motionPath is present, reconstruct x/y as keyframe waypoints.\n if (motionPathResult) {\n const { waypoints } = motionPathResult;\n if (!keyframesData) {\n // No explicit keyframes — create synthetic percentage keyframes from waypoints.\n const kf: GsapPercentageKeyframe[] = waypoints.map((wp, i) => ({\n percentage: waypoints.length > 1 ? Math.round((i / (waypoints.length - 1)) * 100) : 0,\n properties: { x: wp.x, y: wp.y },\n }));\n keyframesData = { format: \"percentage\", keyframes: kf };\n } else {\n // Merge waypoint positions into existing keyframes at matching percentages.\n // If keyframe count matches waypoint count, assign positionally.\n const kfs = keyframesData.keyframes;\n if (kfs.length === waypoints.length) {\n for (let i = 0; i < kfs.length; i++) {\n kfs[i]!.properties.x = waypoints[i]!.x;\n kfs[i]!.properties.y = waypoints[i]!.y;\n }\n }\n }\n // arcPath is attached below on the animation result.\n }\n\n let fromProperties: Record<string, number | string> | undefined;\n if (call.method === \"fromTo\" && call.fromArg) {\n fromProperties = {};\n const fromVars = objectExpressionToRecord(call.fromArg, scope);\n for (const [key, val] of Object.entries(fromVars)) {\n if (typeof val === \"number\" || typeof val === \"string\") {\n fromProperties[key] = val;\n }\n }\n }\n\n const hasPositionArg = !!call.positionArg;\n const posVal = call.positionArg ? extractLiteralValue(call.positionArg, scope) : 0;\n const position: number | string =\n typeof posVal === \"number\" ? posVal : typeof posVal === \"string\" ? posVal : 0;\n let duration = typeof vars.duration === \"number\" ? vars.duration : undefined;\n const ease = typeof vars.ease === \"string\" ? vars.ease : undefined;\n\n if (duration === undefined && keyframesData) {\n duration = computeKeyframesTotalDuration(call.varsArg, scope);\n }\n\n const anim: Omit<GsapAnimation, \"id\"> = {\n targetSelector: call.selector,\n method: call.method,\n position,\n properties,\n fromProperties,\n duration,\n ease,\n };\n if (!hasPositionArg) anim.implicitPosition = true;\n let group = classifyTweenPropertyGroup(properties);\n if (!group && keyframesData) {\n const kfProps: Record<string, unknown> = {};\n for (const kf of keyframesData.keyframes) {\n for (const k of Object.keys(kf.properties)) kfProps[k] = true;\n }\n group = classifyTweenPropertyGroup(kfProps);\n }\n if (group) anim.propertyGroup = group;\n if (call.global) anim.global = true;\n if (Object.keys(extras).length > 0) anim.extras = extras;\n if (keyframesData) anim.keyframes = keyframesData;\n if (motionPathResult) anim.arcPath = motionPathResult.arcPath;\n if (hasUnresolvedKeyframes) anim.hasUnresolvedKeyframes = true;\n if (call.selector === \"__unresolved__\") anim.hasUnresolvedSelector = true;\n return anim;\n}\n\n// ── Timeline Position Resolution ──────────────────────────────────────────\n\nconst GSAP_DEFAULT_DURATION = 0.5;\n\n// NOTE: Label-based positions (e.g. \"myLabel+=0.5\") are not yet resolved —\n// they fall through to parseFloat which returns null for non-numeric strings.\nfunction resolvePositionString(pos: string, cursor: number, prevStart: number): number | null {\n const trimmed = pos.trim();\n if (trimmed === \"\") return cursor;\n if (trimmed.startsWith(\"+=\")) {\n const n = Number.parseFloat(trimmed.slice(2));\n return Number.isFinite(n) ? cursor + n : null;\n }\n if (trimmed.startsWith(\"-=\")) {\n const n = Number.parseFloat(trimmed.slice(2));\n return Number.isFinite(n) ? cursor - n : null;\n }\n if (trimmed === \"<\") return prevStart;\n if (trimmed === \">\") return cursor;\n if (trimmed.startsWith(\"<\")) {\n const n = Number.parseFloat(trimmed.slice(1));\n return Number.isFinite(n) ? prevStart + n : null;\n }\n if (trimmed.startsWith(\">\")) {\n const n = Number.parseFloat(trimmed.slice(1));\n return Number.isFinite(n) ? cursor + n : null;\n }\n const n = Number.parseFloat(trimmed);\n return Number.isFinite(n) ? n : null;\n}\n\nfunction applyTimelineDefaults(\n anims: Omit<GsapAnimation, \"id\">[],\n defaults?: TimelineDefaults,\n): void {\n if (!defaults) return;\n for (const anim of anims) {\n if (anim.method === \"set\") continue;\n if (anim.duration === undefined && defaults.duration !== undefined) {\n anim.duration = defaults.duration;\n }\n if (anim.ease === undefined && defaults.ease !== undefined) {\n anim.ease = defaults.ease;\n }\n }\n}\n\nfunction resolveTimelinePositions(anims: Omit<GsapAnimation, \"id\">[]): void {\n let cursor = 0;\n let prevStart = 0;\n for (const anim of anims) {\n // A global `gsap.set(...)` is off-timeline — it's applied once at load, not\n // sequenced on the master timeline. It carries no position arg, so the\n // cursor-based fallback below would otherwise hand it the comp-end time\n // (every prior tween's duration summed). Pin it to 0 (its load-time start)\n // and don't let it advance the cursor/prevStart for following tweens.\n if (anim.method === \"set\" && anim.global) {\n anim.resolvedStart = 0;\n continue;\n }\n const duration = anim.method === \"set\" ? 0 : (anim.duration ?? GSAP_DEFAULT_DURATION);\n let start: number | null;\n\n if (anim.implicitPosition) {\n start = cursor;\n } else if (typeof anim.position === \"number\") {\n start = anim.position;\n } else if (typeof anim.position === \"string\") {\n start = resolvePositionString(anim.position, cursor, prevStart);\n } else {\n start = cursor;\n }\n\n if (start != null) {\n anim.resolvedStart = Math.max(0, start);\n prevStart = anim.resolvedStart;\n cursor = Math.max(cursor, anim.resolvedStart + duration);\n }\n }\n}\n\nfunction sortBySourcePosition(calls: TweenCallInfo[]): void {\n calls.sort((a, b) => {\n const aLoc = a.node.callee?.property?.loc?.start;\n const bLoc = b.node.callee?.property?.loc?.start;\n if (!aLoc || !bLoc) return 0;\n return aLoc.line - bLoc.line || aLoc.column - bLoc.column;\n });\n}\n\n// ── Stable ID Generation ───────────────────────────────────────────────────\n\n/**\n * IDs are transient — recomputed on every parse, never persisted across sessions.\n * They exist only in ephemeral request/response payloads, React component state,\n * and the in-memory keyframe cache (rebuilt on every page load). No database,\n * localStorage, or file stores animation IDs, so changing the ID format (e.g.\n * adding a `-scale`/`-position` suffix) is safe.\n */\nfunction assignStableIds(anims: Omit<GsapAnimation, \"id\">[]): GsapAnimation[] {\n const counts = new Map<string, number>();\n return anims.map((anim) => {\n const posKey =\n typeof anim.position === \"number\"\n ? String(Math.round(anim.position * 1000))\n : String(anim.position);\n const groupSuffix = anim.propertyGroup ? `-${anim.propertyGroup}` : \"\";\n const base = `${anim.targetSelector}-${anim.method}-${posKey}${groupSuffix}`;\n const count = (counts.get(base) ?? 0) + 1;\n counts.set(base, count);\n const id = count === 1 ? base : `${base}-${count}`;\n return { ...anim, id };\n });\n}\n\n// ── Shared parse (AST + located tween calls) ────────────────────────────────\n\ninterface ParsedGsapAst {\n ast: AstNode;\n scope: ScopeBindings;\n timelineVar: string;\n detection: TimelineDetection;\n /** Tween calls in document order, each paired with its stable animation id. */\n located: Array<{ id: string; call: TweenCallInfo; animation: GsapAnimation }>;\n}\n\n/**\n * Parse a script to its recast AST plus the located tween calls. The mutation\n * functions reuse this so they can edit the exact call node in place (recast\n * preserves all surrounding source — interleaved `gsap.set`, element variable\n * declarations, the IIFE wrapper, comments and formatting).\n */\nfunction parseGsapAst(script: string): ParsedGsapAst {\n const ast = parseScript(script);\n const scope = collectScopeBindings(ast);\n const targetBindings = collectTargetBindings(ast, scope);\n const detection = findTimelineVar(ast, scope);\n const timelineVar = detection.timelineVar ?? \"tl\";\n const calls = findAllTweenCalls(ast, timelineVar, scope, targetBindings);\n sortBySourcePosition(calls);\n const rawAnims = calls.map((call) => tweenCallToAnimation(call, scope));\n applyTimelineDefaults(rawAnims, detection.defaults);\n resolveTimelinePositions(rawAnims);\n const animations = assignStableIds(rawAnims);\n const located = animations.map((animation, i) => ({\n id: animation.id,\n call: calls[i]!,\n animation,\n }));\n return { ast, scope, timelineVar, detection, located };\n}\n\n// ── Public API ──────────────────────────────────────────────────────────────\n\nexport function parseGsapScript(script: string): ParsedGsap {\n try {\n const { detection, timelineVar, located } = parseGsapAst(script);\n const animations = located.map((l) => l.animation);\n\n const timelineMatch = script.match(\n new RegExp(\n `^[\\\\s\\\\S]*?(?:const|let|var)\\\\s+${timelineVar}\\\\s*=\\\\s*gsap\\\\.timeline\\\\s*\\\\([^)]*\\\\)\\\\s*;?`,\n ),\n );\n const preamble =\n timelineMatch?.[0] ?? `const ${timelineVar} = gsap.timeline({ paused: true });`;\n\n const lastCallIdx = script.lastIndexOf(`${timelineVar}.`);\n let postamble = \"\";\n if (lastCallIdx !== -1) {\n const afterLast = script.slice(lastCallIdx);\n const endOfCall = afterLast.indexOf(\";\");\n if (endOfCall !== -1) {\n postamble = script.slice(lastCallIdx + endOfCall + 1).trim();\n }\n }\n\n const result: ParsedGsap = { animations, timelineVar, preamble, postamble };\n if (detection.timelineCount > 1) result.multipleTimelines = true;\n if (detection.timelineCount > 0 && detection.timelineVar === null)\n result.unsupportedTimelinePattern = true;\n return result;\n } catch {\n return { animations: [], timelineVar: \"tl\", preamble: \"\", postamble: \"\" };\n }\n}\n\n// ── In-place AST mutation helpers ───────────────────────────────────────────\n//\n// Edits operate directly on the located call's AST node and reprint via recast,\n// which preserves every untouched statement. This is what lets us edit tweens\n// in real compositions (variable targets, interleaved `gsap.set`, IIFE wrapper)\n// without regenerating — and discarding — the surrounding code.\n\n/**\n * Parse a value/expression snippet into a standalone AST expression node.\n * Uses an assignment (`__hf__ = <code>`) rather than wrapping in parens so an\n * object literal parses as an expression without recast re-emitting the\n * surrounding parentheses.\n */\nfunction parseExpr(code: string): AstNode {\n return parseScript(`__hf__ = ${code};`).program.body[0].expression.right;\n}\n\nfunction propKeyName(prop: AstNode): string | undefined {\n return prop?.key?.name ?? prop?.key?.value;\n}\n\nfunction isObjectProperty(prop: AstNode): boolean {\n return prop?.type === \"ObjectProperty\" || prop?.type === \"Property\";\n}\n\n/** A key the inspector treats as an editable transform/style property. */\nfunction isEditablePropertyKey(key: string): boolean {\n return !BUILTIN_VAR_KEYS.has(key) && !DROPPED_VAR_KEYS.has(key) && !EXTRAS_KEYS.has(key);\n}\n\nfunction makeObjectProperty(key: string, value: number | string): AstNode {\n const obj = parseExpr(`{ ${safeKey(key)}: ${valueToCode(value)} }`);\n return obj.properties[0];\n}\n\n/** Set (or insert) a single key on an ObjectExpression, preserving sibling keys. */\nfunction setVarsKey(varsArg: AstNode, key: string, value: number | string): void {\n if (varsArg?.type !== \"ObjectExpression\") return;\n const existing = varsArg.properties.find(\n (p: AstNode) => isObjectProperty(p) && propKeyName(p) === key,\n );\n if (existing) {\n existing.value = parseExpr(valueToCode(value));\n } else {\n varsArg.properties.push(makeObjectProperty(key, value));\n }\n}\n\n/**\n * Filter an ObjectExpression's properties, keeping non-editable keys\n * and delegating the keep/drop decision for editable keys to `shouldKeep`.\n */\nfunction filterEditableKeys(varsArg: AstNode, shouldKeep: (key: string) => boolean): void {\n if (varsArg?.type !== \"ObjectExpression\") return;\n varsArg.properties = varsArg.properties.filter((p: AstNode) => {\n if (!isObjectProperty(p)) return true;\n const key = propKeyName(p);\n if (typeof key !== \"string\") return true;\n if (!isEditablePropertyKey(key)) return true;\n return shouldKeep(key);\n });\n}\n\n/**\n * Replace the editable-property keys on an ObjectExpression with `newProps`,\n * leaving `duration`, `ease`, `stagger`, callbacks and other non-editable keys\n * untouched.\n */\nfunction reconcileEditableProperties(\n varsArg: AstNode,\n newProps: Record<string, number | string>,\n): void {\n filterEditableKeys(varsArg, (key) => key in newProps);\n // Upsert each new prop, preserving the order keys first appeared.\n for (const [key, value] of Object.entries(newProps)) {\n setVarsKey(varsArg, key, value);\n }\n}\n\nfunction applyEaseUpdate(varsArg: AstNode, ease: string): void {\n const kfNode = findKeyframesObjectNode(varsArg);\n if (kfNode) {\n setVarsKey(kfNode, \"easeEach\", ease);\n removeVarsKey(varsArg, \"ease\");\n } else {\n setVarsKey(varsArg, \"ease\", ease);\n }\n}\n\n/**\n * \"Apply to all segments\": drop every per-keyframe `ease` override so the single\n * `easeEach` governs all segments uniformly (AE select-all + F9). Mirrors the\n * acorn writer's resetKeyframeEases branch.\n */\nfunction stripKeyframeEases(varsArg: AstNode): void {\n const kfNode = findKeyframesObjectNode(varsArg);\n const props = kfNode?.properties;\n if (!Array.isArray(props)) return;\n for (const entry of props) {\n if (isObjectProperty(entry)) removeVarsKey(entry.value, \"ease\");\n }\n}\n\nfunction applyUpdatesToCall(\n call: TweenCallInfo,\n updates: Partial<GsapAnimation> & { easeEach?: string; resetKeyframeEases?: boolean },\n): void {\n if (updates.properties) reconcileEditableProperties(call.varsArg, updates.properties);\n if (updates.fromProperties && call.method === \"fromTo\" && call.fromArg) {\n reconcileEditableProperties(call.fromArg, updates.fromProperties);\n }\n if (updates.duration !== undefined) setVarsKey(call.varsArg, \"duration\", updates.duration);\n if (updates.easeEach !== undefined) applyEaseUpdate(call.varsArg, updates.easeEach);\n else if (updates.ease !== undefined) applyEaseUpdate(call.varsArg, updates.ease);\n if (updates.resetKeyframeEases) stripKeyframeEases(call.varsArg);\n if (updates.position !== undefined) {\n const posIdx = call.method === \"fromTo\" ? 3 : 2;\n call.node.arguments[posIdx] = parseExpr(valueToCode(updates.position));\n }\n}\n\n/** Walk up to the enclosing ExpressionStatement path (for prune / insertAfter). */\nfunction findStatementPath(path: AstPath): AstPath | null {\n let p = path;\n while (p) {\n if (p.node?.type === \"ExpressionStatement\") return p;\n p = p.parentPath;\n }\n return null;\n}\n\nfunction insertAfterAnchor(parsed: ParsedGsapAst, newStatement: AstNode): void {\n const lastCall = parsed.located[parsed.located.length - 1]?.call;\n const anchorPath = lastCall\n ? findStatementPath(lastCall.path)\n : findTimelineDeclarationPath(parsed.ast, parsed.timelineVar);\n if (anchorPath) {\n anchorPath.insertAfter(newStatement);\n } else {\n parsed.ast.program.body.push(newStatement);\n }\n}\n\n/** Build the source for a single `tl.method(selector, vars, position)` call. */\nfunction buildTweenStatementCode(timelineVar: string, anim: Omit<GsapAnimation, \"id\">): string {\n const selector = JSON.stringify(anim.targetSelector);\n const props: Record<string, number | string> = { ...anim.properties };\n if (anim.method !== \"set\" && anim.duration !== undefined) props.duration = anim.duration;\n if (anim.ease) props.ease = anim.ease;\n const entries = Object.entries(props).map(([k, v]) => `${safeKey(k)}: ${valueToCode(v)}`);\n // immediateRender forces GSAP to apply the set when added to the timeline,\n // not on the first seek — without it, tl.set at position 0 on a paused\n // timeline is invisible until the playhead moves past 0. A base `gsap.set`\n // already runs immediately, so it doesn't need (or get) the flag.\n if (anim.method === \"set\" && !anim.global) entries.push(\"immediateRender: true\");\n if (anim.extras) {\n for (const [k, v] of Object.entries(anim.extras)) {\n entries.push(`${safeKey(k)}: ${valueToCode(v as number | string)}`);\n }\n }\n const objCode = `{ ${entries.join(\", \")} }`;\n const posCode = valueToCode(\n typeof anim.position === \"number\" ? anim.position : (anim.position ?? 0),\n );\n if (anim.method === \"fromTo\") {\n const fromEntries = Object.entries(anim.fromProperties ?? {}).map(\n ([k, v]) => `${safeKey(k)}: ${valueToCode(v)}`,\n );\n const fromCode = `{ ${fromEntries.join(\", \")} }`;\n return `${timelineVar}.fromTo(${selector}, ${fromCode}, ${objCode}, ${posCode});`;\n }\n // A base `gsap.set` is off the timeline: no timeline var, no position arg.\n if (anim.method === \"set\" && anim.global) {\n return `gsap.set(${selector}, ${objCode});`;\n }\n return `${timelineVar}.${anim.method}(${selector}, ${objCode}, ${posCode});`;\n}\n\nexport function updateAnimationInScript(\n script: string,\n animationId: string,\n updates: Partial<GsapAnimation> & { easeEach?: string; resetKeyframeEases?: boolean },\n): string {\n let parsed: ParsedGsapAst;\n try {\n parsed = parseGsapAst(script);\n } catch (e) {\n console.warn(\"[gsap-parser] updateAnimationInScript parse failed:\", e);\n return script;\n }\n const target = parsed.located.find((l) => l.id === animationId);\n if (!target) return script;\n applyUpdatesToCall(target.call, updates);\n return recast.print(parsed.ast).code;\n}\n\nexport function shiftPositionsInScript(\n script: string,\n targetSelector: string,\n delta: number,\n): string {\n let parsed: ParsedGsapAst;\n try {\n parsed = parseGsapAst(script);\n } catch (e) {\n console.warn(\"[gsap-parser] shiftPositionsInScript parse failed:\", e);\n return script;\n }\n let changed = false;\n for (const entry of parsed.located) {\n if (entry.animation.targetSelector !== targetSelector) continue;\n if (typeof entry.animation.position !== \"number\") continue;\n const newPos = Math.max(0, Math.round((entry.animation.position + delta) * 1000) / 1000);\n applyUpdatesToCall(entry.call, { position: newPos });\n changed = true;\n }\n return changed ? recast.print(parsed.ast).code : script;\n}\n\nexport function scalePositionsInScript(\n script: string,\n targetSelector: string,\n oldStart: number,\n oldDuration: number,\n newStart: number,\n newDuration: number,\n): string {\n if (oldDuration <= 0 || newDuration <= 0) return script;\n const ratio = newDuration / oldDuration;\n let parsed: ParsedGsapAst;\n try {\n parsed = parseGsapAst(script);\n } catch (e) {\n console.warn(\"[gsap-parser] scalePositionsInScript parse failed:\", e);\n return script;\n }\n let changed = false;\n for (const entry of parsed.located) {\n if (entry.animation.targetSelector !== targetSelector) continue;\n if (typeof entry.animation.position !== \"number\") continue;\n const newPos = Math.max(\n 0,\n Math.round((newStart + (entry.animation.position - oldStart) * ratio) * 1000) / 1000,\n );\n const updates: Partial<GsapAnimation> = { position: newPos };\n if (typeof entry.animation.duration === \"number\" && entry.animation.duration > 0) {\n updates.duration = Math.max(\n 0.001,\n Math.round(entry.animation.duration * ratio * 1000) / 1000,\n );\n }\n applyUpdatesToCall(entry.call, updates);\n changed = true;\n }\n return changed ? recast.print(parsed.ast).code : script;\n}\n\nfunction updateAnimationSelector(script: string, animationId: string, newSelector: string): string {\n let parsed: ParsedGsapAst;\n try {\n parsed = parseGsapAst(script);\n } catch {\n return script;\n }\n const target = parsed.located.find((l) => l.id === animationId);\n if (!target) return script;\n const selectorArg = target.call.path.node.arguments?.[0];\n if (selectorArg?.type === \"StringLiteral\") {\n selectorArg.value = newSelector;\n } else if (selectorArg?.type === \"Identifier\") {\n target.call.path.node.arguments[0] = { type: \"StringLiteral\", value: newSelector };\n }\n return recast.print(parsed.ast).code;\n}\n\nexport function addAnimationToScript(\n script: string,\n animation: Omit<GsapAnimation, \"id\">,\n): { script: string; id: string } {\n let parsed: ParsedGsapAst;\n try {\n parsed = parseGsapAst(script);\n } catch (e) {\n console.warn(\"[gsap-parser] addAnimationToScript parse failed:\", e);\n return { script, id: \"\" };\n }\n // Nothing to anchor against and no timeline to target — treat as parse failure.\n if (parsed.located.length === 0 && parsed.detection.timelineVar === null) {\n return { script, id: \"\" };\n }\n\n const id = `anim-${Date.now()}`;\n const statementCode = buildTweenStatementCode(parsed.timelineVar, animation);\n const newStatement = parseScript(statementCode).program.body[0];\n insertAfterAnchor(parsed, newStatement);\n return { script: recast.print(parsed.ast).code, id };\n}\n\nexport function addAnimationWithKeyframesToScript(\n script: string,\n targetSelector: string,\n position: number,\n duration: number,\n keyframes: Array<{\n percentage: number;\n properties: Record<string, number | string>;\n ease?: string;\n auto?: boolean;\n }>,\n ease?: string,\n easeEach?: string,\n): { script: string; id: string } {\n let parsed: ParsedGsapAst;\n try {\n parsed = parseGsapAst(script);\n } catch (e) {\n console.warn(\"[gsap-parser] addAnimationWithKeyframesToScript parse failed:\", e);\n return { script, id: \"\" };\n }\n if (parsed.located.length === 0 && parsed.detection.timelineVar === null) {\n return { script, id: \"\" };\n }\n\n const selector = JSON.stringify(targetSelector);\n const kfCode = buildKeyframeObjectCode(keyframes, easeEach ? { easeEach } : undefined);\n const varEntries = [`keyframes: ${kfCode}`, `duration: ${valueToCode(duration)}`];\n if (ease) varEntries.push(`ease: ${JSON.stringify(ease)}`);\n const posCode = valueToCode(position);\n const stmtCode = `${parsed.timelineVar}.to(${selector}, { ${varEntries.join(\", \")} }, ${posCode});`;\n\n const newStatement = parseScript(stmtCode).program.body[0];\n insertAfterAnchor(parsed, newStatement);\n\n const result = recast.print(parsed.ast).code;\n const reParsed = parseGsapAst(result);\n const newId = reParsed.located[reParsed.located.length - 1]?.id ?? \"\";\n return { script: result, id: newId };\n}\n\n/** Find the statement path of `const <timelineVar> = gsap.timeline(...)`. */\nfunction findTimelineDeclarationPath(ast: AstNode, timelineVar: string): AstPath | null {\n let found: AstPath | null = null;\n recast.types.visit(ast, {\n visitVariableDeclaration(path: AstPath) {\n if (found) return false;\n for (const decl of path.node.declarations ?? []) {\n if (decl.id?.name === timelineVar && isGsapTimelineCall(decl.init)) {\n found = path;\n return false;\n }\n }\n this.traverse(path);\n },\n });\n return found;\n}\n\n/** Find the call that chains off `targetNode` (i.e. whose callee object IS it). */\nfunction findChainParentCall(stmtNode: AstNode, targetNode: AstNode): AstNode | null {\n let found: AstNode | null = null;\n recast.types.visit(stmtNode, {\n visitCallExpression(p: AstPath) {\n if (found) return false;\n if (p.node.callee?.type === \"MemberExpression\" && p.node.callee.object === targetNode) {\n found = p.node;\n return false;\n }\n this.traverse(p);\n },\n });\n return found;\n}\n\nexport function removeAnimationFromScript(script: string, animationId: string): string {\n let parsed: ParsedGsapAst;\n try {\n parsed = parseGsapAst(script);\n } catch (e) {\n console.warn(\"[gsap-parser] removeAnimationFromScript parse failed:\", e);\n return script;\n }\n let target = parsed.located.find((l) => l.id === animationId);\n if (!target) {\n const convertedId = animationId.replace(/-from-|-fromTo-/, \"-to-\");\n target = parsed.located.find((l) => l.id === convertedId);\n }\n if (!target) return script;\n const node = target.call.node;\n const stmtPath = findStatementPath(target.call.path);\n if (!stmtPath) return script;\n\n const parentCall = findChainParentCall(stmtPath.node, node);\n if (parentCall) {\n // Inner link of a chain — splice it out by re-pointing the next link.\n parentCall.callee.object = node.callee.object;\n } else if (node.callee?.object?.type === \"CallExpression\") {\n // Outermost link of a chain with earlier links — drop just this link.\n stmtPath.node.expression = node.callee.object;\n } else {\n // Standalone tween — remove the whole statement.\n stmtPath.prune();\n }\n return recast.print(parsed.ast).code;\n}\n\nfunction insertInheritedStateSet(\n script: string,\n selector: string,\n position: number,\n properties: Record<string, number | string>,\n): string {\n let parsed: ParsedGsapAst;\n try {\n parsed = parseGsapAst(script);\n } catch {\n return script;\n }\n const tlVar = parsed.timelineVar;\n const props = Object.entries(properties)\n .map(([k, v]) => `${k}: ${typeof v === \"string\" ? JSON.stringify(v) : v}`)\n .join(\", \");\n const code = `${tlVar}.set(${JSON.stringify(selector)}, { ${props} }, ${position});`;\n const newStatement = parseScript(code).program.body[0];\n const anchor = findTimelineDeclarationPath(parsed.ast, tlVar);\n if (anchor) {\n anchor.insertAfter(newStatement);\n } else if (parsed.located.length > 0) {\n const firstTween = parsed.located[0]!.call;\n const stmtPath = findStatementPath(firstTween.path);\n if (stmtPath) stmtPath.insertBefore(newStatement);\n else parsed.ast.program.body.unshift(newStatement);\n } else {\n parsed.ast.program.body.push(newStatement);\n }\n return recast.print(parsed.ast).code;\n}\n\n/** Marker on Studio-emitted pre-keyframe hold `set`s. `data` is a GSAP-reserved\n * config key (attached to the tween, never applied to the target), so it carries\n * the tag without triggering GSAP's \"Invalid property\" warning. */\nconst STUDIO_HOLD_MARKER = \"hf-hold\";\n\n/** True for a `tl.set(...)` this module emitted to hold a keyframe before its tween.\n * The Studio filters these out so they never appear as user keyframes/diamonds. */\nexport function isStudioHoldSet(anim: GsapAnimation): boolean {\n return anim.method === \"set\" && anim.properties?.data === STUDIO_HOLD_MARKER;\n}\n\n/**\n * Keep a `tl.set(selector, {x,y}, 0)` \"hold\" in front of every position-keyframed\n * tween that starts after t=0, so the element holds its first keyframe's position\n * BEFORE the tween plays instead of snapping to its CSS base (the universal NLE\n * \"hold before first keyframe\" behavior). The set is tagged with `data: \"hf-hold\"`\n * so this pass owns it: every call wipes the prior holds and recomputes from the\n * current keyframes, keeping them in sync as keyframes are added/moved/deleted.\n *\n * Idempotent. Only position props (x/y/xPercent/yPercent) are held — opacity/scale\n * keep their authored pre-tween behavior. A tween already starting at 0 needs no\n * hold (no gap before it).\n */\nexport function syncPositionHoldsBeforeKeyframes(script: string): string {\n let parsed: ParsedGsap;\n try {\n parsed = parseGsapScript(script);\n } catch {\n return script;\n }\n // 1. Drop every hold this pass previously emitted, so we recompute fresh.\n let result = script;\n const staleHoldIds = parsed.animations.filter(isStudioHoldSet).map((a) => a.id);\n for (const id of staleHoldIds) result = removeAnimationFromScript(result, id);\n\n // 2. Re-add a hold for each position-keyframed tween that starts after t=0.\n let reparsed: ParsedGsap;\n try {\n reparsed = parseGsapScript(result);\n } catch {\n return result;\n }\n for (const anim of reparsed.animations) {\n if (!anim.keyframes) continue;\n const start = anim.resolvedStart ?? (typeof anim.position === \"number\" ? anim.position : 0);\n if (!(start > 0.001)) continue;\n const firstKf = [...anim.keyframes.keyframes].sort((a, b) => a.percentage - b.percentage)[0];\n if (!firstKf) continue;\n const posProps: Record<string, number | string> = {};\n for (const [k, v] of Object.entries(firstKf.properties)) {\n if (classifyPropertyGroup(k) === \"position\" && typeof v === \"number\") posProps[k] = v;\n }\n if (Object.keys(posProps).length === 0) continue;\n result = insertInheritedStateSet(result, anim.targetSelector, 0, {\n ...posProps,\n data: STUDIO_HOLD_MARKER,\n });\n }\n return result;\n}\n\n// ── Split Animation Functions ─────────────────────────────────────────────\n\nexport interface SplitAnimationsOptions {\n originalId: string;\n newId: string;\n splitTime: number;\n elementStart: number;\n elementDuration: number;\n}\n\nexport interface SplitAnimationsResult {\n script: string;\n /** Non-ID-selector animations that the engine cannot safely retarget. */\n skippedSelectors: string[];\n}\n\n// fallow-ignore-next-line complexity\nexport function splitAnimationsInScript(\n script: string,\n opts: SplitAnimationsOptions,\n): SplitAnimationsResult {\n const parsed = parseGsapScript(script);\n const originalSelector = `#${opts.originalId}`;\n const newSelector = `#${opts.newId}`;\n\n const skippedSelectors: string[] = [];\n for (const a of parsed.animations) {\n if (a.targetSelector !== originalSelector && a.targetSelector.includes(opts.originalId)) {\n skippedSelectors.push(a.targetSelector);\n }\n }\n\n const matching = parsed.animations.filter((a) => a.targetSelector === originalSelector);\n if (matching.length === 0) return { script, skippedSelectors };\n\n let result = script;\n const newElementStart = opts.splitTime;\n const inheritedProps: Record<string, number | string> = {};\n\n // Reverse iteration: updateAnimationSelector mutates selectors in the source\n // string, which can shift count-based ID suffixes (e.g. \"#hero-1\" → \"#hero-2\")\n // for later animations. Processing last-to-first prevents stale ID collisions.\n for (let i = matching.length - 1; i >= 0; i--) {\n const anim = matching[i]!;\n const pos = typeof anim.position === \"number\" ? anim.position : 0;\n const dur = anim.duration ?? 0;\n const animEnd = pos + dur;\n\n if (anim.keyframes) {\n if (pos >= opts.splitTime) {\n result = updateAnimationSelector(result, anim.id, newSelector);\n } else if (animEnd > opts.splitTime) {\n // Spanning keyframes can't be correctly split without renormalizing\n // percentages and durations — leave on original, warn the caller.\n skippedSelectors.push(`${originalSelector} (keyframes spanning split)`);\n const kfs = anim.keyframes.keyframes;\n for (const kf of kfs) {\n const kfTime = pos + (kf.percentage / 100) * dur;\n if (kfTime <= opts.splitTime) {\n for (const [k, v] of Object.entries(kf.properties)) {\n inheritedProps[k] = v;\n }\n }\n }\n } else {\n // Entirely before split — extract final keyframe properties\n const kfs = anim.keyframes.keyframes;\n if (kfs.length > 0) {\n for (const [k, v] of Object.entries(kfs[kfs.length - 1]!.properties)) {\n inheritedProps[k] = v;\n }\n }\n }\n continue;\n }\n\n // `<=` (not `<`) is deliberate: a tween whose end coincides exactly with\n // the split boundary has fully played by splitTime, so it belongs to the\n // first half and contributes its resting state to the clone. The spanning\n // branch below handles only strictly-mid-flight tweens (pos < split < end).\n if (animEnd <= opts.splitTime) {\n // Only a completed .from() reverts the element to its natural state, so\n // its recorded properties are the HIDDEN start (e.g. opacity:0), not the\n // resting state — clearing them keeps the clone at its natural value\n // instead of pinning it to the from-values (which made it invisible).\n // .fromTo() and .to() both END at their to-values (no revert), so they\n // fall through to `else` and inherit `anim.properties` (the to-values) —\n // .fromTo() must NOT join the .from() clear-branch or the clone would\n // drop the very state the fromTo just established.\n if (anim.method === \"from\") {\n for (const k of Object.keys(anim.properties)) delete inheritedProps[k];\n } else {\n for (const [k, v] of Object.entries(anim.properties)) {\n inheritedProps[k] = v;\n }\n }\n continue;\n }\n\n if (pos >= opts.splitTime) {\n result = updateAnimationSelector(result, anim.id, newSelector);\n continue;\n }\n\n // Spans the split — use linear interpolation to compute mid-values,\n // then .fromTo() on the clone so both halves play the correct range.\n // For .fromTo() tweens we have explicit from-values; for .to() tweens\n // we use accumulated state from prior animations, defaulting to 0 for\n // unknown numeric properties (the standard GSAP transform initial state).\n const progress = dur > 0 ? (opts.splitTime - pos) / dur : 0;\n const fromSource = anim.fromProperties ?? inheritedProps;\n const midProps: Record<string, number | string> = {};\n for (const [k, v] of Object.entries(anim.properties)) {\n if (typeof v !== \"number\") {\n midProps[k] = v;\n continue;\n }\n const fromVal = typeof fromSource[k] === \"number\" ? (fromSource[k] as number) : 0;\n midProps[k] = fromVal + (v - fromVal) * progress;\n }\n\n const firstHalfDuration = opts.splitTime - pos;\n result = updateAnimationInScript(result, anim.id, {\n duration: firstHalfDuration,\n properties: midProps,\n });\n\n const secondHalfDuration = animEnd - opts.splitTime;\n const addResult = addAnimationToScript(result, {\n targetSelector: newSelector,\n method: \"fromTo\",\n position: newElementStart,\n duration: secondHalfDuration,\n properties: { ...anim.properties },\n fromProperties: { ...midProps },\n ease: anim.ease,\n extras: anim.extras,\n });\n result = addResult.script;\n\n for (const [k, v] of Object.entries(midProps)) {\n inheritedProps[k] = v;\n }\n }\n\n if (Object.keys(inheritedProps).length > 0) {\n result = insertInheritedStateSet(result, newSelector, newElementStart, inheritedProps);\n }\n\n return { script: result, skippedSelectors };\n}\n\n// ── Keyframe Mutation Functions ────────────────────────────────────────────\n\nfunction sortedKeyframes(\n kfs: Array<{ percentage: number; properties: Record<string, number | string>; ease?: string }>,\n) {\n return kfs.slice().sort((a, b) => a.percentage - b.percentage);\n}\n\nfunction keyframePropsToCode(kf: { properties: Record<string, number | string> }): string[] {\n return Object.entries(kf.properties).map(([k, v]) => `${safeKey(k)}: ${valueToCode(v)}`);\n}\n\nfunction buildKeyframeObjectCode(\n keyframes: Array<{\n percentage: number;\n properties: Record<string, number | string>;\n ease?: string;\n auto?: boolean;\n }>,\n options?: { easeEach?: string },\n): string {\n const entries = keyframes.map((kf) => {\n const props = keyframePropsToCode(kf);\n if (kf.ease) props.push(`ease: ${JSON.stringify(kf.ease)}`);\n if (kf.auto) props.push(`_auto: 1`);\n return `${JSON.stringify(`${kf.percentage}%`)}: { ${props.join(\", \")} }`;\n });\n if (options?.easeEach) entries.push(`easeEach: ${JSON.stringify(options.easeEach)}`);\n return `{ ${entries.join(\", \")} }`;\n}\n\n/** Remove a named property from an ObjectExpression's properties array. */\nfunction removeVarsKey(varsArg: AstNode, key: string): void {\n if (varsArg?.type !== \"ObjectExpression\") return;\n varsArg.properties = varsArg.properties.filter(\n (p: AstNode) => !(isObjectProperty(p) && propKeyName(p) === key),\n );\n}\n\n/** Extract the numeric percentage from a key like \"50%\". Returns NaN for non-percentage keys. */\nfunction percentageFromKey(key: string): number {\n const m = PERCENTAGE_KEY_RE.exec(key);\n return m ? Number.parseFloat(m[1]!) : Number.NaN;\n}\n\nconst PCT_TOLERANCE = 2;\n\nfunction findKeyframePropByPct(\n kfNode: AstNode,\n percentage: number,\n): { idx: number; prop: AstNode } | null {\n const props = kfNode.properties;\n for (let i = 0; i < props.length; i++) {\n if (!isObjectProperty(props[i])) continue;\n const key = propKeyName(props[i]);\n if (typeof key !== \"string\") continue;\n const parsed = percentageFromKey(key);\n if (Number.isNaN(parsed)) continue;\n if (Math.abs(parsed - percentage) <= PCT_TOLERANCE) return { idx: i, prop: props[i] };\n }\n return null;\n}\n\n/** Build a keyframe value AST node from properties and optional ease. */\nfunction buildKeyframeValueNode(\n properties: Record<string, number | string>,\n ease?: string,\n): AstNode {\n const entries = Object.entries(properties).map(([k, v]) => `${safeKey(k)}: ${valueToCode(v)}`);\n if (ease) entries.push(`ease: ${JSON.stringify(ease)}`);\n return parseExpr(`{ ${entries.join(\", \")} }`);\n}\n\n/** Parse + locate a target animation, returning null on failure. */\nfunction locateAnimation(\n script: string,\n animationId: string,\n): { parsed: ParsedGsapAst; target: ParsedGsapAst[\"located\"][number] } | null {\n let parsed: ParsedGsapAst;\n try {\n parsed = parseGsapAst(script);\n } catch {\n return null;\n }\n const target = parsed.located.find((l) => l.id === animationId);\n return target ? { parsed, target } : null;\n}\n\n// Animation ids encode the tween's timeline position in ms\n// (`#puck-a-to-1200-position`). A gesture/convert can re-emit a tween at a\n// different position, changing its id — so a client that cached the old id (its\n// selectedGsapAnimations hasn't refreshed) edits a now-nonexistent id and the op\n// no-ops. Parse `{selector}-{method}-{posMs}-{group}` so we can fall back to the\n// same selector+method+group tween nearest the requested position.\nconst ANIM_ID_RE = /^(.*)-(fromTo|from|to|set)-(\\d+)-([a-z]+)$/;\n\nfunction locateAnimationWithFallback(\n script: string,\n animationId: string,\n): ReturnType<typeof locateAnimation> {\n const loc = locateAnimation(script, animationId);\n if (loc) return loc;\n const convertedId = animationId.replace(/-from-|-fromTo-/, \"-to-\");\n if (convertedId !== animationId) {\n const converted = locateAnimation(script, convertedId);\n if (converted) return converted;\n }\n // Position-drift fallback: match by stable identity (selector+method+group),\n // disambiguating by the position closest to the one the caller asked for.\n const want = ANIM_ID_RE.exec(animationId);\n if (!want) return null;\n const [, sel, method, wantPosStr, group] = want;\n const wantPos = Number(wantPosStr);\n let parsed: ParsedGsapAst;\n try {\n parsed = parseGsapAst(script);\n } catch {\n return null;\n }\n let best: ParsedGsapAst[\"located\"][number] | null = null;\n let bestDist = Number.POSITIVE_INFINITY;\n for (const l of parsed.located) {\n const m = ANIM_ID_RE.exec(l.id);\n if (!m || m[1] !== sel || m[2] !== method || m[4] !== group) continue;\n const dist = Math.abs(Number(m[3]) - wantPos);\n if (dist < bestDist) {\n best = l;\n bestDist = dist;\n }\n }\n return best ? { parsed, target: best } : null;\n}\n\n/** Find the keyframes ObjectExpression node on a tween's varsArg, or null. */\nfunction findKeyframesObjectNode(varsArg: AstNode): AstNode | null {\n const node = findPropertyNode(varsArg, \"keyframes\");\n return node?.type === \"ObjectExpression\" ? node : null;\n}\n\n/**\n * Convert array-form keyframes (`keyframes: [{x,y}, …]`) to even-percentage object\n * form (`{ \"0%\": {…}, \"33.3%\": {…}, … }`) IN PLACE, returning the new object node\n * (or null if not array-form). GSAP distributes an array evenly, so this is\n * runtime-identical — but it gives the percentage-keyed write ops something to\n * target. Needed before INSERTING a keyframe at an arbitrary percentage, which an\n * even array can't host.\n */\nfunction convertArrayKeyframesToObjectNode(varsArg: AstNode): AstNode | null {\n if (varsArg?.type !== \"ObjectExpression\") return null;\n const prop = (varsArg.properties ?? []).find(\n (p: AstNode) => isObjectProperty(p) && propKeyName(p) === \"keyframes\",\n );\n if (!prop || prop.value?.type !== \"ArrayExpression\") return null;\n const els: AstNode[] = (prop.value.elements ?? []).filter(\n (e: AstNode | null): e is AstNode => !!e && e.type === \"ObjectExpression\",\n );\n const n = els.length;\n if (n === 0) return null;\n const entries = els.map((el: AstNode, i: number) => {\n const pct = n > 1 ? Math.round((i / (n - 1)) * 1000) / 10 : 0;\n return `${JSON.stringify(`${pct}%`)}: ${recast.print(el).code}`;\n });\n prop.value = parseExpr(`{ ${entries.join(\", \")} }`);\n return prop.value;\n}\n\n/** Filter percentage-keyed properties from a keyframes ObjectExpression. */\nfunction filterPercentageProps(kfNode: AstNode): AstNode[] {\n return kfNode.properties.filter((p: AstNode) => {\n if (!isObjectProperty(p)) return false;\n const key = propKeyName(p);\n return typeof key === \"string\" && PERCENTAGE_KEY_RE.test(key);\n });\n}\n\n/**\n * Collapse a keyframes node to flat tween: apply `record` entries as vars keys,\n * then remove `keyframes` and `easeEach` from varsArg. Skips the `ease` key\n * from the record (per-keyframe ease, not a tween ease).\n */\nfunction collapseKeyframesToFlat(varsArg: AstNode, record: Record<string, unknown>): void {\n for (const [k, v] of Object.entries(record)) {\n if (k === \"ease\") continue;\n if (typeof v === \"number\" || typeof v === \"string\") setVarsKey(varsArg, k, v);\n }\n removeVarsKey(varsArg, \"keyframes\");\n removeVarsKey(varsArg, \"easeEach\");\n}\n\n/**\n * Locate an animation's keyframes ObjectExpression and build the percentage key.\n * Shared preamble for addKeyframeToScript, removeKeyframeFromScript, and\n * updateKeyframeInScript.\n */\nfunction locateKeyframeCtx(script: string, animationId: string, percentage: number) {\n const loc = locateAnimationWithFallback(script, animationId);\n if (!loc) return null;\n const kfNode = findKeyframesObjectNode(loc.target.call.varsArg);\n if (!kfNode) return null;\n return { loc, kfNode, pctKey: `${percentage}%` };\n}\n\n/**\n * Insert a keyframe at the given percentage in an existing percentage-keyframes\n * object. If the percentage already exists, its value is replaced.\n */\nexport function addKeyframeToScript(\n script: string,\n animationId: string,\n percentage: number,\n properties: Record<string, number | string>,\n ease?: string,\n backfillDefaults?: Record<string, number | string>,\n): string {\n let loc = locateAnimationWithFallback(script, animationId);\n if (!loc) return script;\n let kfNode = findKeyframesObjectNode(loc.target.call.varsArg);\n\n // Array-form keyframes can't host an arbitrary new percentage — normalize to\n // object form in place first. (convertToKeyframesInScript below only converts\n // FLAT tweens; it early-returns when keyframes already exist.)\n if (!kfNode) kfNode = convertArrayKeyframesToObjectNode(loc.target.call.varsArg);\n\n if (!kfNode) {\n script = convertToKeyframesInScript(script, animationId);\n loc = locateAnimationWithFallback(script, animationId);\n if (!loc) return script;\n kfNode = findKeyframesObjectNode(loc.target.call.varsArg);\n if (!kfNode) return script;\n }\n const pctKey = `${percentage}%`;\n\n const newValueNode = buildKeyframeValueNode(properties, ease);\n\n // Merge into existing keyframe at this percentage, or insert new\n const existing = findKeyframePropByPct(kfNode, percentage);\n if (existing) {\n if (existing.prop.value?.type === \"ObjectExpression\") {\n const existingRecord = objectExpressionToRecord(existing.prop.value, loc.parsed.scope);\n const merged = { ...existingRecord };\n for (const [k, v] of Object.entries(properties)) merged[k] = v;\n existing.prop.value = buildKeyframeValueNode(\n merged as Record<string, number | string>,\n ease ?? (typeof existingRecord.ease === \"string\" ? existingRecord.ease : undefined),\n );\n } else {\n existing.prop.value = newValueNode;\n }\n } else {\n // Build the new property node with a quoted percentage key\n const newProp = parseExpr(`{ ${JSON.stringify(pctKey)}: {} }`).properties[0];\n newProp.value = newValueNode;\n\n // Insert in sorted order by percentage\n let insertIdx = kfNode.properties.length;\n for (let i = 0; i < kfNode.properties.length; i++) {\n const key = isObjectProperty(kfNode.properties[i])\n ? propKeyName(kfNode.properties[i])\n : undefined;\n if (typeof key === \"string\" && percentageFromKey(key) > percentage) {\n insertIdx = i;\n break;\n }\n }\n kfNode.properties.splice(insertIdx, 0, newProp);\n }\n\n // Auto-update adjacent endpoints: only update an `_auto` 0% or 100%\n // keyframe when the new keyframe is directly next to it (no other keyframe\n // between them). This prevents a keyframe at 74% from clobbering 100% when\n // 75% already exists, and a keyframe at 30% from clobbering 0% when 25%\n // already exists.\n if (percentage > 0 && percentage < 100) {\n const pctProps = filterPercentageProps(kfNode);\n const allPcts = pctProps\n .map((p: AstNode) => percentageFromKey(propKeyName(p) ?? \"\"))\n .filter((n: number) => !Number.isNaN(n) && n !== percentage)\n .sort((a: number, b: number) => a - b);\n const leftNeighbor = allPcts.filter((p: number) => p < percentage).pop();\n const rightNeighbor = allPcts.find((p: number) => p > percentage);\n for (const endPct of [0, 100]) {\n const isNeighbor = endPct === 0 ? leftNeighbor === 0 : rightNeighbor === 100;\n if (!isNeighbor) continue;\n const endProp = pctProps.find(\n (p: AstNode) => percentageFromKey(propKeyName(p) ?? \"\") === endPct,\n );\n if (!endProp?.value || endProp.value.type !== \"ObjectExpression\") continue;\n const hasAuto = endProp.value.properties.some(\n (p: AstNode) => isObjectProperty(p) && propKeyName(p) === \"_auto\",\n );\n if (!hasAuto) continue;\n const updatedProps = { ...properties, _auto: 1 as number | string };\n endProp.value = buildKeyframeValueNode(updatedProps, undefined);\n }\n }\n\n // Backfill: when the new keyframe introduces properties absent from other\n // keyframes, add default values so GSAP can interpolate them.\n if (backfillDefaults) {\n const newPropKeys = Object.keys(properties);\n const pctProps = filterPercentageProps(kfNode);\n for (const prop of pctProps) {\n const key = propKeyName(prop);\n if (key === pctKey) continue;\n const valObj = prop.value;\n if (!valObj || valObj.type !== \"ObjectExpression\") continue;\n const existingKeys = new Set(\n valObj.properties\n .filter((p: AstNode) => isObjectProperty(p))\n .map((p: AstNode) => propKeyName(p)),\n );\n for (const pk of newPropKeys) {\n if (existingKeys.has(pk)) continue;\n const defaultVal = backfillDefaults[pk];\n if (defaultVal == null) continue;\n const fillProp = parseExpr(`{ ${safeKey(pk)}: ${valueToCode(defaultVal)} }`).properties[0];\n valObj.properties.push(fillProp);\n }\n }\n }\n\n return recast.print(loc.parsed.ast).code;\n}\n\n/**\n * Remove a keyframe at the given percentage. If fewer than 2 keyframes remain\n * after removal, collapse the keyframes object to a flat tween using the\n * remaining keyframe's properties.\n */\nexport function removeKeyframeFromScript(\n script: string,\n animationId: string,\n percentage: number,\n): string {\n // Array-form keyframes (`keyframes: [{x,y}, …]`) have no explicit percentages —\n // GSAP distributes them evenly. The object-form path below can't see them\n // (findKeyframesObjectNode only matches ObjectExpression), so removing from an\n // array-form tween silently no-op'd. Resolve the element by its implicit\n // percentage and splice it; collapse to a flat tween when fewer than two remain.\n const arrLoc = locateAnimationWithFallback(script, animationId);\n // findPropertyNode here returns the property's VALUE node directly.\n const arrVal = arrLoc && findPropertyNode(arrLoc.target.call.varsArg, \"keyframes\");\n if (arrLoc && arrVal?.type === \"ArrayExpression\") {\n const elements: AstNode[] = (arrVal.elements ?? []).filter(\n (e: AstNode | null): e is AstNode => !!e && e.type === \"ObjectExpression\",\n );\n const n = elements.length;\n if (n === 0) return script;\n let matchIdx = -1;\n let bestDist = Number.POSITIVE_INFINITY;\n for (let i = 0; i < n; i++) {\n const pct = n > 1 ? (i / (n - 1)) * 100 : 0;\n const dist = Math.abs(pct - percentage);\n if (dist <= PCT_TOLERANCE && dist < bestDist) {\n matchIdx = i;\n bestDist = dist;\n }\n }\n if (matchIdx === -1) return script;\n const remaining = elements.filter((_, i) => i !== matchIdx);\n if (remaining.length < 2) {\n const sole = remaining[0];\n const record = sole ? objectExpressionToRecord(sole, arrLoc.parsed.scope) : {};\n collapseKeyframesToFlat(arrLoc.target.call.varsArg, record);\n } else {\n const realIdx = arrVal.elements.indexOf(elements[matchIdx]);\n arrVal.elements.splice(realIdx, 1);\n }\n return recast.print(arrLoc.parsed.ast).code;\n }\n\n const ctx = locateKeyframeCtx(script, animationId, percentage);\n if (!ctx) return script;\n const { loc, kfNode } = ctx;\n\n const match = findKeyframePropByPct(kfNode, percentage);\n if (!match) return script;\n const removeIdx = match.idx;\n\n kfNode.properties.splice(removeIdx, 1);\n\n const remainingKfs = filterPercentageProps(kfNode);\n if (remainingKfs.length < 2) {\n const record =\n remainingKfs.length === 1\n ? objectExpressionToRecord(remainingKfs[0]!.value, loc.parsed.scope)\n : {};\n collapseKeyframesToFlat(loc.target.call.varsArg, record);\n }\n\n return recast.print(loc.parsed.ast).code;\n}\n\n/**\n * Replace the properties (and optionally ease) at an existing keyframe percentage.\n */\nexport function updateKeyframeInScript(\n script: string,\n animationId: string,\n percentage: number,\n properties: Record<string, number | string>,\n ease?: string,\n): string {\n // Array-form keyframes (`keyframes: [{x,y}, …]`) have no explicit percentages —\n // GSAP distributes them evenly. The percentage-keyed object path below can't\n // match them (findKeyframesObjectNode only matches ObjectExpression), so dragging\n // a motion-path node on an array-authored tween silently no-op'd. Resolve the\n // element by its implicit percentage and replace it in place. Mirrors the array\n // branch in removeKeyframeFromScript.\n const arrLoc = locateAnimationWithFallback(script, animationId);\n const arrVal = arrLoc && findPropertyNode(arrLoc.target.call.varsArg, \"keyframes\");\n if (arrLoc && arrVal?.type === \"ArrayExpression\") {\n const elements: AstNode[] = (arrVal.elements ?? []).filter(\n (e: AstNode | null): e is AstNode => !!e && e.type === \"ObjectExpression\",\n );\n const n = elements.length;\n if (n === 0) return script;\n let matchIdx = -1;\n let bestDist = Number.POSITIVE_INFINITY;\n for (let i = 0; i < n; i++) {\n const pct = n > 1 ? (i / (n - 1)) * 100 : 0;\n const dist = Math.abs(pct - percentage);\n if (dist <= PCT_TOLERANCE && dist < bestDist) {\n matchIdx = i;\n bestDist = dist;\n }\n }\n if (matchIdx === -1) return script;\n const realIdx = arrVal.elements.indexOf(elements[matchIdx]);\n arrVal.elements[realIdx] = buildKeyframeValueNode(properties, ease);\n return recast.print(arrLoc.parsed.ast).code;\n }\n\n const ctx = locateKeyframeCtx(script, animationId, percentage);\n if (!ctx) return script;\n const { loc, kfNode } = ctx;\n\n const match = findKeyframePropByPct(kfNode, percentage);\n if (!match) return script;\n\n if (Object.keys(properties).length === 0 && ease) {\n // Ease-only update: preserve existing properties, just add/replace ease\n const existing = match.prop.value;\n if (existing?.type === \"ObjectExpression\") {\n const props = (existing.properties ?? []) as AstNode[];\n const easeIdx = props.findIndex(\n (p: AstNode) => isObjectProperty(p) && propKeyName(p) === \"ease\",\n );\n const easeNode = parseExpr(`({ ease: ${JSON.stringify(ease)} })`).properties[0];\n if (easeIdx >= 0) {\n props[easeIdx] = easeNode;\n } else {\n props.push(easeNode);\n }\n return recast.print(loc.parsed.ast).code;\n }\n // Non-object keyframe value (primitive shorthand, e.g. \"50%\": \"0.5\"): there\n // is no property bag to merge the ease into. Rebuilding from empty\n // `properties` would wipe the primitive — leave the keyframe untouched.\n return script;\n }\n match.prop.value = buildKeyframeValueNode(properties, ease);\n return recast.print(loc.parsed.ast).code;\n}\n\n/** Strip editable properties and ease/keyframes keys from a varsArg. */\nfunction stripEditableAndEase(varsArg: AstNode): void {\n // ease is a BUILTIN_VAR_KEY (not editable), so filterEditableKeys won't remove it —\n // drop it explicitly before filtering, along with keyframes.\n if (varsArg?.type !== \"ObjectExpression\") return;\n varsArg.properties = varsArg.properties.filter((p: AstNode) => {\n if (!isObjectProperty(p)) return true;\n const key = propKeyName(p);\n return key !== \"ease\" && key !== \"keyframes\";\n });\n filterEditableKeys(varsArg, () => false);\n}\n\n/** Build and prepend a keyframes property node onto varsArg. */\nfunction insertKeyframesProp(\n varsArg: AstNode,\n fromProps: Record<string, number | string>,\n toProps: Record<string, number | string>,\n easeEach?: string,\n): void {\n const fromEntries = Object.entries(fromProps).map(([k, v]) => `${safeKey(k)}: ${valueToCode(v)}`);\n const toEntries = Object.entries(toProps).map(([k, v]) => `${safeKey(k)}: ${valueToCode(v)}`);\n const easeEntry = easeEach ? `, easeEach: ${JSON.stringify(easeEach)}` : \"\";\n const kfCode = `{ \"0%\": { ${fromEntries.join(\", \")} }, \"100%\": { ${toEntries.join(\", \")} }${easeEntry} }`;\n const kfProp = parseExpr(`{ keyframes: {} }`).properties[0];\n kfProp.value = parseExpr(kfCode);\n if (varsArg?.type === \"ObjectExpression\") varsArg.properties.unshift(kfProp);\n}\n\n/**\n * Convert a flat tween (to/from/fromTo) to percentage-keyframes format.\n * `resolvedFromValues` supplies the \"from\" state for `to()` tweens or\n * the \"to\" state for `from()` tweens (the values the DOM would resolve to).\n */\nexport function convertToKeyframesInScript(\n script: string,\n animationId: string,\n resolvedFromValues?: Record<string, number | string>,\n setDuration = 1,\n): string {\n let loc = locateAnimationWithFallback(script, animationId);\n if (!loc) return script;\n\n const anim = loc.target.animation;\n if (anim.keyframes) return script;\n\n const { fromProps, toProps } = resolveConversionProps(anim, resolvedFromValues);\n const varsArg = loc.target.call.varsArg;\n const originalEase = anim.ease;\n\n stripEditableAndEase(varsArg);\n insertKeyframesProp(varsArg, fromProps, toProps, originalEase || undefined);\n\n if (originalEase) {\n setVarsKey(varsArg, \"ease\", \"none\");\n }\n\n // For from() or fromTo(), convert to to()\n if (anim.method === \"from\" || anim.method === \"fromTo\") {\n loc.target.call.node.callee.property.name = \"to\";\n if (anim.method === \"fromTo\") loc.target.call.node.arguments.splice(1, 1);\n }\n\n // A static `set` becomes an animatable `to`: flip the method, drop the\n // immediateRender hold marker, and give it a real duration so the keyframes\n // span time. This is what makes a static 3D transform keyframeable.\n if (anim.method === \"set\") {\n // A GLOBAL `gsap.set(...)` is off-timeline; flipping only the method would\n // emit `gsap.to(...)`, which fires once at load and is NOT on the paused\n // master timeline (the engine can't seek/render it). Re-root it onto the\n // timeline var and add the position arg (a gsap.set has none) so the\n // converted tween is seekable. A `tl.set` already has the right object.\n const calleeObj = loc.target.call.node.callee.object;\n if (anim.global && calleeObj?.type === \"Identifier\") {\n calleeObj.name = loc.parsed.timelineVar;\n if (loc.target.call.node.arguments.length < 3) {\n loc.target.call.node.arguments.push(parseExpr(\"0\"));\n }\n }\n loc.target.call.node.callee.property.name = \"to\";\n removeVarsKey(varsArg, \"immediateRender\");\n setVarsKey(varsArg, \"duration\", Math.max(0.001, setDuration));\n }\n\n return recast.print(loc.parsed.ast).code;\n}\n\n/**\n * Remove all keyframes from a tween, collapsing to a flat tween with the\n * last keyframe's properties.\n */\nexport function removeAllKeyframesFromScript(script: string, animationId: string): string {\n let loc = locateAnimationWithFallback(script, animationId);\n if (!loc) return script;\n const kfNode = findKeyframesObjectNode(loc.target.call.varsArg);\n if (!kfNode) return script;\n\n const kfEntries = filterPercentageProps(kfNode)\n .map((p: AstNode) => ({ pct: percentageFromKey(propKeyName(p)!), prop: p }))\n .filter((e) => !Number.isNaN(e.pct))\n .sort((a, b) => a.pct - b.pct);\n if (kfEntries.length === 0) return script;\n\n // For to()/set(): collapse to last keyframe (the destination = visible state).\n // For from(): collapse to first keyframe (the starting state).\n const method = loc.target.call.method;\n const collapseEntry = method === \"from\" ? kfEntries[0]! : kfEntries[kfEntries.length - 1]!;\n const record = objectExpressionToRecord(collapseEntry.prop.value, loc.parsed.scope);\n collapseKeyframesToFlat(loc.target.call.varsArg, record);\n\n return recast.print(loc.parsed.ast).code;\n}\n\n/**\n * Replace a dynamic `keyframes: <expr>` with a static percentage-keyframes object.\n * Called when the user first edits a dynamically-generated keyframe in the studio.\n */\nexport function materializeKeyframesInScript(\n script: string,\n animationId: string,\n keyframes: Array<{\n percentage: number;\n properties: Record<string, number | string>;\n ease?: string;\n }>,\n easeEach?: string,\n resolvedSelector?: string,\n): string {\n let loc = locateAnimationWithFallback(script, animationId);\n if (!loc) return script;\n\n const varsArg = loc.target.call.varsArg;\n\n // Replace dynamic selector with resolved static string\n if (resolvedSelector && loc.target.call.node.arguments[0]) {\n loc.target.call.node.arguments[0] = parseExpr(JSON.stringify(resolvedSelector));\n }\n\n const kfObjCode = buildKeyframeObjectCode(sortedKeyframes(keyframes), { easeEach });\n const kfParent = varsArg.properties.find(\n (p: AstNode) => isObjectProperty(p) && propKeyName(p) === \"keyframes\",\n );\n if (kfParent) {\n kfParent.value = parseExpr(kfObjCode);\n } else {\n const kfProp = parseExpr(`{ keyframes: ${kfObjCode} }`).properties[0];\n varsArg.properties.unshift(kfProp);\n }\n\n removeVarsKey(varsArg, \"easeEach\");\n\n return recast.print(loc.parsed.ast).code;\n}\n\n// ── Arc Path (motionPath) AST Mutations ──────────────────────────────────\n\nfunction numericXY(props: Record<string, number | string>): { x: number; y: number } | null {\n const x = props.x;\n const y = props.y;\n return typeof x === \"number\" && typeof y === \"number\" ? { x, y } : null;\n}\n\nfunction extractArcWaypoints(anim: GsapAnimation): Array<{ x: number; y: number }> {\n const kfs = anim.keyframes?.keyframes ?? [];\n const waypoints = kfs.map((kf) => numericXY(kf.properties)).filter((p) => p !== null);\n if (waypoints.length >= 2) return waypoints;\n const px = anim.properties.x;\n const py = anim.properties.y;\n if (typeof px !== \"number\" && typeof py !== \"number\") return waypoints;\n return [\n { x: 0, y: 0 },\n { x: typeof px === \"number\" ? px : 0, y: typeof py === \"number\" ? py : 0 },\n ];\n}\n\nfunction buildMotionPathObjectCode(config: {\n waypoints: Array<{ x: number; y: number }>;\n segments: ArcPathSegment[];\n autoRotate: boolean | number;\n}): string {\n const { waypoints, segments, autoRotate } = config;\n const hasExplicitControlPoints = segments.some((s) => s.cp1 && s.cp2);\n // The simple `path` array supports only one scalar curviness for the whole\n // path, so per-segment curviness must use the cubic form (curviness baked into\n // each segment's control points). Without this, the simple branch serializes\n // only segments[0].curviness and silently drops every other segment's curve.\n const curvinessVaries = segments.some(\n (s) => (s.curviness ?? 1) !== (segments[0]?.curviness ?? 1),\n );\n\n let pathEntries: string[];\n if ((hasExplicitControlPoints || curvinessVaries) && waypoints.length >= 2) {\n // type: \"cubic\" — interleave control points: [anchor, cp1, cp2, anchor, ...]\n pathEntries = [`{x: ${waypoints[0]!.x}, y: ${waypoints[0]!.y}}`];\n for (let i = 0; i < segments.length; i++) {\n const seg = segments[i]!;\n const nextWp = waypoints[i + 1]!;\n if (seg.cp1 && seg.cp2) {\n pathEntries.push(`{x: ${seg.cp1.x}, y: ${seg.cp1.y}}`);\n pathEntries.push(`{x: ${seg.cp2.x}, y: ${seg.cp2.y}}`);\n } else {\n // Auto-generate simple midpoint control points from curviness\n const wp = waypoints[i]!;\n const dx = nextWp.x - wp.x;\n const dy = nextWp.y - wp.y;\n const c = seg.curviness ?? 1;\n pathEntries.push(\n `{x: ${wp.x + dx * 0.33}, y: ${wp.y + dy * 0.33 - c * Math.abs(dx) * 0.25}}`,\n );\n pathEntries.push(\n `{x: ${wp.x + dx * 0.66}, y: ${wp.y + dy * 0.66 - c * Math.abs(dx) * 0.25}}`,\n );\n }\n pathEntries.push(`{x: ${nextWp.x}, y: ${nextWp.y}}`);\n }\n const pathStr = pathEntries.join(\", \");\n const parts = [`path: [${pathStr}]`, `type: \"cubic\"`];\n if (autoRotate === true) parts.push(\"autoRotate: true\");\n else if (typeof autoRotate === \"number\") parts.push(`autoRotate: ${autoRotate}`);\n return `{ ${parts.join(\", \")} }`;\n }\n\n // Simple waypoint array with curviness\n pathEntries = waypoints.map((wp) => `{x: ${wp.x}, y: ${wp.y}}`);\n const curviness = segments[0]?.curviness ?? 1;\n const parts = [`path: [${pathEntries.join(\", \")}]`];\n if (curviness !== 1) parts.push(`curviness: ${curviness}`);\n if (autoRotate === true) parts.push(\"autoRotate: true\");\n else if (typeof autoRotate === \"number\") parts.push(`autoRotate: ${autoRotate}`);\n return `{ ${parts.join(\", \")} }`;\n}\n\nexport function setArcPathInScript(\n script: string,\n animationId: string,\n config: ArcPathConfig,\n): string {\n const loc = locateAnimation(script, animationId);\n if (!loc) return script;\n\n const varsArg = loc.target.call.varsArg;\n const anim = loc.target.animation;\n\n if (!config.enabled) {\n // Disable arc: restore x/y from motionPath's last waypoint, then remove motionPath\n const motionPathProp = varsArg.properties.find(\n (p: AstNode) => isObjectProperty(p) && propKeyName(p) === \"motionPath\",\n );\n if (motionPathProp) {\n const mpVal = motionPathProp.value;\n let pathArr: AstNode[] | undefined;\n if (mpVal?.type === \"ObjectExpression\") {\n const pathProp = mpVal.properties.find(\n (p: AstNode) => isObjectProperty(p) && propKeyName(p) === \"path\",\n );\n if (pathProp?.value?.type === \"ArrayExpression\") pathArr = pathProp.value.elements;\n }\n if (pathArr && pathArr.length > 0) {\n const last = pathArr[pathArr.length - 1];\n if (last?.type === \"ObjectExpression\") {\n for (const p of last.properties) {\n const k = propKeyName(p);\n if (k === \"x\" || k === \"y\") {\n const v = p.value?.value;\n if (typeof v === \"number\") setVarsKey(varsArg, k, v);\n }\n }\n }\n }\n }\n removeVarsKey(varsArg, \"motionPath\");\n return recast.print(loc.parsed.ast).code;\n }\n\n const waypoints = extractArcWaypoints(anim);\n if (waypoints.length < 2) return script;\n\n // Build segments — use provided segments or create defaults\n const segments: ArcPathSegment[] =\n config.segments.length === waypoints.length - 1\n ? config.segments\n : Array.from({ length: waypoints.length - 1 }, () => ({ curviness: 1 }));\n\n const motionPathCode = buildMotionPathObjectCode({\n waypoints,\n segments,\n autoRotate: config.autoRotate,\n });\n\n // Set motionPath on the vars\n const motionPathNode = parseExpr(motionPathCode);\n const existingProp = varsArg.properties.find(\n (p: AstNode) => isObjectProperty(p) && propKeyName(p) === \"motionPath\",\n );\n if (existingProp) {\n existingProp.value = motionPathNode;\n } else {\n const prop = parseExpr(`{ motionPath: ${motionPathCode} }`).properties[0];\n varsArg.properties.push(prop);\n }\n\n // Strip x/y from keyframes (they're now in motionPath)\n const kfNode = findKeyframesObjectNode(varsArg);\n if (kfNode) {\n for (const pctProp of filterPercentageProps(kfNode)) {\n if (pctProp.value?.type === \"ObjectExpression\") {\n pctProp.value.properties = pctProp.value.properties.filter((p: AstNode) => {\n const k = propKeyName(p);\n return k !== \"x\" && k !== \"y\";\n });\n }\n }\n }\n\n // Strip flat x/y from vars (they're now in motionPath)\n removeVarsKey(varsArg, \"x\");\n removeVarsKey(varsArg, \"y\");\n\n return recast.print(loc.parsed.ast).code;\n}\n\nexport function updateArcSegmentInScript(\n script: string,\n animationId: string,\n segmentIndex: number,\n update: Partial<ArcPathSegment>,\n): string {\n const loc = locateAnimation(script, animationId);\n if (!loc) return script;\n\n const anim = loc.target.animation;\n if (!anim.arcPath?.enabled) return script;\n\n const segments = [...anim.arcPath.segments];\n if (segmentIndex < 0 || segmentIndex >= segments.length) return script;\n\n segments[segmentIndex] = { ...segments[segmentIndex]!, ...update };\n\n const waypoints = extractArcWaypoints(anim);\n if (waypoints.length < 2) return script;\n\n const motionPathCode = buildMotionPathObjectCode({\n waypoints,\n segments,\n autoRotate: anim.arcPath.autoRotate,\n });\n\n const varsArg = loc.target.call.varsArg;\n const existingProp = varsArg.properties.find(\n (p: AstNode) => isObjectProperty(p) && propKeyName(p) === \"motionPath\",\n );\n if (existingProp) {\n existingProp.value = parseExpr(motionPathCode);\n }\n\n return recast.print(loc.parsed.ast).code;\n}\n\n/**\n * Move a single motionPath waypoint (anchor) to a new position. The waypoint\n * list is normalized to anchors for both straight and cubic paths, so\n * `pointIndex` matches the node order the studio overlay renders; cubic control\n * points are preserved. No-op when the animation/arc is missing or the index is\n * out of range.\n */\nexport function updateMotionPathPointInScript(\n script: string,\n animationId: string,\n pointIndex: number,\n point: { x: number; y: number },\n): string {\n const loc = locateAnimation(script, animationId);\n if (!loc) return script;\n\n const anim = loc.target.animation;\n if (!anim.arcPath?.enabled) return script;\n\n const waypoints = extractArcWaypoints(anim);\n if (pointIndex < 0 || pointIndex >= waypoints.length || waypoints.length < 2) return script;\n\n const nextWaypoints = waypoints.map((wp, i) =>\n i === pointIndex ? { x: point.x, y: point.y } : wp,\n );\n\n const motionPathCode = buildMotionPathObjectCode({\n waypoints: nextWaypoints,\n segments: anim.arcPath.segments,\n autoRotate: anim.arcPath.autoRotate,\n });\n\n const varsArg = loc.target.call.varsArg;\n const existingProp = varsArg.properties.find(\n (p: AstNode) => isObjectProperty(p) && propKeyName(p) === \"motionPath\",\n );\n if (existingProp) {\n existingProp.value = parseExpr(motionPathCode);\n }\n\n return recast.print(loc.parsed.ast).code;\n}\n\n/** True when any segment carries explicit cubic control points. Add/remove are\n * restricted to curviness (non-cubic) paths — synthesizing control points for\n * an inserted cubic anchor is out of scope. */\nfunction hasCubicSegments(segments: ArcPathSegment[]): boolean {\n return segments.some((s) => s.cp1 != null || s.cp2 != null);\n}\n\nfunction writeMotionPathValue(\n loc: NonNullable<ReturnType<typeof locateAnimation>>,\n waypoints: Array<{ x: number; y: number }>,\n segments: ArcPathSegment[],\n autoRotate: boolean | number,\n): string {\n const motionPathCode = buildMotionPathObjectCode({ waypoints, segments, autoRotate });\n const varsArg = loc.target.call.varsArg;\n const existingProp = varsArg.properties.find(\n (p: AstNode) => isObjectProperty(p) && propKeyName(p) === \"motionPath\",\n );\n if (existingProp) existingProp.value = parseExpr(motionPathCode);\n return recast.print(loc.parsed.ast).code;\n}\n\n/**\n * Insert a waypoint at `index` (between existing anchors), splitting the segment\n * it lands on so the new neighbor inherits its curviness. Non-cubic paths only.\n * No-op for missing animation/arc, out-of-range index, or cubic paths.\n */\nexport function addMotionPathPointInScript(\n script: string,\n animationId: string,\n index: number,\n point: { x: number; y: number },\n): string {\n const loc = locateAnimation(script, animationId);\n if (!loc) return script;\n const anim = loc.target.animation;\n if (!anim.arcPath?.enabled || hasCubicSegments(anim.arcPath.segments)) return script;\n\n const waypoints = extractArcWaypoints(anim);\n // Insert strictly between two anchors: index 1..length-1.\n if (index < 1 || index > waypoints.length - 1) return script;\n\n const segments = [...anim.arcPath.segments];\n waypoints.splice(index, 0, { x: point.x, y: point.y });\n const splitCurviness = segments[index - 1]?.curviness ?? 1;\n segments.splice(index - 1, 0, { curviness: splitCurviness });\n\n return writeMotionPathValue(loc, waypoints, segments, anim.arcPath.autoRotate);\n}\n\n/**\n * Remove the waypoint at `index`. Refuses to drop below two anchors (a path\n * can't have fewer). Non-cubic paths only. No-op for missing animation/arc,\n * out-of-range index, cubic paths, or a 2-point path.\n */\nexport function removeMotionPathPointInScript(\n script: string,\n animationId: string,\n index: number,\n): string {\n const loc = locateAnimation(script, animationId);\n if (!loc) return script;\n const anim = loc.target.animation;\n if (!anim.arcPath?.enabled || hasCubicSegments(anim.arcPath.segments)) return script;\n\n const waypoints = extractArcWaypoints(anim);\n if (waypoints.length <= 2 || index < 0 || index >= waypoints.length) return script;\n\n const segments = [...anim.arcPath.segments];\n waypoints.splice(index, 1);\n // Drop the segment on the side that still exists (last anchor → preceding segment).\n segments.splice(Math.min(index, segments.length - 1), 1);\n\n return writeMotionPathValue(loc, waypoints, segments, anim.arcPath.autoRotate);\n}\n\n/**\n * Author a fresh 2-anchor motionPath tween on a target element: a straight line\n * from the element's home (0,0) to `point`, gentle ease, ready for waypoint\n * editing. Mirrors `addAnimationWithKeyframesToScript`.\n */\nexport function addMotionPathToScript(\n script: string,\n targetSelector: string,\n position: number,\n duration: number,\n point: { x: number; y: number },\n ease = \"power1.inOut\",\n): { script: string; id: string | null } {\n // `id: null` on the failure paths is a deliberate sentinel: callers must\n // null-check before chaining (e.g. locating the new tween). An empty string\n // would silently flow into selector/locate calls and match nothing.\n let parsed: ParsedGsapAst;\n try {\n parsed = parseGsapAst(script);\n } catch (e) {\n console.warn(\"[gsap-parser] addMotionPathToScript parse failed:\", e);\n return { script, id: null };\n }\n if (parsed.located.length === 0 && parsed.detection.timelineVar === null) {\n return { script, id: null };\n }\n\n const motionPathCode = buildMotionPathObjectCode({\n waypoints: [\n { x: 0, y: 0 },\n { x: point.x, y: point.y },\n ],\n segments: [{ curviness: 1 }],\n autoRotate: false,\n });\n const selector = JSON.stringify(targetSelector);\n const varEntries = [\n `motionPath: ${motionPathCode}`,\n `duration: ${valueToCode(duration)}`,\n `ease: ${JSON.stringify(ease)}`,\n ];\n const stmtCode = `${parsed.timelineVar}.to(${selector}, { ${varEntries.join(\", \")} }, ${valueToCode(position)});`;\n const newStatement = parseScript(stmtCode).program.body[0];\n insertAfterAnchor(parsed, newStatement);\n\n const result = recast.print(parsed.ast).code;\n const reParsed = parseGsapAst(result);\n const newId = reParsed.located[reParsed.located.length - 1]?.id ?? null;\n return { script: result, id: newId };\n}\n\nexport function removeArcPathFromScript(script: string, animationId: string): string {\n return setArcPathInScript(script, animationId, {\n enabled: false,\n autoRotate: false,\n segments: [],\n });\n}\n\n// ── Split Into Property Groups ────────────────────────────────────────────\n\n/**\n * Split a multi-group tween into separate per-group tweens. Each resulting\n * tween contains only properties belonging to one property group (position,\n * scale, rotation, visual, etc.). `transformOrigin` stays with the group that\n * has the most properties. If the tween already belongs to a single group,\n * returns the script unchanged with the original ID.\n */\n// fallow-ignore-next-line complexity\nexport function splitIntoPropertyGroups(\n script: string,\n animationId: string,\n): { script: string; ids: string[] } {\n let loc = locateAnimationWithFallback(script, animationId);\n if (!loc) return { script, ids: [animationId] };\n\n const anim = loc.target.animation;\n\n // Collect the properties to partition. For keyframed tweens, gather the\n // union of all properties across all keyframes. For flat tweens, use the\n // tween's own properties map.\n const allPropKeys = new Set<string>();\n if (anim.keyframes) {\n for (const kf of anim.keyframes.keyframes) {\n for (const k of Object.keys(kf.properties)) allPropKeys.add(k);\n }\n } else {\n for (const k of Object.keys(anim.properties)) allPropKeys.add(k);\n }\n\n // Partition properties into groups (excluding transformOrigin — handled below).\n const groupProps = new Map<PropertyGroupName, string[]>();\n for (const key of allPropKeys) {\n if (key === \"transformOrigin\") continue;\n const group = classifyPropertyGroup(key);\n let arr = groupProps.get(group);\n if (!arr) {\n arr = [];\n groupProps.set(group, arr);\n }\n arr.push(key);\n }\n\n // Only one group (or zero) — no split needed.\n if (groupProps.size <= 1) return { script, ids: [anim.id] };\n\n // Assign transformOrigin to the group with the most properties.\n if (allPropKeys.has(\"transformOrigin\")) {\n let largestGroup: PropertyGroupName | undefined;\n let largestCount = 0;\n for (const [group, props] of groupProps) {\n if (props.length > largestCount) {\n largestCount = props.length;\n largestGroup = group;\n }\n }\n if (largestGroup) {\n groupProps.get(largestGroup)!.push(\"transformOrigin\");\n }\n }\n\n // Build per-group tweens and insert them, then remove the original.\n let result = script;\n\n // Remove the original tween first.\n result = removeAnimationFromScript(result, anim.id);\n\n // Insert one tween per group. Iteration order of the Map follows insertion\n // order, which mirrors the order properties were encountered.\n for (const [, props] of groupProps) {\n const propSet = new Set(props);\n\n if (anim.keyframes) {\n // Build keyframes containing only this group's properties per keyframe.\n const groupKeyframes: Array<{\n percentage: number;\n properties: Record<string, number | string>;\n ease?: string;\n auto?: boolean;\n }> = [];\n\n for (const kf of anim.keyframes.keyframes) {\n const filtered: Record<string, number | string> = {};\n for (const [k, v] of Object.entries(kf.properties)) {\n if (propSet.has(k)) filtered[k] = v;\n }\n // Skip keyframes where this group has zero properties.\n if (Object.keys(filtered).length === 0) continue;\n groupKeyframes.push({\n percentage: kf.percentage,\n properties: filtered,\n ...(kf.ease ? { ease: kf.ease } : {}),\n });\n }\n\n if (groupKeyframes.length === 0) continue;\n\n const addResult = addAnimationWithKeyframesToScript(\n result,\n anim.targetSelector,\n typeof anim.position === \"number\" ? anim.position : 0,\n anim.duration ?? 0.5,\n groupKeyframes,\n anim.keyframes.easeEach ?? anim.ease,\n );\n result = addResult.script;\n } else {\n // Flat tween — filter properties to this group.\n const groupProperties: Record<string, number | string> = {};\n for (const [k, v] of Object.entries(anim.properties)) {\n if (propSet.has(k)) groupProperties[k] = v;\n }\n if (Object.keys(groupProperties).length === 0) continue;\n\n let fromProperties: Record<string, number | string> | undefined;\n if (anim.method === \"fromTo\" && anim.fromProperties) {\n fromProperties = {};\n for (const [k, v] of Object.entries(anim.fromProperties)) {\n if (propSet.has(k)) fromProperties[k] = v;\n }\n }\n\n const addResult = addAnimationToScript(result, {\n targetSelector: anim.targetSelector,\n method: anim.method,\n position: anim.position,\n duration: anim.duration,\n ease: anim.ease,\n properties: groupProperties,\n fromProperties,\n extras: anim.extras,\n });\n result = addResult.script;\n }\n }\n\n // Re-parse to collect the new IDs.\n const reParsed = parseGsapAst(result);\n const newIds = reParsed.located\n .filter((l) => l.animation.targetSelector === anim.targetSelector)\n .map((l) => l.id);\n\n return { script: result, ids: newIds };\n}\n\n/**\n * Replace a dynamic loop that generates multiple tween calls with individual\n * static `tl.to()` calls — one per element. Finds the loop containing the\n * animation and replaces the entire loop body with unrolled static calls.\n */\nexport function unrollDynamicAnimations(\n script: string,\n animationId: string,\n elements: Array<{\n selector: string;\n keyframes: Array<{ percentage: number; properties: Record<string, number | string> }>;\n easeEach?: string;\n }>,\n): string {\n const loc = locateAnimation(script, animationId);\n if (!loc) return script;\n\n const varsArg = loc.target.call.varsArg;\n\n // Read duration and ease from the original tween vars\n const durationVal = extractLiteralValue(findPropertyNode(varsArg, \"duration\"), loc.parsed.scope);\n const easeVal = extractLiteralValue(findPropertyNode(varsArg, \"ease\"), loc.parsed.scope);\n const duration = typeof durationVal === \"number\" ? durationVal : 8;\n const ease = typeof easeVal === \"string\" ? easeVal : \"none\";\n const posArg = loc.target.call.positionArg;\n const position = posArg ? extractLiteralValue(posArg, loc.parsed.scope) : 0;\n const posCode =\n typeof position === \"number\"\n ? String(position)\n : typeof position === \"string\"\n ? JSON.stringify(position)\n : \"0\";\n\n // Find the enclosing loop (for/forEach) by walking up the AST path\n let loopNode: AstNode | null = null;\n let current = loc.target.call.path;\n while (current) {\n const node = current.node ?? current.value;\n if (\n node?.type === \"ForStatement\" ||\n node?.type === \"ForInStatement\" ||\n node?.type === \"ForOfStatement\" ||\n node?.type === \"WhileStatement\"\n ) {\n loopNode = node;\n break;\n }\n if (\n node?.type === \"ExpressionStatement\" &&\n node.expression?.type === \"CallExpression\" &&\n node.expression.callee?.property?.name === \"forEach\"\n ) {\n loopNode = node;\n break;\n }\n current = current.parent ?? current.parentPath;\n }\n\n // Build replacement code: individual tl.to() calls for each element\n const calls: string[] = [];\n for (const el of elements) {\n const kfCode = buildKeyframeObjectCode(sortedKeyframes(el.keyframes), {\n easeEach: el.easeEach,\n });\n calls.push(\n `${loc.parsed.timelineVar}.to(${JSON.stringify(el.selector)}, { keyframes: ${kfCode}, duration: ${duration}, ease: ${JSON.stringify(ease)} }, ${posCode});`,\n );\n }\n\n const replacement = calls.join(\"\\n \");\n\n if (loopNode) {\n // Replace the entire loop with the unrolled calls\n const start = loopNode.start ?? loopNode.range?.[0];\n const end = loopNode.end ?? loopNode.range?.[1];\n if (typeof start === \"number\" && typeof end === \"number\") {\n return script.slice(0, start) + replacement + script.slice(end);\n }\n }\n\n // Fallback: replace just the tween call's enclosing expression statement\n const stmtNode = loc.target.call.path?.parent?.node ?? loc.target.call.path?.parentPath?.node;\n if (stmtNode?.type === \"ExpressionStatement\") {\n const start = stmtNode.start ?? stmtNode.range?.[0];\n const end = stmtNode.end ?? stmtNode.range?.[1];\n if (typeof start === \"number\" && typeof end === \"number\") {\n return script.slice(0, start) + replacement + script.slice(end);\n }\n }\n\n return script;\n}\n","/**\n * Damped harmonic oscillator solver for GSAP CustomEase spring curves.\n *\n * Generates an SVG path data string compatible with `CustomEase.create(id, data)`.\n * The solver supports underdamped (bouncy), critically damped, and overdamped\n * spring configurations. Output is normalized to x ∈ [0,1] with y starting at 0\n * and settling to 1.\n */\n\nexport interface SpringPreset {\n name: string;\n label: string;\n mass: number;\n stiffness: number;\n damping: number;\n}\n\nexport const SPRING_PRESETS: SpringPreset[] = [\n { name: \"spring-gentle\", label: \"Gentle\", mass: 1, stiffness: 100, damping: 15 },\n { name: \"spring-bouncy\", label: \"Bouncy\", mass: 1, stiffness: 180, damping: 12 },\n { name: \"spring-stiff\", label: \"Stiff\", mass: 1, stiffness: 300, damping: 20 },\n { name: \"spring-wobbly\", label: \"Wobbly\", mass: 1, stiffness: 120, damping: 8 },\n { name: \"spring-heavy\", label: \"Heavy\", mass: 3, stiffness: 200, damping: 20 },\n];\n\n/**\n * Solve a damped harmonic oscillator and return a GSAP CustomEase data string.\n *\n * The output is an SVG path (`M0,0 L... L...`) that CustomEase.create() accepts.\n * The curve is normalized so x spans [0,1] and the spring settles at y = 1.\n *\n * @param mass - Spring mass (> 0)\n * @param stiffness - Spring stiffness constant (> 0)\n * @param damping - Damping coefficient (> 0)\n * @param steps - Number of sample points (default 120)\n */\nexport function generateSpringEaseData(\n mass: number,\n stiffness: number,\n damping: number,\n steps = 120,\n): string {\n const w0 = Math.sqrt(stiffness / mass);\n const zeta = damping / (2 * Math.sqrt(stiffness * mass));\n\n // Determine simulation duration: time until oscillation settles within threshold of 1.0.\n // Underdamped: ~5 time constants. Critically/overdamped: characteristic decay time.\n let settleDuration: number;\n if (zeta < 1) {\n settleDuration = Math.min(5 / (zeta * w0), 10);\n } else {\n const decayRate = zeta * w0 - w0 * Math.sqrt(zeta * zeta - 1);\n settleDuration = Math.min(4 / Math.max(decayRate, 0.01), 10);\n }\n const simDuration = Math.max(settleDuration, 1);\n\n const segments: string[] = [\"M0,0\"];\n\n for (let i = 1; i <= steps; i++) {\n const t = i / steps;\n const simT = t * simDuration;\n let value: number;\n\n if (zeta < 1) {\n // Underdamped — oscillates before settling\n const wd = w0 * Math.sqrt(1 - zeta * zeta);\n value =\n 1 -\n Math.exp(-zeta * w0 * simT) *\n (Math.cos(wd * simT) + ((zeta * w0) / wd) * Math.sin(wd * simT));\n } else if (zeta === 1) {\n // Critically damped — fastest approach without oscillation\n value = 1 - (1 + w0 * simT) * Math.exp(-w0 * simT);\n } else {\n // Overdamped — slow exponential approach\n const s1 = -w0 * (zeta - Math.sqrt(zeta * zeta - 1));\n const s2 = -w0 * (zeta + Math.sqrt(zeta * zeta - 1));\n value = 1 + (s1 * Math.exp(s2 * simT) - s2 * Math.exp(s1 * simT)) / (s2 - s1);\n }\n\n segments.push(`${t.toFixed(4)},${value.toFixed(4)}`);\n }\n\n // Force exact endpoint\n segments[segments.length - 1] = \"1,1\";\n\n return `${segments[0]} L${segments.slice(1).join(\" \")}`;\n}\n","// fallow-ignore-file code-duplication\n/**\n * Browser-safe GSAP read path — acorn + acorn-walk.\n *\n * T6b oracle: produces identical ParsedGsap output to gsapParser.ts (recast).\n * Replaces recast as the shared implementation once T6d passes.\n *\n * Write path (T6c) will add magic-string splice once read parity is confirmed.\n * No Node globals, no fs, no require — safe to bundle for browser use.\n */\nimport * as acorn from \"acorn\";\nimport * as acornWalk from \"acorn-walk\";\nimport type {\n ArcPathConfig,\n GsapAnimation,\n GsapKeyframesData,\n GsapMethod,\n GsapPercentageKeyframe,\n ParsedGsap,\n} from \"./gsapSerialize.js\";\nimport { classifyTweenPropertyGroup } from \"./gsapConstants.js\";\nimport { buildArcPath } from \"./gsapSerialize.js\";\nimport { inlineComputedTimelines, readProvenance } from \"./gsapInline.js\";\n\n// Browser-safe re-exports so studio code can build arc config without importing\n// the recast parser (this acorn module is the browser-safe gsap subpath).\nexport { buildArcPath, editabilityForProvenance } from \"./gsapSerialize.js\";\nexport type {\n ArcPathConfig,\n ArcPathSegment,\n MotionPathShape,\n GsapProvenance,\n GsapProvenanceKind,\n KeyframeEditability,\n} from \"./gsapSerialize.js\";\n\nconst GSAP_METHODS = new Set<string>([\"set\", \"to\", \"from\", \"fromTo\"]);\nconst QUERY_METHODS = new Set([\"querySelector\", \"querySelectorAll\"]);\nconst ITERATION_METHODS = new Set([\"forEach\", \"map\"]);\nconst SCOPE_NODE_TYPES = new Set([\n \"Program\",\n \"FunctionDeclaration\",\n \"FunctionExpression\",\n \"ArrowFunctionExpression\",\n]);\n\n// ── Types ────────────────────────────────────────────────────────────────────\n\ntype ScopeBindings = ReadonlyMap<string, number | string | boolean>;\n/** Per-scope element bindings: scopeNode → (variable name → selector). */\ntype TargetBindings = Map<any, Map<string, string>>;\n\n// ── Value resolution ─────────────────────────────────────────────────────────\n\n// fallow-ignore-next-line complexity\nfunction resolveNode(\n node: any,\n scope: ReadonlyMap<string, number | string | boolean>,\n): number | string | boolean | undefined {\n if (!node) return undefined;\n if (node.type === \"NumericLiteral\" || (node.type === \"Literal\" && typeof node.value === \"number\"))\n return node.value;\n if (node.type === \"StringLiteral\" || (node.type === \"Literal\" && typeof node.value === \"string\"))\n return node.value;\n if (\n node.type === \"BooleanLiteral\" ||\n (node.type === \"Literal\" && typeof node.value === \"boolean\")\n )\n return node.value;\n if (node.type === \"UnaryExpression\" && node.operator === \"-\" && node.argument) {\n const val = resolveNode(node.argument, scope);\n return typeof val === \"number\" ? -val : undefined;\n }\n if (node.type === \"BinaryExpression\") {\n const left = resolveNode(node.left, scope);\n const right = resolveNode(node.right, scope);\n if (typeof left === \"number\" && typeof right === \"number\") {\n switch (node.operator) {\n case \"+\":\n return left + right;\n case \"-\":\n return left - right;\n case \"*\":\n return left * right;\n case \"/\":\n return right !== 0 ? left / right : undefined;\n }\n }\n if (typeof left === \"string\" && node.operator === \"+\") return left + String(right ?? \"\");\n if (typeof right === \"string\" && node.operator === \"+\") return String(left ?? \"\") + right;\n }\n if (node.type === \"Identifier\" && scope.has(node.name)) {\n return scope.get(node.name);\n }\n if (node.type === \"TemplateLiteral\" && node.expressions?.length === 0) {\n return node.quasis?.[0]?.value?.cooked ?? undefined;\n }\n return undefined;\n}\n\nfunction extractLiteralValue(node: any, scope: ScopeBindings): unknown {\n return resolveNode(node, scope);\n}\n\n// ── DOM selector resolution ───────────────────────────────────────────────────\n\n// fallow-ignore-next-line complexity\nfunction selectorFromQueryCall(node: any, scope: ScopeBindings): string | null {\n if (node?.type !== \"CallExpression\") return null;\n const callee = node.callee;\n if (callee?.type !== \"MemberExpression\" || callee.property?.type !== \"Identifier\") return null;\n const method = callee.property.name;\n const argValue = resolveNode(node.arguments?.[0], scope);\n if (typeof argValue !== \"string\" || argValue.length === 0) return null;\n if (QUERY_METHODS.has(method) || method === \"toArray\") return argValue;\n if (method === \"getElementById\") return `#${argValue}`;\n return null;\n}\n\n// ── Ancestor-based scope helpers (replaces NodePath walking) ──────────────────\n\n/**\n * Return the nearest ancestor node whose type is in SCOPE_NODE_TYPES.\n * `ancestors` is the acorn-walk ancestor array (root→current, current is last).\n */\nfunction enclosingScopeNodeFromAncestors(ancestors: any[]): any {\n for (let i = ancestors.length - 2; i >= 0; i--) {\n const node = ancestors[i];\n if (node && SCOPE_NODE_TYPES.has(node.type)) return node;\n }\n return null;\n}\n\n/** Scope chain innermost-first, derived from the acorn-walk ancestors array. */\nfunction scopeChainFromAncestors(ancestors: any[]): any[] {\n const chain: any[] = [];\n for (let i = ancestors.length - 1; i >= 0; i--) {\n const node = ancestors[i];\n if (node && SCOPE_NODE_TYPES.has(node.type)) chain.push(node);\n }\n return chain;\n}\n\n// ── Target bindings ───────────────────────────────────────────────────────────\n\nfunction addBinding(\n bindings: TargetBindings,\n scopeNode: any,\n name: string,\n selector: string,\n): void {\n let scoped = bindings.get(scopeNode);\n if (!scoped) {\n scoped = new Map();\n bindings.set(scopeNode, scoped);\n }\n if (!scoped.has(name)) scoped.set(name, selector);\n}\n\nfunction lookupBindingFromAncestors(\n name: string,\n ancestors: any[],\n bindings: TargetBindings,\n): string | null {\n for (const scopeNode of scopeChainFromAncestors(ancestors)) {\n const selector = bindings.get(scopeNode)?.get(name);\n if (selector !== undefined) return selector;\n }\n // Program-scope bindings are stored under null (enclosingScopeNodeFromAncestors\n // returns null when no function wrapper exists — the common case in HF scripts).\n return bindings.get(null)?.get(name) ?? null;\n}\n\nfunction isFunctionNode(node: any): boolean {\n return (\n node?.type === \"ArrowFunctionExpression\" ||\n node?.type === \"FunctionExpression\" ||\n node?.type === \"FunctionDeclaration\"\n );\n}\n\nfunction resolveCollectionSelector(\n node: any,\n ancestors: any[],\n scope: ScopeBindings,\n bindings: TargetBindings,\n): string | null {\n if (node?.type === \"Identifier\")\n return lookupBindingFromAncestors(node.name, ancestors, bindings);\n if (node?.type === \"CallExpression\") return selectorFromQueryCall(node, scope);\n return null;\n}\n\nfunction collectScopeBindings(ast: any): ScopeBindings {\n const bindings = new Map<string, number | string | boolean>();\n acornWalk.simple(ast, {\n VariableDeclarator(node: any) {\n const name = node.id?.name;\n const init = node.init;\n if (name && init) {\n const val = resolveNode(init, bindings);\n if (val !== undefined) bindings.set(name, val);\n }\n },\n });\n return bindings;\n}\n\n/**\n * Build a lexically-scoped index of element variables → selector.\n * Pass 1: direct DOM-lookup assignments.\n * Pass 2: forEach/map callback params whose collection's selector is known.\n */\nfunction collectTargetBindings(ast: any, scope: ScopeBindings): TargetBindings {\n const bindings: TargetBindings = new Map();\n\n acornWalk.ancestor(ast, {\n VariableDeclarator(node: any, _: unknown, ancestors: any[]) {\n const name = node.id?.name;\n const selector = selectorFromQueryCall(node.init, scope);\n if (name && selector !== null) {\n addBinding(bindings, enclosingScopeNodeFromAncestors(ancestors), name, selector);\n }\n },\n AssignmentExpression(node: any, _: unknown, ancestors: any[]) {\n const left = node.left;\n const selector = selectorFromQueryCall(node.right, scope);\n if (left?.type === \"Identifier\" && selector !== null) {\n addBinding(bindings, enclosingScopeNodeFromAncestors(ancestors), left.name, selector);\n }\n },\n } as any);\n\n // Pass 2: forEach/map callback params take the collection's selector.\n acornWalk.ancestor(ast, {\n // fallow-ignore-next-line complexity\n CallExpression(node: any, _: unknown, ancestors: any[]) {\n const callee = node.callee;\n if (\n callee?.type === \"MemberExpression\" &&\n callee.property?.type === \"Identifier\" &&\n ITERATION_METHODS.has(callee.property.name)\n ) {\n const collectionSelector = resolveCollectionSelector(\n callee.object,\n ancestors,\n scope,\n bindings,\n );\n const fn = node.arguments?.[0];\n const param = fn?.params?.[0];\n if (collectionSelector && param?.type === \"Identifier\" && isFunctionNode(fn)) {\n addBinding(bindings, fn, param.name, collectionSelector);\n }\n }\n },\n } as any);\n\n return bindings;\n}\n\n// fallow-ignore-next-line complexity\nfunction resolveTargetSelector(\n node: any,\n ancestors: any[],\n scope: ScopeBindings,\n bindings: TargetBindings,\n): string | null {\n if (!node) return null;\n if (node.type === \"StringLiteral\" || node.type === \"Literal\") {\n return typeof node.value === \"string\" ? node.value : null;\n }\n if (node.type === \"Identifier\") {\n return lookupBindingFromAncestors(node.name, ancestors, bindings);\n }\n if (node.type === \"CallExpression\") {\n return selectorFromQueryCall(node, scope);\n }\n if (node.type === \"ArrayExpression\") {\n const parts = node.elements\n .map((el: any) => resolveTargetSelector(el, ancestors, scope, bindings))\n .filter((s: string | null): s is string => typeof s === \"string\" && s.length > 0);\n return parts.length > 0 ? parts.join(\", \") : null;\n }\n if (node.type === \"MemberExpression\" && node.object?.type === \"Identifier\") {\n return lookupBindingFromAncestors(node.object.name, ancestors, bindings);\n }\n return null;\n}\n\n// ── ObjectExpression utilities ────────────────────────────────────────────────\n\nfunction isObjectProperty(prop: any): boolean {\n return prop?.type === \"ObjectProperty\" || prop?.type === \"Property\";\n}\n\nfunction propKeyName(prop: any): string | undefined {\n return prop?.key?.name ?? prop?.key?.value;\n}\n\nfunction findPropertyNode(varsArgNode: any, key: string): any | undefined {\n if (varsArgNode?.type !== \"ObjectExpression\") return undefined;\n for (const prop of varsArgNode.properties ?? []) {\n if (!isObjectProperty(prop)) continue;\n if (propKeyName(prop) === key) return prop.value;\n }\n return undefined;\n}\n\n/**\n * Extract raw source text for a property value — the offset-splice primitive.\n * Equivalent to `recast.print(node).code` for unmodified nodes.\n */\nfunction extractRawPropertySource(\n varsArgNode: any,\n key: string,\n source: string,\n): string | undefined {\n const node = findPropertyNode(varsArgNode, key);\n return node ? source.slice(node.start, node.end) : undefined;\n}\n\n// fallow-ignore-next-line complexity\nfunction objectExpressionToRecord(\n node: any,\n scope: ScopeBindings,\n source: string,\n): Record<string, unknown> {\n const result: Record<string, unknown> = {};\n if (node?.type !== \"ObjectExpression\") return result;\n for (const prop of node.properties ?? []) {\n if (!isObjectProperty(prop)) continue;\n const key = prop.key?.name ?? prop.key?.value;\n if (!key) continue;\n const resolved = resolveNode(prop.value, scope);\n if (resolved !== undefined) {\n result[key] = resolved;\n } else {\n result[key] = `__raw:${source.slice(prop.value.start, prop.value.end)}`;\n }\n }\n return result;\n}\n\n// ── Timeline detection ────────────────────────────────────────────────────────\n\nfunction isGsapTimelineCall(node: any): boolean {\n return (\n node?.type === \"CallExpression\" &&\n node.callee?.type === \"MemberExpression\" &&\n node.callee.object?.name === \"gsap\" &&\n node.callee.property?.name === \"timeline\"\n );\n}\n\ninterface TimelineDefaults {\n ease?: string;\n duration?: number;\n}\n\ninterface TimelineDetection {\n timelineVar: string | null;\n timelineCount: number;\n defaults?: TimelineDefaults;\n}\n\n// fallow-ignore-next-line complexity\nfunction extractTimelineDefaults(\n callNode: any,\n scope: ScopeBindings,\n): TimelineDefaults | undefined {\n const arg = callNode.arguments?.[0];\n if (!arg || arg.type !== \"ObjectExpression\") return undefined;\n const defaultsProp = arg.properties?.find(\n (p: any) => isObjectProperty(p) && propKeyName(p) === \"defaults\",\n );\n if (!defaultsProp?.value || defaultsProp.value.type !== \"ObjectExpression\") return undefined;\n const result: TimelineDefaults = {};\n for (const prop of defaultsProp.value.properties ?? []) {\n if (!isObjectProperty(prop)) continue;\n const key = propKeyName(prop);\n const val = resolveNode(prop.value, scope);\n if (key === \"ease\" && typeof val === \"string\") result.ease = val;\n if (key === \"duration\" && typeof val === \"number\") result.duration = val;\n }\n return Object.keys(result).length > 0 ? result : undefined;\n}\n\nfunction findTimelineVar(ast: any, scope?: ScopeBindings): TimelineDetection {\n let timelineVar: string | null = null;\n let timelineCount = 0;\n let defaults: TimelineDefaults | undefined;\n const emptyScope: ScopeBindings = scope ?? new Map();\n\n acornWalk.simple(ast, {\n VariableDeclarator(node: any) {\n if (isGsapTimelineCall(node.init)) {\n timelineCount += 1;\n if (!timelineVar) {\n timelineVar = node.id?.name ?? null;\n defaults = extractTimelineDefaults(node.init, emptyScope);\n }\n }\n },\n AssignmentExpression(node: any) {\n if (isGsapTimelineCall(node.right)) {\n timelineCount += 1;\n if (!timelineVar) {\n const left = node.left;\n if (left?.type === \"Identifier\") timelineVar = left.name;\n defaults = extractTimelineDefaults(node.right, emptyScope);\n }\n }\n },\n });\n\n return { timelineVar, timelineCount, defaults };\n}\n\n// ── Tween call collection ─────────────────────────────────────────────────────\n\n/** Keys stored on dedicated GsapAnimation fields (not in properties/extras). */\nconst BUILTIN_VAR_KEYS = new Set([\"duration\", \"ease\", \"delay\"]);\n/** Keys never preserved (callbacks / advanced patterns). */\nconst DROPPED_VAR_KEYS = new Set([\"onComplete\", \"onStart\", \"onUpdate\", \"onRepeat\"]);\n/** Keys that go in `extras` — non-editable GSAP config that must survive round-trips. */\nconst EXTRAS_KEYS = new Set([\n \"stagger\",\n \"yoyo\",\n \"repeat\",\n \"repeatDelay\",\n \"snap\",\n \"overwrite\",\n \"immediateRender\",\n]);\n\nexport interface TweenCallInfo {\n node: any;\n /** acorn-walk ancestor array at the call site (root→call, call is last). */\n ancestors: any[];\n method: GsapMethod;\n selector: string;\n varsArg: any;\n fromArg?: any;\n positionArg?: any;\n /** True for a base `gsap.set(...)` (off-timeline) rather than `tl.set(...)`. */\n global?: boolean;\n}\n\n/** True when callee chain is rooted at the timeline variable. */\nfunction isTimelineRootedCall(callNode: any, timelineVar: string): boolean {\n let obj = callNode.callee?.object;\n while (obj?.type === \"CallExpression\") {\n obj = obj.callee?.object;\n }\n return obj?.type === \"Identifier\" && obj.name === timelineVar;\n}\n\n/**\n * Pre-order recursive walk for tween collection.\n *\n * acorn-walk is POST-order (visitor fires after children), which reverses\n * chained calls vs recast.types.visit (PRE-order). We need pre-order to\n * match the golden ordering where the outermost chained call appears first.\n */\nfunction findAllTweenCalls(\n ast: any,\n timelineVar: string,\n scope: ScopeBindings,\n targetBindings: TargetBindings,\n): TweenCallInfo[] {\n const results: TweenCallInfo[] = [];\n\n // fallow-ignore-next-line complexity\n function visit(node: any, ancestors: readonly any[]): void {\n if (!node || typeof node !== \"object\") return;\n const nodeAncestors = [...ancestors, node];\n\n // Fire BEFORE children (pre-order) so chained outer calls come first.\n if (node.type === \"CallExpression\") {\n const callee = node.callee;\n // A base `gsap.set(\"#sel\", props)` is an off-timeline static hold — parse it as\n // an editable global `set` so a static value round-trips and re-edits in place.\n // STRING-LITERAL selectors only: variable-target holds stay surrounding source.\n const gsapSetArg = node.arguments?.[0];\n const isGlobalSet =\n callee?.type === \"MemberExpression\" &&\n callee.object?.type === \"Identifier\" &&\n callee.object.name === \"gsap\" &&\n callee.property?.type === \"Identifier\" &&\n callee.property.name === \"set\" &&\n (gsapSetArg?.type === \"StringLiteral\" ||\n (gsapSetArg?.type === \"Literal\" && typeof gsapSetArg.value === \"string\"));\n if (\n callee?.type === \"MemberExpression\" &&\n callee.property?.type === \"Identifier\" &&\n (isTimelineRootedCall(node, timelineVar) || isGlobalSet) &&\n GSAP_METHODS.has(callee.property.name)\n ) {\n const method = callee.property.name;\n const args = node.arguments;\n const selectorValue =\n args.length >= 1\n ? (resolveTargetSelector(args[0], nodeAncestors, scope, targetBindings) ??\n \"__unresolved__\")\n : \"__unresolved__\";\n\n if (method === \"fromTo\" && args.length >= 3) {\n results.push({\n node,\n ancestors: nodeAncestors,\n method: \"fromTo\",\n selector: selectorValue,\n fromArg: args[1],\n varsArg: args[2],\n positionArg: args[3],\n });\n } else if (method !== \"fromTo\" && args.length >= 2) {\n results.push({\n node,\n ancestors: nodeAncestors,\n method: method as GsapMethod,\n selector: selectorValue,\n varsArg: args[1],\n positionArg: args[2],\n ...(isGlobalSet ? { global: true } : {}),\n });\n }\n }\n }\n\n // Traverse children. Object.keys preserves insertion order, so callee\n // comes before arguments in acorn's CallExpression nodes.\n for (const key of Object.keys(node)) {\n if (key === \"type\" || key === \"start\" || key === \"end\" || key === \"loc\") continue;\n const child = (node as any)[key];\n if (Array.isArray(child)) {\n for (const item of child) {\n if (item && typeof item === \"object\" && item.type) visit(item, nodeAncestors);\n }\n } else if (child && typeof child === \"object\" && (child as any).type) {\n visit(child, nodeAncestors);\n }\n }\n }\n\n visit(ast, []);\n return results;\n}\n\n// ── Keyframes parsing ─────────────────────────────────────────────────────────\n\nconst PERCENTAGE_KEY_RE = /^(\\d+(?:\\.\\d+)?)%$/;\n\nfunction tryResolveStringProp(propValue: any, scope: ScopeBindings): string | undefined {\n const val = resolveNode(propValue, scope);\n return typeof val === \"string\" ? val : undefined;\n}\n\n// fallow-ignore-next-line complexity\nfunction parsePercentageKeyframes(\n node: any,\n scope: ScopeBindings,\n source: string,\n): GsapKeyframesData {\n const keyframes: GsapPercentageKeyframe[] = [];\n let ease: string | undefined;\n let easeEach: string | undefined;\n\n for (const prop of node.properties ?? []) {\n if (prop.type !== \"ObjectProperty\" && prop.type !== \"Property\") continue;\n const key = prop.key?.value ?? prop.key?.name;\n if (typeof key !== \"string\") continue;\n\n const pctMatch = PERCENTAGE_KEY_RE.exec(key);\n if (pctMatch) {\n const percentage = Number.parseFloat(pctMatch[1] ?? \"0\");\n const record = objectExpressionToRecord(prop.value, scope, source);\n const properties: Record<string, number | string> = {};\n let kfEase: string | undefined;\n for (const [k, v] of Object.entries(record)) {\n if (k === \"ease\" && typeof v === \"string\") {\n kfEase = v;\n } else if (typeof v === \"number\" || typeof v === \"string\") {\n properties[k] = v;\n }\n }\n keyframes.push({ percentage, properties, ...(kfEase ? { ease: kfEase } : {}) });\n } else if (key === \"ease\") {\n ease = tryResolveStringProp(prop.value, scope) ?? ease;\n } else if (key === \"easeEach\") {\n easeEach = tryResolveStringProp(prop.value, scope) ?? easeEach;\n }\n }\n\n keyframes.sort((a, b) => a.percentage - b.percentage);\n\n return {\n format: \"percentage\",\n keyframes,\n ...(ease ? { ease } : {}),\n ...(easeEach ? { easeEach } : {}),\n };\n}\n\n// fallow-ignore-next-line complexity\nfunction computeKeyframesTotalDuration(\n varsNode: any,\n scope: ScopeBindings,\n source: string,\n): number | undefined {\n const kfNode = (varsNode.properties ?? []).find(\n (p: any) => (p.key?.name ?? p.key?.value) === \"keyframes\",\n )?.value;\n if (!kfNode || kfNode.type !== \"ArrayExpression\") return undefined;\n let total = 0;\n for (const el of kfNode.elements ?? []) {\n if (!el || el.type !== \"ObjectExpression\") continue;\n const r = objectExpressionToRecord(el, scope, source);\n if (typeof r.duration === \"number\") total += r.duration;\n }\n return total > 0 ? total : undefined;\n}\n\n// fallow-ignore-next-line complexity\nfunction parseObjectArrayKeyframes(\n node: any,\n scope: ScopeBindings,\n source: string,\n): GsapKeyframesData {\n const elements = node.elements ?? [];\n const raw: Array<{\n properties: Record<string, number | string>;\n duration?: number;\n ease?: string;\n }> = [];\n\n for (const el of elements) {\n if (!el || el.type !== \"ObjectExpression\") continue;\n const record = objectExpressionToRecord(el, scope, source);\n const properties: Record<string, number | string> = {};\n let duration: number | undefined;\n let ease: string | undefined;\n for (const [k, v] of Object.entries(record)) {\n if (k === \"duration\" && typeof v === \"number\") {\n duration = v;\n } else if (k === \"ease\" && typeof v === \"string\") {\n ease = v;\n } else if (typeof v === \"number\" || typeof v === \"string\") {\n properties[k] = v;\n }\n }\n raw.push({ properties, duration, ease });\n }\n\n const totalDuration = raw.reduce((sum, r) => sum + (r.duration ?? 0), 0);\n const keyframes: GsapPercentageKeyframe[] = [];\n\n if (totalDuration > 0) {\n let cumulative = 0;\n for (const entry of raw) {\n cumulative += entry.duration ?? 0;\n const percentage = Math.round((cumulative / totalDuration) * 100);\n keyframes.push({\n percentage,\n properties: entry.properties,\n ...(entry.ease ? { ease: entry.ease } : {}),\n });\n }\n } else {\n for (let i = 0; i < raw.length; i++) {\n const entry = raw[i];\n if (!entry) continue;\n const percentage = raw.length > 1 ? Math.round((i / (raw.length - 1)) * 100) : 0;\n keyframes.push({\n percentage,\n properties: entry.properties,\n ...(entry.ease ? { ease: entry.ease } : {}),\n });\n }\n }\n\n return { format: \"object-array\", keyframes };\n}\n\n// fallow-ignore-next-line complexity\nfunction parseSimpleArrayKeyframes(node: any, scope: ScopeBindings): GsapKeyframesData {\n const arrayProps: Map<string, (number | string)[]> = new Map();\n let ease: string | undefined;\n let easeEach: string | undefined;\n\n for (const prop of node.properties ?? []) {\n if (prop.type !== \"ObjectProperty\" && prop.type !== \"Property\") continue;\n const key = prop.key?.name ?? prop.key?.value;\n if (typeof key !== \"string\") continue;\n\n if (prop.value?.type === \"ArrayExpression\") {\n const values: (number | string)[] = [];\n for (const el of prop.value.elements ?? []) {\n const val = resolveNode(el, scope);\n if (typeof val === \"number\" || typeof val === \"string\") {\n values.push(val);\n }\n }\n if (values.length > 0) arrayProps.set(key, values);\n } else if (key === \"ease\") {\n ease = tryResolveStringProp(prop.value, scope) ?? ease;\n } else if (key === \"easeEach\") {\n easeEach = tryResolveStringProp(prop.value, scope) ?? easeEach;\n }\n }\n\n const maxLen = Math.max(...[...arrayProps.values()].map((a) => a.length), 0);\n const keyframes: GsapPercentageKeyframe[] = [];\n\n for (let i = 0; i < maxLen; i++) {\n const percentage = maxLen > 1 ? Math.round((i / (maxLen - 1)) * 100) : 0;\n const properties: Record<string, number | string> = {};\n for (const [key, values] of arrayProps) {\n if (i < values.length) properties[key] = values[i] as number | string;\n }\n keyframes.push({ percentage, properties });\n }\n\n return {\n format: \"simple-array\",\n keyframes,\n ...(ease ? { ease } : {}),\n ...(easeEach ? { easeEach } : {}),\n };\n}\n\n// fallow-ignore-next-line complexity\nfunction parseKeyframesNode(\n node: any,\n scope: ScopeBindings,\n source: string,\n): GsapKeyframesData | undefined {\n if (!node) return undefined;\n\n if (node.type === \"ArrayExpression\") {\n return parseObjectArrayKeyframes(node, scope, source);\n }\n\n if (node.type !== \"ObjectExpression\") return undefined;\n\n const props = node.properties ?? [];\n let hasPercentageKey = false;\n let hasArrayValue = false;\n\n for (const prop of props) {\n if (prop.type !== \"ObjectProperty\" && prop.type !== \"Property\") continue;\n const key = prop.key?.value ?? prop.key?.name;\n if (typeof key === \"string\" && PERCENTAGE_KEY_RE.test(key)) {\n hasPercentageKey = true;\n break;\n }\n if (prop.value?.type === \"ArrayExpression\") {\n hasArrayValue = true;\n }\n }\n\n if (hasPercentageKey) return parsePercentageKeyframes(node, scope, source);\n if (hasArrayValue) return parseSimpleArrayKeyframes(node, scope);\n\n return undefined;\n}\n\n// ── MotionPath parsing ────────────────────────────────────────────────────────\n\ninterface MotionPathParseResult {\n arcPath: ArcPathConfig;\n waypoints: Array<{ x: number; y: number }>;\n}\n\n// fallow-ignore-next-line complexity\nfunction parseMotionPathNode(\n node: any,\n scope: ScopeBindings,\n source: string,\n): MotionPathParseResult | undefined {\n if (!node) return undefined;\n\n let pathNode: any;\n let autoRotate: boolean | number = false;\n let curviness = 1;\n let isCubic = false;\n\n if (node.type === \"ObjectExpression\") {\n for (const prop of node.properties ?? []) {\n if (!isObjectProperty(prop)) continue;\n const key = propKeyName(prop);\n if (key === \"path\") pathNode = prop.value;\n else if (key === \"autoRotate\") {\n const val = resolveNode(prop.value, scope);\n autoRotate = typeof val === \"number\" ? val : val === true;\n } else if (key === \"curviness\") {\n const val = resolveNode(prop.value, scope);\n if (typeof val === \"number\") curviness = val;\n } else if (key === \"type\") {\n const val = resolveNode(prop.value, scope);\n if (val === \"cubic\") isCubic = true;\n }\n }\n } else if (node.type === \"ArrayExpression\") {\n pathNode = node;\n }\n\n if (!pathNode || pathNode.type !== \"ArrayExpression\") return undefined;\n\n const elements = pathNode.elements ?? [];\n const coords: Array<{ x: number; y: number }> = [];\n for (const elem of elements) {\n if (!elem || elem.type !== \"ObjectExpression\") continue;\n const rec = objectExpressionToRecord(elem, scope, source);\n const x = typeof rec.x === \"number\" ? rec.x : undefined;\n const y = typeof rec.y === \"number\" ? rec.y : undefined;\n if (x !== undefined && y !== undefined) coords.push({ x, y });\n }\n\n return buildArcPath(coords, curviness, autoRotate, isCubic);\n}\n\n// ── Animation assembly ────────────────────────────────────────────────────────\n\n// fallow-ignore-next-line complexity\nfunction tweenCallToAnimation(\n call: TweenCallInfo,\n scope: ScopeBindings,\n source: string,\n): Omit<GsapAnimation, \"id\"> {\n const vars = objectExpressionToRecord(call.varsArg, scope, source);\n const properties: Record<string, number | string> = {};\n const extras: Record<string, unknown> = {};\n let keyframesData: GsapKeyframesData | undefined;\n let hasUnresolvedKeyframes = false;\n let motionPathResult: MotionPathParseResult | undefined;\n\n for (const [key, val] of Object.entries(vars)) {\n if (BUILTIN_VAR_KEYS.has(key)) continue;\n if (DROPPED_VAR_KEYS.has(key)) continue;\n\n if (key === \"keyframes\") {\n const kfNode = findPropertyNode(call.varsArg, \"keyframes\");\n keyframesData = parseKeyframesNode(kfNode, scope, source);\n if (!keyframesData && kfNode) hasUnresolvedKeyframes = true;\n continue;\n }\n\n if (key === \"motionPath\") {\n const mpNode = findPropertyNode(call.varsArg, \"motionPath\");\n motionPathResult = parseMotionPathNode(mpNode, scope, source);\n continue;\n }\n\n if (key === \"easeEach\") continue;\n\n if (EXTRAS_KEYS.has(key)) {\n const rawSource = extractRawPropertySource(call.varsArg, key, source);\n if (rawSource !== undefined) {\n extras[key] = `__raw:${rawSource}`;\n } else if (val !== undefined) {\n extras[key] = val;\n }\n continue;\n }\n\n if (typeof val === \"number\" || typeof val === \"string\") {\n properties[key] = val;\n }\n }\n\n if (keyframesData && typeof vars.easeEach === \"string\") {\n keyframesData.easeEach = vars.easeEach as string;\n }\n\n if (motionPathResult) {\n const { waypoints } = motionPathResult;\n if (!keyframesData) {\n const kf: GsapPercentageKeyframe[] = waypoints.map((wp, i) => ({\n percentage: waypoints.length > 1 ? Math.round((i / (waypoints.length - 1)) * 100) : 0,\n properties: { x: wp.x, y: wp.y },\n }));\n keyframesData = { format: \"percentage\", keyframes: kf };\n } else {\n const kfs = keyframesData.keyframes;\n if (kfs.length === waypoints.length) {\n for (let i = 0; i < kfs.length; i++) {\n const kf = kfs[i];\n const wp = waypoints[i];\n if (kf && wp) {\n kf.properties.x = wp.x;\n kf.properties.y = wp.y;\n }\n }\n }\n }\n }\n\n let fromProperties: Record<string, number | string> | undefined;\n if (call.method === \"fromTo\" && call.fromArg) {\n fromProperties = {};\n const fromVars = objectExpressionToRecord(call.fromArg, scope, source);\n for (const [key, val] of Object.entries(fromVars)) {\n if (typeof val === \"number\" || typeof val === \"string\") {\n fromProperties[key] = val;\n }\n }\n }\n\n const hasPositionArg = !!call.positionArg;\n const posVal = hasPositionArg ? extractLiteralValue(call.positionArg, scope) : 0;\n const position: number | string =\n typeof posVal === \"number\" ? posVal : typeof posVal === \"string\" ? posVal : 0;\n let duration = typeof vars.duration === \"number\" ? vars.duration : undefined;\n const ease = typeof vars.ease === \"string\" ? vars.ease : undefined;\n\n if (duration === undefined && keyframesData) {\n duration = computeKeyframesTotalDuration(call.varsArg, scope, source);\n }\n\n const anim: Omit<GsapAnimation, \"id\"> = {\n targetSelector: call.selector,\n method: call.method,\n position,\n properties,\n fromProperties,\n duration,\n ease,\n };\n if (!hasPositionArg) anim.implicitPosition = true;\n let group = classifyTweenPropertyGroup(properties);\n if (!group && keyframesData) {\n const kfProps: Record<string, unknown> = {};\n for (const kf of keyframesData.keyframes) {\n for (const k of Object.keys(kf.properties)) kfProps[k] = true;\n }\n group = classifyTweenPropertyGroup(kfProps);\n }\n if (group) anim.propertyGroup = group;\n if (call.global) anim.global = true;\n if (Object.keys(extras).length > 0) anim.extras = extras;\n if (keyframesData) anim.keyframes = keyframesData;\n if (motionPathResult) anim.arcPath = motionPathResult.arcPath;\n if (hasUnresolvedKeyframes) anim.hasUnresolvedKeyframes = true;\n if (call.selector === \"__unresolved__\") anim.hasUnresolvedSelector = true;\n const provenance = readProvenance(call.node);\n if (provenance) anim.provenance = provenance;\n return anim;\n}\n\n// ── Timeline position resolution ─────────────────────────────────────────────\n\nconst GSAP_DEFAULT_DURATION = 0.5;\n\n// fallow-ignore-next-line complexity\nfunction resolvePositionString(pos: string, cursor: number, prevStart: number): number | null {\n const trimmed = pos.trim();\n if (trimmed === \"\") return cursor;\n if (trimmed.startsWith(\"+=\")) {\n const n = Number.parseFloat(trimmed.slice(2));\n return Number.isFinite(n) ? cursor + n : null;\n }\n if (trimmed.startsWith(\"-=\")) {\n const n = Number.parseFloat(trimmed.slice(2));\n return Number.isFinite(n) ? cursor - n : null;\n }\n if (trimmed === \"<\") return prevStart;\n if (trimmed === \">\") return cursor;\n if (trimmed.startsWith(\"<\")) {\n const n = Number.parseFloat(trimmed.slice(1));\n return Number.isFinite(n) ? prevStart + n : null;\n }\n if (trimmed.startsWith(\">\")) {\n const n = Number.parseFloat(trimmed.slice(1));\n return Number.isFinite(n) ? cursor + n : null;\n }\n const n = Number.parseFloat(trimmed);\n return Number.isFinite(n) ? n : null;\n}\n\nfunction applyTimelineDefaults(\n anims: Omit<GsapAnimation, \"id\">[],\n defaults?: TimelineDefaults,\n): void {\n if (!defaults) return;\n for (const anim of anims) {\n if (anim.method === \"set\") continue;\n if (anim.duration === undefined && defaults.duration !== undefined) {\n anim.duration = defaults.duration;\n }\n if (anim.ease === undefined && defaults.ease !== undefined) {\n anim.ease = defaults.ease;\n }\n }\n}\n\n// fallow-ignore-next-line complexity\nfunction resolveTimelinePositions(anims: Omit<GsapAnimation, \"id\">[]): void {\n let cursor = 0;\n let prevStart = 0;\n for (const anim of anims) {\n // A global `gsap.set(...)` is off-timeline — applied once at load, not\n // sequenced on the master timeline. It carries no position arg, so the\n // cursor fallback would otherwise hand it the comp-end time. Pin it to 0\n // (its load-time start) and don't advance the cursor/prevStart.\n if (anim.method === \"set\" && anim.global) {\n anim.resolvedStart = 0;\n continue;\n }\n const duration = anim.method === \"set\" ? 0 : (anim.duration ?? GSAP_DEFAULT_DURATION);\n let start: number | null;\n\n if (anim.implicitPosition) {\n start = cursor;\n } else if (typeof anim.position === \"number\") {\n start = anim.position;\n } else if (typeof anim.position === \"string\") {\n start = resolvePositionString(anim.position, cursor, prevStart);\n } else {\n start = cursor;\n }\n\n if (start != null) {\n anim.resolvedStart = Math.max(0, start);\n prevStart = anim.resolvedStart;\n cursor = Math.max(cursor, anim.resolvedStart + duration);\n }\n }\n}\n\nfunction compareByLoc(a: TweenCallInfo, b: TweenCallInfo): number {\n const aLoc = a.node.callee?.property?.loc?.start;\n const bLoc = b.node.callee?.property?.loc?.start;\n if (!aLoc || !bLoc) return 0;\n return aLoc.line - bLoc.line || aLoc.column - bLoc.column;\n}\n\n// Inlined tweens carry a monotonic __hfOrder (clones share source loc, so loc\n// can't order them); they sort by that, after all literal (loc-ordered) tweens.\nfunction compareCallOrder(a: TweenCallInfo, b: TweenCallInfo): number {\n const ao = a.node.__hfOrder;\n const bo = b.node.__hfOrder;\n if (ao === undefined && bo === undefined) return compareByLoc(a, b);\n if (ao === undefined) return -1;\n if (bo === undefined) return 1;\n return ao - bo;\n}\n\nfunction sortBySourcePosition(calls: TweenCallInfo[]): void {\n calls.sort(compareCallOrder);\n}\n\n// ── Stable ID generation ──────────────────────────────────────────────────────\n\nfunction assignStableIds(anims: Omit<GsapAnimation, \"id\">[]): GsapAnimation[] {\n const counts = new Map<string, number>();\n return anims.map((anim) => {\n const posKey =\n typeof anim.position === \"number\"\n ? String(Math.round(anim.position * 1000))\n : String(anim.position);\n const groupSuffix = anim.propertyGroup ? `-${anim.propertyGroup}` : \"\";\n const base = `${anim.targetSelector}-${anim.method}-${posKey}${groupSuffix}`;\n const count = (counts.get(base) ?? 0) + 1;\n counts.set(base, count);\n const id = count === 1 ? base : `${base}-${count}`;\n return { ...anim, id };\n });\n}\n\n// ── Write-path internal parse ─────────────────────────────────────────────────\n\nexport interface ParsedGsapAcornForWrite {\n ast: any;\n timelineVar: string;\n hasTimeline: boolean;\n located: Array<{ id: string; call: TweenCallInfo; animation: GsapAnimation }>;\n}\n\n/**\n * Parse a GSAP script and return internal AST + call nodes for the write path.\n * Consumed by gsapWriterAcorn.ts (magic-string offset-splice).\n */\nexport function parseGsapScriptAcornForWrite(script: string): ParsedGsapAcornForWrite | null {\n try {\n const ast = acorn.parse(script, {\n ecmaVersion: \"latest\",\n sourceType: \"script\",\n locations: true,\n });\n const scope = collectScopeBindings(ast);\n const targetBindings = collectTargetBindings(ast, scope);\n const detection = findTimelineVar(ast, scope);\n const timelineVar = detection.timelineVar ?? \"tl\";\n const calls = findAllTweenCalls(ast, timelineVar, scope, targetBindings);\n sortBySourcePosition(calls);\n const rawAnims = calls.map((call) => tweenCallToAnimation(call, scope, script));\n applyTimelineDefaults(rawAnims, detection.defaults);\n resolveTimelinePositions(rawAnims);\n const animations = assignStableIds(rawAnims);\n const located = calls.map((call, i) => ({\n id: animations[i]!.id,\n call,\n animation: animations[i]!,\n }));\n return { ast, timelineVar, hasTimeline: detection.timelineVar !== null, located };\n } catch {\n return null;\n }\n}\n\n// ── Public API ────────────────────────────────────────────────────────────────\n\n/**\n * Browser-safe equivalent of `parseGsapScript` (gsapParser.ts).\n * Uses acorn + acorn-walk instead of recast + @babel/parser.\n */\nexport function parseGsapScriptAcorn(script: string): ParsedGsap {\n try {\n const ast = acorn.parse(script, {\n ecmaVersion: \"latest\",\n sourceType: \"script\",\n locations: true,\n });\n const scope = collectScopeBindings(ast);\n const detection = findTimelineVar(ast, scope);\n const timelineVar = detection.timelineVar ?? \"tl\";\n // Expand helper-built / bounded-loop timelines before analysis so their\n // tweens resolve at true positions (read path only — the write path keeps\n // original source nodes). Degrades to the un-inlined AST on any failure.\n try {\n inlineComputedTimelines(ast, timelineVar, (node) => resolveNode(node, scope));\n } catch {\n /* fall back to current behavior */\n }\n const targetBindings = collectTargetBindings(ast, scope);\n const calls = findAllTweenCalls(ast, timelineVar, scope, targetBindings);\n sortBySourcePosition(calls);\n const rawAnims = calls.map((call) => tweenCallToAnimation(call, scope, script));\n applyTimelineDefaults(rawAnims, detection.defaults);\n resolveTimelinePositions(rawAnims);\n const animations = assignStableIds(rawAnims);\n\n const timelineMatch = script.match(\n new RegExp(\n `^[\\\\s\\\\S]*?(?:const|let|var)\\\\s+${timelineVar}\\\\s*=\\\\s*gsap\\\\.timeline\\\\s*\\\\([^)]*\\\\)\\\\s*;?`,\n ),\n );\n const preamble =\n timelineMatch?.[0] ?? `const ${timelineVar} = gsap.timeline({ paused: true });`;\n\n const lastCallIdx = script.lastIndexOf(`${timelineVar}.`);\n let postamble = \"\";\n if (lastCallIdx !== -1) {\n const afterLast = script.slice(lastCallIdx);\n const endOfCall = afterLast.indexOf(\";\");\n if (endOfCall !== -1) {\n postamble = script.slice(lastCallIdx + endOfCall + 1).trim();\n }\n }\n\n const result: ParsedGsap = { animations, timelineVar, preamble, postamble };\n if (detection.timelineCount > 1) result.multipleTimelines = true;\n if (detection.timelineCount > 0 && detection.timelineVar === null)\n result.unsupportedTimelinePattern = true;\n return result;\n } catch {\n return { animations: [], timelineVar: \"tl\", preamble: \"\", postamble: \"\" };\n }\n}\n\n// ── Label extraction (WS-C) ──────────────────────────────────────────────────\n\nexport interface GsapLabelEntry {\n name: string;\n position: number;\n}\n\n/**\n * Extract all `tl.addLabel(\"name\", position)` calls from a GSAP script.\n *\n * Returns labels in source order. Position must be a numeric literal; labels\n * with non-numeric positions (e.g. label-relative offsets) are skipped.\n *\n * Pure — no side effects, no DOM, no Date.now.\n */\nexport function extractGsapLabels(script: string): GsapLabelEntry[] {\n try {\n const ast = acorn.parse(script, {\n ecmaVersion: \"latest\",\n sourceType: \"script\",\n locations: true,\n });\n const scope = collectScopeBindings(ast);\n const detection = findTimelineVar(ast, scope);\n const timelineVar = detection.timelineVar ?? \"tl\";\n\n const labels: GsapLabelEntry[] = [];\n\n acornWalk.simple(ast, {\n // fallow-ignore-next-line complexity\n ExpressionStatement(node: any) {\n const expr = node.expression;\n if (!expr || expr.type !== \"CallExpression\") return;\n const callee = expr.callee;\n // Match tl.addLabel(...)\n if (\n callee?.type !== \"MemberExpression\" ||\n callee.object?.name !== timelineVar ||\n callee.property?.name !== \"addLabel\"\n )\n return;\n const args = expr.arguments ?? [];\n const nameNode = args[0];\n const posNode = args[1];\n if (nameNode?.type !== \"Literal\" || typeof nameNode.value !== \"string\") return;\n if (!posNode) return;\n const pos = resolveNode(posNode, scope);\n if (typeof pos !== \"number\" || !Number.isFinite(pos)) return;\n labels.push({ name: nameNode.value, position: pos });\n },\n });\n\n return labels;\n } catch {\n // Labels are best-effort/supplementary, not load-bearing — a malformed or\n // unparseable script yields no labels rather than failing the caller.\n return [];\n }\n}\n","/**\n * Static evaluation for computed GSAP timelines (browser-safe, acorn/ESTree).\n *\n * The read parser resolves only literals and top-level consts, so timelines\n * built by a helper called N times or by a bounded loop collapse to position 0.\n * This module expands those constructs into a synthetic analysis AST: each\n * helper invocation and each loop iteration becomes its own concrete set of\n * `tl.*` calls, with parameters/loop-vars substituted by the call's argument\n * (or element/index) AST nodes — after which the existing parse pipeline\n * resolves positions and `motionPath` arcs unchanged.\n *\n * Substituted nodes keep their original source offsets, so downstream\n * source-slicing (raw extras, keyframes) stays correct. The substitution\n * primitives never mutate their input; `inlineComputedTimelines` rewrites the\n * Program body of the freshly-parsed AST it is handed (owned by the caller).\n */\nimport type { GsapProvenance } from \"./gsapSerialize.js\";\n\n// acorn ESTree nodes are structurally untyped; mirror gsapParserAcorn.ts.\ntype Node = any;\n\n/** Node keys that are metadata, not child AST to traverse/substitute. */\nconst SKIP_KEYS = new Set([\"type\", \"start\", \"end\", \"loc\", \"range\", \"__hfProvenance\", \"__hfOrder\"]);\n\nconst FUNCTION_TYPES = new Set([\n \"ArrowFunctionExpression\",\n \"FunctionExpression\",\n \"FunctionDeclaration\",\n]);\nconst GSAP_METHODS = new Set([\"set\", \"to\", \"from\", \"fromTo\"]);\n\n// Bounds on synthetic expansion (recursion + iteration runaway guards).\nconst MAX_DEPTH = 8;\nconst MAX_ITERS = 512;\n\nfunction isFunctionNode(node: Node): boolean {\n return !!node && FUNCTION_TYPES.has(node.type);\n}\n\nfunction isNode(x: Node): boolean {\n return !!x && typeof x === \"object\" && typeof x.type === \"string\";\n}\n\n/**\n * Apply `fn` to each child AST node, writing back its return value. Skips\n * metadata keys and key/member slots that must not be treated as values.\n * The one place array-vs-single child traversal lives, so walkers stay flat.\n */\nfunction transformChildren(node: Node, fn: (child: Node) => Node): void {\n for (const key of Object.keys(node)) {\n if (SKIP_KEYS.has(key) || isNonValueIdentifierSlot(node, key)) continue;\n const child = node[key];\n if (Array.isArray(child)) {\n for (let i = 0; i < child.length; i++) child[i] = fn(child[i]);\n } else {\n node[key] = fn(child);\n }\n }\n}\n\n/** Deep structural clone preserving `start`/`end`/`loc` (needed for source slicing). */\nexport function cloneNode<T extends Node>(node: T): T {\n return structuredClone(node);\n}\n\n// ponytail: Identifier + default + rest only. Destructured bindings (`{x}`, `[x]`)\n// aren't inlined (U2 inlines Identifier-param helpers / loop vars only), so a\n// destructuring shadow is a double-rare miss that just falls back. Add the\n// pattern cases here if that ever bites.\nfunction collectPatternNames(pattern: Node, out: Set<string>): void {\n if (pattern?.type === \"Identifier\") out.add(pattern.name);\n else if (pattern?.type === \"AssignmentPattern\") collectPatternNames(pattern.left, out);\n else if (pattern?.type === \"RestElement\") collectPatternNames(pattern.argument, out);\n}\n\n/** Every identifier name bound anywhere inside the subtree (fn params, declared vars, catch params). */\nfunction collectBoundNames(root: Node): Set<string> {\n const names = new Set<string>();\n const visit = (node: Node): Node => {\n if (!isNode(node)) return node;\n if (isFunctionNode(node)) for (const p of node.params ?? []) collectPatternNames(p, names);\n else if (node.type === \"VariableDeclarator\") collectPatternNames(node.id, names);\n else if (node.type === \"CatchClause\") collectPatternNames(node.param, names);\n transformChildren(node, visit);\n return node;\n };\n visit(root);\n return names;\n}\n\n/** A child in key/property position that must not be treated as a value identifier. */\nfunction isNonValueIdentifierSlot(node: Node, key: string): boolean {\n if (node.computed) return false;\n return (\n (node.type === \"MemberExpression\" && key === \"property\") ||\n (node.type === \"Property\" && key === \"key\")\n );\n}\n\n/**\n * Substitute bound identifiers in an already-cloned subtree, returning the\n * (possibly replaced) root. Names shadowed anywhere inside (nested function\n * params, declared vars) are dropped up front rather than tracked per scope —\n * worst case we under-substitute and the caller falls back to current behavior.\n * Never substitutes identifiers in key/member positions. Mutates the passed\n * clone in place — callers pass `cloneNode(...)`.\n */\nexport function substituteParams(node: Node, bindings: ReadonlyMap<string, Node>): Node {\n const shadowed = collectBoundNames(node);\n let effective = bindings;\n if (shadowed.size > 0) {\n effective = new Map(bindings);\n for (const name of shadowed) (effective as Map<string, Node>).delete(name);\n }\n if (effective.size === 0) return node;\n return replace(node, effective);\n}\n\nfunction replace(node: Node, bindings: ReadonlyMap<string, Node>): Node {\n if (!isNode(node)) return node;\n if (node.type === \"Identifier\" && bindings.has(node.name)) {\n return cloneNode(bindings.get(node.name));\n }\n transformChildren(node, (child) => replace(child, bindings));\n return node;\n}\n\n/** Tag a node (typically a `tl.*` CallExpression) with its construction provenance. */\nexport function tagProvenance(node: Node, provenance: GsapProvenance): Node {\n if (node && typeof node === \"object\") node.__hfProvenance = provenance;\n return node;\n}\n\n/** Read a provenance tag previously set by `tagProvenance`, if any. */\nexport function readProvenance(node: Node): GsapProvenance | undefined {\n return node?.__hfProvenance;\n}\n\n/** Synthesize a numeric `Literal` node (for loop indices, which have no source node). */\nexport function numericLiteral(value: number): Node {\n return { type: \"Literal\", value, raw: String(value) };\n}\n\n// ── Expansion engine (U2) ─────────────────────────────────────────────────────\n\n/** Resolve an expression to a literal value (top-level consts in scope, arithmetic). */\ntype LiteralResolver = (node: Node) => number | string | boolean | undefined;\n\ninterface ExpandCtx {\n helpers: Map<string, Node>;\n timelineVar: string;\n resolve: LiteralResolver;\n depth: number;\n /** Mutable source-order counter for provenance call-site ordinals. */\n site: { n: number };\n /** Mutable counter stamping expansion order onto tweens (clones share source loc). */\n order: { n: number };\n}\n\nfunction walkNodes(node: Node, fn: (n: Node) => void): void {\n if (!isNode(node)) return;\n fn(node);\n for (const key of Object.keys(node)) {\n if (SKIP_KEYS.has(key)) continue;\n const child = node[key];\n if (Array.isArray(child)) for (const c of child) walkNodes(c, fn);\n else walkNodes(child, fn);\n }\n}\n\n/** The identifier a (possibly chained) call's member expression is rooted at. */\nfunction timelineRootName(call: Node): string | null {\n let obj = call.callee?.object;\n while (obj?.type === \"CallExpression\") obj = obj.callee?.object;\n return obj?.type === \"Identifier\" ? obj.name : null;\n}\n\nfunction isTimelineRooted(call: Node, timelineVar: string): boolean {\n if (timelineRootName(call) !== timelineVar) return false;\n return (\n call.callee?.property?.type === \"Identifier\" && GSAP_METHODS.has(call.callee.property.name)\n );\n}\n\nfunction containsTimelineCall(node: Node, timelineVar: string): boolean {\n let found = false;\n walkNodes(node, (n) => {\n if (n.type === \"CallExpression\" && isTimelineRooted(n, timelineVar)) found = true;\n });\n return found;\n}\n\nfunction rangeOf(node: Node): [number, number] | undefined {\n return typeof node.start === \"number\" && typeof node.end === \"number\"\n ? [node.start, node.end]\n : undefined;\n}\n\n/** Plain identifier params + block body (shape we can inline). Timeline content checked separately. */\nfunction isShapeEligible(fn: Node): boolean {\n return (\n isFunctionNode(fn) &&\n fn.body?.type === \"BlockStatement\" &&\n !(fn.params ?? []).some((p: Node) => p.type !== \"Identifier\")\n );\n}\n\n/** True if the subtree calls any function named in `names`. */\nfunction callsAny(node: Node, names: Set<string>): boolean {\n let hit = false;\n walkNodes(node, (n) => {\n if (\n n.type === \"CallExpression\" &&\n n.callee?.type === \"Identifier\" &&\n names.has(n.callee.name)\n ) {\n hit = true;\n }\n });\n return hit;\n}\n\n/** `[name, fnNode]` if a single-declarator `const f = fn` is an inlinable-shaped helper. */\nfunction varDeclHelper(stmt: Node): [string, Node] | null {\n if (stmt.declarations?.length !== 1) return null;\n const d = stmt.declarations[0];\n return d.id?.type === \"Identifier\" && isShapeEligible(d.init) ? [d.id.name, d.init] : null;\n}\n\n/** `[name, fnNode]` if `stmt` declares an inlinable-shaped helper, else null. */\nfunction helperFromStatement(stmt: Node): [string, Node] | null {\n if (stmt.type === \"FunctionDeclaration\") {\n return stmt.id && isShapeEligible(stmt) ? [stmt.id.name, stmt] : null;\n }\n if (stmt.type === \"VariableDeclaration\") return varDeclHelper(stmt);\n return null;\n}\n\n/** Top-level functions whose shape we can inline (Identifier params + block body). */\nfunction gatherHelperCandidates(program: Node): Map<string, Node> {\n const candidates = new Map<string, Node>();\n for (const stmt of program.body ?? []) {\n const helper = helperFromStatement(stmt);\n if (helper) candidates.set(helper[0], helper[1]);\n }\n return candidates;\n}\n\n/** Names that build the timeline directly or by calling another builder (transitive closure). */\nfunction timelineBuildingNames(candidates: Map<string, Node>, timelineVar: string): Set<string> {\n const building = new Set<string>();\n for (const [name, fn] of candidates) {\n if (containsTimelineCall(fn.body, timelineVar)) building.add(name);\n }\n for (let changed = true; changed; ) {\n changed = false;\n for (const [name, fn] of candidates) {\n if (!building.has(name) && callsAny(fn.body, building)) {\n building.add(name);\n changed = true;\n }\n }\n }\n return building;\n}\n\nfunction bump(counts: Map<string, number>, key: string): void {\n counts.set(key, (counts.get(key) ?? 0) + 1);\n}\n\n/**\n * Keep only candidates safe to drop: every reference to the name is its\n * declaration or a statement-level call. (1 decl id + 1 callee id per\n * statement-level call ⇒ total occurrences with no stray uses.)\n */\nfunction safelyDroppable(program: Node, candidates: Map<string, Node>): Map<string, Node> {\n const names = new Set(candidates.keys());\n const totalIds = new Map<string, number>();\n const stmtCalls = new Map<string, number>();\n walkNodes(program, (n) => {\n if (n.type === \"Identifier\" && names.has(n.name)) bump(totalIds, n.name);\n const e = n.type === \"ExpressionStatement\" ? n.expression : undefined;\n if (\n e?.type === \"CallExpression\" &&\n e.callee?.type === \"Identifier\" &&\n names.has(e.callee.name)\n ) {\n bump(stmtCalls, e.callee.name);\n }\n });\n const safe = new Map<string, Node>();\n for (const [name, fn] of candidates) {\n if ((totalIds.get(name) ?? 0) === 1 + (stmtCalls.get(name) ?? 0)) safe.set(name, fn);\n }\n return safe;\n}\n\n/** Top-level timeline-building helpers that are safe to inline-and-drop. */\nfunction collectInlinableHelpers(program: Node, timelineVar: string): Map<string, Node> {\n const candidates = gatherHelperCandidates(program);\n if (candidates.size === 0) return candidates;\n const building = timelineBuildingNames(candidates, timelineVar);\n for (const name of [...candidates.keys()]) if (!building.has(name)) candidates.delete(name);\n if (candidates.size === 0) return candidates;\n return safelyDroppable(program, candidates);\n}\n\nfunction isHelperDecl(stmt: Node, helpers: Map<string, Node>): boolean {\n if (stmt.type === \"FunctionDeclaration\") return !!stmt.id && helpers.get(stmt.id.name) === stmt;\n if (stmt.type === \"VariableDeclaration\" && stmt.declarations?.length === 1) {\n const d = stmt.declarations[0];\n return d.id?.type === \"Identifier\" && helpers.get(d.id.name) === d.init;\n }\n return false;\n}\n\nfunction bodyStatements(node: Node): Node[] {\n if (node?.type === \"BlockStatement\") return node.body ?? [];\n return node ? [{ type: \"ExpressionStatement\", expression: node }] : [];\n}\n\n/** Tag this body's direct timeline tweens with provenance + a monotonic expansion-order stamp. */\nfunction tagTimelineCalls(stmts: Node[], prov: GsapProvenance, ctx: ExpandCtx): void {\n for (const stmt of stmts) {\n walkNodes(stmt, (n) => {\n if (n.type === \"CallExpression\" && isTimelineRooted(n, ctx.timelineVar)) {\n tagProvenance(n, { ...prov });\n n.__hfOrder = ctx.order.n++;\n }\n });\n }\n}\n\n/** Clone a body as one scope, substitute the bindings, tag provenance, recurse. */\nfunction expandBody(\n bodyStmts: Node[],\n bindings: Map<string, Node>,\n prov: GsapProvenance,\n ctx: ExpandCtx,\n): Node[] {\n const block = substituteParams(cloneNode({ type: \"BlockStatement\", body: bodyStmts }), bindings);\n tagTimelineCalls(block.body, prov, ctx);\n return expandStatements(block.body, { ...ctx, depth: ctx.depth + 1 });\n}\n\nfunction inlineHelper(call: Node, ctx: ExpandCtx): Node[] {\n const fn = ctx.helpers.get(call.callee.name);\n const bindings = new Map<string, Node>();\n (fn.params ?? []).forEach((p: Node, i: number) => {\n const arg = call.arguments?.[i];\n if (arg) bindings.set(p.name, arg);\n });\n const prov: GsapProvenance = {\n kind: \"helper\",\n fn: call.callee.name,\n callSite: ++ctx.site.n,\n sourceRange: rangeOf(call),\n };\n return expandBody(fn.body.body, bindings, prov, ctx);\n}\n\nfunction assignStep(update: Node, resolve: LiteralResolver): number | undefined {\n if (update.operator === \"+=\") return asNum(resolve(update.right));\n if (update.operator === \"-=\") {\n const s = asNum(resolve(update.right));\n return s === undefined ? undefined : -s;\n }\n // `i = i + S` — the step is the right operand of the addition.\n if (update.operator === \"=\" && update.right?.type === \"BinaryExpression\") {\n return asNum(resolve(update.right.right));\n }\n return undefined;\n}\n\n/** The loop variable a `for` update clause mutates (`i++` or `i += S`), or null. */\nfunction updatedVarName(update: Node): string | null {\n if (update?.type === \"UpdateExpression\") return update.argument?.name ?? null;\n if (update?.type === \"AssignmentExpression\") return update.left?.name ?? null;\n return null;\n}\n\nfunction loopStep(update: Node, varName: string, resolve: LiteralResolver): number | undefined {\n if (updatedVarName(update) !== varName) return undefined;\n if (update.type === \"UpdateExpression\") return update.operator === \"++\" ? 1 : -1;\n return assignStep(update, resolve);\n}\n\nfunction asNum(v: unknown): number | undefined {\n return typeof v === \"number\" && Number.isFinite(v) ? v : undefined;\n}\n\nfunction loopSatisfied(op: string, x: number, end: number): boolean {\n if (op === \"<\") return x < end;\n if (op === \"<=\") return x <= end;\n if (op === \">\") return x > end;\n if (op === \">=\") return x >= end;\n return false;\n}\n\ninterface ForHeader {\n v: string;\n start: number;\n end: number;\n op: string;\n step: number;\n}\n\n/** The single `let v = <init>` of a for-loop init clause, or null. */\nfunction forInitVar(init: Node): { name: string; initExpr: Node } | null {\n if (init?.type !== \"VariableDeclaration\" || init.declarations?.length !== 1) return null;\n const d = init.declarations[0];\n return d.id?.type === \"Identifier\" ? { name: d.id.name, initExpr: d.init } : null;\n}\n\n/** Parse `for (let v = A; v <op> B; v += S)` into resolved bounds, or null if not statically bounded. */\nfunction parseForHeader(stmt: Node, resolve: LiteralResolver): ForHeader | null {\n const iv = forInitVar(stmt.init);\n const test = stmt.test;\n if (!iv || test?.type !== \"BinaryExpression\" || test.left?.name !== iv.name) return null;\n const start = asNum(resolve(iv.initExpr));\n const end = asNum(resolve(test.right));\n const step = loopStep(stmt.update, iv.name, resolve);\n if (start === undefined || end === undefined || !step) return null;\n return { v: iv.name, start, end, op: test.operator, step };\n}\n\nfunction unrollFor(stmt: Node, ctx: ExpandCtx): Node[] | null {\n const h = parseForHeader(stmt, ctx.resolve);\n if (!h) return null;\n const body = bodyStatements(stmt.body);\n const out: Node[] = [];\n const site = ++ctx.site.n;\n let iteration = 0;\n for (let x = h.start; loopSatisfied(h.op, x, h.end); x += h.step) {\n if (iteration >= MAX_ITERS) return null;\n const prov: GsapProvenance = {\n kind: \"loop\",\n callSite: site,\n iteration,\n sourceRange: rangeOf(stmt),\n };\n out.push(...expandBody(body, new Map([[h.v, numericLiteral(x)]]), prov, ctx));\n iteration++;\n }\n return out;\n}\n\nfunction forOfVarName(left: Node): string | null {\n if (left?.type === \"VariableDeclaration\") {\n const id = left.declarations?.[0]?.id;\n return id?.type === \"Identifier\" ? id.name : null;\n }\n return left?.type === \"Identifier\" ? left.name : null;\n}\n\n/** Expand `for (const el of [literal array]) {...}` and `[literal array].forEach((el, i) => {...})`. */\nfunction unrollOverArray(\n elements: Node[],\n body: Node[],\n elName: string | null,\n idxName: string | null,\n range: [number, number] | undefined,\n ctx: ExpandCtx,\n): Node[] {\n const out: Node[] = [];\n const site = ++ctx.site.n;\n elements.forEach((el, i) => {\n if (!el) return;\n const bindings = new Map<string, Node>();\n if (elName) bindings.set(elName, el);\n if (idxName) bindings.set(idxName, numericLiteral(i));\n const prov: GsapProvenance = { kind: \"loop\", callSite: site, iteration: i, sourceRange: range };\n out.push(...expandBody(body, bindings, prov, ctx));\n });\n return out;\n}\n\nfunction unrollForOf(stmt: Node, ctx: ExpandCtx): Node[] | null {\n if (stmt.right?.type !== \"ArrayExpression\") return null;\n const elName = forOfVarName(stmt.left);\n if (!elName) return null;\n return unrollOverArray(\n stmt.right.elements ?? [],\n bodyStatements(stmt.body),\n elName,\n null,\n rangeOf(stmt),\n ctx,\n );\n}\n\n/** The (element, index) param names of a callback, or null if either is non-Identifier. */\nfunction callbackParamNames(cb: Node): { el: string | null; idx: string | null } | null {\n const names: Array<string | null> = [];\n for (const p of [cb.params?.[0], cb.params?.[1]]) {\n if (!p) names.push(null);\n else if (p.type !== \"Identifier\") return null;\n else names.push(p.name);\n }\n return { el: names[0]!, idx: names[1]! };\n}\n\n/** True for `[arrayLiteral].forEach` member callees. */\nfunction isForEachCall(callee: Node): boolean {\n return (\n callee?.type === \"MemberExpression\" &&\n callee.property?.name === \"forEach\" &&\n callee.object?.type === \"ArrayExpression\"\n );\n}\n\n/** The element array + callback of `[...].forEach(cb)`, or null. */\nfunction forEachTarget(call: Node): { elements: Node[]; cb: Node } | null {\n if (!isForEachCall(call.callee)) return null;\n const cb = call.arguments?.[0];\n return isFunctionNode(cb) ? { elements: call.callee.object.elements ?? [], cb } : null;\n}\n\nfunction unrollForEach(call: Node, ctx: ExpandCtx): Node[] | null {\n const target = forEachTarget(call);\n if (!target) return null;\n const params = callbackParamNames(target.cb);\n if (!params) return null;\n return unrollOverArray(\n target.elements,\n bodyStatements(target.cb.body),\n params.el,\n params.idx,\n rangeOf(call),\n ctx,\n );\n}\n\nfunction expandCall(call: Node, ctx: ExpandCtx): Node[] | null {\n if (call.callee?.type === \"Identifier\" && ctx.helpers.has(call.callee.name)) {\n return inlineHelper(call, ctx);\n }\n return unrollForEach(call, ctx);\n}\n\nfunction expandStatement(stmt: Node, ctx: ExpandCtx): Node[] | null {\n if (ctx.depth >= MAX_DEPTH) return null;\n if (stmt.type === \"ForStatement\") return unrollFor(stmt, ctx);\n if (stmt.type === \"ForOfStatement\") return unrollForOf(stmt, ctx);\n if (stmt.type === \"ExpressionStatement\" && stmt.expression?.type === \"CallExpression\") {\n return expandCall(stmt.expression, ctx);\n }\n return null;\n}\n\nfunction expandStatements(stmts: Node[], ctx: ExpandCtx): Node[] {\n const out: Node[] = [];\n for (const stmt of stmts) {\n const expanded = expandStatement(stmt, ctx);\n if (expanded) out.push(...expanded);\n else out.push(stmt);\n }\n return out;\n}\n\n/**\n * Rewrite the Program body so helper invocations and bounded loops that build\n * the timeline are expanded into concrete per-call / per-iteration `tl.*`\n * statements, each tagged with provenance. Mutates `ast` in place (caller owns\n * the freshly-parsed tree). Constructs it can't statically resolve are left\n * untouched, so the parser falls back to current behavior for them.\n */\nexport function inlineComputedTimelines(\n ast: Node,\n timelineVar: string,\n resolve: LiteralResolver,\n): void {\n const helpers = collectInlinableHelpers(ast, timelineVar);\n const ctx: ExpandCtx = {\n helpers,\n timelineVar,\n resolve,\n depth: 0,\n site: { n: 0 },\n order: { n: 0 },\n };\n const body = (ast.body ?? []).filter((stmt: Node) => !isHelperDecl(stmt, helpers));\n ast.body = expandStatements(body, ctx);\n}\n","/**\n * Stable hf- element id minting (R1). Node-safe (linkedom only, not browser DOM).\n *\n * Two surfaces share these helpers:\n * - ensureHfIds(html): node-id surface — mints data-hf-id on every element.\n * - mintHfId(el, assigned): shared by htmlParser for clip ids.\n *\n * Hash is CONTENT ONLY (tag + sorted attrs + own text) — no sibling position,\n * so inserting a non-identical sibling never shifts another element's id.\n */\nimport { parseHTML } from \"linkedom\";\n\n// Non-editable / non-visual elements that should never receive a stable id.\nexport const EXCLUDED_TAGS = new Set([\n \"script\",\n \"style\",\n \"template\",\n \"meta\",\n \"link\",\n \"noscript\",\n \"base\",\n]);\n\n// 32-bit FNV-1a. Pure, deterministic, no crypto, no Math.random.\nfunction fnv1a(str: string): number {\n let h = 0x811c9dc5;\n for (let i = 0; i < str.length; i++) {\n h ^= str.charCodeAt(i);\n h = Math.imul(h, 0x01000193);\n }\n return h >>> 0;\n}\n\n// 4 base-36 chars · 36^4 ≈ 1.68M ids per document. Birthday-paradox collision\n// ≈ N²/(2·36^4): well under 1% per document after dup rehash at realistic\n// clip-model sizes (≤ a few hundred elements). The dup-rehash in mintHfId\n// resolves the rare collision; width is deliberately small for readable ids.\nfunction toHfId(hash: number): string {\n const s = (hash >>> 0).toString(36);\n // Use suffix (most-avalanched bits) for better distribution within the 4-char window.\n const four = s.length >= 4 ? s.slice(-4) : s.padStart(4, \"0\");\n return `hf-${four}`;\n}\n\n// Element's own direct text (TEXT_NODE children), not descendants'.\nfunction ownText(el: Element): string {\n let text = \"\";\n el.childNodes.forEach((n) => {\n if (n.nodeType === 3) text += (n as Text).nodeValue ?? \"\";\n });\n return text.trim();\n}\n\nfunction contentKey(el: Element): string {\n // Exclude all data-hf-* attrs (ids, studio state) — they must not influence the hash.\n // Use \\x00 / \\x01 separators (invalid in HTML attrs) to prevent ambiguous serialization.\n const attrs = Array.from(el.attributes)\n .filter((a) => !a.name.startsWith(\"data-hf-\"))\n .map((a) => `${a.name}\\x00${a.value}`)\n .sort()\n .join(\"\\x01\");\n return `${el.tagName.toLowerCase()}|${attrs}|${ownText(el)}`;\n}\n\n/**\n * Collision tiebreak for byte-identical siblings: document-order dup counter\n * (`hash(key#N)`). This IS order-dependent — two identical `<span></span>`\n * get different ids based on which comes first in the DOM. This is unavoidable:\n * unique ids for byte-identical elements require a positional signal.\n *\n * Why this is safe in practice: once `ensureHfIds` write-back persists\n * `data-hf-id` to source the attribute is physically bound to its element.\n * Reordering identical siblings carries the attribute along → zero\n * order-dependence post-persist. `ensureHfIds` skips pinned elements\n * (`if (el.getAttribute(\"data-hf-id\")) continue`), so normal operation\n * never re-exposes the ordering after first persist.\n */\n// WIRE CONTRACT: id minting is content-keyed (FNV1a of innerHTML + tag). R7's\n// preview route relies on mintHfId producing identical ids across mint contexts\n// (disk-persist pass vs. in-memory bundle pass) — see preview.test.ts\n// \"bundle returning untagged HTML gets same ids as disk\". Any change that adds\n// positional, session, or random input to the hash breaks that invariant and\n// makes hf- ids diverge between disk and served HTML, silently corrupting\n// drag-to-edit targeting.\nexport function mintHfId(el: Element, assigned: Set<string>): string {\n const key = contentKey(el);\n let id = toHfId(fnv1a(key));\n let dup = 0;\n while (assigned.has(id)) {\n dup += 1;\n // Graceful fallback instead of a hard throw: rehashing only fails to find a\n // free 4-char slot in a pathological document (~1.6M identical elements).\n // Rather than crash the whole parse, widen the id with the dup counter —\n // still deterministic and unique, just longer than the 4-char norm.\n if (dup > 10000) {\n id = `hf-${(fnv1a(key) >>> 0).toString(36)}-${dup}`;\n break;\n }\n id = toHfId(fnv1a(`${key}#${dup}`));\n }\n assigned.add(id);\n return id;\n}\n\nexport function ensureHfIds(html: string): string {\n // Mirror parseSourceDocument's fragment-wrapping so bare fragments don't land\n // outside <body> in linkedom, which would cause body.querySelectorAll to return [].\n const hasDocumentShell = /<!doctype|<html[\\s>]/i.test(html);\n const wrapped = !hasDocumentShell;\n const { document } = wrapped\n ? parseHTML(`<!DOCTYPE html><html><head></head><body>${html}</body></html>`)\n : parseHTML(html);\n const body = document.body;\n if (!body) return html;\n\n const assigned = new Set<string>();\n // Seed with already-present ids (pin) so fresh mints never collide with them.\n // Scope to <body> to match the mint walk below — a stray data-hf-id in <head>\n // must not pin an id into the set that a body element would then be bumped off.\n for (const el of Array.from(body.querySelectorAll(\"[data-hf-id]\"))) {\n const existing = el.getAttribute(\"data-hf-id\");\n if (existing) assigned.add(existing);\n }\n\n for (const el of Array.from(body.querySelectorAll(\"*\"))) {\n if (EXCLUDED_TAGS.has(el.tagName.toLowerCase())) continue;\n if (el.getAttribute(\"data-hf-id\")) continue; // pinned\n el.setAttribute(\"data-hf-id\", mintHfId(el, assigned));\n }\n\n return wrapped ? document.body.innerHTML || \"\" : document.toString();\n}\n","// ponytail: queries DOM by exact attribute match without interpolating\n// the value into a selector string — zero injection surface.\nexport function queryByAttr(\n root: ParentNode,\n attr: string,\n value: string,\n tag?: string,\n): Element | null {\n const selector = tag ? `${tag}[${attr}]` : `[${attr}]`;\n for (const el of root.querySelectorAll(selector)) {\n if (el.getAttribute(attr) === value) return el;\n }\n return null;\n}\n","// fallow-ignore-file code-duplication\n/**\n * Browser-safe GSAP write path — magic-string offset-splice.\n *\n * T6c: edits GSAP scripts by overwriting/removing byte ranges in the original\n * source. Every byte outside the edited span is preserved verbatim — no\n * pretty-printer churn. Consumes ParsedGsapAcornForWrite from gsapParserAcorn.ts.\n */\nimport MagicString from \"magic-string\";\nimport type {\n GsapAnimation,\n GsapPercentageKeyframe,\n ArcPathConfig,\n ArcPathSegment,\n} from \"./gsapSerialize.js\";\nimport {\n resolveConversionProps,\n extractArcWaypoints,\n buildMotionPathObjectCode,\n} from \"./gsapSerialize.js\";\nimport {\n parseGsapScriptAcornForWrite,\n type ParsedGsapAcornForWrite,\n type TweenCallInfo,\n} from \"./gsapParserAcorn.js\";\nimport { classifyPropertyGroup } from \"./gsapConstants.js\";\nimport type { PropertyGroupName } from \"./gsapConstants.js\";\nimport type { SplitAnimationsOptions, SplitAnimationsResult } from \"./gsapSerialize.js\";\nimport * as acornWalk from \"acorn-walk\";\n\n// acorn ESTree nodes are structurally untyped here; mirror gsapParserAcorn.ts /\n// gsapInline.ts rather than re-deriving the full ESTree union for every access.\ntype Node = any;\n\n// ── Code generation helpers ──────────────────────────────────────────────────\n\n// Local serializer for the tween-statement path, which may carry boolean/object\n// extras (stagger config). serializeValue stringifies objects to \"[object\n// Object]\", so keep this richer JSON fallback for that path. Keyframe values are\n// always number|string and use the shared serializeValue (recast parity).\nfunction valueToCode(value: unknown): string {\n if (typeof value === \"string\" && value.startsWith(\"__raw:\")) return value.slice(6);\n if (typeof value === \"string\") return JSON.stringify(value);\n if (typeof value === \"number\") return Number.isNaN(value) ? \"0\" : String(value);\n if (typeof value === \"boolean\") return String(value);\n return JSON.stringify(value);\n}\n\nfunction safeKey(key: string): string {\n return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key) ? key : JSON.stringify(key);\n}\n\n// fallow-ignore-next-line complexity\nfunction buildTweenStatementCode(timelineVar: string, anim: Omit<GsapAnimation, \"id\">): string {\n const selector = JSON.stringify(anim.targetSelector);\n const props: Record<string, number | string> = { ...anim.properties };\n if (anim.method !== \"set\" && anim.duration !== undefined) props.duration = anim.duration;\n if (anim.ease) props.ease = anim.ease;\n const entries = Object.entries(props).map(([k, v]) => `${safeKey(k)}: ${valueToCode(v)}`);\n if (anim.extras) {\n for (const [k, v] of Object.entries(anim.extras)) {\n entries.push(`${safeKey(k)}: ${valueToCode(v)}`);\n }\n }\n const objCode = `{ ${entries.join(\", \")} }`;\n const posCode = valueToCode(\n typeof anim.position === \"number\" ? anim.position : (anim.position ?? 0),\n );\n if (anim.method === \"fromTo\") {\n const fromEntries = Object.entries(anim.fromProperties ?? {}).map(\n ([k, v]) => `${safeKey(k)}: ${valueToCode(v)}`,\n );\n return `${timelineVar}.fromTo(${selector}, { ${fromEntries.join(\", \")} }, ${objCode}, ${posCode});`;\n }\n // A base `gsap.set` is off the timeline: no timeline var, no position arg.\n if (anim.method === \"set\" && anim.global) {\n return `gsap.set(${selector}, ${objCode});`;\n }\n return `${timelineVar}.${anim.method}(${selector}, ${objCode}, ${posCode});`;\n}\n\n// ── AST node helpers ─────────────────────────────────────────────────────────\n\nfunction isObjectProperty(prop: Node): boolean {\n return prop?.type === \"ObjectProperty\" || prop?.type === \"Property\";\n}\n\nfunction propKeyName(prop: Node): string | undefined {\n return prop?.key?.name ?? prop?.key?.value;\n}\n\nfunction findPropertyNode(varsArgNode: Node, key: string): Node | undefined {\n if (varsArgNode?.type !== \"ObjectExpression\") return undefined;\n for (const prop of varsArgNode.properties ?? []) {\n if (!isObjectProperty(prop)) continue;\n if (propKeyName(prop) === key) return prop;\n }\n return undefined;\n}\n\n/** The `keyframes` property's ObjectExpression value, or null when not a keyframe tween. */\nfunction keyframesObjectNode(varsNode: Node): Node | null {\n const kfProp = findPropertyNode(varsNode, \"keyframes\");\n return kfProp?.value?.type === \"ObjectExpression\" ? kfProp.value : null;\n}\n\nfunction findEnclosingExpressionStatement(ancestors: Node[]): Node | null {\n for (let i = ancestors.length - 2; i >= 0; i--) {\n if (ancestors[i]?.type === \"ExpressionStatement\") return ancestors[i];\n }\n return null;\n}\n\n/** Find the VariableDeclaration statement for `tl = gsap.timeline(...)`. */\nfunction findTimelineDeclarationStatement(ast: Node, timelineVar: string): Node | null {\n let found: Node = null;\n acornWalk.simple(ast, {\n // fallow-ignore-next-line complexity\n VariableDeclaration(node: Node) {\n if (found) return;\n for (const decl of node.declarations ?? []) {\n if (\n decl.id?.name === timelineVar &&\n decl.init?.type === \"CallExpression\" &&\n decl.init.callee?.type === \"MemberExpression\" &&\n decl.init.callee.object?.name === \"gsap\" &&\n decl.init.callee.property?.name === \"timeline\"\n ) {\n found = node;\n }\n }\n },\n });\n return found;\n}\n\n// ── Property splice helpers ───────────────────────────────────────────────────\n\n/**\n * Remove a property from a properties array, handling its comma.\n * `editableProps` must be the isObjectProperty-filtered subset in source order.\n */\nfunction removeProp(ms: MagicString, propNode: Node, editableProps: Node[]): void {\n const idx = editableProps.indexOf(propNode);\n if (idx === -1) return;\n if (editableProps.length === 1) {\n ms.remove(propNode.start, propNode.end);\n } else if (idx === 0) {\n // First prop: remove from its start to next prop start (drops trailing \", \")\n ms.remove(editableProps[0].start, editableProps[1].start);\n } else {\n // Non-first: remove from prev prop end to this prop end (drops leading \", \")\n ms.remove(editableProps[idx - 1].end, propNode.end);\n }\n}\n\n/** Serialize a vars record to an object-literal source: `{ k: v, ... }`. */\nfunction buildVarsObjectCode(record: Record<string, number | string>): string {\n const entries = Object.entries(record).map(([k, v]) => `${safeKey(k)}: ${valueToCode(v)}`);\n return entries.length > 0 ? `{ ${entries.join(\", \")} }` : \"{}\";\n}\n\n/** Overwrite a tween call's vars ObjectExpression with freshly-built source. */\nfunction overwriteVarsArg(ms: MagicString, call: TweenCallInfo, objCode: string): void {\n if (!call.varsArg) return;\n ms.overwrite(call.varsArg.start, call.varsArg.end, objCode);\n}\n\n/**\n * Update a property value if it exists, or append a new key: val before the\n * closing `}`. Call with the full ObjectExpression node.\n */\nfunction upsertProp(ms: MagicString, objNode: Node, key: string, value: unknown): void {\n if (objNode?.type !== \"ObjectExpression\") return;\n const existing = findPropertyNode(objNode, key);\n if (existing) {\n ms.overwrite(existing.value.start, existing.value.end, valueToCode(value));\n } else {\n const sep = objNode.properties.length > 0 ? \", \" : \"\";\n ms.appendLeft(objNode.end - 1, `${sep}${safeKey(key)}: ${valueToCode(value)}`);\n }\n}\n\n/**\n * Vars keys that are NOT editable transform/style props: builtins\n * (duration/ease/delay), dropped callbacks, and extras (stagger/yoyo/repeat/…).\n * The exact union of recast's BUILTIN_VAR_KEYS + DROPPED_VAR_KEYS + EXTRAS_KEYS,\n * so both writers classify vars keys identically. (Distinct from the keyframe-\n * conversion NON_EDITABLE_VAR_KEYS below, which intentionally omits `ease`\n * because that path re-emits ease separately.)\n */\nconst NON_EDITABLE_PROP_KEYS = new Set([\n \"duration\",\n \"ease\",\n \"delay\",\n \"onComplete\",\n \"onStart\",\n \"onUpdate\",\n \"onRepeat\",\n \"stagger\",\n \"yoyo\",\n \"repeat\",\n \"repeatDelay\",\n \"snap\",\n \"overwrite\",\n \"immediateRender\",\n]);\n\n/**\n * Editable transform/style key test: anything NOT a builtin, dropped callback, or\n * extras key. Mirrors recast's isEditablePropertyKey so both writers classify\n * vars keys identically.\n */\nfunction isEditableVarKey(key: string): boolean {\n return !NON_EDITABLE_PROP_KEYS.has(key);\n}\n\n/**\n * Collect verbatim `key: value` entries to PRESERVE from a vars/keyframe\n * ObjectExpression: every property whose key `drop` does not reject, sliced from\n * source — except keys present in `overrides`, whose value is replaced. Returns\n * the entries plus the set of keys it kept, so callers can append new keys.\n */\nfunction preservedEntries(\n objNode: Node,\n source: string,\n drop: (key: string) => boolean,\n overrides: Record<string, unknown>,\n): { entries: string[]; keys: Set<string> } {\n const entries: string[] = [];\n const keys = new Set<string>();\n for (const prop of objNode.properties ?? []) {\n if (!isObjectProperty(prop)) continue;\n const key = propKeyName(prop);\n if (typeof key !== \"string\" || drop(key)) continue;\n keys.add(key);\n const code =\n key in overrides\n ? valueToCode(overrides[key])\n : source.slice(prop.value.start, prop.value.end);\n entries.push(`${safeKey(key)}: ${code}`);\n }\n return { entries, keys };\n}\n\n/**\n * Replace the editable-property keys on a vars ObjectExpression with exactly\n * `newProps`, leaving non-editable keys (duration/ease/stagger/callbacks/…)\n * untouched unless overridden in `nonEditableOverrides`. Mirrors recast's\n * reconcileEditableProperties: editable keys absent from `newProps` are DROPPED,\n * not merged. Rebuilt in a single ms.overwrite so the splice can never overlap a\n * sibling edit — non-editable updates that also target this node (duration/ease/\n * extras) are folded into the same rebuild rather than spliced separately.\n */\nfunction reconcileEditableProps(\n ms: MagicString,\n objNode: Node,\n source: string,\n newProps: Record<string, number | string>,\n nonEditableOverrides?: Record<string, unknown>,\n): void {\n if (objNode?.type !== \"ObjectExpression\") return;\n const overrides = nonEditableOverrides ?? {};\n const { entries, keys } = preservedEntries(objNode, source, isEditableVarKey, overrides);\n for (const [key, value] of Object.entries(overrides)) {\n if (!keys.has(key)) entries.push(`${safeKey(key)}: ${valueToCode(value)}`);\n }\n for (const [key, value] of Object.entries(newProps)) {\n entries.push(`${safeKey(key)}: ${valueToCode(value)}`);\n }\n ms.overwrite(objNode.start, objNode.end, `{ ${entries.join(\", \")} }`);\n}\n\n// ── Insertion helpers ─────────────────────────────────────────────────────────\n\n/** Traverse callee.object chain to check if a call ultimately roots at timelineVar. */\nfunction isTimelineRooted(node: Node, timelineVar: string): boolean {\n if (node?.type === \"Identifier\") return node.name === timelineVar;\n if (node?.type === \"CallExpression\") return isTimelineRooted(node.callee?.object, timelineVar);\n return false;\n}\n\n/**\n * Find the byte offset after which to insert a new statement (tween or label).\n * Returns null when no timeline declaration exists in the script — callers must\n * not emit `tl.xxx()` calls in that case as `tl` would be undefined at render.\n */\nfunction findInsertionPoint(parsed: ParsedGsapAcornForWrite): number | null {\n const lastLocated = parsed.located[parsed.located.length - 1];\n if (lastLocated) {\n const lastCall = lastLocated.call;\n const exprStmt = findEnclosingExpressionStatement(lastCall.ancestors);\n return exprStmt?.end ?? lastCall.node.end;\n }\n if (!parsed.hasTimeline) return null;\n const tlDecl = findTimelineDeclarationStatement(parsed.ast, parsed.timelineVar);\n return tlDecl?.end ?? (parsed.ast.end as number);\n}\n\n// ── Public write API ─────────────────────────────────────────────────────────\n\n// fallow-ignore-next-line complexity\nexport function updateAnimationInScript(\n script: string,\n animationId: string,\n updates: Partial<GsapAnimation> & { easeEach?: string; resetKeyframeEases?: boolean },\n): string {\n if (!Object.keys(updates).length) return script;\n const parsed = parseGsapScriptAcornForWrite(script);\n if (!parsed) return script;\n const target = parsed.located.find((l) => l.id === animationId);\n if (!target) return script;\n\n const ms = new MagicString(script);\n const { call }: { call: TweenCallInfo } = target;\n\n // When `properties` is present we REPLACE the editable set (recast parity:\n // editable keys absent from the update are dropped). Fold any concurrent\n // non-editable updates (duration/ease/extras) into the single varsArg rebuild\n // so their splices can't overlap the rebuild's overwrite of the whole node.\n if (updates.properties) {\n const overrides: Record<string, unknown> = {};\n if (updates.duration !== undefined) overrides.duration = updates.duration;\n if (updates.ease !== undefined) overrides.ease = updates.ease;\n if (updates.extras) Object.assign(overrides, updates.extras);\n reconcileEditableProps(ms, call.varsArg, script, updates.properties, overrides);\n } else {\n if (updates.duration !== undefined) {\n upsertProp(ms, call.varsArg, \"duration\", updates.duration);\n }\n const easeValue = updates.easeEach ?? updates.ease;\n if (easeValue !== undefined) {\n const kfNode = keyframesObjectNode(call.varsArg);\n if (kfNode) {\n upsertProp(ms, kfNode, \"easeEach\", easeValue);\n // \"Apply to all segments\": drop every per-keyframe `ease` override so the\n // single easeEach governs all segments uniformly (AE select-all + F9).\n if (updates.resetKeyframeEases) {\n for (const kfEntry of kfNode.properties ?? []) {\n if (!isObjectProperty(kfEntry)) continue;\n const val = kfEntry.value;\n if (val?.type !== \"ObjectExpression\") continue;\n const easeNode = findPropertyNode(val, \"ease\");\n if (easeNode) removeProp(ms, easeNode, val.properties);\n }\n }\n } else {\n upsertProp(ms, call.varsArg, \"ease\", easeValue);\n }\n }\n if (updates.extras) {\n for (const [key, value] of Object.entries(updates.extras)) {\n upsertProp(ms, call.varsArg, key, value);\n }\n }\n }\n\n if (updates.fromProperties && call.method === \"fromTo\" && call.fromArg) {\n // fromTo's from-vars carry only editable props — REPLACE them too (recast\n // parity). fromArg is a distinct node from varsArg, so this rebuild never\n // overlaps the varsArg edits above.\n reconcileEditableProps(ms, call.fromArg, script, updates.fromProperties);\n }\n\n if (updates.position !== undefined) {\n overwritePosition(ms, call, updates.position);\n }\n\n return ms.toString();\n}\n\n/**\n * Overwrite a tween call's numeric position argument (the positionArg the parser\n * located: 3rd arg for fromTo, else 2nd), or append one when the call has no\n * explicit position. Shared by updateAnimationInScript and the\n * shift/scalePositionsInScript timeline ops.\n */\nfunction overwritePosition(ms: MagicString, call: TweenCallInfo, position: number | string): void {\n if (call.positionArg) {\n ms.overwrite(call.positionArg.start, call.positionArg.end, valueToCode(position));\n } else {\n ms.appendLeft(call.node.end - 1, `, ${valueToCode(position)}`);\n }\n}\n\n/**\n * Shift every tween targeting `targetSelector` by `delta` seconds (clamped ≥0),\n * rewriting each call's position argument. Mirrors recast's shiftPositionsInScript\n * (used by timeline clip-move to keep GSAP positions in sync with the clip start).\n */\nexport function shiftPositionsInScript(\n script: string,\n targetSelector: string,\n delta: number,\n): string {\n const parsed = parseGsapScriptAcornForWrite(script);\n if (!parsed) return script;\n const ms = new MagicString(script);\n let changed = false;\n for (const entry of parsed.located) {\n if (entry.animation.targetSelector !== targetSelector) continue;\n if (typeof entry.animation.position !== \"number\") continue;\n const newPos = Math.max(0, Math.round((entry.animation.position + delta) * 1000) / 1000);\n overwritePosition(ms, entry.call, newPos);\n changed = true;\n }\n return changed ? ms.toString() : script;\n}\n\n/**\n * Linearly remap every tween targeting `targetSelector` from the old clip\n * [oldStart, oldDuration] onto the new [newStart, newDuration] (position and,\n * when present, duration scaled by the duration ratio). Mirrors recast's\n * scalePositionsInScript (used by timeline clip-resize).\n */\nexport function scalePositionsInScript(\n script: string,\n targetSelector: string,\n oldStart: number,\n oldDuration: number,\n newStart: number,\n newDuration: number,\n): string {\n if (oldDuration <= 0 || newDuration <= 0) return script;\n const ratio = newDuration / oldDuration;\n const parsed = parseGsapScriptAcornForWrite(script);\n if (!parsed) return script;\n const ms = new MagicString(script);\n let changed = false;\n for (const entry of parsed.located) {\n if (entry.animation.targetSelector !== targetSelector) continue;\n if (typeof entry.animation.position !== \"number\") continue;\n const newPos = Math.max(\n 0,\n Math.round((newStart + (entry.animation.position - oldStart) * ratio) * 1000) / 1000,\n );\n overwritePosition(ms, entry.call, newPos);\n if (typeof entry.animation.duration === \"number\" && entry.animation.duration > 0) {\n const newDur = Math.max(0.001, Math.round(entry.animation.duration * ratio * 1000) / 1000);\n upsertProp(ms, entry.call.varsArg, \"duration\", newDur);\n }\n changed = true;\n }\n return changed ? ms.toString() : script;\n}\n\nexport function addAnimationToScript(\n script: string,\n animation: Omit<GsapAnimation, \"id\">,\n): { script: string; id: string } {\n const parsed = parseGsapScriptAcornForWrite(script);\n if (!parsed) return { script, id: \"\" };\n\n const insertionPoint = findInsertionPoint(parsed);\n if (insertionPoint === null) return { script, id: \"\" };\n\n const ms = new MagicString(script);\n const statementCode = buildTweenStatementCode(parsed.timelineVar, animation);\n ms.appendLeft(insertionPoint, \"\\n\" + statementCode);\n\n const result = ms.toString();\n const reParsed = parseGsapScriptAcornForWrite(result);\n const newId = reParsed?.located[reParsed.located.length - 1]?.id ?? \"\";\n return { script: result, id: newId };\n}\n\nexport function removeAnimationFromScript(script: string, animationId: string): string {\n const parsed = parseGsapScriptAcornForWrite(script);\n if (!parsed) return script;\n const target = parsed.located.find((l) => l.id === animationId);\n if (!target) return script;\n\n const ms = new MagicString(script);\n const N = target.call.node;\n const exprStmt = findEnclosingExpressionStatement(target.call.ancestors);\n\n if (N.callee?.object?.type !== \"CallExpression\" && exprStmt?.expression === N) {\n // Standalone `tl.method(...)` — remove the whole ExpressionStatement\n const end =\n exprStmt.end < script.length && script[exprStmt.end] === \"\\n\"\n ? exprStmt.end + 1\n : exprStmt.end;\n ms.remove(exprStmt.start, end);\n } else {\n // Chain link — splice out `.method(args)` from N.callee.object.end to N.end\n ms.remove(N.callee.object.end, N.end);\n }\n\n return ms.toString();\n}\n\n// ── Flat-tween → keyframes conversion ──────────────────────────────────────────\n//\n// Mirror recast's convertToKeyframesInScript: when the first keyframe op lands\n// on a flat to()/from()/fromTo() tween, rewrite its vars object to\n// `{ keyframes: { \"0%\": {from}, \"100%\": {to} }, <preserved non-editable keys>,\n// ease: \"none\"? }` and convert from()/fromTo() to to(). We rebuild the whole\n// vars ObjectExpression in one ms.overwrite (single-edit-per-node), so the next\n// keyframe-add re-parses cleanly.\n\n// Identity value for an editable transform/style prop (recast's CSS_IDENTITY).\nconst CSS_IDENTITY: Record<string, number> = {\n opacity: 1,\n autoAlpha: 1,\n scale: 1,\n scaleX: 1,\n scaleY: 1,\n};\n\nfunction cssIdentityValue(prop: string): number {\n return CSS_IDENTITY[prop] ?? 0;\n}\n\n// Keys NOT in the editable set — preserved verbatim on the converted vars object\n// (matches the parser's classification: builtin/dropped/extras keys).\nconst NON_EDITABLE_VAR_KEYS = new Set([\n \"duration\",\n \"delay\",\n \"onComplete\",\n \"onStart\",\n \"onUpdate\",\n \"onRepeat\",\n \"stagger\",\n \"yoyo\",\n \"repeat\",\n \"repeatDelay\",\n \"snap\",\n \"overwrite\",\n \"immediateRender\",\n]);\n\n/** The CSS-identity counterpart of a props record (numbers → identity value). */\nfunction identityProps(\n properties: Record<string, number | string>,\n): Record<string, number | string> {\n const identity: Record<string, number | string> = {};\n for (const [k, v] of Object.entries(properties)) {\n if (v != null) identity[k] = typeof v === \"number\" ? cssIdentityValue(k) : v;\n }\n return identity;\n}\n\n/** Resolve the 0%/100% endpoint records for a tween being converted. */\nfunction conversionEndpoints(animation: GsapAnimation): {\n fromProps: Record<string, number | string>;\n toProps: Record<string, number | string>;\n} {\n if (animation.method === \"from\") {\n return { fromProps: { ...animation.properties }, toProps: identityProps(animation.properties) };\n }\n if (animation.method === \"fromTo\") {\n return {\n fromProps: { ...(animation.fromProperties ?? {}) },\n toProps: { ...animation.properties },\n };\n }\n // to(): 0% is the CSS identity state, 100% is the authored props.\n return { fromProps: identityProps(animation.properties), toProps: { ...animation.properties } };\n}\n\n/** Collect preserved (non-editable) `key: value` entries from the original vars node. */\nfunction preservedVarsEntries(varsNode: Node, source: string): string[] {\n const entries: string[] = [];\n if (varsNode?.type !== \"ObjectExpression\") return entries;\n for (const prop of varsNode.properties ?? []) {\n if (!isObjectProperty(prop)) continue;\n const key = propKeyName(prop);\n if (typeof key !== \"string\" || !NON_EDITABLE_VAR_KEYS.has(key)) continue;\n entries.push(`${safeKey(key)}: ${source.slice(prop.value.start, prop.value.end)}`);\n }\n return entries;\n}\n\n/** Build the rebuilt vars-object code for a converted flat tween. */\nfunction buildConvertedVarsCode(animation: GsapAnimation, varsNode: Node, source: string): string {\n const { fromProps, toProps } = conversionEndpoints(animation);\n const easeEach = animation.ease;\n const easeEachEntry = easeEach ? `, easeEach: ${JSON.stringify(easeEach)}` : \"\";\n const kfCode = `{ \"0%\": ${recordToCode(fromProps)}, \"100%\": ${recordToCode(toProps)}${easeEachEntry} }`;\n const entries = [`keyframes: ${kfCode}`, ...preservedVarsEntries(varsNode, source)];\n if (easeEach) entries.push(`ease: \"none\"`);\n return `{ ${entries.join(\", \")} }`;\n}\n\n/** Rename a from()/fromTo() call to to(), dropping fromTo's leading from-vars arg. */\nfunction convertMethodToTo(\n ms: MagicString,\n animation: GsapAnimation,\n call: Node,\n varsNode: Node,\n): void {\n if (animation.method !== \"from\" && animation.method !== \"fromTo\") return;\n const calleeProp = call.node.callee?.property;\n if (calleeProp) ms.overwrite(calleeProp.start, calleeProp.end, \"to\");\n // Remove the from-vars arg and its trailing separator up to the to-vars arg.\n if (animation.method === \"fromTo\" && call.fromArg) ms.remove(call.fromArg.start, varsNode.start);\n}\n\nfunction convertFlatTweenToKeyframes(script: string, target: Node): string {\n const animation: GsapAnimation = target.animation;\n if (animation.keyframes || animation.method === \"set\") return script;\n const call = target.call;\n const varsNode = call.varsArg;\n if (varsNode?.type !== \"ObjectExpression\") return script;\n\n const ms = new MagicString(script);\n ms.overwrite(varsNode.start, varsNode.end, buildConvertedVarsCode(animation, varsNode, script));\n convertMethodToTo(ms, animation, call, varsNode);\n return ms.toString();\n}\n\n// ── Keyframe write ops ────────────────────────────────────────────────────────\n//\n// Design: mirror the recast writer's rebuild-the-node model. The recast writer\n// mutates AST nodes in place and re-prints, so it never has an offset-overlap\n// problem. Here we instead compute the FINAL property record for every keyframe\n// value node that must change (the target merge, `_auto` endpoint sync, and\n// backfilled siblings) against the ORIGINAL parsed AST, then emit exactly ONE\n// `ms.overwrite(valueNode.start, valueNode.end, code)` per changed node (and a\n// single insert for a brand-new key). No node is ever both overwritten and\n// appended into, so the splices can never overlap.\n\nconst PERCENTAGE_KEY_RE = /^(\\d+(?:\\.\\d+)?)%$/;\n\n// Matches recast's PCT_TOLERANCE: percentages within 2 of an existing key are\n// treated as the same keyframe (merge), not a new insert.\nconst PCT_TOLERANCE = 2;\n\nfunction percentageFromKey(key: string): number {\n const m = PERCENTAGE_KEY_RE.exec(key);\n return m ? Number.parseFloat(m[1] ?? \"0\") : Number.NaN;\n}\n\n/** Serialize a final keyframe property record (number|string values) to code. */\nfunction recordToCode(record: Record<string, number | string>): string {\n const entries = Object.entries(record).map(([k, v]) => `${safeKey(k)}: ${valueToCode(v)}`);\n return `{ ${entries.join(\", \")} }`;\n}\n\n/** Percentage-keyed property nodes of a keyframes ObjectExpression, in source order. */\nfunction percentagePropsOf(kfNode: Node): Node[] {\n return (kfNode.properties ?? []).filter((p: Node) => {\n if (!isObjectProperty(p)) return false;\n const key = propKeyName(p);\n return typeof key === \"string\" && PERCENTAGE_KEY_RE.test(key);\n });\n}\n\nconst LITERAL_NODE_TYPES = new Set([\"Literal\", \"NumericLiteral\", \"StringLiteral\"]);\n\n/** Read one value node: a number/string literal, a negative number, or raw source. */\n// fallow-ignore-next-line complexity\nfunction readValueNode(v: Node, source: string): number | string {\n if (\n LITERAL_NODE_TYPES.has(v?.type) &&\n (typeof v.value === \"number\" || typeof v.value === \"string\")\n ) {\n return v.value;\n }\n if (\n v?.type === \"UnaryExpression\" &&\n v.operator === \"-\" &&\n typeof v.argument?.value === \"number\"\n ) {\n return -v.argument.value;\n }\n return `__raw:${source.slice(v.start, v.end)}`;\n}\n\n/**\n * Read a keyframe value ObjectExpression into a record, mirroring the parser's\n * `objectExpressionToRecord`: literals resolve to their value; anything else is\n * preserved as `__raw:<source>` so serializeValue round-trips it verbatim.\n * Keyframe values are literals in practice, so the raw fallback is rarely hit.\n */\nfunction valueNodeToRecord(valueNode: Node, source: string): Record<string, number | string> {\n const record: Record<string, number | string> = {};\n if (valueNode?.type !== \"ObjectExpression\") return record;\n for (const prop of valueNode.properties ?? []) {\n if (!isObjectProperty(prop)) continue;\n const key = propKeyName(prop);\n if (typeof key !== \"string\") continue;\n record[key] = readValueNode(prop.value, source);\n }\n return record;\n}\n\n/** True when a keyframe value record carries the synthetic `_auto` marker. */\nfunction recordHasAuto(record: Record<string, number | string>): boolean {\n return \"_auto\" in record;\n}\n\n/**\n * Compute `_auto` endpoint overwrites: when the new keyframe is the immediate\n * neighbor of an `_auto` 0% or 100% endpoint, that endpoint is rewritten to\n * `{ ...newProps, _auto: 1 }`. Only fires for interior keyframes. Returns the\n * percentage→overwrite map so the caller can fold these into the per-node final\n * records (never a separate splice).\n */\nfunction autoEndpointOverwrites(\n kfNode: Node,\n source: string,\n percentage: number,\n properties: Record<string, number | string>,\n): Map<any, Record<string, number | string>> {\n const result = new Map<any, Record<string, number | string>>();\n if (percentage <= 0 || percentage >= 100) return result;\n const pctProps = percentagePropsOf(kfNode);\n const allPcts = pctProps\n .map((p: Node) => percentageFromKey(propKeyName(p) ?? \"\"))\n .filter((n: number) => !Number.isNaN(n) && n !== percentage)\n .sort((a: number, b: number) => a - b);\n const leftNeighbor = allPcts.filter((p: number) => p < percentage).pop();\n const rightNeighbor = allPcts.find((p: number) => p > percentage);\n for (const endPct of [0, 100]) {\n const isNeighbor = endPct === 0 ? leftNeighbor === 0 : rightNeighbor === 100;\n if (!isNeighbor) continue;\n const endProp = pctProps.find((p: Node) => percentageFromKey(propKeyName(p) ?? \"\") === endPct);\n if (!endProp) continue;\n const rec = valueNodeToRecord(endProp.value, source);\n if (!recordHasAuto(rec)) continue;\n result.set(endProp, { ...properties, _auto: 1 });\n }\n return result;\n}\n\nfunction findKfPropByPct(kfNode: Node, percentage: number): { prop: Node; idx: number } | null {\n // Match the CLOSEST keyframe within tolerance, not the first one within range.\n // Keyframes at e.g. 0/49/50/100 are all valid (the SDK dedups to a unique\n // match at TOLERANCE=0.001 upstream); picking the first-within-PCT_TOLERANCE=2\n // would hit 49% when the caller meant 50%. Tie-break on the earliest index so\n // the choice stays deterministic.\n const props = kfNode.properties ?? [];\n let best: { prop: Node; idx: number } | null = null;\n let bestDist = Number.POSITIVE_INFINITY;\n for (let i = 0; i < props.length; i++) {\n const prop = props[i];\n if (!isObjectProperty(prop)) continue;\n const key = propKeyName(prop);\n if (typeof key !== \"string\") continue;\n const dist = Math.abs(percentageFromKey(key) - percentage);\n if (dist <= PCT_TOLERANCE && dist < bestDist) {\n best = { prop, idx: i };\n bestDist = dist;\n }\n }\n return best;\n}\n\nexport function updateKeyframeInScript(\n script: string,\n animationId: string,\n percentage: number,\n properties: Record<string, number | string>,\n ease?: string,\n): string {\n const parsed = parseGsapScriptAcornForWrite(script);\n if (!parsed) return script;\n const target = parsed.located.find((l) => l.id === animationId);\n if (!target) return script;\n\n const kfPropNode = findPropertyNode(target.call.varsArg, \"keyframes\");\n if (!kfPropNode) return script;\n\n // Array-form keyframes (`keyframes: [{x,y}, ...]`) carry no explicit percentages\n // — GSAP distributes them evenly, and the runtime read assigns even percentages\n // (0, 100/(n-1), …). Map the percentage back to an array index and overwrite that\n // element in place (preserving the array form). Without this the function bailed\n // on the ObjectExpression check, so dragging a motion-path node on an array-form\n // tween committed nothing (server no-op).\n if (kfPropNode.value?.type === \"ArrayExpression\") {\n return updateArrayKeyframeByPct(script, kfPropNode.value, percentage, properties, ease);\n }\n if (kfPropNode.value?.type !== \"ObjectExpression\") return script;\n\n const match = findKfPropByPct(kfPropNode.value, percentage);\n if (!match) return script;\n\n const record: Record<string, number | string> = { ...properties };\n if (ease) record.ease = ease;\n const ms = new MagicString(script);\n ms.overwrite(match.prop.value.start, match.prop.value.end, recordToCode(record));\n return ms.toString();\n}\n\n// ponytail: even-spacing index map; if array keyframes ever carry per-element\n// `duration`, switch to matching the closest cumulative position.\nfunction updateArrayKeyframeByPct(\n script: string,\n arrayNode: Node,\n percentage: number,\n properties: Record<string, number | string>,\n ease?: string,\n): string {\n const elements = ((arrayNode.elements ?? []) as Array<Node | null>).filter(\n (el): el is Node => !!el && el.type === \"ObjectExpression\",\n );\n const n = elements.length;\n if (n === 0) return script;\n const idx = n > 1 ? Math.round((percentage / 100) * (n - 1)) : 0;\n const el = elements[Math.max(0, Math.min(n - 1, idx))];\n if (!el) return script;\n const merged: Record<string, number | string> = {\n ...valueNodeToRecord(el, script),\n ...properties,\n };\n if (ease) merged.ease = ease;\n const ms = new MagicString(script);\n ms.overwrite(el.start, el.end, recordToCode(merged));\n return ms.toString();\n}\n\n/**\n * Build the final property record for the keyframe at `percentage`. If a\n * keyframe already exists there, MERGE the new props over the existing record\n * (preserve untouched props, preserve `_auto`, preserve the existing per-keyframe\n * ease when the op omits one); otherwise it's just the new props.\n */\nfunction buildTargetRecord(\n existing: { prop: Node; idx: number } | null,\n source: string,\n properties: Record<string, number | string>,\n ease: string | undefined,\n): Record<string, number | string> {\n if (!existing || existing.prop.value?.type !== \"ObjectExpression\") {\n const record: Record<string, number | string> = { ...properties };\n if (ease) record.ease = ease;\n return record;\n }\n const existingRecord = valueNodeToRecord(existing.prop.value, source);\n const existingEase = typeof existingRecord.ease === \"string\" ? existingRecord.ease : undefined;\n const merged: Record<string, number | string> = { ...existingRecord };\n for (const [k, v] of Object.entries(properties)) merged[k] = v;\n const finalEase = ease ?? existingEase;\n if (finalEase) merged.ease = finalEase;\n else delete merged.ease;\n return merged;\n}\n\n/**\n * Compute the backfilled final record for one sibling keyframe: append any of\n * `newPropKeys` it's missing, using the backfill default. Returns null when\n * nothing changes (so the caller emits no overwrite for it).\n */\nfunction backfilledSiblingRecord(\n valueNode: Node,\n source: string,\n newPropKeys: string[],\n backfillDefaults: Record<string, number | string>,\n): Record<string, number | string> | null {\n if (valueNode?.type !== \"ObjectExpression\") return null;\n const record = valueNodeToRecord(valueNode, source);\n let changed = false;\n for (const pk of newPropKeys) {\n const defaultVal = backfillDefaults[pk];\n if (pk in record || defaultVal == null) continue;\n record[pk] = defaultVal;\n changed = true;\n }\n return changed ? record : null;\n}\n\n/** A located tween whose varsArg has a static keyframes ObjectExpression, or null. */\nfunction locateWithKeyframes(\n script: string,\n animationId: string,\n): { script: string; parsed: ParsedGsapAcornForWrite; target: Node; kfNode: Node } | null {\n const parsed = parseGsapScriptAcornForWrite(script);\n if (!parsed) return null;\n // Converting from()/fromTo() to to() rewrites the content-derived id; match\n // recast's locateAnimationWithFallback by remapping the method segment.\n const convertedId = animationId.replace(/-from-|-fromTo-/, \"-to-\");\n const target =\n parsed.located.find((l) => l.id === animationId) ??\n parsed.located.find((l) => l.id === convertedId);\n if (!target) return null;\n const kfPropNode = findPropertyNode(target.call.varsArg, \"keyframes\");\n if (!kfPropNode || kfPropNode.value?.type !== \"ObjectExpression\") return null;\n return { script, parsed, target, kfNode: kfPropNode.value };\n}\n\n/** Locate a tween's keyframes object, converting a flat tween first if absent. */\n// Array-form keyframes (`keyframes: [{x,y}, …]`) → even-percentage object form\n// (`{ \"0%\": {…}, \"33.3%\": {…}, … }`). Inserting a keyframe needs percentage keys,\n// which an even array can't host. Runtime-identical; mirrors the recast path.\nfunction convertArrayKeyframesToObject(script: string, target: Node): string {\n const kfPropNode = findPropertyNode(target.call.varsArg, \"keyframes\");\n if (!kfPropNode || kfPropNode.value?.type !== \"ArrayExpression\") return script;\n const els = ((kfPropNode.value.elements ?? []) as Array<Node | null>).filter(\n (el): el is Node => !!el && el.type === \"ObjectExpression\",\n );\n const n = els.length;\n if (n === 0) return script;\n const entries = els.map((el, i) => {\n const pct = n > 1 ? Math.round((i / (n - 1)) * 1000) / 10 : 0;\n return `${JSON.stringify(`${pct}%`)}: ${script.slice(el.start, el.end)}`;\n });\n const ms = new MagicString(script);\n ms.overwrite(kfPropNode.value.start, kfPropNode.value.end, `{ ${entries.join(\", \")} }`);\n return ms.toString();\n}\n\nfunction ensureKeyframesNode(\n script: string,\n animationId: string,\n): { script: string; parsed: ParsedGsapAcornForWrite; target: Node; kfNode: Node } | null {\n const direct = locateWithKeyframes(script, animationId);\n if (direct) return direct;\n\n const parsed = parseGsapScriptAcornForWrite(script);\n const target = parsed?.located.find((l) => l.id === animationId);\n if (!target) return null;\n\n // Array-form keyframes → normalize to object form, then re-locate.\n const kfProp = findPropertyNode(target.call.varsArg, \"keyframes\");\n if (kfProp?.value?.type === \"ArrayExpression\") {\n const normalized = convertArrayKeyframesToObject(script, target);\n if (normalized !== script) return locateWithKeyframes(normalized, animationId);\n return null;\n }\n\n // No static keyframes object — convert the flat tween, then re-locate.\n const converted = convertFlatTweenToKeyframes(script, target);\n if (converted === script) return null;\n return locateWithKeyframes(converted, animationId);\n}\n\n/**\n * Compute the sibling keyframe nodes that need a backfilled prop, excluding the\n * target keyframe and any node already being overwritten as an `_auto` endpoint.\n */\nfunction collectBackfillOverwrites(\n kfNode: Node,\n src: string,\n properties: Record<string, number | string>,\n backfillDefaults: Record<string, number | string> | undefined,\n skip: { existingProp: Node; endpoints: Map<any, unknown> },\n): Map<any, Record<string, number | string>> {\n const result = new Map<any, Record<string, number | string>>();\n if (!backfillDefaults) return result;\n const newPropKeys = Object.keys(properties);\n for (const prop of percentagePropsOf(kfNode)) {\n if (prop === skip.existingProp || skip.endpoints.has(prop)) continue;\n const rec = backfilledSiblingRecord(prop.value, src, newPropKeys, backfillDefaults);\n if (rec) result.set(prop, rec);\n }\n return result;\n}\n\nexport function addKeyframeToScript(\n script: string,\n animationId: string,\n percentage: number,\n properties: Record<string, number | string>,\n ease?: string,\n backfillDefaults?: Record<string, number | string>,\n): string {\n const located = ensureKeyframesNode(script, animationId);\n if (!located) return script;\n const { script: src, kfNode } = located;\n\n const existing = findKfPropByPct(kfNode, percentage);\n\n // Final record for the target keyframe (merge if it already exists).\n const targetRecord = buildTargetRecord(existing, src, properties, ease);\n // `_auto` endpoint syncs fire only on new inserts; a merge landing ON an\n // endpoint already preserves `_auto` via buildTargetRecord.\n const endpointOverwrites = existing\n ? new Map<any, Record<string, number | string>>()\n : autoEndpointOverwrites(kfNode, src, percentage, properties);\n // Backfilled siblings (each node changes at most once).\n const backfillOverwrites = collectBackfillOverwrites(kfNode, src, properties, backfillDefaults, {\n existingProp: existing?.prop,\n endpoints: endpointOverwrites,\n });\n\n // Emit exactly one overwrite per changed node, plus one insert for a new key.\n const ms = new MagicString(src);\n if (existing) {\n // Merge into the existing keyframe at this percentage, preserving sibling\n // properties — overwrite only the given keys. (A whole-value overwrite here\n // would silently drop other properties already keyframed at this percent.)\n if (existing.prop.value?.type === \"ObjectExpression\") {\n for (const [k, v] of Object.entries(properties)) {\n upsertProp(ms, existing.prop.value, k, v);\n }\n if (ease !== undefined) upsertProp(ms, existing.prop.value, \"ease\", ease);\n } else {\n ms.overwrite(existing.prop.value.start, existing.prop.value.end, recordToCode(targetRecord));\n }\n } else {\n insertNewKeyframe(ms, kfNode, percentage, `${percentage}%`, recordToCode(targetRecord));\n }\n for (const [prop, rec] of [...endpointOverwrites, ...backfillOverwrites]) {\n ms.overwrite(prop.value.start, prop.value.end, recordToCode(rec));\n }\n\n return ms.toString();\n}\n\n/** Insert a brand-new `\"pct%\": {...}` property in sorted order. */\nfunction insertNewKeyframe(\n ms: MagicString,\n kfNode: Node,\n percentage: number,\n pctKey: string,\n valueCode: string,\n): void {\n const allProps = (kfNode.properties ?? []).filter((p: Node) => isObjectProperty(p));\n let insertBeforeProp: Node = null;\n for (const prop of allProps) {\n const key = propKeyName(prop);\n if (typeof key === \"string\" && percentageFromKey(key) > percentage) {\n insertBeforeProp = prop;\n break;\n }\n }\n if (insertBeforeProp) {\n ms.appendLeft(insertBeforeProp.start, `${JSON.stringify(pctKey)}: ${valueCode}, `);\n } else {\n const sep = allProps.length > 0 ? \", \" : \"\";\n ms.appendLeft(kfNode.end - 1, `${sep}${JSON.stringify(pctKey)}: ${valueCode}`);\n }\n}\n\n/**\n * Rebuild a vars ObjectExpression that has just dropped below two keyframes,\n * collapsing `keyframes: {…}` back to a flat tween. Mirrors recast's\n * collapseKeyframesToFlat: drop the `keyframes` + `easeEach` keys, preserve every\n * other vars key verbatim, and splice the remaining keyframe's properties (minus\n * its per-keyframe `ease`) in as flat vars keys. Single ms.overwrite of the whole\n * vars node so the splice can't overlap the keyframe removal.\n */\nfunction collapseKeyframesToFlat(\n ms: MagicString,\n varsNode: Node,\n source: string,\n remainingRecord: Record<string, number | string>,\n): void {\n if (varsNode?.type !== \"ObjectExpression\") return;\n const dropKeyframeKeys = (key: string) => key === \"keyframes\" || key === \"easeEach\";\n const { entries } = preservedEntries(varsNode, source, dropKeyframeKeys, {});\n for (const [k, v] of Object.entries(remainingRecord)) {\n if (k !== \"ease\") entries.push(`${safeKey(k)}: ${valueToCode(v)}`);\n }\n ms.overwrite(varsNode.start, varsNode.end, `{ ${entries.join(\", \")} }`);\n}\n\n/** Implicit tween-relative percentage of array-form keyframe index `i` of `n`\n * (GSAP distributes array keyframes evenly: 0%, 1/(n-1), …, 100%). */\nfunction arrayKeyframePct(i: number, n: number): number {\n return n > 1 ? (i / (n - 1)) * 100 : 0;\n}\n\n// Array-form keyframes (`keyframes: [{x,y}, …]`) carry no explicit percentages —\n// GSAP distributes them evenly. removeKeyframeFromScript only handled the\n// object-form (`keyframes: { \"50%\": {…} }`), so removing from an array-form tween\n// was a silent no-op (and the downstream hold-sync then stranded an `hf-hold`).\n// Resolve the element by its implicit percentage and splice it out; collapse to a\n// flat tween when fewer than two remain (parity with the object-form path).\nfunction removeArrayKeyframe(\n ms: MagicString,\n varsArg: Node,\n arrNode: Node,\n script: string,\n percentage: number,\n): boolean {\n const elements: Node[] = (arrNode.elements ?? []).filter(\n (e: Node | null): e is Node => !!e && e.type === \"ObjectExpression\",\n );\n const n = elements.length;\n if (n === 0) return false;\n\n let matchIdx = -1;\n let bestDist = Number.POSITIVE_INFINITY;\n for (let i = 0; i < n; i++) {\n const dist = Math.abs(arrayKeyframePct(i, n) - percentage);\n if (dist <= PCT_TOLERANCE && dist < bestDist) {\n matchIdx = i;\n bestDist = dist;\n }\n }\n if (matchIdx === -1) return false;\n\n const remaining = elements.filter((_, i) => i !== matchIdx);\n if (remaining.length < 2) {\n const sole = remaining[0];\n const record = sole ? valueNodeToRecord(sole, script) : {};\n collapseKeyframesToFlat(ms, varsArg, script, record);\n return true;\n }\n removeProp(ms, elements[matchIdx], elements);\n return true;\n}\n\nexport function removeKeyframeFromScript(\n script: string,\n animationId: string,\n percentage: number,\n): string {\n const parsed = parseGsapScriptAcornForWrite(script);\n if (!parsed) return script;\n const target = parsed.located.find((l) => l.id === animationId);\n if (!target) return script;\n\n const kfPropNode = findPropertyNode(target.call.varsArg, \"keyframes\");\n if (!kfPropNode) return script;\n\n if (kfPropNode.value?.type === \"ArrayExpression\") {\n const ms = new MagicString(script);\n return removeArrayKeyframe(ms, target.call.varsArg, kfPropNode.value, script, percentage)\n ? ms.toString()\n : script;\n }\n\n if (kfPropNode.value?.type !== \"ObjectExpression\") return script;\n const kfNode = kfPropNode.value;\n\n const match = findKfPropByPct(kfNode, percentage);\n if (!match) return script;\n\n const ms = new MagicString(script);\n\n // If removing this keyframe leaves fewer than two, collapse the keyframes\n // object back to a flat tween (recast parity) instead of leaving a lone\n // keyframe. We rebuild the whole vars node, so we never also splice the kf\n // node — the two edits would overlap.\n const remaining = percentagePropsOf(kfNode).filter((p) => p !== match.prop);\n if (remaining.length < 2) {\n const sole = remaining[0];\n const record = sole ? valueNodeToRecord(sole.value, script) : {};\n collapseKeyframesToFlat(ms, target.call.varsArg, script, record);\n return ms.toString();\n }\n\n const allProps = (kfNode.properties ?? []).filter((p: Node) => isObjectProperty(p));\n removeProp(ms, match.prop, allProps);\n return ms.toString();\n}\n\nexport function removePropertyFromAnimation(\n script: string,\n animationId: string,\n property: string,\n from = false,\n): string {\n const parsed = parseGsapScriptAcornForWrite(script);\n if (!parsed) return script;\n const target = parsed.located.find((l) => l.id === animationId);\n if (!target) return script;\n const { call } = target;\n const objNode = from ? (call.method === \"fromTo\" ? call.fromArg : null) : call.varsArg;\n if (!objNode) return script;\n const propNode = findPropertyNode(objNode, property);\n if (!propNode) return script;\n const allProps = (objNode.properties ?? []).filter((p: Node) => isObjectProperty(p));\n const ms = new MagicString(script);\n removeProp(ms, propNode, allProps);\n return ms.toString();\n}\n\n/**\n * Remove all keyframes from a tween, collapsing to a flat tween with one\n * keyframe's properties: the first for `from()`, the last otherwise (the\n * destination = the visible resting state).\n */\nexport function removeAllKeyframesFromScript(script: string, animationId: string): string {\n const parsed = parseGsapScriptAcornForWrite(script);\n if (!parsed) return script;\n const target = parsed.located.find((l) => l.id === animationId);\n if (!target) return script;\n const kfs = target.animation.keyframes?.keyframes;\n if (!kfs || kfs.length === 0) return script;\n\n const sorted = [...kfs].sort((a, b) => a.percentage - b.percentage);\n const collapse = target.call.method === \"from\" ? sorted[0] : sorted[sorted.length - 1];\n if (!collapse) return script;\n\n const ms = new MagicString(script);\n overwriteVarsArg(\n ms,\n target.call,\n buildVarsObjectCode(buildCollapsedFlatVars(target.animation, collapse)),\n );\n return ms.toString();\n}\n\n// Flat vars for a tween collapsing its keyframes onto one stop: existing\n// top-level props, then the collapse keyframe's props (skip per-keyframe\n// `ease`), then duration/ease/extras. Drops keyframes + easeEach by omission.\nfunction buildCollapsedFlatVars(\n animation: GsapAnimation,\n collapse: { properties: Record<string, number | string> },\n): Record<string, number | string> {\n const flat: Record<string, number | string> = { ...animation.properties };\n for (const [k, v] of Object.entries(collapse.properties)) {\n if (k !== \"ease\") flat[k] = v;\n }\n if (animation.duration !== undefined) flat.duration = animation.duration;\n if (animation.ease) flat.ease = animation.ease;\n for (const [k, v] of Object.entries(animation.extras ?? {})) {\n if (typeof v === \"number\" || typeof v === \"string\") flat[k] = v;\n }\n return flat;\n}\n\n/** Build the full replacement vars object for a tween being converted to keyframes. */\nfunction buildKeyframesVarsCode(\n animation: GsapAnimation,\n fromProps: Record<string, number | string>,\n toProps: Record<string, number | string>,\n varsNode: Node,\n source: string,\n setDuration?: number,\n): string {\n const fromEntries = Object.entries(fromProps).map(([k, v]) => `${safeKey(k)}: ${valueToCode(v)}`);\n const toEntries = Object.entries(toProps).map(([k, v]) => `${safeKey(k)}: ${valueToCode(v)}`);\n const easeEntry = animation.ease ? `, easeEach: ${JSON.stringify(animation.ease)}` : \"\";\n const kfCode = `{ \"0%\": { ${fromEntries.join(\", \")} }, \"100%\": { ${toEntries.join(\", \")} }${easeEntry} }`;\n // Preserve every non-editable key (duration/delay/callbacks/stagger/yoyo/…)\n // verbatim from source — rebuilding from the animation object alone dropped\n // `delay` (not a GsapAnimation field), shifting the tween's start time.\n let preserved = preservedVarsEntries(varsNode, source);\n // Converting a static `set` → drop its hold markers and give it a real duration\n // so the keyframes span time.\n if (setDuration !== undefined) {\n preserved = preserved.filter((e) => !/^\\s*(immediateRender|data|duration)\\s*:/.test(e));\n }\n const parts: string[] = [`keyframes: ${kfCode}`, ...preserved];\n if (setDuration !== undefined) parts.push(`duration: ${Math.max(0.001, setDuration)}`);\n if (animation.ease) parts.push(`ease: \"none\"`);\n return `{ ${parts.join(\", \")} }`;\n}\n\n/**\n * Convert a flat tween (to/from/fromTo) to percentage-keyframes format.\n * `resolvedFromValues` supplies the current DOM state: overrides the 0% endpoint\n * for `to()`, the 100% endpoint for `from()`, or merges into toProps for `fromTo()`.\n */\nexport function convertToKeyframesFromScript(\n script: string,\n animationId: string,\n resolvedFromValues?: Record<string, number | string>,\n setDuration = 1,\n): string {\n const parsed = parseGsapScriptAcornForWrite(script);\n if (!parsed) return script;\n const target = parsed.located.find((l) => l.id === animationId);\n if (!target) return script;\n const { animation, call } = target;\n if (animation.keyframes) return script;\n const isSet = call.method === \"set\";\n\n const { fromProps, toProps } = resolveConversionProps(animation, resolvedFromValues);\n const ms = new MagicString(script);\n\n // A GLOBAL `gsap.set(...)` is off-timeline; rewriting only the method emits\n // `gsap.to(...)`, which fires once at load and isn't on the paused master\n // timeline (the engine can't seek/render it). Re-root onto the timeline var\n // and add the position arg the set lacks so the converted tween is seekable.\n if (isSet && animation.global) {\n const calleeObj = call.node.callee.object;\n if (calleeObj?.type === \"Identifier\") {\n ms.overwrite(calleeObj.start, calleeObj.end, parsed.timelineVar);\n }\n const args = call.node.arguments;\n if (args.length > 0 && args.length < 3) {\n ms.appendLeft(args[args.length - 1].end, \", 0\");\n }\n }\n\n // set/from/fromTo all become `to`; fromTo also drops its `from` argument.\n if (call.method === \"from\" || call.method === \"fromTo\" || isSet) {\n ms.overwrite(call.node.callee.property.start, call.node.callee.property.end, \"to\");\n }\n if (call.method === \"fromTo\" && call.fromArg) {\n ms.remove(call.fromArg.start, call.varsArg.start);\n }\n overwriteVarsArg(\n ms,\n call,\n buildKeyframesVarsCode(\n animation,\n fromProps,\n toProps,\n call.varsArg,\n script,\n isSet ? setDuration : undefined,\n ),\n );\n\n return ms.toString();\n}\n\n// ── Keyframe-object code builder ─────────────────────────────────────────────\n\n/** Build a percentage-keyframes object literal: `{ \"0%\": { x: 0 }, \"100%\": { x: 100 } }`. */\nfunction buildKeyframeObjectCode(\n keyframes: Array<{\n percentage: number;\n properties: Record<string, number | string>;\n ease?: string;\n auto?: boolean;\n }>,\n easeEach?: string,\n): string {\n const entries = keyframes.map((kf) => {\n const props = Object.entries(kf.properties).map(([k, v]) => `${safeKey(k)}: ${valueToCode(v)}`);\n if (kf.ease) props.push(`ease: ${JSON.stringify(kf.ease)}`);\n if (kf.auto) props.push(`_auto: 1`);\n return `${JSON.stringify(`${kf.percentage}%`)}: { ${props.join(\", \")} }`;\n });\n if (easeEach) entries.push(`easeEach: ${JSON.stringify(easeEach)}`);\n return `{ ${entries.join(\", \")} }`;\n}\n\n// ── Materialize keyframes ────────────────────────────────────────────────────\n\n/**\n * Replace a dynamic or static keyframes expression with a fully-resolved\n * percentage-keyframes object. Called when a user first edits a dynamically-\n * generated keyframe in the studio so it becomes statically editable.\n */\nexport function materializeKeyframesFromScript(\n script: string,\n animationId: string,\n keyframes: Array<{\n percentage: number;\n properties: Record<string, number | string>;\n ease?: string;\n }>,\n easeEach?: string,\n resolvedSelector?: string,\n): string {\n // An empty keyframe list has no materialized form — rebuilding vars with an\n // empty keyframes object would empty the animation. No-op instead.\n if (keyframes.length === 0) return script;\n const parsed = parseGsapScriptAcornForWrite(script);\n if (!parsed) return script;\n const target = parsed.located.find((l) => l.id === animationId);\n if (!target) return script;\n\n const { call } = target;\n const sorted = [...keyframes].sort((a, b) => a.percentage - b.percentage);\n const kfObjCode = buildKeyframeObjectCode(sorted, easeEach);\n const ms = new MagicString(script);\n\n if (resolvedSelector) {\n const selectorArg = call.node.arguments[0];\n if (selectorArg)\n ms.overwrite(selectorArg.start, selectorArg.end, JSON.stringify(resolvedSelector));\n }\n\n const kfProp = findPropertyNode(call.varsArg, \"keyframes\");\n if (kfProp) {\n ms.overwrite(kfProp.value.start, kfProp.value.end, kfObjCode);\n } else if (call.varsArg?.type === \"ObjectExpression\") {\n const vars = call.varsArg;\n if (vars.properties.length > 0) {\n ms.prependLeft(vars.properties[0].start, `keyframes: ${kfObjCode}, `);\n } else {\n ms.appendLeft(vars.end - 1, `keyframes: ${kfObjCode}`);\n }\n }\n\n const eachProp = findPropertyNode(call.varsArg, \"easeEach\");\n if (eachProp) {\n const allProps = (call.varsArg.properties ?? []).filter((p: Node) => isObjectProperty(p));\n removeProp(ms, eachProp, allProps);\n }\n\n return ms.toString();\n}\n\n// ── Add animation with keyframes ──────────────────────────────────────────────\n\n/** Insert a new keyframed `to()` call and return the new animation ID. */\nexport function addAnimationWithKeyframesToScript(\n script: string,\n targetSelector: string,\n position: number,\n duration: number,\n keyframes: Array<{\n percentage: number;\n properties: Record<string, number | string>;\n ease?: string;\n auto?: boolean;\n }>,\n ease?: string,\n easeEach?: string,\n): { script: string; id: string } {\n const parsed = parseGsapScriptAcornForWrite(script);\n if (!parsed) return { script, id: \"\" };\n const insertionPoint = findInsertionPoint(parsed);\n if (insertionPoint === null) return { script, id: \"\" };\n\n const sorted = [...keyframes].sort((a, b) => a.percentage - b.percentage);\n const kfObjCode = buildKeyframeObjectCode(sorted, easeEach);\n const varParts = [`keyframes: ${kfObjCode}`, `duration: ${valueToCode(duration)}`];\n if (ease) varParts.push(`ease: ${JSON.stringify(ease)}`);\n const stmtCode = `${parsed.timelineVar}.to(${JSON.stringify(targetSelector)}, { ${varParts.join(\", \")} }, ${valueToCode(position)});`;\n\n const ms = new MagicString(script);\n ms.appendLeft(insertionPoint, \"\\n\" + stmtCode);\n\n const result = ms.toString();\n const reParsed = parseGsapScriptAcornForWrite(result);\n const newId = reParsed?.located[reParsed.located.length - 1]?.id ?? \"\";\n return { script: result, id: newId };\n}\n\n// ── Split into property groups ────────────────────────────────────────────────\n\nfunction collectPropertyKeys(anim: GsapAnimation): Set<string> {\n const keys = new Set<string>();\n if (anim.keyframes) {\n for (const kf of anim.keyframes.keyframes) {\n for (const k of Object.keys(kf.properties)) keys.add(k);\n }\n } else {\n for (const k of Object.keys(anim.properties)) keys.add(k);\n }\n return keys;\n}\n\nfunction partitionPropertyGroups(keys: Set<string>): Map<PropertyGroupName, string[]> {\n const groups = new Map<PropertyGroupName, string[]>();\n for (const key of keys) {\n if (key === \"transformOrigin\") continue;\n const group = classifyPropertyGroup(key);\n let arr = groups.get(group);\n if (!arr) {\n arr = [];\n groups.set(group, arr);\n }\n arr.push(key);\n }\n return groups;\n}\n\nfunction assignTransformOrigin(groupProps: Map<PropertyGroupName, string[]>): void {\n let largestGroup: PropertyGroupName | undefined;\n let largestCount = 0;\n for (const [group, props] of groupProps) {\n if (props.length > largestCount) {\n largestCount = props.length;\n largestGroup = group;\n }\n }\n const largest = largestGroup ? groupProps.get(largestGroup) : undefined;\n if (largest) largest.push(\"transformOrigin\");\n}\n\nfunction filterGroupKeyframes(\n kfs: GsapPercentageKeyframe[],\n propSet: Set<string>,\n): Array<{ percentage: number; properties: Record<string, number | string>; ease?: string }> {\n const result: Array<{\n percentage: number;\n properties: Record<string, number | string>;\n ease?: string;\n }> = [];\n for (const kf of kfs) {\n const filtered: Record<string, number | string> = {};\n for (const [k, v] of Object.entries(kf.properties)) {\n if (propSet.has(k)) filtered[k] = v;\n }\n if (Object.keys(filtered).length > 0) {\n result.push({\n percentage: kf.percentage,\n properties: filtered,\n ...(kf.ease ? { ease: kf.ease } : {}),\n });\n }\n }\n return result;\n}\n\nfunction filterGroupProperties(\n properties: Record<string, number | string>,\n propSet: Set<string>,\n): Record<string, number | string> {\n const result: Record<string, number | string> = {};\n for (const [k, v] of Object.entries(properties)) {\n if (propSet.has(k)) result[k] = v;\n }\n return result;\n}\n\nfunction addGroupAnimToScript(\n script: string,\n anim: GsapAnimation,\n propSet: Set<string>,\n): { script: string; id: string } {\n if (anim.keyframes) {\n const groupKeyframes = filterGroupKeyframes(anim.keyframes.keyframes, propSet);\n if (groupKeyframes.length === 0) return { script, id: \"\" };\n const pos = typeof anim.position === \"number\" ? anim.position : 0;\n return addAnimationWithKeyframesToScript(\n script,\n anim.targetSelector,\n pos,\n anim.duration ?? 0.5,\n groupKeyframes,\n anim.keyframes.easeEach ?? anim.ease,\n );\n }\n const groupProperties = filterGroupProperties(anim.properties, propSet);\n if (Object.keys(groupProperties).length === 0) return { script, id: \"\" };\n const fromProperties =\n anim.method === \"fromTo\" && anim.fromProperties\n ? filterGroupProperties(anim.fromProperties, propSet)\n : undefined;\n return addAnimationToScript(script, {\n targetSelector: anim.targetSelector,\n method: anim.method,\n position: anim.position,\n duration: anim.duration,\n ease: anim.ease,\n properties: groupProperties,\n fromProperties,\n extras: anim.extras,\n });\n}\n\n/**\n * Split a mixed-property tween into one tween per property group (position,\n * scale, visual, etc.) so each group can be edited independently.\n * Returns the updated script and the IDs of the newly-created tweens.\n */\nexport function splitIntoPropertyGroupsFromScript(\n script: string,\n animationId: string,\n): { script: string; ids: string[] } {\n const parsed = parseGsapScriptAcornForWrite(script);\n if (!parsed) return { script, ids: [animationId] };\n const target = parsed.located.find((l) => l.id === animationId);\n if (!target) return { script, ids: [animationId] };\n const { animation } = target;\n\n const allPropKeys = collectPropertyKeys(animation);\n const groupProps = partitionPropertyGroups(allPropKeys);\n if (groupProps.size <= 1) return { script, ids: [animationId] };\n if (allPropKeys.has(\"transformOrigin\")) assignTransformOrigin(groupProps);\n\n let result = removeAnimationFromScript(script, animationId);\n for (const [, props] of groupProps) {\n const { script: next, id } = addGroupAnimToScript(result, animation, new Set(props));\n if (id) result = next;\n }\n\n const reParsed = parseGsapScriptAcornForWrite(result);\n const newIds = (reParsed?.located ?? [])\n .filter((l) => l.animation.targetSelector === animation.targetSelector)\n .map((l) => l.id);\n return { script: result, ids: newIds };\n}\n\n// ── Label write ops ───────────────────────────────────────────────────────────\n\n/** True when `expr` is `tl.<method>(…)` rooted at the timeline var. */\nfunction isTimelineMethodCall(expr: Node, timelineVar: string, method: string): boolean {\n return (\n expr?.type === \"CallExpression\" &&\n expr.callee?.type === \"MemberExpression\" &&\n isTimelineRooted(expr.callee.object, timelineVar) &&\n expr.callee.property?.name === method\n );\n}\n\n/** True when `expr` is `tl.addLabel(\"<name>\", …)` rooted at the timeline var. */\nfunction isAddLabelCall(expr: Node, timelineVar: string, name: string): boolean {\n const firstArg = expr?.arguments?.[0];\n return (\n isTimelineMethodCall(expr, timelineVar, \"addLabel\") &&\n firstArg?.type === \"Literal\" &&\n firstArg.value === name\n );\n}\n\n/** Every `tl.addLabel(\"<name>\", …)` ExpressionStatement in the script. */\nfunction findLabelStatements(parsed: ParsedGsapAcornForWrite, name: string): Node[] {\n const targets: Node[] = [];\n acornWalk.simple(parsed.ast, {\n ExpressionStatement(node: Node) {\n if (isAddLabelCall(node.expression, parsed.timelineVar, name)) targets.push(node);\n },\n });\n return targets;\n}\n\nexport function addLabelToScript(script: string, name: string, position: number): string {\n const parsed = parseGsapScriptAcornForWrite(script);\n if (!parsed) return script;\n\n // If the label already exists, MOVE it (overwrite its position) rather than\n // appending a duplicate. Two same-named addLabel statements make removeLabel\n // over-remove — it deletes every match, including a pre-existing label the\n // user never touched.\n const existing = findLabelStatements(parsed, name)[0];\n if (existing) {\n const ms = new MagicString(script);\n const posArg = existing.expression.arguments?.[1];\n if (posArg) ms.overwrite(posArg.start, posArg.end, valueToCode(position));\n else ms.appendLeft(existing.expression.end - 1, `, ${valueToCode(position)}`);\n return ms.toString();\n }\n\n const insertionPoint = findInsertionPoint(parsed);\n if (insertionPoint === null) return script;\n\n const ms = new MagicString(script);\n const labelCode = `${parsed.timelineVar}.addLabel(${JSON.stringify(name)}, ${valueToCode(position)});`;\n ms.appendLeft(insertionPoint, \"\\n\" + labelCode);\n return ms.toString();\n}\n\nexport function removeLabelFromScript(script: string, name: string): string {\n const parsed = parseGsapScriptAcornForWrite(script);\n if (!parsed) return script;\n\n const targets = findLabelStatements(parsed, name);\n if (!targets.length) return script;\n\n const ms = new MagicString(script);\n for (const target of targets) {\n const end =\n target.end < script.length && script[target.end] === \"\\n\" ? target.end + 1 : target.end;\n ms.remove(target.start, end);\n }\n return ms.toString();\n}\n\n// ── Arc path helpers ─────────────────────────────────────────────────────────\n\n/**\n * Remove a set of properties from an ObjectExpression in a single pass.\n * Groups consecutive marked props into blocks to avoid overlapping remove ranges.\n */\nfunction removePropsByKey(ms: MagicString, objNode: Node, keys: Set<string>): void {\n if (objNode?.type !== \"ObjectExpression\") return;\n const allProps = (objNode.properties ?? []).filter(isObjectProperty);\n const marked = allProps.map((p: Node) => keys.has(propKeyName(p) ?? \"\"));\n let i = 0;\n while (i < allProps.length) {\n if (!marked[i]) {\n i++;\n continue;\n }\n const blockStart = i;\n while (i < allProps.length && marked[i]) i++;\n ms.remove(...blockRemoveRange(allProps, blockStart, i));\n }\n}\n\nfunction blockRemoveRange(\n allProps: Node[],\n blockStart: number,\n blockEnd: number,\n): [number, number] {\n if (blockStart === 0 && blockEnd === allProps.length)\n return [allProps[0].start, allProps[allProps.length - 1].end];\n if (blockStart === 0) return [allProps[0].start, allProps[blockEnd].start];\n return [allProps[blockStart - 1].end, allProps[blockEnd - 1].end];\n}\n\n// fallow-ignore-next-line complexity\nfunction readLastWaypointXY(mpVal: Node): { x: number | null; y: number | null } {\n if (mpVal?.type !== \"ObjectExpression\") return { x: null, y: null };\n const pathProp = findPropertyNode(mpVal, \"path\");\n if (pathProp?.value?.type !== \"ArrayExpression\") return { x: null, y: null };\n const elems: Node[] = pathProp.value.elements ?? [];\n const last = elems[elems.length - 1];\n if (last?.type !== \"ObjectExpression\") return { x: null, y: null };\n return {\n x: readNumericLiteralNode(findPropertyNode(last, \"x\")?.value),\n y: readNumericLiteralNode(findPropertyNode(last, \"y\")?.value),\n };\n}\n\n/**\n * Read a numeric value node — a plain numeric literal or a unary-minus negative\n * literal (e.g. `-120`). Returns null for anything non-numeric. Without the\n * UnaryExpression branch, negative waypoint coords (parsed as a UnaryExpression\n * with no `.value`) would be lost when disabling an arc path.\n */\nfunction readNumericLiteralNode(v: Node): number | null {\n if (LITERAL_NODE_TYPES.has(v?.type) && typeof v.value === \"number\") return v.value;\n if (\n v?.type === \"UnaryExpression\" &&\n v.operator === \"-\" &&\n typeof v.argument?.value === \"number\"\n ) {\n return -v.argument.value;\n }\n return null;\n}\n\nfunction disableArcPath(ms: MagicString, call: TweenCallInfo): boolean {\n const mpProp = findPropertyNode(call.varsArg, \"motionPath\");\n if (!mpProp) return false;\n const { x, y } = readLastWaypointXY(mpProp.value);\n if (x === null && y === null) {\n const allProps = (call.varsArg.properties ?? []).filter(isObjectProperty);\n removeProp(ms, mpProp, allProps);\n return true;\n }\n // Overwrite the entire motionPath property with the recovered x/y pair — avoids\n // the appendLeft+remove range-boundary issue in MagicString.\n const parts: string[] = [];\n if (x !== null) parts.push(`x: ${x}`);\n if (y !== null) parts.push(`y: ${y}`);\n ms.overwrite(mpProp.start, mpProp.end, parts.join(\", \"));\n return true;\n}\n\nfunction stripXYFromKeyframes(ms: MagicString, kfPropNode: Node): void {\n if (kfPropNode?.value?.type !== \"ObjectExpression\") return;\n const xyKeys = new Set([\"x\", \"y\"]);\n for (const pctProp of (kfPropNode.value.properties ?? []).filter(isObjectProperty)) {\n const k = propKeyName(pctProp);\n if (typeof k === \"string\" && k.endsWith(\"%\") && pctProp.value?.type === \"ObjectExpression\") {\n removePropsByKey(ms, pctProp.value, xyKeys);\n }\n }\n}\n\nfunction enableArcPath(\n ms: MagicString,\n call: TweenCallInfo,\n animation: GsapAnimation,\n config: ArcPathConfig,\n): boolean {\n const waypoints = extractArcWaypoints(animation);\n if (waypoints.length < 2) return false;\n const segments: ArcPathSegment[] =\n config.segments.length === waypoints.length - 1\n ? config.segments\n : Array.from({ length: waypoints.length - 1 }, () => ({ curviness: 1 }));\n const motionPathCode = buildMotionPathObjectCode({\n waypoints,\n segments,\n autoRotate: config.autoRotate,\n });\n const vars = call.varsArg;\n if (vars?.type !== \"ObjectExpression\") return false;\n // Insert motionPath right after the opening `{` (appendRight at start+1) so the\n // insertion point can never coincide with the end boundary of the x/y removal\n // range. upsertProp would appendLeft at `end - 1`, which collides with a\n // remove-range that ends at the same offset when x/y are the only props —\n // MagicString then discards the append and the output loses everything.\n const editable = (vars.properties ?? []).filter(isObjectProperty);\n const survivesRemoval = editable.some((p: Node) => {\n const k = propKeyName(p);\n return k !== \"x\" && k !== \"y\";\n });\n const sep = survivesRemoval ? \", \" : \"\";\n ms.appendRight(vars.start + 1, ` motionPath: ${motionPathCode}${sep}`);\n stripXYFromKeyframes(ms, findPropertyNode(call.varsArg, \"keyframes\"));\n removePropsByKey(ms, call.varsArg, new Set([\"x\", \"y\"]));\n return true;\n}\n\nexport function setArcPathInScript(\n script: string,\n animationId: string,\n config: ArcPathConfig,\n): string {\n const parsed = parseGsapScriptAcornForWrite(script);\n if (!parsed) return script;\n const target = parsed.located.find((l) => l.id === animationId);\n if (!target) return script;\n const ms = new MagicString(script);\n const handled = config.enabled\n ? enableArcPath(ms, target.call, target.animation, config)\n : disableArcPath(ms, target.call);\n return handled ? ms.toString() : script;\n}\n\nexport function updateArcSegmentInScript(\n script: string,\n animationId: string,\n segmentIndex: number,\n update: Partial<ArcPathSegment>,\n): string {\n const parsed = parseGsapScriptAcornForWrite(script);\n if (!parsed) return script;\n const target = parsed.located.find((l) => l.id === animationId);\n if (!target) return script;\n\n const { call, animation } = target;\n if (!animation.arcPath?.enabled) return script;\n\n const segments = [...animation.arcPath.segments];\n const existingSeg = segments[segmentIndex];\n if (segmentIndex < 0 || segmentIndex >= segments.length || !existingSeg) return script;\n\n segments[segmentIndex] = { ...existingSeg, ...update };\n\n const waypoints = extractArcWaypoints(animation);\n if (waypoints.length < 2) return script;\n\n const motionPathCode = buildMotionPathObjectCode({\n waypoints,\n segments,\n autoRotate: animation.arcPath.autoRotate,\n });\n\n const mpProp = findPropertyNode(call.varsArg, \"motionPath\");\n if (!mpProp) return script;\n\n const ms = new MagicString(script);\n ms.overwrite(mpProp.value.start, mpProp.value.end, motionPathCode);\n return ms.toString();\n}\n\nexport function removeArcPathFromScript(script: string, animationId: string): string {\n return setArcPathInScript(script, animationId, {\n enabled: false,\n autoRotate: false,\n segments: [],\n });\n}\n\n// ── splitAnimationsInScript helpers ──────────────────────────────────────────\n\n/** Overwrite the selector (first arg) of a tween call. */\nfunction updateAnimationSelectorInScript(\n script: string,\n animationId: string,\n newSelector: string,\n): string {\n const parsed = parseGsapScriptAcornForWrite(script);\n if (!parsed) return script;\n const target = parsed.located.find((l) => l.id === animationId);\n if (!target) return script;\n const selectorArg = target.call.node.arguments?.[0];\n if (!selectorArg) return script;\n const ms = new MagicString(script);\n ms.overwrite(selectorArg.start, selectorArg.end, JSON.stringify(newSelector));\n return ms.toString();\n}\n\n/**\n * Insert a `tl.set()` call immediately after the timeline declaration\n * (before existing tweens) to establish inherited state on a new element.\n */\nfunction insertInheritedStateSetInScript(\n script: string,\n selector: string,\n position: number,\n properties: Record<string, number | string>,\n): string {\n const parsed = parseGsapScriptAcornForWrite(script);\n if (!parsed) return script;\n const props = Object.entries(properties)\n .map(([k, v]) => `${safeKey(k)}: ${valueToCode(v)}`)\n .join(\", \");\n const code = `${parsed.timelineVar}.set(${JSON.stringify(selector)}, { ${props} }, ${position});`;\n const ms = new MagicString(script);\n const tlDecl = findTimelineDeclarationStatement(parsed.ast, parsed.timelineVar);\n const firstLocated = parsed.located[0];\n if (tlDecl) {\n ms.appendLeft(tlDecl.end, \"\\n\" + code);\n } else if (firstLocated) {\n const firstCall = firstLocated.call;\n const exprStmt = findEnclosingExpressionStatement(firstCall.ancestors);\n const insertAt = exprStmt?.start ?? firstCall.node.start;\n ms.prependLeft(insertAt, code + \"\\n\");\n } else {\n ms.append(\"\\n\" + code);\n }\n return ms.toString();\n}\n\n/**\n * Compute, in forward (timeline) order, the inherited-props baseline available\n * BEFORE each matching tween, plus the final cumulative state at the split point.\n * A tween contributes to later baselines when it ends at/before the split (full\n * props or last keyframe), spans the split via keyframes (kfs at/before split),\n * or spans the split as a flat tween (its interpolated midpoint). Decoupled from\n * the reverse write loop so the spanning-tween midpoint reads earlier tweens.\n */\n// fallow-ignore-next-line complexity\nfunction computeForwardBaselines(\n matching: GsapAnimation[],\n splitTime: number,\n): { before: Array<Record<string, number | string>>; final: Record<string, number | string> } {\n const before: Array<Record<string, number | string>> = [];\n const acc: Record<string, number | string> = {};\n for (const anim of matching) {\n before.push({ ...acc });\n const pos = typeof anim.position === \"number\" ? anim.position : 0;\n const dur = anim.duration ?? 0;\n const animEnd = pos + dur;\n\n if (anim.keyframes) {\n const kfs = anim.keyframes.keyframes;\n if (pos >= splitTime) {\n // Moves wholly to the new element — contributes nothing to the baseline.\n } else if (animEnd > splitTime) {\n for (const kf of kfs) {\n const kfTime = pos + (kf.percentage / 100) * dur;\n if (kfTime <= splitTime) {\n for (const [k, v] of Object.entries(kf.properties)) acc[k] = v;\n }\n }\n } else {\n const lastKf = kfs[kfs.length - 1];\n if (lastKf) {\n for (const [k, v] of Object.entries(lastKf.properties)) acc[k] = v;\n }\n }\n continue;\n }\n\n if (animEnd <= splitTime) {\n for (const [k, v] of Object.entries(anim.properties)) acc[k] = v;\n continue;\n }\n\n if (pos >= splitTime) continue;\n\n // Flat tween spanning the split — its midpoint becomes the inherited value.\n const progress = dur > 0 ? (splitTime - pos) / dur : 0;\n const fromSource = anim.fromProperties ?? acc;\n for (const [k, v] of Object.entries(anim.properties)) {\n if (typeof v !== \"number\") {\n acc[k] = v;\n continue;\n }\n const fromVal = typeof fromSource[k] === \"number\" ? (fromSource[k] as number) : 0;\n acc[k] = fromVal + (v - fromVal) * progress;\n }\n }\n return { before, final: { ...acc } };\n}\n\n// Split one tween that straddles the split point: trim the original to the\n// first half (interpolated midpoint as its new end) and add a fromTo for the\n// second half on the new element. `fromSource` is the forward baseline.\nfunction buildSpanningSplit(\n result: string,\n anim: GsapAnimation,\n pos: number,\n dur: number,\n fromSource: Record<string, number | string>,\n ctx: { splitTime: number; newSelector: string; newElementStart: number },\n): string {\n const progress = dur > 0 ? (ctx.splitTime - pos) / dur : 0;\n const midProps: Record<string, number | string> = {};\n for (const [k, v] of Object.entries(anim.properties)) {\n if (typeof v !== \"number\") {\n midProps[k] = v;\n continue;\n }\n const fromVal = typeof fromSource[k] === \"number\" ? (fromSource[k] as number) : 0;\n midProps[k] = fromVal + (v - fromVal) * progress;\n }\n const trimmed = updateAnimationInScript(result, anim.id, {\n duration: ctx.splitTime - pos,\n properties: midProps,\n });\n return addAnimationToScript(trimmed, {\n targetSelector: ctx.newSelector,\n method: \"fromTo\",\n position: ctx.newElementStart,\n duration: pos + dur - ctx.splitTime,\n properties: { ...anim.properties },\n fromProperties: { ...midProps },\n ease: anim.ease,\n extras: anim.extras,\n }).script;\n}\n\ntype SplitCtx = {\n splitTime: number;\n originalSelector: string;\n newSelector: string;\n newElementStart: number;\n};\n\n// Decide what one matching tween does at the split point: move to the new\n// element (wholly after), stay (wholly before / keyframes before), get skipped\n// (keyframes spanning), or get interpolated in half (spanning). Returns the\n// updated script; pushes any skip reason into `skippedSelectors`.\nfunction applyTweenSplit(\n result: string,\n anim: GsapAnimation,\n baselineBefore: Record<string, number | string>,\n ctx: SplitCtx,\n skippedSelectors: string[],\n): string {\n const pos = typeof anim.position === \"number\" ? anim.position : 0;\n const dur = anim.duration ?? 0;\n const animEnd = pos + dur;\n\n if (anim.keyframes) {\n if (pos >= ctx.splitTime)\n return updateAnimationSelectorInScript(result, anim.id, ctx.newSelector);\n if (animEnd > ctx.splitTime) {\n skippedSelectors.push(`${ctx.originalSelector} (keyframes spanning split)`);\n }\n // Inherited-state for kf tweens is handled by computeForwardBaselines.\n return result;\n }\n // Wholly before the split — kept on the original element.\n if (animEnd <= ctx.splitTime) return result;\n // Wholly after — move to the new element.\n if (pos >= ctx.splitTime)\n return updateAnimationSelectorInScript(result, anim.id, ctx.newSelector);\n // Spans the split — interpolate the midpoint from the FORWARD baseline.\n const fromSource = anim.fromProperties ?? baselineBefore;\n return buildSpanningSplit(result, anim, pos, dur, fromSource, ctx);\n}\n\nexport function splitAnimationsInScript(\n script: string,\n opts: SplitAnimationsOptions,\n): SplitAnimationsResult {\n const parsed = parseGsapScriptAcornForWrite(script);\n if (!parsed) return { script, skippedSelectors: [] };\n\n const originalSelector = `#${opts.originalId}`;\n const newSelector = `#${opts.newId}`;\n\n const animations = parsed.located.map((l) => l.animation);\n const skippedSelectors: string[] = [];\n\n for (const a of animations) {\n if (a.targetSelector !== originalSelector && a.targetSelector.includes(opts.originalId)) {\n skippedSelectors.push(a.targetSelector);\n }\n }\n\n const matching = animations.filter((a) => a.targetSelector === originalSelector);\n if (matching.length === 0) return { script, skippedSelectors };\n\n let result = script;\n const newElementStart = opts.splitTime;\n\n // Forward pre-pass: compute the inherited-props baseline available BEFORE each\n // matching tween, in source/timeline order. The write loop below runs in\n // REVERSE (so updateAnimationSelectorInScript's selector edits can't shift the\n // count-based IDs of not-yet-processed tweens), but the spanning-tween midpoint\n // interpolation needs the baseline from EARLIER tweens — which a reverse\n // accumulator hasn't seen yet. Decoupling the two fixes the wrong midpoint.\n const { before: baselineBefore, final: finalInheritedProps } = computeForwardBaselines(\n matching,\n opts.splitTime,\n );\n\n // Reverse iteration: updateAnimationSelectorInScript mutates selectors which\n // can shift count-based ID suffixes for later animations.\n const ctx = { splitTime: opts.splitTime, originalSelector, newSelector, newElementStart };\n for (let i = matching.length - 1; i >= 0; i--) {\n const anim = matching[i];\n if (!anim) continue;\n result = applyTweenSplit(result, anim, baselineBefore[i] ?? {}, ctx, skippedSelectors);\n }\n\n if (Object.keys(finalInheritedProps).length > 0) {\n result = insertInheritedStateSetInScript(\n result,\n newSelector,\n newElementStart,\n finalInheritedProps,\n );\n }\n\n return { script: result, skippedSelectors };\n}\n\n// ── Unroll dynamic animations ────────────────────────────────────────────────\n\nfunction isLoopNode(node: Node): boolean {\n const t = node?.type;\n return (\n t === \"ForStatement\" ||\n t === \"ForInStatement\" ||\n t === \"ForOfStatement\" ||\n t === \"WhileStatement\"\n );\n}\n\nfunction isForEachStatement(node: Node): boolean {\n return (\n node?.type === \"ExpressionStatement\" &&\n node.expression?.type === \"CallExpression\" &&\n node.expression.callee?.property?.name === \"forEach\"\n );\n}\n\n/** The nearest enclosing loop / forEach AST node (not just its byte range). */\nfunction findEnclosingLoopNode(ancestors: Node[]): Node | null {\n for (let i = ancestors.length - 2; i >= 0; i--) {\n const node = ancestors[i];\n if (isLoopNode(node) || isForEachStatement(node)) return node;\n }\n return null;\n}\n\n/** Statements making up a loop's body block, or null when not a simple block. */\nfunction loopBodyStatements(loopNode: Node): Node[] | null {\n let body: Node;\n if (loopNode?.type === \"ExpressionStatement\") {\n // forEach(cb): body is the callback's block.\n const cb = loopNode.expression?.arguments?.[0];\n body = cb?.body;\n } else {\n body = loopNode?.body;\n }\n if (body?.type !== \"BlockStatement\") return null;\n return (body.body ?? []).filter((s: Node) => s?.type === \"ExpressionStatement\");\n}\n\n/** The loop's index identifier name (`for (let i …)`), used for per-iteration substitution. */\nfunction loopIndexVarName(loopNode: Node): string | null {\n if (loopNode?.type === \"ForStatement\") {\n const decl = loopNode.init?.declarations?.[0];\n return typeof decl?.id?.name === \"string\" ? decl.id.name : null;\n }\n return null;\n}\n\n/**\n * Rewrite one body statement's source for iteration `idx`: replace USES of the\n * loop index variable (AST Identifier nodes) with the literal index. AST-based,\n * not a text regex, so the index name appearing inside a string literal (e.g. a\n * selector \".row-i\") or as a non-computed member/key (`obj.i`, `{ i: … }`) is\n * left untouched — only real references to the variable are substituted.\n */\n// An identifier in \"binding position\" is a name, not a value reference: a\n// non-computed member property (`obj.i`) or object-literal key (`{ i: … }`).\n// Those must NOT be substituted with the iteration index.\nfunction isIndexBindingPosition(node: Node, parent: Node): boolean {\n if (parent?.type === \"MemberExpression\") return parent.property === node && !parent.computed;\n if (parent?.type === \"Property\" || parent?.type === \"ObjectProperty\") {\n return parent.key === node && !parent.computed;\n }\n return false;\n}\n\nfunction substituteLoopIndex(stmt: Node, indexVar: string, idx: number, script: string): string {\n const base = stmt.start as number;\n const src = script.slice(base, stmt.end as number);\n const ranges: Array<[number, number]> = [];\n acornWalk.ancestor(stmt, {\n Identifier(node: Node, _state: unknown, ancestors: Node[]) {\n if (node.name !== indexVar) return;\n if (isIndexBindingPosition(node, ancestors[ancestors.length - 2])) return;\n ranges.push([(node.start as number) - base, (node.end as number) - base]);\n },\n });\n if (ranges.length === 0) return src;\n ranges.sort((a, b) => b[0] - a[0]);\n let out = src;\n for (const [s, e] of ranges) out = out.slice(0, s) + String(idx) + out.slice(e);\n return out;\n}\n\nfunction buildUnrollReplacement(\n timelineVar: string,\n animation: GsapAnimation,\n elements: Array<{\n selector: string;\n keyframes: Array<{ percentage: number; properties: Record<string, number | string> }>;\n easeEach?: string;\n }>,\n): string {\n const duration = typeof animation.duration === \"number\" ? animation.duration : 8;\n const ease = typeof animation.ease === \"string\" ? animation.ease : \"none\";\n const pos = animation.position ?? 0;\n const posCode = typeof pos === \"number\" ? String(pos) : JSON.stringify(pos);\n const calls = elements.map((el) => {\n const sorted = [...el.keyframes].sort((a, b) => a.percentage - b.percentage);\n const kfCode = buildKeyframeObjectCode(sorted, el.easeEach);\n return `${timelineVar}.to(${JSON.stringify(el.selector)}, { keyframes: ${kfCode}, duration: ${duration}, ease: ${JSON.stringify(ease)} }, ${posCode});`;\n });\n return calls.join(\"\\n \");\n}\n\nexport type UnrollElement = {\n selector: string;\n keyframes: Array<{ percentage: number; properties: Record<string, number | string> }>;\n easeEach?: string;\n};\n\n/** Build one element's unrolled `tl.to(...)` call from the target animation. */\nfunction buildUnrollCallForElement(\n timelineVar: string,\n animation: GsapAnimation,\n el: UnrollElement,\n): string {\n const duration = typeof animation.duration === \"number\" ? animation.duration : 8;\n const ease = typeof animation.ease === \"string\" ? animation.ease : \"none\";\n const pos = animation.position ?? 0;\n const posCode = typeof pos === \"number\" ? String(pos) : JSON.stringify(pos);\n const sorted = [...el.keyframes].sort((a, b) => a.percentage - b.percentage);\n const kfCode = buildKeyframeObjectCode(sorted, el.easeEach);\n return `${timelineVar}.to(${JSON.stringify(el.selector)}, { keyframes: ${kfCode}, duration: ${duration}, ease: ${JSON.stringify(ease)} }, ${posCode});`;\n}\n\n/** Sentinel: the unroll cannot safely reproduce the loop body — caller no-ops. */\nconst REFUSE_UNROLL = Symbol(\"refuse-unroll\");\n\n/** Every statement in a loop's body block (unfiltered), or [] when not a block. */\nfunction loopBodyRawStatements(loopNode: Node): Node[] {\n const body =\n loopNode?.type === \"ExpressionStatement\"\n ? loopNode.expression?.arguments?.[0]?.body\n : loopNode?.body;\n return body?.type === \"BlockStatement\" ? (body.body ?? []) : [];\n}\n\n/** A node that re-binds `indexVar`: a re-declaration or a function param. */\nfunction rebindsIndex(node: Node, indexVar: string): boolean {\n if (node.type === \"VariableDeclarator\") return node.id?.name === indexVar;\n if (\n node.type === \"FunctionExpression\" ||\n node.type === \"FunctionDeclaration\" ||\n node.type === \"ArrowFunctionExpression\"\n ) {\n return (node.params ?? []).some((p: Node) => p?.name === indexVar);\n }\n return false;\n}\n\n/** Object shorthand `{ i }` — substituting the value would yield invalid `{ 0 }`. */\nfunction isShorthandIndexUse(node: Node, indexVar: string): boolean {\n return (\n (node.type === \"Property\" || node.type === \"ObjectProperty\") &&\n node.shorthand === true &&\n propKeyName(node) === indexVar\n );\n}\n\n/**\n * A sibling statement can't be safely index-substituted when it re-binds the\n * loop index (shadowing — a nested `for (let i …)`, a callback param `i`) or\n * uses it in object shorthand (`{ i }`, which would splice to the invalid\n * `{ 0 }`). substituteLoopIndex has no scope analysis, so in these cases it\n * would emit broken or wrong code — the unroll must refuse instead.\n */\nfunction hasUnsafeLoopIndexUse(stmt: Node, indexVar: string): boolean {\n let unsafe = false;\n acornWalk.full(stmt, (node: Node) => {\n if (!unsafe && (isShorthandIndexUse(node, indexVar) || rebindsIndex(node, indexVar))) {\n unsafe = true;\n }\n });\n return unsafe;\n}\n\n/** How to handle the loop body's non-target siblings when unrolling. */\nfunction unrollSiblingStrategy(\n loopNode: Node,\n targetStmt: Node,\n stmts: Node[],\n indexVar: string | null,\n): \"blanket\" | \"refuse\" | \"preserve\" {\n const siblings = stmts.filter((s) => s !== targetStmt);\n // A sibling the filtered statement list doesn't model (non-ExpressionStatement)\n // would be silently lost by either path — refuse if any exists.\n const hasUnmodeledSibling = loopBodyRawStatements(loopNode).some(\n (s) => s !== targetStmt && !stmts.includes(s),\n );\n if (siblings.length === 0 && !hasUnmodeledSibling) return \"blanket\";\n if (hasUnmodeledSibling || !indexVar) return \"refuse\";\n return siblings.some((s) => hasUnsafeLoopIndexUse(s, indexVar)) ? \"refuse\" : \"preserve\";\n}\n\n/** Emit the per-iteration unrolled lines (target → static tl.to, siblings → index-substituted). */\nfunction emitUnrolledLines(\n stmts: Node[],\n targetStmt: Node,\n elements: UnrollElement[],\n timelineVar: string,\n animation: GsapAnimation,\n indexVar: string,\n script: string,\n): string {\n const lines: string[] = [];\n for (let idx = 0; idx < elements.length; idx++) {\n const el = elements[idx];\n if (!el) continue;\n for (const stmt of stmts) {\n lines.push(\n stmt === targetStmt\n ? buildUnrollCallForElement(timelineVar, animation, el)\n : substituteLoopIndex(stmt, indexVar, idx, script),\n );\n }\n }\n return lines.join(\"\\n \");\n}\n\n/**\n * Unroll the loop body, preserving every statement that is NOT the target tween.\n * For each iteration, emit each non-target statement with the loop index\n * substituted (e.g. `tl.set(items[i], …)` → `tl.set(items[0], …)`), and replace\n * the target tween statement with that element's static `tl.to()` call.\n *\n * Returns null when a blanket overwrite is lossless (no sibling statements), and\n * REFUSE_UNROLL when siblings exist but can't be safely reproduced — a non-`for`\n * loop (no numeric index to splice), a statement we don't model, or an unsafe\n * index use (shadowing / shorthand). Refusing no-ops the unroll, which is safe:\n * the dynamic loop keeps rendering correctly, just un-flattened.\n */\nfunction buildLoopUnrollPreserving(\n script: string,\n timelineVar: string,\n animation: GsapAnimation,\n elements: UnrollElement[],\n loopNode: Node,\n targetStmt: Node,\n): string | null | typeof REFUSE_UNROLL {\n const stmts = loopBodyStatements(loopNode);\n if (!stmts || !stmts.includes(targetStmt)) return null;\n const indexVar = loopIndexVarName(loopNode);\n const strategy = unrollSiblingStrategy(loopNode, targetStmt, stmts, indexVar);\n if (strategy === \"blanket\") return null;\n if (strategy === \"refuse\" || !indexVar) return REFUSE_UNROLL;\n return emitUnrolledLines(stmts, targetStmt, elements, timelineVar, animation, indexVar, script);\n}\n\n/**\n * Replace a dynamic loop that generates multiple tween calls with individual\n * static `tl.to()` calls — one per element. Finds the loop containing the\n * animation and replaces the loop with unrolled static calls, preserving every\n * non-target statement in the loop body per iteration.\n */\nexport function unrollDynamicAnimations(\n script: string,\n animationId: string,\n elements: UnrollElement[],\n): string {\n // An empty element list has no unrolled form — replacing the loop/statement\n // with zero calls would silently delete the animation. No-op instead.\n if (elements.length === 0) return script;\n const parsed = parseGsapScriptAcornForWrite(script);\n if (!parsed) return script;\n const target = parsed.located.find((l) => l.id === animationId);\n if (!target) return script;\n\n const ms = new MagicString(script);\n const loopNode = findEnclosingLoopNode(target.call.ancestors);\n if (loopNode) {\n const targetStmt = findEnclosingExpressionStatement(target.call.ancestors);\n const preserving = targetStmt\n ? buildLoopUnrollPreserving(\n script,\n parsed.timelineVar,\n target.animation,\n elements,\n loopNode,\n targetStmt,\n )\n : null;\n // Siblings exist but can't be safely reproduced — leave the loop untouched\n // rather than drop or corrupt them. The op no-ops (before === after).\n if (preserving === REFUSE_UNROLL) return script;\n // Fall back to the simple whole-body replacement when the body isn't a plain\n // block of statements we can preserve.\n const replacement =\n preserving ?? buildUnrollReplacement(parsed.timelineVar, target.animation, elements);\n ms.overwrite(loopNode.start as number, loopNode.end as number, replacement);\n } else {\n const stmt = findEnclosingExpressionStatement(target.call.ancestors);\n if (!stmt) return script;\n const replacement = buildUnrollReplacement(parsed.timelineVar, target.animation, elements);\n ms.overwrite(stmt.start as number, stmt.end as number, replacement);\n }\n return ms.toString();\n}\n","import type {\n TimelineElement,\n TimelineElementType,\n TimelineMediaElement,\n TimelineTextElement,\n TimelineCompositionElement,\n CanvasResolution,\n Keyframe,\n KeyframeProperties,\n StageZoomKeyframe,\n CompositionVariable,\n ValidationResult,\n} from \"./types.js\";\nimport { validateCompositionGsap } from \"./gsapSerialize\";\nimport { ensureHfIds } from \"./hfIds.js\";\nimport { parseGsapScriptAcornForWrite } from \"./gsapParserAcorn.js\";\nimport { queryByAttr } from \"./utils/cssSelector.js\";\nimport { removeAnimationFromScript } from \"./gsapWriterAcorn.js\";\n\nconst MEDIA_TYPES = new Set<string>([\"video\", \"image\", \"audio\"]);\n\nexport interface ParsedHtml {\n elements: TimelineElement[];\n gsapScript: string | null;\n styles: string | null;\n resolution: CanvasResolution;\n keyframes: Record<string, Keyframe[]>;\n stageZoomKeyframes: StageZoomKeyframe[];\n}\n\nfunction getElementType(el: Element): TimelineElementType | null {\n const tag = el.tagName.toLowerCase();\n if (tag === \"video\") return \"video\";\n if (tag === \"img\") return \"image\";\n if (tag === \"audio\") return \"audio\";\n // Check for explicit data-type attribute first\n const dataType = el.getAttribute(\"data-type\");\n if (dataType === \"composition\") return \"composition\";\n if (dataType === \"text\") return \"text\";\n // Fall back to tag-based detection for backwards compatibility\n if (\n tag === \"div\" ||\n tag === \"p\" ||\n tag === \"h1\" ||\n tag === \"h2\" ||\n tag === \"h3\" ||\n tag === \"span\"\n ) {\n return \"text\";\n }\n return null;\n}\n\nfunction getElementName(el: Element): string {\n const dataName = el.getAttribute(\"data-name\");\n if (dataName) return dataName;\n\n const type = getElementType(el);\n if (type === \"text\") {\n const text = el.textContent?.trim().slice(0, 30) || \"Text\";\n return text.length === 30 ? text + \"...\" : text;\n }\n\n const src = el.getAttribute(\"src\");\n if (src) {\n const filename = src.split(\"/\").pop() || src;\n return filename.split(\"?\")[0] ?? filename;\n }\n\n return el.id || el.className?.toString().split(\" \")[0] || \"Element\";\n}\n\nfunction getZIndex(el: Element): number {\n const dataLayer = el.getAttribute(\"data-layer\");\n if (dataLayer) return parseInt(dataLayer, 10) || 0;\n\n const style = (el as HTMLElement).style?.zIndex;\n if (style) return parseInt(style, 10) || 0;\n\n return 0;\n}\n\nfunction parseResolutionFromCss(doc: Document, cssText: string | null): CanvasResolution {\n const stage = doc.getElementById(\"stage\") || doc.querySelector(\"#stage\");\n if (stage) {\n const inlineStyle = (stage as HTMLElement).style;\n if (inlineStyle?.width && inlineStyle?.height) {\n const w = parseInt(inlineStyle.width, 10);\n const h = parseInt(inlineStyle.height, 10);\n if (w && h) {\n return resolveResolutionFromDimensions(w, h);\n }\n }\n }\n\n if (cssText) {\n const stageMatch = cssText.match(\n /#stage\\s*\\{[^}]*width:\\s*(\\d+)px[^}]*height:\\s*(\\d+)px[^}]*\\}/,\n );\n if (stageMatch) {\n const w = parseInt(stageMatch[1] ?? \"\", 10);\n const h = parseInt(stageMatch[2] ?? \"\", 10);\n return resolveResolutionFromDimensions(w, h);\n }\n const stageMatchReverse = cssText.match(\n /#stage\\s*\\{[^}]*height:\\s*(\\d+)px[^}]*width:\\s*(\\d+)px[^}]*\\}/,\n );\n if (stageMatchReverse) {\n const h = parseInt(stageMatchReverse[1] ?? \"\", 10);\n const w = parseInt(stageMatchReverse[2] ?? \"\", 10);\n return resolveResolutionFromDimensions(w, h);\n }\n }\n\n return \"portrait\";\n}\n\nfunction parseResolutionFromHtml(doc: Document): CanvasResolution | null {\n const htmlEl = doc.documentElement;\n const resolutionAttr = htmlEl.getAttribute(\"data-resolution\");\n if (\n resolutionAttr === \"landscape\" ||\n resolutionAttr === \"portrait\" ||\n resolutionAttr === \"landscape-4k\" ||\n resolutionAttr === \"portrait-4k\" ||\n resolutionAttr === \"square\" ||\n resolutionAttr === \"square-4k\"\n ) {\n return resolutionAttr;\n }\n\n const widthAttr = htmlEl.getAttribute(\"data-composition-width\");\n const heightAttr = htmlEl.getAttribute(\"data-composition-height\");\n if (widthAttr && heightAttr) {\n const width = parseInt(widthAttr, 10);\n const height = parseInt(heightAttr, 10);\n if (width && height) {\n return resolveResolutionFromDimensions(width, height);\n }\n }\n\n return null;\n}\n\nconst UHD_SQUARE_MIN = 2160;\nconst UHD_RECT_MIN = 3840;\n\nfunction resolveResolutionFromDimensions(width: number, height: number): CanvasResolution {\n const longSide = Math.max(width, height);\n if (width === height) {\n return longSide >= UHD_SQUARE_MIN ? \"square-4k\" : \"square\";\n }\n const isLandscape = width > height;\n const isUhd = longSide >= UHD_RECT_MIN;\n if (isLandscape) return isUhd ? \"landscape-4k\" : \"landscape\";\n return isUhd ? \"portrait-4k\" : \"portrait\";\n}\n\nexport function parseHtml(html: string): ParsedHtml {\n const withIds = ensureHfIds(html);\n const parser = new DOMParser();\n const doc = parser.parseFromString(withIds, \"text/html\");\n\n const elements: TimelineElement[] = [];\n const keyframes: Record<string, Keyframe[]> = {};\n let idCounter = 0;\n\n const htmlEl = doc.documentElement;\n const customStylesAttr = htmlEl.getAttribute(\"data-custom-styles\");\n let customStyles: string | null = null;\n if (customStylesAttr) {\n try {\n customStyles = JSON.parse(customStylesAttr);\n } catch {\n customStyles = customStylesAttr;\n }\n }\n\n const timedElements = doc.querySelectorAll(\"[data-start]\");\n\n timedElements.forEach((el) => {\n const type = getElementType(el);\n if (!type) return;\n\n const start = parseFloat(el.getAttribute(\"data-start\") || \"0\");\n const dataEnd = el.getAttribute(\"data-end\");\n\n let duration: number;\n if (dataEnd) {\n duration = Math.max(0, parseFloat(dataEnd) - start);\n } else {\n duration = 5;\n }\n\n // R1: stable hf- id minted by ensureHfIds above; clips just read it.\n // Legacy/migration note: ensureHfIds pins a pre-existing `data-hf-id`, and\n // the generator emits `data-hf-id=\"${element.id}\"`. So a clip authored\n // before R1 with `id=\"my-title\"` round-trips as `data-hf-id=\"my-title\"` —\n // a non-`hf-`-shaped but still stable, exact-match handle. This is safe\n // indefinitely: targeting uses exact `[data-hf-id=\"…\"]` match (it does not\n // require the hf- prefix). ensureHfIds skips elements that already carry\n // data-hf-id, so legacy values are NOT re-minted automatically — they\n // persist until the user re-saves the composition through Studio. Not a bug.\n const id = el.getAttribute(\"data-hf-id\") || el.id || `element-${++idCounter}`;\n const name = getElementName(el);\n const zIndex = getZIndex(el);\n\n // Parse data-keyframes attribute if present\n const keyframesAttr = el.getAttribute(\"data-keyframes\");\n if (keyframesAttr) {\n try {\n const parsedKeyframes = JSON.parse(keyframesAttr);\n if (Array.isArray(parsedKeyframes) && parsedKeyframes.length > 0) {\n keyframes[id] = parsedKeyframes;\n }\n } catch {\n // skip invalid keyframes\n }\n }\n\n // Parse transform properties (x, y, scale, opacity)\n const xAttr = el.getAttribute(\"data-x\");\n const yAttr = el.getAttribute(\"data-y\");\n const scaleAttr = el.getAttribute(\"data-scale\");\n const opacityAttr = el.getAttribute(\"data-opacity\");\n const x = xAttr ? parseFloat(xAttr) : undefined;\n const y = yAttr ? parseFloat(yAttr) : undefined;\n const scale = scaleAttr ? parseFloat(scaleAttr) : undefined;\n const opacity = opacityAttr ? parseFloat(opacityAttr) : undefined;\n\n if (type === \"text\") {\n const textEl = el.firstElementChild;\n const content = textEl?.textContent || name;\n const color = el.getAttribute(\"data-color\") || undefined;\n const fontSizeAttr = el.getAttribute(\"data-font-size\");\n const fontSize = fontSizeAttr ? parseInt(fontSizeAttr, 10) : undefined;\n const fontWeightAttr = el.getAttribute(\"data-font-weight\");\n const fontWeight = fontWeightAttr ? parseInt(fontWeightAttr, 10) : undefined;\n const fontFamily = el.getAttribute(\"data-font-family\") || undefined;\n const textShadowAttr = el.getAttribute(\"data-text-shadow\");\n const textShadow = textShadowAttr === \"false\" ? false : undefined;\n\n // Parse outline properties\n const textOutlineAttr = el.getAttribute(\"data-text-outline\");\n const textOutline = textOutlineAttr === \"true\" ? true : undefined;\n const textOutlineColor = el.getAttribute(\"data-text-outline-color\") || undefined;\n const textOutlineWidthAttr = el.getAttribute(\"data-text-outline-width\");\n const textOutlineWidth = textOutlineWidthAttr\n ? parseInt(textOutlineWidthAttr, 10)\n : undefined;\n\n // Parse highlight properties\n const textHighlightAttr = el.getAttribute(\"data-text-highlight\");\n const textHighlight = textHighlightAttr === \"true\" ? true : undefined;\n const textHighlightColor = el.getAttribute(\"data-text-highlight-color\") || undefined;\n const textHighlightPaddingAttr = el.getAttribute(\"data-text-highlight-padding\");\n const textHighlightPadding = textHighlightPaddingAttr\n ? parseInt(textHighlightPaddingAttr, 10)\n : undefined;\n const textHighlightRadiusAttr = el.getAttribute(\"data-text-highlight-radius\");\n const textHighlightRadius = textHighlightRadiusAttr\n ? parseInt(textHighlightRadiusAttr, 10)\n : undefined;\n\n const textElement: TimelineTextElement = {\n id,\n type: \"text\",\n name,\n content,\n startTime: start,\n duration,\n zIndex,\n x,\n y,\n scale,\n opacity,\n color,\n fontSize,\n fontWeight,\n fontFamily,\n textShadow,\n textOutline,\n textOutlineColor,\n textOutlineWidth,\n textHighlight,\n textHighlightColor,\n textHighlightPadding,\n textHighlightRadius,\n };\n elements.push(textElement);\n } else if (type === \"composition\") {\n // Composition is a div container with iframe inside\n const iframe = el.querySelector(\"iframe\");\n const src = iframe?.getAttribute(\"src\") || el.getAttribute(\"src\") || \"\";\n const compositionId = el.getAttribute(\"data-composition-id\") || \"\";\n const sourceDurationAttr = el.getAttribute(\"data-source-duration\");\n const sourceDuration = sourceDurationAttr ? parseFloat(sourceDurationAttr) : undefined;\n const sourceWidthAttr = el.getAttribute(\"data-source-width\");\n const sourceWidth = sourceWidthAttr ? parseInt(sourceWidthAttr, 10) : undefined;\n const sourceHeightAttr = el.getAttribute(\"data-source-height\");\n const sourceHeight = sourceHeightAttr ? parseInt(sourceHeightAttr, 10) : undefined;\n\n // Parse variable values if present\n const variableValuesAttr = el.getAttribute(\"data-variable-values\");\n let variableValues: Record<string, string | number | boolean> | undefined;\n if (variableValuesAttr) {\n try {\n variableValues = JSON.parse(variableValuesAttr);\n } catch {\n // skip invalid variable values\n }\n }\n\n const compositionElement: TimelineCompositionElement = {\n id,\n type: \"composition\",\n name,\n src,\n compositionId,\n startTime: start,\n duration,\n zIndex,\n x,\n y,\n scale,\n opacity,\n sourceDuration,\n sourceWidth,\n sourceHeight,\n variableValues,\n };\n elements.push(compositionElement);\n } else {\n if (!MEDIA_TYPES.has(type)) return;\n\n const src = el.getAttribute(\"src\") || \"\";\n const mediaStartTimeAttr = el.getAttribute(\"data-media-start\");\n const mediaStartTime = mediaStartTimeAttr ? parseFloat(mediaStartTimeAttr) : undefined;\n const sourceDurationAttr = el.getAttribute(\"data-source-duration\");\n const sourceDuration = sourceDurationAttr ? parseFloat(sourceDurationAttr) : undefined;\n const isArollAttr = el.getAttribute(\"data-aroll\");\n const isAroll = isArollAttr === \"true\" ? true : undefined;\n const volumeAttr = el.getAttribute(\"data-volume\");\n const volume = volumeAttr ? parseFloat(volumeAttr) : undefined;\n const hasAudioAttr = el.getAttribute(\"data-has-audio\");\n const hasAudio = hasAudioAttr === \"true\" ? true : undefined;\n\n const mediaElement: TimelineMediaElement = {\n id,\n type: type as \"video\" | \"image\" | \"audio\",\n name,\n src,\n startTime: start,\n duration,\n zIndex,\n x,\n y,\n scale,\n opacity,\n mediaStartTime,\n sourceDuration,\n isAroll,\n volume,\n hasAudio,\n };\n elements.push(mediaElement);\n }\n });\n\n const scriptTags = doc.querySelectorAll(\"script\");\n let gsapScript: string | null = null;\n\n for (const script of scriptTags) {\n const src = script.getAttribute(\"src\");\n if (src && src.includes(\"gsap\")) continue;\n\n const content = script.textContent?.trim();\n if (content && (content.includes(\"gsap\") || content.includes(\"timeline\"))) {\n gsapScript = content;\n break;\n }\n }\n\n // Normalize keyframes (clamp negative time, convert absolute -> relative if detected)\n for (const element of elements) {\n const elementKeyframes = keyframes[element.id];\n if (!elementKeyframes || elementKeyframes.length === 0) continue;\n\n const baseX = element.x ?? 0;\n const baseY = element.y ?? 0;\n const baseScale =\n element.type === \"video\" || element.type === \"image\" || element.type === \"composition\"\n ? ((element as TimelineMediaElement | TimelineCompositionElement).scale ?? 1)\n : 1;\n\n keyframes[element.id] = normalizeKeyframes(elementKeyframes, baseX, baseY, baseScale);\n }\n\n const styleTags = doc.querySelectorAll(\"style\");\n const allStyles =\n Array.from(styleTags)\n .map((s) => s.textContent?.trim())\n .filter(Boolean)\n .join(\"\\n\\n\") || null;\n\n const customStyleTags = Array.from(styleTags).filter(\n (s) => s.getAttribute(\"data-hf-custom\") === \"true\",\n );\n const customStylesFromTags =\n customStyleTags\n .map((s) => s.textContent?.trim())\n .filter(Boolean)\n .join(\"\\n\\n\") || null;\n\n const styles = customStyles ?? customStylesFromTags ?? null;\n\n const resolution = parseResolutionFromHtml(doc) ?? parseResolutionFromCss(doc, allStyles);\n\n // Parse stage zoom keyframes from zoom container\n const stageZoomKeyframes = parseStageZoomKeyframes(doc);\n\n return {\n elements,\n gsapScript,\n styles,\n resolution,\n keyframes,\n stageZoomKeyframes,\n };\n}\n\nfunction parseStageZoomKeyframes(doc: Document): StageZoomKeyframe[] {\n const zoomContainer = doc.getElementById(\"stage-zoom-container\");\n if (!zoomContainer) {\n return [];\n }\n\n const zoomKeyframesAttr = zoomContainer.getAttribute(\"data-zoom-keyframes\");\n if (!zoomKeyframesAttr) {\n return [];\n }\n\n try {\n const parsed = JSON.parse(zoomKeyframesAttr);\n if (Array.isArray(parsed)) {\n return parsed.filter(\n (kf): kf is StageZoomKeyframe =>\n typeof kf === \"object\" &&\n kf !== null &&\n typeof kf.id === \"string\" &&\n typeof kf.time === \"number\" &&\n typeof kf.zoom === \"object\" &&\n kf.zoom !== null &&\n typeof kf.zoom.scale === \"number\" &&\n typeof kf.zoom.focusX === \"number\" &&\n typeof kf.zoom.focusY === \"number\",\n );\n }\n } catch {\n // skip invalid zoom keyframes\n }\n\n return [];\n}\n\nfunction normalizeKeyframes(\n keyframes: Keyframe[],\n baseX: number,\n baseY: number,\n baseScale: number,\n): Keyframe[] {\n const timeEpsilon = 0.001;\n const valueEpsilon = 0.00001;\n\n const hasBaseCheck = (value: number | undefined, base: number): boolean =>\n value !== undefined && Math.abs(value - base) <= valueEpsilon && Math.abs(base) > valueEpsilon;\n\n const timeZeroKeyframes = keyframes.filter((kf) => Math.abs(kf.time) <= timeEpsilon);\n\n const treatAsAbsolute = timeZeroKeyframes.some((kf) => {\n const props = kf.properties || {};\n if (\n hasBaseCheck(props.x, baseX) ||\n hasBaseCheck(props.y, baseY) ||\n (baseScale !== 1 && hasBaseCheck(props.scale, baseScale))\n ) {\n return true;\n }\n return false;\n });\n\n return keyframes.map((kf) => {\n const normalizedProps: Partial<KeyframeProperties> = {};\n for (const [key, value] of Object.entries(kf.properties || {})) {\n if (typeof value !== \"number\") continue;\n if (treatAsAbsolute && key === \"x\") {\n normalizedProps.x = value - baseX;\n } else if (treatAsAbsolute && key === \"y\") {\n normalizedProps.y = value - baseY;\n } else if (treatAsAbsolute && key === \"scale\") {\n normalizedProps.scale = baseScale !== 0 ? value / baseScale : value;\n } else {\n (normalizedProps as Record<string, number>)[key] = value;\n }\n }\n\n return {\n ...kf,\n time: Math.max(0, kf.time),\n properties: normalizedProps,\n };\n });\n}\n\nexport function updateElementInHtml(\n html: string,\n elementId: string,\n updates: Partial<TimelineElement>,\n): string {\n const parser = new DOMParser();\n const doc = parser.parseFromString(html, \"text/html\");\n\n const el = doc.getElementById(elementId) || queryByAttr(doc, \"data-name\", elementId);\n if (!el) return html;\n\n if (updates.startTime !== undefined) {\n el.setAttribute(\"data-start\", String(updates.startTime));\n if (el.hasAttribute(\"data-end\") && updates.duration !== undefined) {\n el.setAttribute(\"data-end\", String(updates.startTime + updates.duration));\n }\n }\n\n if (updates.duration !== undefined) {\n const start = parseFloat(el.getAttribute(\"data-start\") || \"0\");\n el.setAttribute(\"data-end\", String(start + updates.duration));\n el.removeAttribute(\"data-duration\"); // Clean up legacy\n }\n\n if (updates.name !== undefined) {\n el.setAttribute(\"data-name\", updates.name);\n }\n\n if (updates.zIndex !== undefined) {\n el.setAttribute(\"data-layer\", String(updates.zIndex));\n }\n\n // Handle media-specific property\n if (\"src\" in updates && updates.src !== undefined) {\n el.setAttribute(\"src\", updates.src);\n }\n\n // Handle text-specific properties\n if (\"content\" in updates && updates.content !== undefined) {\n const textEl = el.firstElementChild;\n if (textEl) {\n textEl.textContent = updates.content;\n }\n }\n\n if (\"color\" in updates && updates.color !== undefined) {\n el.setAttribute(\"data-color\", updates.color);\n }\n\n if (\"fontSize\" in updates && updates.fontSize !== undefined) {\n el.setAttribute(\"data-font-size\", String(updates.fontSize));\n }\n\n if (\"textShadow\" in updates) {\n if (updates.textShadow === false) {\n el.setAttribute(\"data-text-shadow\", \"false\");\n } else {\n el.removeAttribute(\"data-text-shadow\");\n }\n }\n\n // Handle volume property for audio/video\n if (\"volume\" in updates) {\n if (updates.volume !== undefined && updates.volume !== 1) {\n el.setAttribute(\"data-volume\", String(updates.volume));\n } else {\n el.removeAttribute(\"data-volume\");\n }\n }\n\n // Handle hasAudio property for videos\n if (\"hasAudio\" in updates) {\n if (updates.hasAudio === true) {\n el.setAttribute(\"data-has-audio\", \"true\");\n } else {\n el.removeAttribute(\"data-has-audio\");\n }\n }\n\n return \"<!DOCTYPE html>\\n\" + doc.documentElement.outerHTML;\n}\n\nexport function addElementToHtml(\n html: string,\n element: Omit<TimelineElement, \"id\"> & { id?: string },\n): { html: string; id: string } {\n const parser = new DOMParser();\n const doc = parser.parseFromString(html, \"text/html\");\n\n // Prefer zoom container, fall back to stage, then container, then body\n const container =\n doc.querySelector(\"#stage-zoom-container\") ||\n doc.querySelector(\".container\") ||\n doc.querySelector(\"#stage\") ||\n doc.body;\n\n const id = element.id || `element-${Date.now()}`;\n\n let newEl: Element;\n\n function applyMediaAttrs(el: Element, mediaEl: TimelineMediaElement): void {\n if (mediaEl.src) el.setAttribute(\"src\", mediaEl.src);\n if (mediaEl.volume !== undefined && mediaEl.volume !== 1) {\n el.setAttribute(\"data-volume\", String(mediaEl.volume));\n }\n }\n\n switch (element.type) {\n case \"video\": {\n const mediaEl = element as TimelineMediaElement;\n newEl = doc.createElement(\"video\");\n newEl.setAttribute(\"muted\", \"\");\n newEl.setAttribute(\"playsinline\", \"\");\n applyMediaAttrs(newEl, mediaEl);\n if (mediaEl.hasAudio) {\n newEl.setAttribute(\"data-has-audio\", \"true\");\n }\n break;\n }\n case \"image\": {\n const mediaEl = element as TimelineMediaElement;\n newEl = doc.createElement(\"img\");\n if (mediaEl.src) newEl.setAttribute(\"src\", mediaEl.src);\n newEl.setAttribute(\"alt\", element.name);\n break;\n }\n case \"audio\": {\n const mediaEl = element as TimelineMediaElement;\n newEl = doc.createElement(\"audio\");\n applyMediaAttrs(newEl, mediaEl);\n break;\n }\n case \"text\":\n default: {\n const textEl = element as TimelineTextElement;\n newEl = doc.createElement(\"div\");\n const textContent = doc.createElement(\"div\");\n textContent.textContent = textEl.content || element.name;\n newEl.appendChild(textContent);\n if (textEl.color) {\n newEl.setAttribute(\"data-color\", textEl.color);\n }\n if (textEl.fontSize) {\n newEl.setAttribute(\"data-font-size\", String(textEl.fontSize));\n }\n break;\n }\n }\n\n newEl.id = id;\n newEl.setAttribute(\"data-start\", String(element.startTime));\n newEl.setAttribute(\"data-end\", String(element.startTime + element.duration));\n newEl.setAttribute(\"data-layer\", String(element.zIndex));\n newEl.setAttribute(\"data-name\", element.name);\n\n container.appendChild(newEl);\n\n return {\n html: \"<!DOCTYPE html>\\n\" + doc.documentElement.outerHTML,\n id,\n };\n}\n\nfunction selectorTargetsId(selector: string, id: string): boolean {\n return (\n selector === `#${id}` ||\n selector === `[data-hf-id=\"${id}\"]` ||\n selector === `[data-hf-id='${id}']`\n );\n}\n\nfunction stripGsapForId(script: string, elementId: string): string {\n // Re-parse after every removal. Animation ids are count-based (positional), so\n // removing one tween renumbers the survivors — ids captured from a single\n // up-front parse go stale and silently no-op, orphaning later tweens on the\n // now-deleted element. Always remove the FIRST still-matching animation in a\n // freshly-parsed script until none remain.\n let current = script;\n for (;;) {\n const parsed = parseGsapScriptAcornForWrite(current);\n if (!parsed) return current;\n const match = parsed.located.find((l) =>\n selectorTargetsId(l.animation.targetSelector, elementId),\n );\n if (!match) return current;\n const updated = removeAnimationFromScript(current, match.id);\n // Guard against a non-removing match (would otherwise loop forever).\n if (updated === current) return current;\n current = updated;\n }\n}\n\nfunction cascadeRemoveGsapById(doc: Document, elementId: string): void {\n for (const script of Array.from(doc.querySelectorAll(\"script\"))) {\n const text = script.textContent ?? \"\";\n if (!text.includes(\"gsap\") && !text.includes(\"ScrollTrigger\")) continue;\n const updated = stripGsapForId(text, elementId);\n if (updated !== text) script.textContent = updated;\n }\n}\n\nexport function removeElementFromHtml(html: string, elementId: string): string {\n const parser = new DOMParser();\n const doc = parser.parseFromString(html, \"text/html\");\n doc.getElementById(elementId)?.remove();\n cascadeRemoveGsapById(doc, elementId);\n return \"<!DOCTYPE html>\\n\" + doc.documentElement.outerHTML;\n}\n\nexport interface CompositionMetadata {\n compositionId: string | null;\n compositionDuration: number | null;\n variables: CompositionVariable[];\n}\n\nexport function extractCompositionMetadata(html: string): CompositionMetadata {\n const parser = new DOMParser();\n const doc = parser.parseFromString(html, \"text/html\");\n const htmlEl = doc.documentElement;\n\n const compositionId = htmlEl.getAttribute(\"data-composition-id\");\n const durationStr = htmlEl.getAttribute(\"data-composition-duration\");\n const compositionDuration = durationStr ? parseFloat(durationStr) : null;\n\n const variables = parseCompositionVariables(htmlEl);\n\n return {\n compositionId,\n compositionDuration:\n compositionDuration && isFinite(compositionDuration) ? compositionDuration : null,\n variables,\n };\n}\n\nfunction parseCompositionVariables(htmlEl: Element): CompositionVariable[] {\n const variablesAttr = htmlEl.getAttribute(\"data-composition-variables\");\n if (!variablesAttr) {\n return [];\n }\n\n try {\n const parsed = JSON.parse(variablesAttr);\n if (!Array.isArray(parsed)) {\n return [];\n }\n\n return parsed.filter((v): v is CompositionVariable => {\n if (typeof v !== \"object\" || v === null) return false;\n if (typeof v.id !== \"string\" || typeof v.label !== \"string\") return false;\n if (![\"string\", \"number\", \"color\", \"boolean\", \"enum\", \"font\", \"image\"].includes(v.type))\n return false;\n\n switch (v.type) {\n case \"string\":\n return typeof v.default === \"string\";\n case \"number\":\n return typeof v.default === \"number\";\n case \"color\":\n return typeof v.default === \"string\";\n case \"boolean\":\n return typeof v.default === \"boolean\";\n case \"enum\":\n return typeof v.default === \"string\" && Array.isArray(v.options);\n case \"font\":\n // default is the font-family name string; extra metadata fields are optional\n return typeof v.default === \"string\";\n case \"image\":\n // default is the fallback image URL string; extra metadata fields are optional\n return typeof v.default === \"string\";\n default:\n return false;\n }\n });\n } catch {\n return [];\n }\n}\n\nexport function validateCompositionHtml(html: string): ValidationResult {\n const errors: string[] = [];\n const warnings: string[] = [];\n\n const parser = new DOMParser();\n const doc = parser.parseFromString(html, \"text/html\");\n const htmlEl = doc.documentElement;\n\n const compositionId = htmlEl.getAttribute(\"data-composition-id\");\n if (!compositionId) {\n errors.push(\"Missing data-composition-id attribute on <html> element\");\n }\n\n const durationStr = htmlEl.getAttribute(\"data-composition-duration\");\n if (!durationStr) {\n errors.push(\"Missing data-composition-duration attribute on <html> element\");\n } else {\n const duration = parseFloat(durationStr);\n if (!isFinite(duration) || duration <= 0) {\n errors.push(\"data-composition-duration must be a positive finite number\");\n }\n }\n\n const stage = doc.getElementById(\"stage\");\n if (!stage) {\n errors.push(\"Missing #stage element\");\n }\n\n if (/\\son\\w+\\s*=/i.test(html)) {\n errors.push(\"Inline event handlers (onclick, onload, etc.) not allowed\");\n }\n\n if (/javascript\\s*:/i.test(html)) {\n errors.push(\"javascript: URLs not allowed\");\n }\n\n const scripts = doc.querySelectorAll(\"script\");\n if (scripts.length > 2) {\n warnings.push(\"Multiple script tags detected - only GSAP CDN and main script expected\");\n }\n\n const gsapScript = extractGsapScript(doc);\n if (gsapScript) {\n const gsapValidation = validateCompositionGsap(gsapScript);\n errors.push(...gsapValidation.errors);\n warnings.push(...gsapValidation.warnings);\n }\n\n return {\n valid: errors.length === 0,\n errors,\n warnings,\n };\n}\n\nfunction extractGsapScript(doc: Document): string | null {\n const scripts = doc.querySelectorAll(\"script\");\n for (const script of scripts) {\n const content = script.textContent || \"\";\n if (\n content.includes(\"gsap.timeline\") ||\n content.includes(\".set(\") ||\n content.includes(\".to(\")\n ) {\n return content;\n }\n }\n return null;\n}\n","/**\n * Unroll computed GSAP timelines (helpers / bounded loops) into explicit literal\n * tweens — the source-rewrite behind the Studio \"Unroll to edit\" action.\n *\n * Strategy: the read parser already resolves each computed tween (positions,\n * motionPath arcs, keyframes, provenance). We serialize those resolved\n * animations back to literal `tl.*` statements and surgically replace the\n * top-level helper-call / loop statements that produced them (and drop the now\n * dead helper declarations) via magic-string, leaving the rest of the source —\n * literal tweens, comments, formatting — untouched. The result is a visual\n * no-op: re-parsing it yields the same animations, now all literal.\n *\n * Scope: top-level helper calls and loops (the common authoring shape). Tweens\n * whose origin can't be mapped to a top-level statement (e.g. helpers nested\n * inside other helpers) are left as-is rather than guessed at.\n */\nimport * as acorn from \"acorn\";\nimport MagicString from \"magic-string\";\nimport type { GsapAnimation } from \"./gsapSerialize.js\";\nimport { serializeValue as valueToCode, safeJsKey as safeKey } from \"./gsapSerialize.js\";\nimport { parseGsapScriptAcorn } from \"./gsapParserAcorn.js\";\n\n// acorn nodes are structurally untyped here.\ntype Node = any;\n\nfunction propEntries(props: Record<string, number | string>): string[] {\n return Object.entries(props).map(([k, v]) => `${safeKey(k)}: ${valueToCode(v)}`);\n}\n\nfunction motionPathEntry(anim: GsapAnimation): string {\n const waypoints = (anim.keyframes?.keyframes ?? [])\n .filter((k) => typeof k.properties.x === \"number\" && typeof k.properties.y === \"number\")\n .map((k) => `{ x: ${valueToCode(k.properties.x!)}, y: ${valueToCode(k.properties.y!)} }`);\n const curviness = anim.arcPath?.segments[0]?.curviness ?? 1;\n const autoRotate = anim.arcPath?.autoRotate;\n const extra = autoRotate ? `, autoRotate: ${valueToCode(autoRotate as number | string)}` : \"\";\n return `motionPath: { path: [${waypoints.join(\", \")}], curviness: ${curviness}${extra} }`;\n}\n\nfunction keyframesEntry(anim: GsapAnimation): string {\n const kfs = (anim.keyframes?.keyframes ?? []).map((k) => {\n const body = propEntries(k.properties);\n if (k.ease) body.push(`ease: ${valueToCode(k.ease)}`);\n return `\"${k.percentage}%\": { ${body.join(\", \")} }`;\n });\n if (anim.keyframes?.easeEach) kfs.push(`easeEach: ${valueToCode(anim.keyframes.easeEach)}`);\n return `keyframes: { ${kfs.join(\", \")} }`;\n}\n\n/** The vars-object entries for a tween: motionPath/keyframes block, props, duration, ease, extras. */\nfunction buildVarsParts(anim: GsapAnimation): string[] {\n const parts: string[] = [];\n if (anim.arcPath?.enabled) parts.push(motionPathEntry(anim));\n else if (anim.keyframes) parts.push(keyframesEntry(anim));\n parts.push(...propEntries(anim.properties));\n if (anim.method !== \"set\" && anim.duration !== undefined) {\n parts.push(`duration: ${valueToCode(anim.duration)}`);\n }\n if (anim.ease) parts.push(`ease: ${valueToCode(anim.ease)}`);\n for (const [k, v] of Object.entries(anim.extras ?? {})) {\n parts.push(`${safeKey(k)}: ${valueToCode(v as number | string)}`);\n }\n return parts;\n}\n\n/** Serialize one resolved animation to a literal `tl.*` statement (arc/keyframe-aware). */\nfunction serializeTweenStatement(timelineVar: string, anim: GsapAnimation): string {\n const obj = `{ ${buildVarsParts(anim).join(\", \")} }`;\n const pos = valueToCode(\n anim.resolvedStart ?? (typeof anim.position === \"number\" ? anim.position : 0),\n );\n const sel = valueToCode(anim.targetSelector);\n if (anim.method === \"fromTo\") {\n const from = `{ ${propEntries(anim.fromProperties ?? {}).join(\", \")} }`;\n return `${timelineVar}.fromTo(${sel}, ${from}, ${obj}, ${pos});`;\n }\n return `${timelineVar}.${anim.method}(${sel}, ${obj}, ${pos});`;\n}\n\n/** A computed animation is one expanded from a helper or loop (not literal/dynamic). */\nfunction isComputed(anim: GsapAnimation): boolean {\n return anim.provenance?.kind === \"helper\" || anim.provenance?.kind === \"loop\";\n}\n\n/** Top-level statements of the parsed program. */\nfunction topLevelStatements(script: string): Node[] {\n return acorn.parse(script, { ecmaVersion: \"latest\", sourceType: \"script\" }).body ?? [];\n}\n\n/** The top-level statement whose source span contains [start, end], or null. */\nfunction enclosingTopLevel(statements: Node[], start: number, end: number): Node | null {\n for (const stmt of statements) {\n if (stmt.start <= start && stmt.end >= end) return stmt;\n }\n return null;\n}\n\nfunction isHelperDeclNamed(stmt: Node, names: Set<string>): boolean {\n if (stmt.type === \"FunctionDeclaration\") return names.has(stmt.id?.name);\n if (stmt.type === \"VariableDeclaration\") {\n return (stmt.declarations ?? []).some((d: Node) => names.has(d.id?.name));\n }\n return false;\n}\n\n/**\n * Rewrite `script` so top-level helper calls / loops that build the timeline\n * become explicit literal tweens. Returns the original script unchanged when\n * there is nothing statically-resolvable to unroll.\n */\nexport function unrollComputedTimeline(script: string): string {\n const parsed = parseGsapScriptAcorn(script);\n const computed = parsed.animations.filter((a) => isComputed(a) && a.provenance?.sourceRange);\n if (computed.length === 0) return script;\n\n const statements = topLevelStatements(script);\n\n // Group computed animations by the top-level statement that produced them,\n // preserving source order within each group.\n const byStatement = new Map<Node, GsapAnimation[]>();\n const helperNames = new Set<string>();\n for (const anim of computed) {\n if (anim.provenance?.fn) helperNames.add(anim.provenance.fn);\n const [s, e] = anim.provenance!.sourceRange!;\n const stmt = enclosingTopLevel(statements, s, e);\n if (!stmt) continue; // nested origin — leave it; can't map to a top-level edit\n const list = byStatement.get(stmt) ?? [];\n list.push(anim);\n byStatement.set(stmt, list);\n }\n if (byStatement.size === 0) return script;\n\n const ms = new MagicString(script);\n for (const [stmt, anims] of byStatement) {\n const literals = anims.map((a) => serializeTweenStatement(parsed.timelineVar, a)).join(\"\\n\");\n ms.overwrite(stmt.start, stmt.end, literals);\n }\n // Drop the now-dead helper declarations.\n for (const stmt of statements) {\n if (isHelperDeclNamed(stmt, helperNames)) ms.remove(stmt.start, stmt.end);\n }\n return ms.toString();\n}\n"],"mappings":"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","names":["GSAP_METHODS","isFunctionNode","n","acornWalk","acorn","MagicString","MagicString"]}
|
|
1
|
+
{"version":3,"sources":["../src/types.ts","../src/gsapConstants.ts","../src/gsapSerialize.ts","../src/gsapParser.ts","../src/springEase.ts","../src/gsapParserAcorn.ts","../src/gsapInline.ts","../src/hfIds.ts","../src/utils/cssSelector.ts","../src/gsapWriterAcorn.ts","../src/htmlParser.ts","../src/gsapUnroll.ts"],"sourcesContent":["// ── Composition data types ───────────────────────────────────────────────────\n// Moved from @hyperframes/core/core.types in the parsers extraction refactor.\n// These are the types produced and consumed by the parser pipeline.\n\nexport interface Asset {\n id: string;\n url: string;\n type: string;\n is_reference?: boolean;\n /** Duration in seconds for video/audio assets */\n duration?: number;\n}\n\n// ── Timeline types ──────────────────────────────────────────────────────────\n\nexport type TimelineElementType = \"video\" | \"image\" | \"text\" | \"audio\" | \"composition\";\nexport type MediaElementType = \"video\" | \"image\" | \"audio\";\n\nexport const CANVAS_DIMENSIONS = {\n landscape: { width: 1920, height: 1080 },\n portrait: { width: 1080, height: 1920 },\n \"landscape-4k\": { width: 3840, height: 2160 },\n \"portrait-4k\": { width: 2160, height: 3840 },\n square: { width: 1080, height: 1080 },\n \"square-4k\": { width: 2160, height: 2160 },\n} as const;\n\n// Single source of truth: derive the type from the table so adding a preset\n// extends the union automatically. Avoids the prior `as readonly CanvasResolution[]`\n// cast on `VALID_CANVAS_RESOLUTIONS` quietly drifting if the table grew but\n// the union didn't.\nexport type CanvasResolution = keyof typeof CANVAS_DIMENSIONS;\n\n// `Object.keys` ordering matches insertion order in `CANVAS_DIMENSIONS` on\n// every supported JS engine; tests pin the order in `index.test.ts`. Reorder\n// the table above with care.\nexport const VALID_CANVAS_RESOLUTIONS = Object.keys(\n CANVAS_DIMENSIONS,\n) as readonly CanvasResolution[];\n\nconst RESOLUTION_ALIASES: Record<string, CanvasResolution> = {\n \"1080p\": \"landscape\",\n hd: \"landscape\",\n \"1080p-portrait\": \"portrait\",\n \"portrait-1080p\": \"portrait\",\n \"4k\": \"landscape-4k\",\n uhd: \"landscape-4k\",\n \"4k-portrait\": \"portrait-4k\",\n \"1080p-square\": \"square\",\n \"square-1080p\": \"square\",\n \"4k-square\": \"square-4k\",\n};\n\n/**\n * Map a user-facing resolution string (canonical name or alias) to a\n * `CanvasResolution`. Returns undefined for unknown values so callers\n * can produce their own \"invalid\" UX (CLI exit, route validation, etc.).\n */\nexport function normalizeResolutionFlag(input: string | undefined): CanvasResolution | undefined {\n if (!input) return undefined;\n const lowered = input.toLowerCase();\n if ((VALID_CANVAS_RESOLUTIONS as readonly string[]).includes(lowered)) {\n return lowered as CanvasResolution;\n }\n return RESOLUTION_ALIASES[lowered];\n}\n\nexport interface TimelineElementBase {\n id: string;\n type: TimelineElementType;\n name: string;\n startTime: number;\n duration: number;\n zIndex: number;\n x?: number;\n y?: number;\n scale?: number;\n opacity?: number;\n}\n\nexport interface TimelineMediaElement extends TimelineElementBase {\n type: MediaElementType;\n src: string;\n mediaStartTime?: number;\n sourceDuration?: number;\n isAroll?: boolean;\n sourceWidth?: number;\n sourceHeight?: number;\n volume?: number; // 0-1 (0% to 100%), default 1.0\n hasAudio?: boolean; // For videos - indicates if video has audio track\n}\n\nexport interface WaveformData {\n peaks: number[];\n duration: number;\n sampleRate?: number;\n}\n\nexport interface TimelineTextElement extends TimelineElementBase {\n type: \"text\";\n content: string;\n color?: string;\n fontSize?: number;\n textShadow?: boolean;\n fontFamily?: string;\n fontWeight?: number;\n textOutline?: boolean;\n textOutlineColor?: string;\n textOutlineWidth?: number;\n textHighlight?: boolean;\n textHighlightColor?: string;\n textHighlightPadding?: number;\n textHighlightRadius?: number;\n}\n\nexport interface TimelineCompositionElement extends TimelineElementBase {\n type: \"composition\";\n src: string;\n compositionId: string;\n scale?: number;\n sourceDuration?: number;\n variableValues?: Record<string, string | number | boolean>;\n sourceWidth?: number;\n sourceHeight?: number;\n}\n\n// Composition Variable Types\nexport type CompositionVariableType =\n | \"string\"\n | \"number\"\n | \"color\"\n | \"boolean\"\n | \"enum\"\n | \"font\"\n | \"image\";\n\n/**\n * Runtime list of every valid `CompositionVariableType`. Use this anywhere\n * a Set/array of valid type strings is needed (lint rules, validators).\n * The `satisfies` guard turns adding a new variant to the union without\n * also adding it here into a compile error.\n */\nexport const COMPOSITION_VARIABLE_TYPES = [\n \"string\",\n \"number\",\n \"color\",\n \"boolean\",\n \"enum\",\n \"font\",\n \"image\",\n] as const satisfies readonly CompositionVariableType[];\n\nexport interface CompositionVariableBase {\n id: string;\n type: CompositionVariableType;\n label: string;\n description?: string;\n}\n\nexport interface StringVariable extends CompositionVariableBase {\n type: \"string\";\n default: string;\n placeholder?: string;\n maxLength?: number;\n}\n\nexport interface NumberVariable extends CompositionVariableBase {\n type: \"number\";\n default: number;\n min?: number;\n max?: number;\n step?: number;\n unit?: string;\n}\n\nexport interface ColorVariable extends CompositionVariableBase {\n type: \"color\";\n default: string;\n /** Brand role identifier, e.g. \"color:primary\". */\n brandRole?: string;\n}\n\nexport interface BooleanVariable extends CompositionVariableBase {\n type: \"boolean\";\n default: boolean;\n}\n\nexport interface EnumVariable extends CompositionVariableBase {\n type: \"enum\";\n default: string;\n options: { value: string; label: string }[];\n}\n\n/**\n * Font variable — value is a `{name, source}` object (object-valued; LOCKED §7).\n * `default` is the fallback font-family name string.\n * `source` is the font stylesheet URL (e.g. Google Fonts CSS).\n * `default_name` / `default_source` are the CSS-level fallbacks when the\n * brand font is absent.\n */\nexport interface FontVariable extends CompositionVariableBase {\n type: \"font\";\n /** Fallback font-family name, e.g. \"Inter\". */\n default: string;\n /** Font stylesheet URL (e.g. Google Fonts CSS link). */\n source?: string;\n /** CSS font-family name to use when source is unavailable, e.g. \"sans-serif\". */\n default_name?: string;\n /** Fallback font stylesheet URL (empty string = system font). */\n default_source?: string;\n}\n\n/**\n * Image variable — value is a `{url, …}` object (object-valued; LOCKED §7).\n * `default` is the fallback image URL string.\n * `brandRole` is an optional semantic label, e.g. \"logo:primary\".\n */\nexport interface ImageVariable extends CompositionVariableBase {\n type: \"image\";\n /** Fallback image URL. */\n default: string;\n /** Brand role identifier, e.g. \"logo:primary\". */\n brandRole?: string;\n}\n\nexport type CompositionVariable =\n | StringVariable\n | NumberVariable\n | ColorVariable\n | BooleanVariable\n | EnumVariable\n | FontVariable\n | ImageVariable;\n\nexport interface CompositionSpec {\n id: string;\n duration: number;\n variables: CompositionVariable[];\n}\n\nexport type TimelineElement =\n | TimelineMediaElement\n | TimelineTextElement\n | TimelineCompositionElement;\n\nexport function isTextElement(el: TimelineElement): el is TimelineTextElement {\n return el.type === \"text\";\n}\n\nexport function isMediaElement(el: TimelineElement): el is TimelineMediaElement {\n return el.type === \"video\" || el.type === \"image\" || el.type === \"audio\";\n}\n\nexport function isCompositionElement(el: TimelineElement): el is TimelineCompositionElement {\n return el.type === \"composition\";\n}\n\nexport interface MediaFile {\n id: string;\n name: string;\n type: TimelineElementType;\n src: string;\n file?: File;\n duration?: number;\n compositionId?: string;\n sourceWidth?: number; // Intrinsic width for compositions\n sourceHeight?: number; // Intrinsic height for compositions\n}\n\nexport const TIMELINE_COLORS: Record<TimelineElementType, string> = {\n video: \"#ec4899\",\n image: \"#3b82f6\",\n text: \"#06b6d4\",\n audio: \"#10b981\",\n composition: \"#f97316\",\n};\n\nexport const DEFAULT_DURATIONS: Record<TimelineElementType, number> = {\n video: 5,\n image: 5,\n text: 2,\n audio: 5,\n composition: 5,\n};\n\nexport interface CompositionAPI {\n id: string;\n duration: number;\n seek(time: number): void;\n getTime(): number;\n getDuration(): number;\n}\n\n// ── Player API types (used by runtime) ────────────────────────────────────\n\nexport interface PlayerAPI {\n play(): void;\n pause(): void;\n seek(time: number, options?: { keepPlaying?: boolean }): void;\n getTime(): number;\n getDuration(): number;\n isPlaying(): boolean;\n getMainTimeline(): unknown;\n getElementBounds(elementId: string): void;\n getElementsAtPoint(x: number, y: number): void;\n setElementPosition(elementId: string, x: number, y: number): void;\n previewElementPosition(elementId: string, x: number, y: number): void;\n setElementKeyframes(\n elementId: string,\n keyframes: Array<{\n id: string;\n time: number;\n properties: { x?: number; y?: number };\n }> | null,\n ): void;\n setElementScale(elementId: string, scale: number): void;\n setElementFontSize(elementId: string, fontSize: number): void;\n setElementTextContent(elementId: string, content: string): void;\n setElementTextColor(elementId: string, color: string): void;\n setElementTextShadow(elementId: string, enabled: boolean): void;\n setElementTextFontWeight(elementId: string, weight: number): void;\n setElementTextFontFamily(elementId: string, fontFamily: string): void;\n setElementTextOutline(elementId: string, enabled: boolean, color?: string, width?: number): void;\n setElementTextHighlight(\n elementId: string,\n enabled: boolean,\n color?: string,\n padding?: number,\n radius?: number,\n ): void;\n setElementVolume(elementId: string, volume: number): void;\n setStageZoom(scale: number, focusX: number, focusY: number): void;\n getStageZoom(): { scale: number; focusX: number; focusY: number };\n setStageZoomKeyframes(\n keyframes: Array<{\n id: string;\n time: number;\n zoom: { scale: number; focusX: number; focusY: number };\n ease?: string;\n }> | null,\n ): void;\n getStageZoomKeyframes(): Array<{\n id: string;\n time: number;\n zoom: { scale: number; focusX: number; focusY: number };\n ease?: string;\n }>;\n addElement(data: AddElementData): boolean;\n removeElement(elementId: string): boolean;\n updateElementTiming(elementId: string, start?: number, end?: number): boolean;\n setElementTiming(\n elementId: string,\n startTime: number,\n duration: number,\n mediaStartTime?: number,\n ): void;\n updateElementSrc(elementId: string, src: string): boolean;\n updateElementLayer(elementId: string, zIndex: number): boolean;\n updateElementBasePosition(elementId: string, x?: number, y?: number, scale?: number): boolean;\n markTimelineDirty(): void;\n isTimelineDirty(): boolean;\n rebuildTimeline(): void;\n ensureTimeline(): void;\n enableRenderMode(): void;\n disableRenderMode(): void;\n renderSeek(time: number): void;\n getElementVisibility(elementId: string): { visible: boolean; opacity?: number };\n getVisibleElements(): Array<{ id: string; tagName: string; start: number; end: number }>;\n getRenderState(): {\n time: number;\n duration: number;\n isPlaying: boolean;\n renderMode: boolean;\n timelineDirty: boolean;\n };\n}\n\nexport interface AddElementData {\n id: string;\n type: \"video\" | \"image\" | \"text\" | \"audio\" | \"composition\";\n name?: string;\n src?: string;\n content?: string;\n start: number;\n end: number;\n zIndex?: number;\n x?: number;\n y?: number;\n scale?: number;\n fontSize?: number;\n color?: string;\n textShadow?: boolean;\n fontWeight?: number;\n textOutline?: boolean;\n textOutlineColor?: string;\n textOutlineWidth?: number;\n textHighlight?: boolean;\n textHighlightColor?: string;\n textHighlightPadding?: number;\n textHighlightRadius?: number;\n compositionId?: string;\n sourceWidth?: number;\n sourceHeight?: number;\n isAroll?: boolean;\n}\n\nexport interface ValidationResult {\n valid: boolean;\n errors: string[];\n warnings: string[];\n}\n\nexport interface CompositionAsset {\n id: string;\n name: string;\n type: \"composition\";\n src: string;\n duration: number;\n compositionId: string;\n thumbnail?: string;\n}\n\nexport interface Keyframe {\n id: string;\n time: number;\n properties: Partial<KeyframeProperties>;\n ease?: string;\n}\n\nexport interface KeyframeProperties {\n x: number;\n y: number;\n opacity: number;\n scale: number;\n scaleX: number;\n scaleY: number;\n rotation: number;\n width: number;\n height: number;\n}\n\nexport interface ElementKeyframes {\n elementId: string;\n keyframes: Keyframe[];\n}\n\nexport interface StageZoom {\n scale: number;\n focusX: number;\n focusY: number;\n}\n\nexport interface StageZoomKeyframe {\n id: string;\n time: number;\n zoom: StageZoom;\n ease?: string;\n}\n\nexport function getDefaultStageZoom(resolution: CanvasResolution): StageZoom {\n const { width, height } = CANVAS_DIMENSIONS[resolution];\n return {\n scale: 1,\n focusX: width / 2,\n focusY: height / 2,\n };\n}\n","/**\n * GSAP property and ease constants.\n *\n * Extracted into a standalone module so browser code can import them\n * without pulling in gsapParser (which depends on recast / @babel/parser).\n */\n\nexport const SUPPORTED_PROPS = [\n // 2D Transforms\n \"x\",\n \"y\",\n \"scale\",\n \"scaleX\",\n \"scaleY\",\n \"rotation\",\n \"skewX\",\n \"skewY\",\n // 3D Transforms\n \"z\",\n \"rotationX\",\n \"rotationY\",\n \"rotationZ\",\n \"perspective\",\n \"transformPerspective\",\n \"transformOrigin\",\n // Visibility\n \"opacity\",\n \"visibility\",\n \"autoAlpha\",\n // Dimensions\n \"width\",\n \"height\",\n // Colors\n \"color\",\n \"backgroundColor\",\n \"borderColor\",\n // Box model\n \"borderRadius\",\n // Typography\n \"fontSize\",\n \"letterSpacing\",\n // Filter & Clipping\n \"filter\",\n \"clipPath\",\n // DOM content (number counters, text roll-ups)\n \"innerText\",\n];\n\n// ── Property Groups ─────────────────────────────────────────────────────────\n// Each group maps to an independent GSAP tween so editing one property\n// (e.g. drag → x/y) never contaminates another (e.g. scale, rotation).\n\nexport type PropertyGroupName = \"position\" | \"scale\" | \"size\" | \"rotation\" | \"visual\" | \"other\";\n\nexport const PROPERTY_GROUPS: Record<PropertyGroupName, ReadonlySet<string>> = {\n position: new Set([\"x\", \"y\", \"xPercent\", \"yPercent\"]),\n scale: new Set([\"scale\", \"scaleX\", \"scaleY\"]),\n size: new Set([\"width\", \"height\"]),\n rotation: new Set([\"rotation\", \"skewX\", \"skewY\"]),\n visual: new Set([\"opacity\", \"autoAlpha\"]),\n other: new Set<string>(),\n};\n\nconst PROP_TO_GROUP = new Map<string, PropertyGroupName>();\nfor (const [group, props] of Object.entries(PROPERTY_GROUPS) as [\n PropertyGroupName,\n ReadonlySet<string>,\n][]) {\n for (const p of props) PROP_TO_GROUP.set(p, group);\n}\n\nexport function classifyPropertyGroup(prop: string): PropertyGroupName {\n return PROP_TO_GROUP.get(prop) ?? \"other\";\n}\n\nexport function classifyTweenPropertyGroup(\n properties: Record<string, unknown>,\n): PropertyGroupName | undefined {\n const groups = new Set<PropertyGroupName>();\n for (const key of Object.keys(properties)) {\n // transformOrigin is a modifier; `_auto` is Studio's internal endpoint marker;\n // `data` is GSAP-reserved (carries the Studio hold-set tag). None is an animated\n // property, so none should affect the group.\n if (key === \"transformOrigin\" || key === \"_auto\" || key === \"data\") continue;\n const g = classifyPropertyGroup(key);\n groups.add(g);\n }\n if (groups.size === 1) return groups.values().next().value;\n return undefined;\n}\n\nexport const SUPPORTED_EASES = [\n \"none\",\n \"power1.in\",\n \"power1.out\",\n \"power1.inOut\",\n \"power2.in\",\n \"power2.out\",\n \"power2.inOut\",\n \"power3.in\",\n \"power3.out\",\n \"power3.inOut\",\n \"power4.in\",\n \"power4.out\",\n \"power4.inOut\",\n \"back.in\",\n \"back.out\",\n \"back.inOut\",\n \"elastic.in\",\n \"elastic.out\",\n \"elastic.inOut\",\n \"bounce.in\",\n \"bounce.out\",\n \"bounce.inOut\",\n \"expo.in\",\n \"expo.out\",\n \"expo.inOut\",\n \"spring-gentle\",\n \"spring-bouncy\",\n \"spring-stiff\",\n \"spring-wobbly\",\n \"spring-heavy\",\n \"steps(1)\",\n];\n","/**\n * Recast-free GSAP helpers: serialization, keyframe<->animation conversion,\n * validation, and shared types.\n *\n * This module MUST NOT import recast / @babel/parser. It is part of the\n * isomorphic core layer that the barrel and browser code depend on. AST\n * parsing of GSAP source lives in the Node-only `./gsapParser` module.\n */\nimport type { Keyframe, KeyframeProperties, ValidationResult } from \"./types.js\";\nimport type { PropertyGroupName } from \"./gsapConstants\";\n\nexport type GsapMethod = \"set\" | \"to\" | \"from\" | \"fromTo\";\n\n/** How a tween was constructed in source — drives display classification and editability. */\nexport type GsapProvenanceKind = \"literal\" | \"helper\" | \"loop\" | \"runtime-dynamic\";\n\n/**\n * Origin of a parsed tween. `literal` tweens map 1:1 to a source call and edit\n * directly; `helper`/`loop` tweens are expanded from a reused construct (unroll\n * to edit); `runtime-dynamic` tweens come from live introspection (override to\n * edit). Absent provenance is treated as `literal`.\n */\nexport interface GsapProvenance {\n kind: GsapProvenanceKind;\n /** Helper function name (kind === \"helper\"). */\n fn?: string;\n /** 1-based ordinal of the originating call site / loop construct in source order. */\n callSite?: number;\n /** 0-based iteration index (kind === \"loop\"). */\n iteration?: number;\n /** Source offset [start, end] of the originating call/loop, when known. */\n sourceRange?: [number, number];\n}\n\n/** How a tween's keyframes can be edited, derived from its provenance. */\nexport type KeyframeEditability = \"direct\" | \"unroll\" | \"source\";\n\n/**\n * Map provenance to an editing strategy:\n * - `direct` — literal tween, maps 1:1 to source; edit in place.\n * - `unroll` — helper/loop expansion; unroll to literal tweens, then edit.\n * - `source` — runtime-dynamic value; not statically editable, edit the code.\n */\nexport function editabilityForProvenance(provenance?: GsapProvenance): KeyframeEditability {\n if (!provenance || provenance.kind === \"literal\") return \"direct\";\n if (provenance.kind === \"runtime-dynamic\") return \"source\";\n return \"unroll\";\n}\n\nexport interface GsapAnimation {\n id: string;\n targetSelector: string;\n method: GsapMethod;\n position: number | string;\n properties: Record<string, number | string>;\n fromProperties?: Record<string, number | string>;\n duration?: number;\n ease?: string;\n /** Non-editable GSAP config (stagger, yoyo, repeat, etc.) preserved for round-trips. */\n extras?: Record<string, unknown>;\n /** Native GSAP keyframes data — present when the tween uses keyframes: { ... }. */\n keyframes?: GsapKeyframesData;\n /** Arc motion path config — present when the tween uses motionPath for curved position interpolation. */\n arcPath?: ArcPathConfig;\n /** True when the tween has a `keyframes` property that couldn't be statically resolved (dynamic). */\n hasUnresolvedKeyframes?: boolean;\n /** True when the tween's target selector couldn't be statically resolved (dynamic). */\n hasUnresolvedSelector?: boolean;\n /** Absolute start time computed by walking the timeline chain (handles +=, -=, <, >, labels). */\n resolvedStart?: number;\n /** True when no position arg was authored — the tween is sequentially placed by GSAP. */\n implicitPosition?: boolean;\n /** Which property group this tween belongs to (position, scale, size, rotation, visual, other).\n * Undefined for legacy mixed tweens that bundle multiple groups. */\n propertyGroup?: PropertyGroupName;\n /** True for a base `gsap.set(...)` (a static hold that runs immediately, OFF the\n * timeline) rather than `tl.set(...)`. Carries no timeline position and shows no\n * keyframe marker — used to persist a static value (e.g. a 3D transform) without\n * introducing a 0% keyframe. */\n global?: boolean;\n /** How this tween was constructed in source. Absent ⇒ literal. */\n provenance?: GsapProvenance;\n}\n\nexport interface GsapPercentageKeyframe {\n percentage: number;\n properties: Record<string, number | string>;\n ease?: string;\n}\n\nexport type GsapKeyframeFormat = \"percentage\" | \"object-array\" | \"simple-array\";\n\nexport interface GsapKeyframesData {\n format: GsapKeyframeFormat;\n keyframes: GsapPercentageKeyframe[];\n ease?: string;\n easeEach?: string;\n}\n\nexport interface ArcPathSegment {\n curviness: number;\n cp1?: { x: number; y: number };\n cp2?: { x: number; y: number };\n}\n\nexport interface ArcPathConfig {\n enabled: boolean;\n autoRotate: boolean | number;\n segments: ArcPathSegment[];\n}\n\nexport interface MotionPathShape {\n arcPath: ArcPathConfig;\n waypoints: Array<{ x: number; y: number }>;\n}\n\n/**\n * Build arcPath segments + waypoints from resolved path coordinates. Shared by\n * the AST parser (coords from literal nodes) and the runtime scanner (coords\n * from a live `vars.motionPath`), so both produce identical arc config.\n */\nexport function buildArcPath(\n coords: Array<{ x: number; y: number }>,\n curviness: number,\n autoRotate: boolean | number,\n isCubic: boolean,\n): MotionPathShape | undefined {\n const first = coords[0];\n if (coords.length < 2 || !first) return undefined;\n const segments: ArcPathSegment[] = [];\n let waypoints: Array<{ x: number; y: number }>;\n if (isCubic && coords.length >= 4) {\n // coords are [anchor, cp1, cp2, anchor, cp1, cp2, anchor, ...].\n waypoints = [first];\n for (let i = 1; i + 2 < coords.length; i += 3) {\n const cp1 = coords[i];\n const cp2 = coords[i + 1];\n const anchor = coords[i + 2];\n if (!cp1 || !cp2 || !anchor) continue;\n waypoints.push(anchor);\n segments.push({ curviness, cp1, cp2 });\n }\n } else {\n waypoints = coords;\n for (let i = 0; i < waypoints.length - 1; i++) segments.push({ curviness });\n }\n return { arcPath: { enabled: true, autoRotate, segments }, waypoints };\n}\n\nexport interface ParsedGsap {\n animations: GsapAnimation[];\n timelineVar: string;\n preamble: string;\n postamble: string;\n multipleTimelines?: boolean;\n unsupportedTimelinePattern?: boolean;\n}\n\nexport { SUPPORTED_PROPS, SUPPORTED_EASES } from \"./gsapConstants\";\n\n// ── Split-animation types (used by gsapWriterAcorn) ─────────────────────────\n\nexport interface SplitAnimationsOptions {\n originalId: string;\n newId: string;\n splitTime: number;\n elementStart: number;\n elementDuration: number;\n}\n\nexport interface SplitAnimationsResult {\n script: string;\n /** Non-ID-selector animations that the engine cannot safely retarget. */\n skippedSelectors: string[];\n}\n\n// ── Serialization ───────────────────────────────────────────────────────────\n\nexport function serializeGsapAnimations(\n animations: GsapAnimation[],\n timelineVar = \"tl\",\n options?: { includeMediaSync?: boolean; preamble?: string; postamble?: string },\n): string {\n const sorted = [...animations].sort((a, b) => {\n const aNum =\n a.resolvedStart ?? (typeof a.position === \"number\" ? a.position : Number.MAX_SAFE_INTEGER);\n const bNum =\n b.resolvedStart ?? (typeof b.position === \"number\" ? b.position : Number.MAX_SAFE_INTEGER);\n return aNum - bNum;\n });\n // fallow-ignore-next-line complexity\n const lines = sorted.map((anim) => {\n const selector = `\"${anim.targetSelector}\"`;\n const props: Record<string, number | string> = { ...anim.properties };\n if (anim.duration !== undefined) props.duration = anim.duration;\n if (anim.ease) props.ease = anim.ease;\n let propsStr = serializeObject(props);\n if (anim.extras && Object.keys(anim.extras).length > 0) {\n const extrasStr = serializeExtras(anim.extras);\n if (Object.keys(props).length === 0) {\n propsStr = `{ ${extrasStr} }`;\n } else {\n // Insert extras before the closing brace\n propsStr = propsStr.slice(0, -2) + `, ${extrasStr} }`;\n }\n }\n const posStr = typeof anim.position === \"string\" ? `\"${anim.position}\"` : anim.position;\n switch (anim.method) {\n case \"set\":\n // A global set is a base `gsap.set` — off the timeline, no position arg.\n return anim.global\n ? ` gsap.set(${selector}, ${propsStr});`\n : ` ${timelineVar}.set(${selector}, ${propsStr}, ${posStr});`;\n case \"to\":\n return ` ${timelineVar}.to(${selector}, ${propsStr}, ${posStr});`;\n case \"from\":\n return ` ${timelineVar}.from(${selector}, ${propsStr}, ${posStr});`;\n case \"fromTo\": {\n const fromStr = serializeObject(anim.fromProperties || {});\n return ` ${timelineVar}.fromTo(${selector}, ${fromStr}, ${propsStr}, ${posStr});`;\n }\n }\n });\n\n let mediaSync = \"\";\n if (options?.includeMediaSync) {\n mediaSync = `\n ${timelineVar}.eventCallback(\"onUpdate\", function() {\n const time = ${timelineVar}.time();\n document.querySelectorAll(\"video[data-start], audio[data-start]\").forEach(function(media) {\n const start = parseFloat(media.dataset.start);\n const end = parseFloat(media.dataset.end) || Infinity;\n const mediaTime = time - start;\n if (time >= start && time < end) {\n if (Math.abs(media.currentTime - mediaTime) > 0.1) {\n media.currentTime = mediaTime;\n }\n if (media.paused && !${timelineVar}.paused()) {\n media.play().catch(function() {});\n }\n } else if (!media.paused) {\n media.pause();\n }\n });\n });`;\n }\n\n const preamble = options?.preamble || `const ${timelineVar} = gsap.timeline({ paused: true });`;\n const postamble = options?.postamble ? `\\n ${options.postamble}` : \"\";\n\n return `\n ${preamble}\n${lines.join(\"\\n\")}${mediaSync}${postamble}\n `;\n}\n\nexport function serializeValue(value: unknown): string {\n if (typeof value === \"string\" && value.startsWith(\"__raw:\")) {\n return value.slice(6);\n }\n if (typeof value === \"string\") return JSON.stringify(value);\n return String(value);\n}\n\nexport function safeJsKey(key: string): string {\n return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key) ? key : JSON.stringify(key);\n}\n\nfunction serializeObject(obj: Record<string, number | string>): string {\n const entries = Object.entries(obj).map(([key, value]) => {\n return `${safeJsKey(key)}: ${serializeValue(value)}`;\n });\n return `{ ${entries.join(\", \")} }`;\n}\n\nfunction serializeExtras(extras: Record<string, unknown>): string {\n return Object.entries(extras)\n .map(([key, value]) => {\n return `${safeJsKey(key)}: ${serializeValue(value)}`;\n })\n .join(\", \");\n}\n\n// ── Element filtering ─────────────────────────────────────────────────────────\n\n/**\n * Filter animations to those targeting `#<elementId>` (id-only match). For the\n * studio panel's id-OR-selector matching, see `getAnimationsForElement` in\n * `useGsapTweenCache.ts` — distinct on purpose, hence the distinct name.\n */\nexport function getAnimationsForElementId(\n animations: GsapAnimation[],\n elementId: string,\n): GsapAnimation[] {\n const selector = `#${elementId}`;\n return animations.filter((a) => a.targetSelector === selector);\n}\n\n// ── Validation (regex-based, no AST needed) ─────────────────────────────────\n\nconst FORBIDDEN_GSAP_PATTERNS: Array<{ pattern: RegExp; message: string }> = [\n { pattern: /\\.call\\s*\\(/, message: \"call() method not allowed\" },\n { pattern: /\\.add\\s*\\(/, message: \"add() method not allowed\" },\n { pattern: /\\.addPause\\s*\\(/, message: \"addPause() method not allowed\" },\n { pattern: /gsap\\.registerEffect\\s*\\(/, message: \"registerEffect() not allowed\" },\n { pattern: /ScrollTrigger/, message: \"ScrollTrigger not allowed\" },\n { pattern: /onComplete\\s*:/, message: \"onComplete callback not allowed\" },\n { pattern: /onUpdate\\s*:/, message: \"onUpdate callback not allowed\" },\n { pattern: /onStart\\s*:/, message: \"onStart callback not allowed\" },\n { pattern: /onRepeat\\s*:/, message: \"onRepeat callback not allowed\" },\n { pattern: /onReverseComplete\\s*:/, message: \"onReverseComplete callback not allowed\" },\n { pattern: /repeat\\s*:\\s*-1/, message: \"Infinite repeat (repeat: -1) not allowed\" },\n { pattern: /Math\\.random\\s*\\(/, message: \"Random values (Math.random) not allowed\" },\n { pattern: /Date\\.now\\s*\\(/, message: \"Date-dependent values (Date.now) not allowed\" },\n { pattern: /new\\s+Date\\s*\\(/, message: \"Date constructor not allowed\" },\n { pattern: /setTimeout\\s*\\(/, message: \"setTimeout not allowed\" },\n { pattern: /setInterval\\s*\\(/, message: \"setInterval not allowed\" },\n { pattern: /requestAnimationFrame\\s*\\(/, message: \"requestAnimationFrame not allowed\" },\n];\n\nexport function validateCompositionGsap(script: string): ValidationResult {\n const errors: string[] = [];\n const warnings: string[] = [];\n for (const { pattern, message } of FORBIDDEN_GSAP_PATTERNS) {\n if (pattern.test(script)) errors.push(message);\n }\n if (/yoyo\\s*:\\s*true/.test(script)) {\n warnings.push(\"yoyo animations may behave unexpectedly when scrubbing\");\n }\n if (/stagger\\s*:/.test(script)) {\n warnings.push(\"stagger animations may not serialize correctly\");\n }\n return { valid: errors.length === 0, errors, warnings };\n}\n\n// ── Keyframe Conversion Helpers ─────────────────────────────────────────────\n\nexport function keyframesToGsapAnimations(\n elementId: string,\n keyframes: Keyframe[],\n elementStartTime: number,\n base?: { x?: number; y?: number; scale?: number },\n): GsapAnimation[] {\n const sorted = [...keyframes].sort((a, b) => a.time - b.time);\n const animations: GsapAnimation[] = [];\n const baseX = base?.x ?? 0;\n const baseY = base?.y ?? 0;\n const baseScale = base?.scale ?? 1;\n\n // fallow-ignore-next-line complexity\n sorted.forEach((kf, i) => {\n const absoluteTime = elementStartTime + kf.time;\n const isFirst = i === 0;\n const prevKf = i > 0 ? sorted[i - 1] : null;\n const duration = prevKf ? kf.time - prevKf.time : undefined;\n const position = prevKf ? elementStartTime + prevKf.time : absoluteTime;\n\n const properties: Record<string, number | string> = {};\n for (const [key, value] of Object.entries(kf.properties)) {\n if (typeof value !== \"number\") continue;\n if (key === \"x\") properties.x = baseX + value;\n else if (key === \"y\") properties.y = baseY + value;\n else if (key === \"scale\") properties.scale = baseScale * value;\n else properties[key] = value;\n }\n\n animations.push({\n id: `${elementId}-kf-${kf.id}`,\n targetSelector: `#${elementId}`,\n method: isFirst ? \"set\" : \"to\",\n position,\n properties,\n duration: isFirst ? undefined : duration,\n ease: kf.ease,\n });\n });\n\n return animations;\n}\n\nexport function gsapAnimationsToKeyframes(\n animations: GsapAnimation[],\n elementStartTime: number,\n options?: {\n baseX?: number;\n baseY?: number;\n baseScale?: number;\n clampTimeToZero?: boolean;\n skipBaseSet?: boolean;\n },\n): Keyframe[] {\n const validMethods: GsapMethod[] = [\"set\", \"to\", \"from\", \"fromTo\"];\n const baseX = options?.baseX ?? 0;\n const baseY = options?.baseY ?? 0;\n const baseScale = options?.baseScale ?? 1;\n const clampTimeToZero = options?.clampTimeToZero ?? true;\n const skipBaseSet = options?.skipBaseSet ?? false;\n const baseTimeEpsilon = 0.001;\n const baseValueEpsilon = 0.00001;\n\n return (\n animations\n .filter(\n (a): a is GsapAnimation & { position: number } =>\n validMethods.includes(a.method) && typeof a.position === \"number\",\n )\n // fallow-ignore-next-line complexity\n .map((a) => {\n const relativeTimeRaw = a.position - elementStartTime;\n const time = clampTimeToZero ? Math.max(0, relativeTimeRaw) : relativeTimeRaw;\n\n const properties: Partial<KeyframeProperties> = {};\n for (const [key, value] of Object.entries(a.properties)) {\n if (typeof value !== \"number\") continue;\n if (key === \"x\") properties.x = value - baseX;\n else if (key === \"y\") properties.y = value - baseY;\n else if (key === \"scale\") {\n properties.scale = baseScale !== 0 ? value / baseScale : value;\n } else {\n (properties as Record<string, number>)[key] = value;\n }\n }\n\n if (\n skipBaseSet &&\n a.method === \"set\" &&\n time < baseTimeEpsilon &&\n Object.values(properties).every(\n (v) => typeof v === \"number\" && Math.abs(v) < baseValueEpsilon,\n )\n ) {\n return null;\n }\n\n return {\n id: a.id.replace(/^.*-kf-/, \"\"),\n time,\n properties: properties as KeyframeProperties,\n ease: a.ease,\n };\n })\n .filter((kf): kf is NonNullable<typeof kf> => kf !== null)\n );\n}\n\n// ── Keyframe-conversion transforms (pure; shared by recast + acorn writers) ────\n\n/**\n * CSS identity values for properties whose \"rest\" state isn't 0 — used to\n * synthesize the missing endpoint when converting a flat tween to keyframes.\n */\nconst CSS_IDENTITY: Record<string, number> = {\n opacity: 1,\n autoAlpha: 1,\n scale: 1,\n scaleX: 1,\n scaleY: 1,\n};\n\nfunction cssIdentityValue(prop: string): number {\n return CSS_IDENTITY[prop] ?? 0;\n}\n\n/** Build the identity-endpoint map for a flat tween's properties. */\nfunction buildIdentityMap(props: Record<string, number | string>): Record<string, number | string> {\n const identity: Record<string, number | string> = {};\n for (const [key, val] of Object.entries(props)) {\n if (val != null) identity[key] = typeof val === \"number\" ? cssIdentityValue(key) : val;\n }\n return identity;\n}\n\n/**\n * Resolve the 0% (from) and 100% (to) property maps for a tween being\n * converted to percentage keyframes.\n *\n * @param resolvedFromValues — Despite the \"from\" in the name (historical), these\n * are runtime-captured DOM values that override the conversion endpoint:\n * - For to(): overrides fromProps (the 0% state / where the element is now).\n * - For from(): overrides toProps (the 100% state / where the element rests).\n * - For fromTo(): merges into toProps (the 100% endpoint the user is editing).\n */\nexport function resolveConversionProps(\n anim: GsapAnimation,\n resolvedFromValues?: Record<string, number | string>,\n): { fromProps: Record<string, number | string>; toProps: Record<string, number | string> } {\n if (anim.method === \"set\") {\n // A static hold becomes a keyframed `to` whose 0% and 100% both start at the\n // set's value — the visual is unchanged until the user edits a keyframe to\n // animate it. (The caller flips the call from `set` to `to` + adds a duration.)\n return { fromProps: { ...anim.properties }, toProps: { ...anim.properties } };\n }\n if (anim.method === \"to\") {\n const identity = buildIdentityMap(anim.properties);\n const fromProps = resolvedFromValues ? { ...identity, ...resolvedFromValues } : identity;\n return { fromProps, toProps: { ...anim.properties } };\n }\n if (anim.method === \"from\") {\n const identity = buildIdentityMap(anim.properties);\n const toProps = resolvedFromValues ? { ...identity, ...resolvedFromValues } : identity;\n return { fromProps: { ...anim.properties }, toProps };\n }\n // fromTo(fromVars, toVars): anim.fromProperties = fromVars (0% state),\n // anim.properties = toVars (100% state). resolvedFromValues contains the\n // current DOM position from a drag — it represents the NEW destination, so\n // it merges into toProps (the 100% endpoint the user is editing), NOT into\n // fromProps. This is intentional and not inverted.\n const toProps = resolvedFromValues\n ? { ...anim.properties, ...resolvedFromValues }\n : { ...anim.properties };\n return { fromProps: { ...(anim.fromProperties ?? {}) }, toProps };\n}\n\n// ── Arc path serialization helpers (shared by recast + acorn writers) ─────────\n\nfunction numericXY(props: Record<string, number | string>): { x: number; y: number } | null {\n const vx = props.x;\n const vy = props.y;\n return typeof vx === \"number\" && typeof vy === \"number\" ? { x: vx, y: vy } : null;\n}\n\nexport function extractArcWaypoints(anim: GsapAnimation): Array<{ x: number; y: number }> {\n const keyframeWps = (anim.keyframes?.keyframes ?? [])\n .map((kf) => numericXY(kf.properties))\n .filter((pt): pt is { x: number; y: number } => pt !== null);\n if (keyframeWps.length >= 2) return keyframeWps;\n const propX = anim.properties.x;\n const propY = anim.properties.y;\n if (typeof propX !== \"number\" && typeof propY !== \"number\") return keyframeWps;\n const destX = typeof propX === \"number\" ? propX : 0;\n const destY = typeof propY === \"number\" ? propY : 0;\n return [\n { x: 0, y: 0 },\n { x: destX, y: destY },\n ];\n}\n\nfunction autoRotateSuffix(autoRotate: boolean | number): string {\n if (autoRotate === true) return \", autoRotate: true\";\n if (typeof autoRotate === \"number\") return `, autoRotate: ${autoRotate}`;\n return \"\";\n}\n\nfunction cubicControlPoints(\n seg: ArcPathSegment,\n wp: { x: number; y: number },\n nextWp: { x: number; y: number },\n): string[] {\n if (seg.cp1 && seg.cp2) {\n return [`{x: ${seg.cp1.x}, y: ${seg.cp1.y}}`, `{x: ${seg.cp2.x}, y: ${seg.cp2.y}}`];\n }\n const dx = nextWp.x - wp.x;\n const dy = nextWp.y - wp.y;\n const c = seg.curviness ?? 1;\n return [\n `{x: ${wp.x + dx * 0.33}, y: ${wp.y + dy * 0.33 - c * Math.abs(dx) * 0.25}}`,\n `{x: ${wp.x + dx * 0.66}, y: ${wp.y + dy * 0.66 - c * Math.abs(dx) * 0.25}}`,\n ];\n}\n\nfunction buildCubicPathEntries(\n waypoints: Array<{ x: number; y: number }>,\n segments: ArcPathSegment[],\n): string[] {\n const first = waypoints[0];\n if (!first) return [];\n const entries = [`{x: ${first.x}, y: ${first.y}}`];\n for (let i = 0; i < segments.length; i++) {\n const seg = segments[i];\n const wp = waypoints[i];\n const nextWp = waypoints[i + 1];\n if (!seg || !wp || !nextWp) continue;\n entries.push(...cubicControlPoints(seg, wp, nextWp));\n entries.push(`{x: ${nextWp.x}, y: ${nextWp.y}}`);\n }\n return entries;\n}\n\nexport function buildMotionPathObjectCode(config: {\n waypoints: Array<{ x: number; y: number }>;\n segments: ArcPathSegment[];\n autoRotate: boolean | number;\n}): string {\n const { waypoints, segments, autoRotate } = config;\n const arSuffix = autoRotateSuffix(autoRotate);\n // GSAP's simple `path` array supports only ONE scalar `curviness` for the whole\n // path, so per-segment curviness can only be expressed in the cubic form (each\n // segment's curviness baked into its control points). Emit cubic when segments\n // carry explicit control points OR when their curviness values differ — the\n // simple branch would otherwise serialize only segments[0].curviness and drop\n // every other segment's curve.\n const hasExplicitCp = segments.some((s) => s.cp1 && s.cp2);\n const curvinessVaries = segments.some(\n (s) => (s.curviness ?? 1) !== (segments[0]?.curviness ?? 1),\n );\n if ((hasExplicitCp || curvinessVaries) && waypoints.length >= 2) {\n const pathStr = buildCubicPathEntries(waypoints, segments).join(\", \");\n return `{ path: [${pathStr}], type: \"cubic\"${arSuffix} }`;\n }\n const pathEntries = waypoints.map((wp) => `{x: ${wp.x}, y: ${wp.y}}`);\n const curviness = segments[0]?.curviness ?? 1;\n const curvPart = curviness !== 1 ? `, curviness: ${curviness}` : \"\";\n return `{ path: [${pathEntries.join(\", \")}]${curvPart}${arSuffix} }`;\n}\n","/**\n * Node-only GSAP AST parser. Depends on recast / @babel/parser, which compile\n * to CommonJS that calls `require(\"fs\")` — so this module must never be in the\n * static import graph of isomorphic/browser code. It is reachable only via the\n * `@hyperframes/core/gsap-parser` subpath (studio-api mutations + the linter).\n *\n * Recast-free helpers (serialization, keyframe conversion, validation, types)\n * live in `./gsapSerialize` and are re-exported here so this subpath exposes the\n * full surface for tests and server-side consumers.\n */\nimport * as recast from \"recast\";\nimport { parse as babelParse } from \"@babel/parser\";\nimport {\n type ArcPathConfig,\n type ArcPathSegment,\n type GsapAnimation,\n type GsapKeyframesData,\n type GsapMethod,\n type GsapPercentageKeyframe,\n type ParsedGsap,\n serializeValue as valueToCode,\n safeJsKey as safeKey,\n resolveConversionProps,\n} from \"./gsapSerialize\";\n\nexport type {\n ArcPathConfig,\n ArcPathSegment,\n GsapAnimation,\n GsapMethod,\n ParsedGsap,\n GsapKeyframesData,\n GsapPercentageKeyframe,\n GsapKeyframeFormat,\n} from \"./gsapSerialize\";\nexport {\n serializeGsapAnimations,\n getAnimationsForElementId,\n validateCompositionGsap,\n keyframesToGsapAnimations,\n gsapAnimationsToKeyframes,\n SUPPORTED_PROPS,\n SUPPORTED_EASES,\n} from \"./gsapSerialize\";\nexport type { PropertyGroupName } from \"./gsapConstants\";\nexport {\n PROPERTY_GROUPS,\n classifyPropertyGroup,\n classifyTweenPropertyGroup,\n} from \"./gsapConstants\";\nimport { classifyPropertyGroup, classifyTweenPropertyGroup } from \"./gsapConstants\";\nimport type { PropertyGroupName } from \"./gsapConstants\";\nexport { generateSpringEaseData, SPRING_PRESETS } from \"./springEase\";\nexport type { SpringPreset } from \"./springEase\";\n\nconst GSAP_METHODS = new Set<string>([\"set\", \"to\", \"from\", \"fromTo\"]);\n\n// ── Recast / Babel AST shape types ────────────────────────────────────────\n//\n// Recast's own typings are loose (`any` everywhere). These local shapes\n// capture the properties we actually access, giving us IDE navigation and\n// catch-at-write-time safety without depending on @babel/types at runtime.\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- recast AST nodes are inherently untyped\ninterface AstNode extends Record<string, any> {\n type: string;\n}\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- recast visitor paths are inherently untyped\ninterface AstPath extends Record<string, any> {\n node: AstNode;\n}\n\n// ── Recast AST Helpers ──────────────────────────────────────────────────────\n\ntype ScopeBindings = ReadonlyMap<string, number | string | boolean>;\n\nfunction parseScript(script: string) {\n return recast.parse(script, {\n parser: {\n parse(source: string) {\n return babelParse(source, { sourceType: \"script\", plugins: [], tokens: true });\n },\n },\n });\n}\n\nfunction collectScopeBindings(ast: AstNode): ScopeBindings {\n const bindings = new Map<string, number | string | boolean>();\n recast.types.visit(ast, {\n visitVariableDeclarator(path: AstPath) {\n const name = path.node.id?.name;\n const init = path.node.init;\n if (name && init) {\n const val = resolveNode(init, bindings);\n if (val !== undefined) bindings.set(name, val);\n }\n this.traverse(path);\n },\n });\n return bindings;\n}\n\nfunction resolveNode(\n node: AstNode | undefined,\n scope: ReadonlyMap<string, number | string | boolean>,\n): number | string | boolean | undefined {\n if (!node) return undefined;\n if (node.type === \"NumericLiteral\" || (node.type === \"Literal\" && typeof node.value === \"number\"))\n return node.value;\n if (node.type === \"StringLiteral\" || (node.type === \"Literal\" && typeof node.value === \"string\"))\n return node.value;\n if (\n node.type === \"BooleanLiteral\" ||\n (node.type === \"Literal\" && typeof node.value === \"boolean\")\n )\n return node.value;\n if (node.type === \"UnaryExpression\" && node.operator === \"-\" && node.argument) {\n const val = resolveNode(node.argument, scope);\n return typeof val === \"number\" ? -val : undefined;\n }\n if (node.type === \"BinaryExpression\") {\n const left = resolveNode(node.left, scope);\n const right = resolveNode(node.right, scope);\n if (typeof left === \"number\" && typeof right === \"number\") {\n switch (node.operator) {\n case \"+\":\n return left + right;\n case \"-\":\n return left - right;\n case \"*\":\n return left * right;\n case \"/\":\n return right !== 0 ? left / right : undefined;\n }\n }\n if (typeof left === \"string\" && node.operator === \"+\") return left + String(right ?? \"\");\n if (typeof right === \"string\" && node.operator === \"+\") return String(left ?? \"\") + right;\n }\n if (node.type === \"Identifier\" && scope.has(node.name)) {\n return scope.get(node.name);\n }\n if (node.type === \"TemplateLiteral\" && node.expressions?.length === 0) {\n return node.quasis?.[0]?.value?.cooked ?? undefined;\n }\n return undefined;\n}\n\nfunction extractLiteralValue(node: AstNode | undefined, scope: ScopeBindings): unknown {\n return resolveNode(node, scope);\n}\n\n// ── Element-target resolution ───────────────────────────────────────────────\n//\n// Real compositions target tweens through element variables resolved from the\n// DOM (`const kicker = root.querySelector(\".kicker\"); tl.to(kicker, …)`), arrays\n// of them (`tl.to([a, b], …)`), `gsap.utils.toArray(\".sel\")`, and per-element\n// loop variables (`items.forEach(el => tl.to(el, …))`) — not inline string\n// selectors. To make those tweens editable we resolve each target back to the\n// CSS selector(s) it addresses. Resolution is lexically scoped: the same\n// variable name can mean different elements in different IIFEs.\n\nconst QUERY_METHODS = new Set([\"querySelector\", \"querySelectorAll\"]);\nconst ITERATION_METHODS = new Set([\"forEach\", \"map\"]);\nconst SCOPE_NODE_TYPES = new Set([\n \"Program\",\n \"FunctionDeclaration\",\n \"FunctionExpression\",\n \"ArrowFunctionExpression\",\n]);\n\n/**\n * If `node` is a DOM lookup call — `x.querySelector(\".sel\")`,\n * `document.querySelectorAll(\".sel\")`, `document.getElementById(\"id\")`, or\n * `gsap.utils.toArray(\".sel\")` — return the CSS selector it resolves to.\n * `getElementById(\"id\")` maps to `#id`. Returns null for anything else.\n */\nfunction selectorFromQueryCall(node: AstNode, scope: ScopeBindings): string | null {\n if (node?.type !== \"CallExpression\") return null;\n const callee = node.callee;\n if (callee?.type !== \"MemberExpression\" || callee.property?.type !== \"Identifier\") return null;\n const method = callee.property.name;\n const argValue = resolveNode(node.arguments?.[0], scope);\n if (typeof argValue !== \"string\" || argValue.length === 0) return null;\n if (QUERY_METHODS.has(method) || method === \"toArray\") return argValue;\n if (method === \"getElementById\") return `#${argValue}`;\n return null;\n}\n\n/** The nearest enclosing function/program node — the binding scope of `path`. */\nfunction enclosingScopeNode(path: AstPath): AstNode | null {\n let p = path?.parentPath;\n while (p) {\n if (SCOPE_NODE_TYPES.has(p.node?.type)) return p.node;\n p = p.parentPath;\n }\n return null;\n}\n\n/** Scope nodes enclosing `path`, innermost first. */\nfunction scopeChainOf(path: AstPath): AstNode[] {\n const chain: AstNode[] = [];\n let p = path;\n while (p) {\n if (SCOPE_NODE_TYPES.has(p.node?.type)) chain.push(p.node);\n p = p.parentPath;\n }\n return chain;\n}\n\n/** Per-scope element bindings: scopeNode → (variable name → selector). */\ntype TargetBindings = Map<any, Map<string, string>>;\n\nfunction addBinding(\n bindings: TargetBindings,\n scopeNode: AstNode,\n name: string,\n selector: string,\n): void {\n let scoped = bindings.get(scopeNode);\n if (!scoped) {\n scoped = new Map();\n bindings.set(scopeNode, scoped);\n }\n if (!scoped.has(name)) scoped.set(name, selector);\n}\n\n/**\n * Build a lexically-scoped index of element variables → selector. Two passes:\n * (1) direct DOM-lookup assignments (`const x = root.querySelector(...)`), then\n * (2) iteration callback params (`coll.forEach(el => …)`), whose element type is\n * the collection's selector — resolved against the pass-1 bindings.\n */\nfunction collectTargetBindings(ast: AstNode, scope: ScopeBindings): TargetBindings {\n const bindings: TargetBindings = new Map();\n\n recast.types.visit(ast, {\n visitVariableDeclarator(path: AstPath) {\n const name = path.node.id?.name;\n const selector = selectorFromQueryCall(path.node.init, scope);\n const scopeNode = enclosingScopeNode(path);\n if (name && selector !== null && scopeNode) addBinding(bindings, scopeNode, name, selector);\n this.traverse(path);\n },\n visitAssignmentExpression(path: AstPath) {\n const left = path.node.left;\n const selector = selectorFromQueryCall(path.node.right, scope);\n const scopeNode = enclosingScopeNode(path);\n if (left?.type === \"Identifier\" && selector !== null && scopeNode) {\n addBinding(bindings, scopeNode, left.name, selector);\n }\n this.traverse(path);\n },\n });\n\n // Pass 2: forEach/map callback params take the collection's selector.\n recast.types.visit(ast, {\n visitCallExpression(path: AstPath) {\n const node = path.node;\n const callee = node.callee;\n if (\n callee?.type === \"MemberExpression\" &&\n callee.property?.type === \"Identifier\" &&\n ITERATION_METHODS.has(callee.property.name)\n ) {\n const collectionSelector = resolveCollectionSelector(callee.object, path, scope, bindings);\n const fn = node.arguments?.[0];\n const param = fn?.params?.[0];\n if (collectionSelector && param?.type === \"Identifier\" && isFunctionNode(fn)) {\n addBinding(bindings, fn, param.name, collectionSelector);\n }\n }\n this.traverse(path);\n },\n });\n\n return bindings;\n}\n\nfunction isFunctionNode(node: AstNode): boolean {\n return (\n node?.type === \"ArrowFunctionExpression\" ||\n node?.type === \"FunctionExpression\" ||\n node?.type === \"FunctionDeclaration\"\n );\n}\n\n/** Resolve the selector a `.forEach`/`.map` is iterating over (variable or inline call). */\nfunction resolveCollectionSelector(\n node: AstNode,\n callPath: AstPath,\n scope: ScopeBindings,\n bindings: TargetBindings,\n): string | null {\n if (node?.type === \"Identifier\") return lookupBinding(node.name, callPath, bindings);\n if (node?.type === \"CallExpression\") return selectorFromQueryCall(node, scope);\n return null;\n}\n\n/** Resolve a variable name to its selector using the lexical scope chain of `path`. */\nfunction lookupBinding(name: string, path: AstPath, bindings: TargetBindings): string | null {\n for (const scopeNode of scopeChainOf(path)) {\n const selector = bindings.get(scopeNode)?.get(name);\n if (selector !== undefined) return selector;\n }\n return null;\n}\n\n/**\n * Resolve a tween's first argument to a CSS selector. Handles inline string\n * literals, element variables (lexically scoped), arrays of elements (joined\n * into a CSS group selector), inline DOM lookup / `toArray` calls, and indexed\n * access (`items[i]`). Returns null when the target can't be resolved\n * statically (e.g. an object-target duration anchor `tl.to({ _: 0 }, …)`, or a\n * runtime-computed selector).\n */\nfunction resolveTargetSelector(\n node: AstNode,\n path: AstPath,\n scope: ScopeBindings,\n bindings: TargetBindings,\n): string | null {\n if (!node) return null;\n if (node.type === \"StringLiteral\" || node.type === \"Literal\") {\n return typeof node.value === \"string\" ? node.value : null;\n }\n if (node.type === \"Identifier\") {\n return lookupBinding(node.name, path, bindings);\n }\n if (node.type === \"CallExpression\") {\n return selectorFromQueryCall(node, scope);\n }\n if (node.type === \"ArrayExpression\") {\n const parts = node.elements\n .map((el: AstNode) => resolveTargetSelector(el, path, scope, bindings))\n .filter((s: string | null): s is string => typeof s === \"string\" && s.length > 0);\n return parts.length > 0 ? parts.join(\", \") : null;\n }\n if (node.type === \"MemberExpression\" && node.object?.type === \"Identifier\") {\n // `items[i]` — the element type is the collection's selector.\n return lookupBinding(node.object.name, path, bindings);\n }\n return null;\n}\n\nfunction objectExpressionToRecord(node: AstNode, scope: ScopeBindings): Record<string, unknown> {\n const result: Record<string, unknown> = {};\n if (node?.type !== \"ObjectExpression\") return result;\n for (const prop of node.properties ?? []) {\n if (prop.type !== \"ObjectProperty\" && prop.type !== \"Property\") continue;\n const key = prop.key?.name ?? prop.key?.value;\n if (!key) continue;\n const resolved = resolveNode(prop.value, scope);\n if (resolved !== undefined) {\n result[key] = resolved;\n } else {\n // Preserve unresolvable values as raw source text so they survive round-trips\n result[key] = `__raw:${recast.print(prop.value).code}`;\n }\n }\n return result;\n}\n\n// ── Timeline Variable Detection ─────────────────────────────────────────────\n\nfunction isGsapTimelineCall(node: AstNode): boolean {\n return (\n node?.type === \"CallExpression\" &&\n node.callee?.type === \"MemberExpression\" &&\n node.callee.object?.name === \"gsap\" &&\n node.callee.property?.name === \"timeline\"\n );\n}\n\ninterface TimelineDefaults {\n ease?: string;\n duration?: number;\n}\n\n// `identifier` is the canonical `const tl = …` form; `member` is the inline\n// `window.__timelines[\"scene\"] = …` form (the timeline IS the member expression).\ntype TimelineRef = { kind: \"identifier\"; name: string } | { kind: \"member\"; node: AstNode };\n\ninterface TimelineDetection {\n timelineVar: string | null;\n ref: TimelineRef | null;\n timelineCount: number;\n defaults?: TimelineDefaults;\n}\n\n/** The static string key of a member access (`window.__timelines[\"scene\"]` → \"scene\"), else null. */\nfunction staticMemberKey(node: AstNode): string | null {\n if (!node || node.type !== \"MemberExpression\") return null;\n if (node.computed) {\n const p = node.property;\n if (p?.type === \"StringLiteral\") return p.value;\n if (p?.type === \"Literal\" && typeof p.value === \"string\") return p.value;\n return null;\n }\n return node.property?.type === \"Identifier\" ? node.property.name : null;\n}\n\nfunction isStaticMemberRef(node: AstNode): boolean {\n return node?.type === \"MemberExpression\" && staticMemberKey(node) !== null;\n}\n\n/** Structural equality of two member accesses (object chain + static key), quote-insensitive. */\nfunction sameMemberAccess(a: AstNode, b: AstNode): boolean {\n if (a?.type !== \"MemberExpression\" || b?.type !== \"MemberExpression\") return false;\n if (staticMemberKey(a) !== staticMemberKey(b) || staticMemberKey(a) === null) return false;\n const ao = a.object;\n const bo = b.object;\n if (ao?.type === \"Identifier\" && bo?.type === \"Identifier\") return ao.name === bo.name;\n if (ao?.type === \"MemberExpression\" && bo?.type === \"MemberExpression\")\n return sameMemberAccess(ao, bo);\n return false;\n}\n\n/** The source string a tween call roots at: identifier name, or the member source as written. */\nfunction timelineRootSource(ref: TimelineRef): string {\n return ref.kind === \"identifier\" ? ref.name : recast.print(ref.node).code;\n}\n\nfunction escapeRegExp(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\nfunction extractTimelineDefaults(\n callNode: AstNode,\n scope: ScopeBindings,\n): TimelineDefaults | undefined {\n const arg = callNode.arguments?.[0];\n if (!arg || arg.type !== \"ObjectExpression\") return undefined;\n const defaultsProp = arg.properties?.find(\n (p: AstNode) => isObjectProperty(p) && propKeyName(p) === \"defaults\",\n );\n if (!defaultsProp?.value || defaultsProp.value.type !== \"ObjectExpression\") return undefined;\n const record = objectExpressionToRecord(defaultsProp.value, scope);\n const result: TimelineDefaults = {};\n if (typeof record.ease === \"string\") result.ease = record.ease;\n if (typeof record.duration === \"number\") result.duration = record.duration;\n return Object.keys(result).length > 0 ? result : undefined;\n}\n\nfunction findTimelineVar(ast: AstNode, scope?: ScopeBindings): TimelineDetection {\n let timelineVar: string | null = null;\n let ref: TimelineRef | null = null;\n let timelineCount = 0;\n let defaults: TimelineDefaults | undefined;\n const emptyScope: ScopeBindings = scope ?? new Map();\n recast.types.visit(ast, {\n visitVariableDeclarator(path: AstPath) {\n if (isGsapTimelineCall(path.node.init)) {\n timelineCount += 1;\n if (!ref && path.node.id?.type === \"Identifier\") {\n timelineVar = path.node.id.name;\n ref = { kind: \"identifier\", name: path.node.id.name };\n defaults = extractTimelineDefaults(path.node.init, emptyScope);\n }\n }\n this.traverse(path);\n },\n visitAssignmentExpression(path: AstPath) {\n if (isGsapTimelineCall(path.node.right)) {\n timelineCount += 1;\n if (!ref) {\n const left = path.node.left;\n if (left?.type === \"Identifier\") {\n timelineVar = left.name;\n ref = { kind: \"identifier\", name: left.name };\n defaults = extractTimelineDefaults(path.node.right, emptyScope);\n } else if (isStaticMemberRef(left)) {\n ref = { kind: \"member\", node: left };\n defaults = extractTimelineDefaults(path.node.right, emptyScope);\n }\n }\n }\n this.traverse(path);\n },\n });\n return { timelineVar, ref, timelineCount, defaults };\n}\n\n// ── Find All Tween Calls ────────────────────────────────────────────────────\n\ninterface TweenCallInfo {\n path: AstPath;\n node: AstNode;\n method: GsapMethod;\n selector: string;\n varsArg: AstNode;\n fromArg?: AstNode;\n positionArg?: AstNode;\n /** True for a base `gsap.set(...)` (off-timeline) rather than `tl.set(...)`. */\n global?: boolean;\n}\n\n/**\n * True when the member chain of `callNode.callee` is rooted at the timeline\n * variable — `tl.to(...)` and every link of a chain `tl.to(...).to(...)`.\n */\nfunction isTimelineRootedCall(callNode: AstNode, ref: TimelineRef): boolean {\n let obj = callNode.callee?.object;\n while (obj?.type === \"CallExpression\") {\n obj = obj.callee?.object;\n }\n if (ref.kind === \"identifier\") return obj?.type === \"Identifier\" && obj.name === ref.name;\n return sameMemberAccess(obj, ref.node);\n}\n\nfunction findAllTweenCalls(\n ast: AstNode,\n ref: TimelineRef,\n scope: ScopeBindings,\n targetBindings: TargetBindings,\n): TweenCallInfo[] {\n const results: TweenCallInfo[] = [];\n recast.types.visit(ast, {\n visitCallExpression(path: AstPath) {\n const node = path.node;\n const callee = node.callee;\n // A base `gsap.set(\"#sel\", props)` is an off-timeline static hold (no position,\n // no keyframe marker). Treat it as an editable `set` animation so a static\n // value (e.g. a 3D transform) round-trips and re-edits in place. Restricted to\n // a STRING-LITERAL selector: variable-target `gsap.set(el, ...)` holds stay\n // opaque surrounding source (editing them by selector would be ambiguous).\n const gsapSetArg = node.arguments?.[0];\n const isGlobalSet =\n callee?.type === \"MemberExpression\" &&\n callee.object?.type === \"Identifier\" &&\n callee.object.name === \"gsap\" &&\n callee.property?.type === \"Identifier\" &&\n callee.property.name === \"set\" &&\n (gsapSetArg?.type === \"StringLiteral\" ||\n (gsapSetArg?.type === \"Literal\" && typeof gsapSetArg.value === \"string\"));\n if (\n callee?.type === \"MemberExpression\" &&\n callee.property?.type === \"Identifier\" &&\n (isTimelineRootedCall(node, ref) || isGlobalSet)\n ) {\n const method = callee.property.name;\n if (!GSAP_METHODS.has(method)) {\n this.traverse(path);\n return;\n }\n const args = node.arguments;\n if (args.length < 2) {\n this.traverse(path);\n return;\n }\n const selectorValue =\n resolveTargetSelector(args[0], path, scope, targetBindings) ?? \"__unresolved__\";\n\n if (method === \"fromTo\") {\n results.push({\n path,\n node,\n method: \"fromTo\",\n selector: selectorValue,\n fromArg: args[1],\n varsArg: args[2],\n positionArg: args[3],\n });\n } else {\n results.push({\n path,\n node,\n method: method as GsapMethod,\n selector: selectorValue,\n varsArg: args[1],\n positionArg: args[2],\n ...(isGlobalSet ? { global: true } : {}),\n });\n }\n }\n this.traverse(path);\n },\n });\n return results;\n}\n\n/** Keys that are stored on dedicated GsapAnimation fields (not in properties/extras). */\nconst BUILTIN_VAR_KEYS = new Set([\"duration\", \"ease\", \"delay\"]);\n\n/** Keys that are never preserved (callbacks / advanced patterns). */\nconst DROPPED_VAR_KEYS = new Set([\"onComplete\", \"onStart\", \"onUpdate\", \"onRepeat\"]);\n\n/** Keys that belong in `extras` — non-editable GSAP config that must survive round-trips. */\nconst EXTRAS_KEYS = new Set([\n \"stagger\",\n \"yoyo\",\n \"repeat\",\n \"repeatDelay\",\n \"snap\",\n \"overwrite\",\n \"immediateRender\",\n]);\n\n/**\n * Extract raw source text for a property in an ObjectExpression AST node.\n * Returns the printed source of the value node, suitable for verbatim re-emission.\n */\nfunction extractRawPropertySource(varsArgNode: AstNode, key: string): string | undefined {\n const node = findPropertyNode(varsArgNode, key);\n return node ? recast.print(node).code : undefined;\n}\n\n/** Find the raw AST node for a named property inside an ObjectExpression. */\nfunction findPropertyNode(varsArgNode: AstNode, key: string): AstNode | undefined {\n if (varsArgNode?.type !== \"ObjectExpression\") return undefined;\n for (const prop of varsArgNode.properties ?? []) {\n if (!isObjectProperty(prop)) continue;\n if (propKeyName(prop) === key) return prop.value;\n }\n return undefined;\n}\n\n// ── Native GSAP Keyframes Parsing ──────────────────────────────────────────\n\nconst PERCENTAGE_KEY_RE = /^(\\d+(?:\\.\\d+)?)%$/;\n\n/** Extract a string-valued ease or easeEach from an AST property node. */\nfunction tryResolveStringProp(propValue: AstNode, scope: ScopeBindings): string | undefined {\n const val = resolveNode(propValue, scope);\n return typeof val === \"string\" ? val : undefined;\n}\n\n/**\n * Parse a `keyframes` property value from a tween vars AST node into a\n * normalized `GsapKeyframesData` structure. Handles all three GSAP formats:\n * percentage objects, object arrays, and simple (property-array) objects.\n */\n// fallow-ignore-next-line complexity\nfunction parseKeyframesNode(\n node: AstNode | undefined,\n scope: ScopeBindings,\n): GsapKeyframesData | undefined {\n if (!node) return undefined;\n\n // ── Object array format: keyframes: [ { x: 0, duration: 0.5 }, ... ] ──\n if (node.type === \"ArrayExpression\") {\n return parseObjectArrayKeyframes(node, scope);\n }\n\n if (node.type !== \"ObjectExpression\") return undefined;\n\n // Distinguish percentage vs simple-array by inspecting property keys/values.\n const props = node.properties ?? [];\n let hasPercentageKey = false;\n let hasArrayValue = false;\n\n for (const prop of props) {\n if (prop.type !== \"ObjectProperty\" && prop.type !== \"Property\") continue;\n const key = prop.key?.value ?? prop.key?.name;\n if (typeof key === \"string\" && PERCENTAGE_KEY_RE.test(key)) {\n hasPercentageKey = true;\n break;\n }\n if (prop.value?.type === \"ArrayExpression\") {\n hasArrayValue = true;\n }\n }\n\n if (hasPercentageKey) return parsePercentageKeyframes(node, scope);\n if (hasArrayValue) return parseSimpleArrayKeyframes(node, scope);\n\n return undefined;\n}\n\n// fallow-ignore-next-line complexity\nfunction parsePercentageKeyframes(node: AstNode, scope: ScopeBindings): GsapKeyframesData {\n const keyframes: GsapPercentageKeyframe[] = [];\n let ease: string | undefined;\n let easeEach: string | undefined;\n\n for (const prop of node.properties ?? []) {\n if (prop.type !== \"ObjectProperty\" && prop.type !== \"Property\") continue;\n const key = prop.key?.value ?? prop.key?.name;\n if (typeof key !== \"string\") continue;\n\n const pctMatch = PERCENTAGE_KEY_RE.exec(key);\n if (pctMatch) {\n const percentage = Number.parseFloat(pctMatch[1]!);\n const record = objectExpressionToRecord(prop.value, scope);\n const properties: Record<string, number | string> = {};\n let kfEase: string | undefined;\n for (const [k, v] of Object.entries(record)) {\n if (k === \"ease\" && typeof v === \"string\") {\n kfEase = v;\n } else if (typeof v === \"number\" || typeof v === \"string\") {\n properties[k] = v;\n }\n }\n keyframes.push({ percentage, properties, ...(kfEase ? { ease: kfEase } : {}) });\n } else if (key === \"ease\") {\n ease = tryResolveStringProp(prop.value, scope) ?? ease;\n } else if (key === \"easeEach\") {\n easeEach = tryResolveStringProp(prop.value, scope) ?? easeEach;\n }\n }\n\n keyframes.sort((a, b) => a.percentage - b.percentage);\n\n return {\n format: \"percentage\",\n keyframes,\n ...(ease ? { ease } : {}),\n ...(easeEach ? { easeEach } : {}),\n };\n}\n\nfunction computeKeyframesTotalDuration(\n varsNode: AstNode,\n scope: ScopeBindings,\n): number | undefined {\n const kfNode = (varsNode.properties ?? []).find(\n (p: AstNode) => (p.key?.name ?? p.key?.value) === \"keyframes\",\n )?.value;\n if (!kfNode || kfNode.type !== \"ArrayExpression\") return undefined;\n let total = 0;\n for (const el of kfNode.elements ?? []) {\n if (!el || el.type !== \"ObjectExpression\") continue;\n const r = objectExpressionToRecord(el, scope);\n if (typeof r.duration === \"number\") total += r.duration;\n }\n return total > 0 ? total : undefined;\n}\n\n// fallow-ignore-next-line complexity\nfunction parseObjectArrayKeyframes(node: AstNode, scope: ScopeBindings): GsapKeyframesData {\n const elements = node.elements ?? [];\n const raw: Array<{\n properties: Record<string, number | string>;\n duration?: number;\n ease?: string;\n }> = [];\n\n for (const el of elements) {\n if (!el || (el.type !== \"ObjectExpression\" && el.type !== \"ObjectProperty\")) {\n // Skip non-object elements\n if (el?.type !== \"ObjectExpression\") continue;\n }\n const record = objectExpressionToRecord(el, scope);\n const properties: Record<string, number | string> = {};\n let duration: number | undefined;\n let ease: string | undefined;\n for (const [k, v] of Object.entries(record)) {\n if (k === \"duration\" && typeof v === \"number\") {\n duration = v;\n } else if (k === \"ease\" && typeof v === \"string\") {\n ease = v;\n } else if (typeof v === \"number\" || typeof v === \"string\") {\n properties[k] = v;\n }\n }\n raw.push({ properties, duration, ease });\n }\n\n // Convert durations to percentage positions. If durations are present, use\n // cumulative ratios; otherwise distribute evenly.\n const totalDuration = raw.reduce((sum, r) => sum + (r.duration ?? 0), 0);\n const keyframes: GsapPercentageKeyframe[] = [];\n\n if (totalDuration > 0) {\n let cumulative = 0;\n for (const entry of raw) {\n cumulative += entry.duration ?? 0;\n const percentage = Math.round((cumulative / totalDuration) * 100);\n keyframes.push({\n percentage,\n properties: entry.properties,\n ...(entry.ease ? { ease: entry.ease } : {}),\n });\n }\n } else {\n for (let i = 0; i < raw.length; i++) {\n const entry = raw[i]!;\n const percentage = raw.length > 1 ? Math.round((i / (raw.length - 1)) * 100) : 0;\n keyframes.push({\n percentage,\n properties: entry.properties,\n ...(entry.ease ? { ease: entry.ease } : {}),\n });\n }\n }\n\n return { format: \"object-array\", keyframes };\n}\n\n// fallow-ignore-next-line complexity\nfunction parseSimpleArrayKeyframes(node: AstNode, scope: ScopeBindings): GsapKeyframesData {\n const arrayProps: Map<string, (number | string)[]> = new Map();\n let ease: string | undefined;\n let easeEach: string | undefined;\n\n for (const prop of node.properties ?? []) {\n if (prop.type !== \"ObjectProperty\" && prop.type !== \"Property\") continue;\n const key = prop.key?.name ?? prop.key?.value;\n if (typeof key !== \"string\") continue;\n\n if (prop.value?.type === \"ArrayExpression\") {\n const values: (number | string)[] = [];\n for (const el of prop.value.elements ?? []) {\n const val = resolveNode(el, scope);\n if (typeof val === \"number\" || typeof val === \"string\") {\n values.push(val);\n }\n }\n if (values.length > 0) arrayProps.set(key, values);\n } else if (key === \"ease\") {\n ease = tryResolveStringProp(prop.value, scope) ?? ease;\n } else if (key === \"easeEach\") {\n easeEach = tryResolveStringProp(prop.value, scope) ?? easeEach;\n }\n }\n\n // Zip arrays into percentage keyframes (evenly spaced).\n const maxLen = Math.max(...[...arrayProps.values()].map((a) => a.length), 0);\n const keyframes: GsapPercentageKeyframe[] = [];\n\n for (let i = 0; i < maxLen; i++) {\n const percentage = maxLen > 1 ? Math.round((i / (maxLen - 1)) * 100) : 0;\n const properties: Record<string, number | string> = {};\n for (const [key, values] of arrayProps) {\n if (i < values.length) properties[key] = values[i]!;\n }\n keyframes.push({ percentage, properties });\n }\n\n return {\n format: \"simple-array\",\n keyframes,\n ...(ease ? { ease } : {}),\n ...(easeEach ? { easeEach } : {}),\n };\n}\n\n// ── MotionPath Parsing ────────────────────────────────────────────────────\n\ninterface MotionPathParseResult {\n arcPath: ArcPathConfig;\n waypoints: Array<{ x: number; y: number }>;\n}\n\nfunction parseMotionPathNode(\n node: AstNode | undefined,\n scope: ScopeBindings,\n): MotionPathParseResult | undefined {\n if (!node) return undefined;\n\n let pathNode: AstNode | undefined;\n let autoRotate: boolean | number = false;\n let curviness = 1;\n let isCubic = false;\n\n if (node.type === \"ObjectExpression\") {\n for (const prop of node.properties ?? []) {\n if (!isObjectProperty(prop)) continue;\n const key = propKeyName(prop);\n if (key === \"path\") pathNode = prop.value;\n else if (key === \"autoRotate\") {\n const val = resolveNode(prop.value, scope);\n autoRotate = typeof val === \"number\" ? val : val === true;\n } else if (key === \"curviness\") {\n const val = resolveNode(prop.value, scope);\n if (typeof val === \"number\") curviness = val;\n } else if (key === \"type\") {\n const val = resolveNode(prop.value, scope);\n if (val === \"cubic\") isCubic = true;\n }\n }\n } else if (node.type === \"ArrayExpression\") {\n pathNode = node;\n }\n\n if (!pathNode || pathNode.type !== \"ArrayExpression\") return undefined;\n\n const elements = pathNode.elements ?? [];\n const coords: Array<{ x: number; y: number }> = [];\n for (const elem of elements) {\n if (!elem || elem.type !== \"ObjectExpression\") continue;\n const rec = objectExpressionToRecord(elem, scope);\n const x = typeof rec.x === \"number\" ? rec.x : undefined;\n const y = typeof rec.y === \"number\" ? rec.y : undefined;\n if (x !== undefined && y !== undefined) coords.push({ x, y });\n }\n\n if (coords.length < 2) return undefined;\n\n let waypoints: Array<{ x: number; y: number }>;\n const segments: ArcPathSegment[] = [];\n\n if (isCubic && coords.length >= 4) {\n // type: \"cubic\" — coords are [anchor, cp1, cp2, anchor, cp1, cp2, anchor, ...]\n // Every 3rd coord starting from 0 is an anchor, the two between are control points.\n waypoints = [];\n waypoints.push(coords[0]!);\n for (let i = 1; i + 2 < coords.length; i += 3) {\n const cp1 = coords[i]!;\n const cp2 = coords[i + 1]!;\n const anchor = coords[i + 2]!;\n waypoints.push(anchor);\n segments.push({ curviness, cp1, cp2 });\n }\n } else {\n // Waypoint array with global curviness\n waypoints = coords;\n for (let i = 0; i < waypoints.length - 1; i++) {\n segments.push({ curviness });\n }\n }\n\n return {\n arcPath: { enabled: true, autoRotate, segments },\n waypoints,\n };\n}\n\n// fallow-ignore-next-line complexity\nfunction tweenCallToAnimation(\n call: TweenCallInfo,\n scope: ScopeBindings,\n): Omit<GsapAnimation, \"id\"> {\n const vars = objectExpressionToRecord(call.varsArg, scope);\n const properties: Record<string, number | string> = {};\n const extras: Record<string, unknown> = {};\n let keyframesData: GsapKeyframesData | undefined;\n let hasUnresolvedKeyframes = false;\n let motionPathResult: MotionPathParseResult | undefined;\n\n for (const [key, val] of Object.entries(vars)) {\n if (BUILTIN_VAR_KEYS.has(key)) continue;\n if (DROPPED_VAR_KEYS.has(key)) continue;\n\n if (key === \"keyframes\") {\n const kfNode = findPropertyNode(call.varsArg, \"keyframes\");\n keyframesData = parseKeyframesNode(kfNode, scope);\n if (!keyframesData && kfNode) hasUnresolvedKeyframes = true;\n continue;\n }\n\n if (key === \"motionPath\") {\n const mpNode = findPropertyNode(call.varsArg, \"motionPath\");\n motionPathResult = parseMotionPathNode(mpNode, scope);\n continue;\n }\n\n if (key === \"easeEach\") {\n // easeEach is only meaningful alongside keyframes — handled below.\n continue;\n }\n\n if (EXTRAS_KEYS.has(key)) {\n // For extras, prefer the raw AST source so complex objects like\n // `stagger: { each: 0.15, from: \"start\" }` survive verbatim.\n const rawSource = extractRawPropertySource(call.varsArg, key);\n if (rawSource !== undefined) {\n extras[key] = `__raw:${rawSource}`;\n } else if (val !== undefined) {\n extras[key] = val;\n }\n continue;\n }\n\n if (typeof val === \"number\" || typeof val === \"string\") {\n properties[key] = val;\n }\n }\n\n // Apply tween-level easeEach to keyframes data.\n if (keyframesData && typeof vars.easeEach === \"string\") {\n keyframesData.easeEach = vars.easeEach as string;\n }\n\n // When motionPath is present, reconstruct x/y as keyframe waypoints.\n if (motionPathResult) {\n const { waypoints } = motionPathResult;\n if (!keyframesData) {\n // No explicit keyframes — create synthetic percentage keyframes from waypoints.\n const kf: GsapPercentageKeyframe[] = waypoints.map((wp, i) => ({\n percentage: waypoints.length > 1 ? Math.round((i / (waypoints.length - 1)) * 100) : 0,\n properties: { x: wp.x, y: wp.y },\n }));\n keyframesData = { format: \"percentage\", keyframes: kf };\n } else {\n // Merge waypoint positions into existing keyframes at matching percentages.\n // If keyframe count matches waypoint count, assign positionally.\n const kfs = keyframesData.keyframes;\n if (kfs.length === waypoints.length) {\n for (let i = 0; i < kfs.length; i++) {\n kfs[i]!.properties.x = waypoints[i]!.x;\n kfs[i]!.properties.y = waypoints[i]!.y;\n }\n }\n }\n // arcPath is attached below on the animation result.\n }\n\n let fromProperties: Record<string, number | string> | undefined;\n if (call.method === \"fromTo\" && call.fromArg) {\n fromProperties = {};\n const fromVars = objectExpressionToRecord(call.fromArg, scope);\n for (const [key, val] of Object.entries(fromVars)) {\n if (typeof val === \"number\" || typeof val === \"string\") {\n fromProperties[key] = val;\n }\n }\n }\n\n const hasPositionArg = !!call.positionArg;\n const posVal = call.positionArg ? extractLiteralValue(call.positionArg, scope) : 0;\n const position: number | string =\n typeof posVal === \"number\" ? posVal : typeof posVal === \"string\" ? posVal : 0;\n let duration = typeof vars.duration === \"number\" ? vars.duration : undefined;\n const ease = typeof vars.ease === \"string\" ? vars.ease : undefined;\n\n if (duration === undefined && keyframesData) {\n duration = computeKeyframesTotalDuration(call.varsArg, scope);\n }\n\n const anim: Omit<GsapAnimation, \"id\"> = {\n targetSelector: call.selector,\n method: call.method,\n position,\n properties,\n fromProperties,\n duration,\n ease,\n };\n if (!hasPositionArg) anim.implicitPosition = true;\n let group = classifyTweenPropertyGroup(properties);\n if (!group && keyframesData) {\n const kfProps: Record<string, unknown> = {};\n for (const kf of keyframesData.keyframes) {\n for (const k of Object.keys(kf.properties)) kfProps[k] = true;\n }\n group = classifyTweenPropertyGroup(kfProps);\n }\n if (group) anim.propertyGroup = group;\n if (call.global) anim.global = true;\n if (Object.keys(extras).length > 0) anim.extras = extras;\n if (keyframesData) anim.keyframes = keyframesData;\n if (motionPathResult) anim.arcPath = motionPathResult.arcPath;\n if (hasUnresolvedKeyframes) anim.hasUnresolvedKeyframes = true;\n if (call.selector === \"__unresolved__\") anim.hasUnresolvedSelector = true;\n return anim;\n}\n\n// ── Timeline Position Resolution ──────────────────────────────────────────\n\nconst GSAP_DEFAULT_DURATION = 0.5;\n\n// NOTE: Label-based positions (e.g. \"myLabel+=0.5\") are not yet resolved —\n// they fall through to parseFloat which returns null for non-numeric strings.\nfunction resolvePositionString(pos: string, cursor: number, prevStart: number): number | null {\n const trimmed = pos.trim();\n if (trimmed === \"\") return cursor;\n if (trimmed.startsWith(\"+=\")) {\n const n = Number.parseFloat(trimmed.slice(2));\n return Number.isFinite(n) ? cursor + n : null;\n }\n if (trimmed.startsWith(\"-=\")) {\n const n = Number.parseFloat(trimmed.slice(2));\n return Number.isFinite(n) ? cursor - n : null;\n }\n if (trimmed === \"<\") return prevStart;\n if (trimmed === \">\") return cursor;\n if (trimmed.startsWith(\"<\")) {\n const n = Number.parseFloat(trimmed.slice(1));\n return Number.isFinite(n) ? prevStart + n : null;\n }\n if (trimmed.startsWith(\">\")) {\n const n = Number.parseFloat(trimmed.slice(1));\n return Number.isFinite(n) ? cursor + n : null;\n }\n const n = Number.parseFloat(trimmed);\n return Number.isFinite(n) ? n : null;\n}\n\nfunction applyTimelineDefaults(\n anims: Omit<GsapAnimation, \"id\">[],\n defaults?: TimelineDefaults,\n): void {\n if (!defaults) return;\n for (const anim of anims) {\n if (anim.method === \"set\") continue;\n if (anim.duration === undefined && defaults.duration !== undefined) {\n anim.duration = defaults.duration;\n }\n if (anim.ease === undefined && defaults.ease !== undefined) {\n anim.ease = defaults.ease;\n }\n }\n}\n\nfunction resolveTimelinePositions(anims: Omit<GsapAnimation, \"id\">[]): void {\n let cursor = 0;\n let prevStart = 0;\n for (const anim of anims) {\n // A global `gsap.set(...)` is off-timeline — it's applied once at load, not\n // sequenced on the master timeline. It carries no position arg, so the\n // cursor-based fallback below would otherwise hand it the comp-end time\n // (every prior tween's duration summed). Pin it to 0 (its load-time start)\n // and don't let it advance the cursor/prevStart for following tweens.\n if (anim.method === \"set\" && anim.global) {\n anim.resolvedStart = 0;\n continue;\n }\n const duration = anim.method === \"set\" ? 0 : (anim.duration ?? GSAP_DEFAULT_DURATION);\n let start: number | null;\n\n if (anim.implicitPosition) {\n start = cursor;\n } else if (typeof anim.position === \"number\") {\n start = anim.position;\n } else if (typeof anim.position === \"string\") {\n start = resolvePositionString(anim.position, cursor, prevStart);\n } else {\n start = cursor;\n }\n\n if (start != null) {\n anim.resolvedStart = Math.max(0, start);\n prevStart = anim.resolvedStart;\n cursor = Math.max(cursor, anim.resolvedStart + duration);\n }\n }\n}\n\nfunction sortBySourcePosition(calls: TweenCallInfo[]): void {\n calls.sort((a, b) => {\n const aLoc = a.node.callee?.property?.loc?.start;\n const bLoc = b.node.callee?.property?.loc?.start;\n if (!aLoc || !bLoc) return 0;\n return aLoc.line - bLoc.line || aLoc.column - bLoc.column;\n });\n}\n\n// ── Stable ID Generation ───────────────────────────────────────────────────\n\n/**\n * IDs are transient — recomputed on every parse, never persisted across sessions.\n * They exist only in ephemeral request/response payloads, React component state,\n * and the in-memory keyframe cache (rebuilt on every page load). No database,\n * localStorage, or file stores animation IDs, so changing the ID format (e.g.\n * adding a `-scale`/`-position` suffix) is safe.\n */\nfunction assignStableIds(anims: Omit<GsapAnimation, \"id\">[]): GsapAnimation[] {\n const counts = new Map<string, number>();\n return anims.map((anim) => {\n const posKey =\n typeof anim.position === \"number\"\n ? String(Math.round(anim.position * 1000))\n : String(anim.position);\n const groupSuffix = anim.propertyGroup ? `-${anim.propertyGroup}` : \"\";\n const base = `${anim.targetSelector}-${anim.method}-${posKey}${groupSuffix}`;\n const count = (counts.get(base) ?? 0) + 1;\n counts.set(base, count);\n const id = count === 1 ? base : `${base}-${count}`;\n return { ...anim, id };\n });\n}\n\n// ── Shared parse (AST + located tween calls) ────────────────────────────────\n\ninterface ParsedGsapAst {\n ast: AstNode;\n scope: ScopeBindings;\n timelineVar: string;\n detection: TimelineDetection;\n /** Tween calls in document order, each paired with its stable animation id. */\n located: Array<{ id: string; call: TweenCallInfo; animation: GsapAnimation }>;\n}\n\n/**\n * Parse a script to its recast AST plus the located tween calls. The mutation\n * functions reuse this so they can edit the exact call node in place (recast\n * preserves all surrounding source — interleaved `gsap.set`, element variable\n * declarations, the IIFE wrapper, comments and formatting).\n */\nfunction parseGsapAst(script: string): ParsedGsapAst {\n const ast = parseScript(script);\n const scope = collectScopeBindings(ast);\n const targetBindings = collectTargetBindings(ast, scope);\n const detection = findTimelineVar(ast, scope);\n const ref: TimelineRef = detection.ref ?? { kind: \"identifier\", name: \"tl\" };\n const timelineVar = timelineRootSource(ref);\n const calls = findAllTweenCalls(ast, ref, scope, targetBindings);\n sortBySourcePosition(calls);\n const rawAnims = calls.map((call) => tweenCallToAnimation(call, scope));\n applyTimelineDefaults(rawAnims, detection.defaults);\n resolveTimelinePositions(rawAnims);\n const animations = assignStableIds(rawAnims);\n const located = animations.map((animation, i) => ({\n id: animation.id,\n call: calls[i]!,\n animation,\n }));\n return { ast, scope, timelineVar, detection, located };\n}\n\n// ── Public API ──────────────────────────────────────────────────────────────\n\nexport function parseGsapScript(script: string): ParsedGsap {\n try {\n const { detection, timelineVar, located } = parseGsapAst(script);\n const ref: TimelineRef = detection.ref ?? { kind: \"identifier\", name: \"tl\" };\n const animations = located.map((l) => l.animation);\n\n const declPattern =\n ref.kind === \"identifier\"\n ? `(?:const|let|var)\\\\s+${timelineVar}\\\\s*=\\\\s*gsap\\\\.timeline\\\\s*\\\\([^)]*\\\\)\\\\s*;?`\n : `${escapeRegExp(timelineVar)}\\\\s*=\\\\s*gsap\\\\.timeline\\\\s*\\\\([^)]*\\\\)\\\\s*;?`;\n const timelineMatch = script.match(new RegExp(`^[\\\\s\\\\S]*?${declPattern}`));\n const fallbackPreamble =\n ref.kind === \"identifier\"\n ? `const ${timelineVar} = gsap.timeline({ paused: true });`\n : `${timelineVar} = gsap.timeline({ paused: true });`;\n const preamble = timelineMatch?.[0] ?? fallbackPreamble;\n\n const lastCallIdx = script.lastIndexOf(`${timelineVar}.`);\n let postamble = \"\";\n if (lastCallIdx !== -1) {\n const afterLast = script.slice(lastCallIdx);\n const endOfCall = afterLast.indexOf(\";\");\n if (endOfCall !== -1) {\n postamble = script.slice(lastCallIdx + endOfCall + 1).trim();\n }\n }\n\n const result: ParsedGsap = { animations, timelineVar, preamble, postamble };\n if (detection.timelineCount > 1) result.multipleTimelines = true;\n if (detection.timelineCount > 0 && detection.ref === null)\n result.unsupportedTimelinePattern = true;\n return result;\n } catch {\n return { animations: [], timelineVar: \"tl\", preamble: \"\", postamble: \"\" };\n }\n}\n\n// ── In-place AST mutation helpers ───────────────────────────────────────────\n//\n// Edits operate directly on the located call's AST node and reprint via recast,\n// which preserves every untouched statement. This is what lets us edit tweens\n// in real compositions (variable targets, interleaved `gsap.set`, IIFE wrapper)\n// without regenerating — and discarding — the surrounding code.\n\n/**\n * Parse a value/expression snippet into a standalone AST expression node.\n * Uses an assignment (`__hf__ = <code>`) rather than wrapping in parens so an\n * object literal parses as an expression without recast re-emitting the\n * surrounding parentheses.\n */\nfunction parseExpr(code: string): AstNode {\n return parseScript(`__hf__ = ${code};`).program.body[0].expression.right;\n}\n\nfunction propKeyName(prop: AstNode): string | undefined {\n return prop?.key?.name ?? prop?.key?.value;\n}\n\nfunction isObjectProperty(prop: AstNode): boolean {\n return prop?.type === \"ObjectProperty\" || prop?.type === \"Property\";\n}\n\n/** A key the inspector treats as an editable transform/style property. */\nfunction isEditablePropertyKey(key: string): boolean {\n return !BUILTIN_VAR_KEYS.has(key) && !DROPPED_VAR_KEYS.has(key) && !EXTRAS_KEYS.has(key);\n}\n\nfunction makeObjectProperty(key: string, value: number | string): AstNode {\n const obj = parseExpr(`{ ${safeKey(key)}: ${valueToCode(value)} }`);\n return obj.properties[0];\n}\n\n/** Set (or insert) a single key on an ObjectExpression, preserving sibling keys. */\nfunction setVarsKey(varsArg: AstNode, key: string, value: number | string): void {\n if (varsArg?.type !== \"ObjectExpression\") return;\n const existing = varsArg.properties.find(\n (p: AstNode) => isObjectProperty(p) && propKeyName(p) === key,\n );\n if (existing) {\n existing.value = parseExpr(valueToCode(value));\n } else {\n varsArg.properties.push(makeObjectProperty(key, value));\n }\n}\n\n/**\n * Filter an ObjectExpression's properties, keeping non-editable keys\n * and delegating the keep/drop decision for editable keys to `shouldKeep`.\n */\nfunction filterEditableKeys(varsArg: AstNode, shouldKeep: (key: string) => boolean): void {\n if (varsArg?.type !== \"ObjectExpression\") return;\n varsArg.properties = varsArg.properties.filter((p: AstNode) => {\n if (!isObjectProperty(p)) return true;\n const key = propKeyName(p);\n if (typeof key !== \"string\") return true;\n if (!isEditablePropertyKey(key)) return true;\n return shouldKeep(key);\n });\n}\n\n/**\n * Replace the editable-property keys on an ObjectExpression with `newProps`,\n * leaving `duration`, `ease`, `stagger`, callbacks and other non-editable keys\n * untouched.\n */\nfunction reconcileEditableProperties(\n varsArg: AstNode,\n newProps: Record<string, number | string>,\n): void {\n filterEditableKeys(varsArg, (key) => key in newProps);\n // Upsert each new prop, preserving the order keys first appeared.\n for (const [key, value] of Object.entries(newProps)) {\n setVarsKey(varsArg, key, value);\n }\n}\n\nfunction applyEaseUpdate(varsArg: AstNode, ease: string): void {\n const kfNode = findKeyframesObjectNode(varsArg);\n if (kfNode) {\n setVarsKey(kfNode, \"easeEach\", ease);\n removeVarsKey(varsArg, \"ease\");\n } else {\n setVarsKey(varsArg, \"ease\", ease);\n }\n}\n\n/**\n * \"Apply to all segments\": drop every per-keyframe `ease` override so the single\n * `easeEach` governs all segments uniformly (AE select-all + F9). Mirrors the\n * acorn writer's resetKeyframeEases branch.\n */\nfunction stripKeyframeEases(varsArg: AstNode): void {\n const kfNode = findKeyframesObjectNode(varsArg);\n const props = kfNode?.properties;\n if (!Array.isArray(props)) return;\n for (const entry of props) {\n if (isObjectProperty(entry)) removeVarsKey(entry.value, \"ease\");\n }\n}\n\nfunction applyUpdatesToCall(\n call: TweenCallInfo,\n updates: Partial<GsapAnimation> & { easeEach?: string; resetKeyframeEases?: boolean },\n): void {\n if (updates.properties) reconcileEditableProperties(call.varsArg, updates.properties);\n if (updates.fromProperties && call.method === \"fromTo\" && call.fromArg) {\n reconcileEditableProperties(call.fromArg, updates.fromProperties);\n }\n if (updates.duration !== undefined) setVarsKey(call.varsArg, \"duration\", updates.duration);\n if (updates.easeEach !== undefined) applyEaseUpdate(call.varsArg, updates.easeEach);\n else if (updates.ease !== undefined) applyEaseUpdate(call.varsArg, updates.ease);\n if (updates.resetKeyframeEases) stripKeyframeEases(call.varsArg);\n if (updates.position !== undefined) {\n const posIdx = call.method === \"fromTo\" ? 3 : 2;\n call.node.arguments[posIdx] = parseExpr(valueToCode(updates.position));\n }\n}\n\n/** Walk up to the enclosing ExpressionStatement path (for prune / insertAfter). */\nfunction findStatementPath(path: AstPath): AstPath | null {\n let p = path;\n while (p) {\n if (p.node?.type === \"ExpressionStatement\") return p;\n p = p.parentPath;\n }\n return null;\n}\n\nfunction insertAfterAnchor(parsed: ParsedGsapAst, newStatement: AstNode): void {\n const lastCall = parsed.located[parsed.located.length - 1]?.call;\n const anchorPath = lastCall\n ? findStatementPath(lastCall.path)\n : findTimelineDeclarationPath(parsed.ast, parsed.timelineVar);\n if (anchorPath) {\n anchorPath.insertAfter(newStatement);\n } else {\n parsed.ast.program.body.push(newStatement);\n }\n}\n\n/** Build the source for a single `tl.method(selector, vars, position)` call. */\nfunction buildTweenStatementCode(timelineVar: string, anim: Omit<GsapAnimation, \"id\">): string {\n const selector = JSON.stringify(anim.targetSelector);\n const props: Record<string, number | string> = { ...anim.properties };\n if (anim.method !== \"set\" && anim.duration !== undefined) props.duration = anim.duration;\n if (anim.ease) props.ease = anim.ease;\n const entries = Object.entries(props).map(([k, v]) => `${safeKey(k)}: ${valueToCode(v)}`);\n // immediateRender forces GSAP to apply the set when added to the timeline,\n // not on the first seek — without it, tl.set at position 0 on a paused\n // timeline is invisible until the playhead moves past 0. A base `gsap.set`\n // already runs immediately, so it doesn't need (or get) the flag.\n if (anim.method === \"set\" && !anim.global) entries.push(\"immediateRender: true\");\n if (anim.extras) {\n for (const [k, v] of Object.entries(anim.extras)) {\n entries.push(`${safeKey(k)}: ${valueToCode(v as number | string)}`);\n }\n }\n const objCode = `{ ${entries.join(\", \")} }`;\n const posCode = valueToCode(\n typeof anim.position === \"number\" ? anim.position : (anim.position ?? 0),\n );\n if (anim.method === \"fromTo\") {\n const fromEntries = Object.entries(anim.fromProperties ?? {}).map(\n ([k, v]) => `${safeKey(k)}: ${valueToCode(v)}`,\n );\n const fromCode = `{ ${fromEntries.join(\", \")} }`;\n return `${timelineVar}.fromTo(${selector}, ${fromCode}, ${objCode}, ${posCode});`;\n }\n // A base `gsap.set` is off the timeline: no timeline var, no position arg.\n if (anim.method === \"set\" && anim.global) {\n return `gsap.set(${selector}, ${objCode});`;\n }\n return `${timelineVar}.${anim.method}(${selector}, ${objCode}, ${posCode});`;\n}\n\nexport function updateAnimationInScript(\n script: string,\n animationId: string,\n updates: Partial<GsapAnimation> & { easeEach?: string; resetKeyframeEases?: boolean },\n): string {\n let parsed: ParsedGsapAst;\n try {\n parsed = parseGsapAst(script);\n } catch (e) {\n console.warn(\"[gsap-parser] updateAnimationInScript parse failed:\", e);\n return script;\n }\n const target = parsed.located.find((l) => l.id === animationId);\n if (!target) return script;\n applyUpdatesToCall(target.call, updates);\n return recast.print(parsed.ast).code;\n}\n\nexport function shiftPositionsInScript(\n script: string,\n targetSelector: string,\n delta: number,\n): string {\n let parsed: ParsedGsapAst;\n try {\n parsed = parseGsapAst(script);\n } catch (e) {\n console.warn(\"[gsap-parser] shiftPositionsInScript parse failed:\", e);\n return script;\n }\n let changed = false;\n for (const entry of parsed.located) {\n if (entry.animation.targetSelector !== targetSelector) continue;\n if (typeof entry.animation.position !== \"number\") continue;\n const newPos = Math.max(0, Math.round((entry.animation.position + delta) * 1000) / 1000);\n applyUpdatesToCall(entry.call, { position: newPos });\n changed = true;\n }\n return changed ? recast.print(parsed.ast).code : script;\n}\n\nexport function scalePositionsInScript(\n script: string,\n targetSelector: string,\n oldStart: number,\n oldDuration: number,\n newStart: number,\n newDuration: number,\n): string {\n if (oldDuration <= 0 || newDuration <= 0) return script;\n const ratio = newDuration / oldDuration;\n let parsed: ParsedGsapAst;\n try {\n parsed = parseGsapAst(script);\n } catch (e) {\n console.warn(\"[gsap-parser] scalePositionsInScript parse failed:\", e);\n return script;\n }\n let changed = false;\n for (const entry of parsed.located) {\n if (entry.animation.targetSelector !== targetSelector) continue;\n if (typeof entry.animation.position !== \"number\") continue;\n const newPos = Math.max(\n 0,\n Math.round((newStart + (entry.animation.position - oldStart) * ratio) * 1000) / 1000,\n );\n const updates: Partial<GsapAnimation> = { position: newPos };\n if (typeof entry.animation.duration === \"number\" && entry.animation.duration > 0) {\n updates.duration = Math.max(\n 0.001,\n Math.round(entry.animation.duration * ratio * 1000) / 1000,\n );\n }\n applyUpdatesToCall(entry.call, updates);\n changed = true;\n }\n return changed ? recast.print(parsed.ast).code : script;\n}\n\nfunction updateAnimationSelector(script: string, animationId: string, newSelector: string): string {\n let parsed: ParsedGsapAst;\n try {\n parsed = parseGsapAst(script);\n } catch {\n return script;\n }\n const target = parsed.located.find((l) => l.id === animationId);\n if (!target) return script;\n const selectorArg = target.call.path.node.arguments?.[0];\n if (selectorArg?.type === \"StringLiteral\") {\n selectorArg.value = newSelector;\n } else if (selectorArg?.type === \"Identifier\") {\n target.call.path.node.arguments[0] = { type: \"StringLiteral\", value: newSelector };\n }\n return recast.print(parsed.ast).code;\n}\n\nexport function addAnimationToScript(\n script: string,\n animation: Omit<GsapAnimation, \"id\">,\n): { script: string; id: string } {\n let parsed: ParsedGsapAst;\n try {\n parsed = parseGsapAst(script);\n } catch (e) {\n console.warn(\"[gsap-parser] addAnimationToScript parse failed:\", e);\n return { script, id: \"\" };\n }\n // Nothing to anchor against and no timeline to target — treat as parse failure.\n if (parsed.located.length === 0 && parsed.detection.ref === null) {\n return { script, id: \"\" };\n }\n\n const id = `anim-${Date.now()}`;\n const statementCode = buildTweenStatementCode(parsed.timelineVar, animation);\n const newStatement = parseScript(statementCode).program.body[0];\n insertAfterAnchor(parsed, newStatement);\n return { script: recast.print(parsed.ast).code, id };\n}\n\nexport function addAnimationWithKeyframesToScript(\n script: string,\n targetSelector: string,\n position: number,\n duration: number,\n keyframes: Array<{\n percentage: number;\n properties: Record<string, number | string>;\n ease?: string;\n auto?: boolean;\n }>,\n ease?: string,\n easeEach?: string,\n): { script: string; id: string } {\n let parsed: ParsedGsapAst;\n try {\n parsed = parseGsapAst(script);\n } catch (e) {\n console.warn(\"[gsap-parser] addAnimationWithKeyframesToScript parse failed:\", e);\n return { script, id: \"\" };\n }\n if (parsed.located.length === 0 && parsed.detection.ref === null) {\n return { script, id: \"\" };\n }\n\n const selector = JSON.stringify(targetSelector);\n const kfCode = buildKeyframeObjectCode(keyframes, easeEach ? { easeEach } : undefined);\n const varEntries = [`keyframes: ${kfCode}`, `duration: ${valueToCode(duration)}`];\n if (ease) varEntries.push(`ease: ${JSON.stringify(ease)}`);\n const posCode = valueToCode(position);\n const stmtCode = `${parsed.timelineVar}.to(${selector}, { ${varEntries.join(\", \")} }, ${posCode});`;\n\n const newStatement = parseScript(stmtCode).program.body[0];\n insertAfterAnchor(parsed, newStatement);\n\n const result = recast.print(parsed.ast).code;\n const reParsed = parseGsapAst(result);\n const newId = reParsed.located[reParsed.located.length - 1]?.id ?? \"\";\n return { script: result, id: newId };\n}\n\n/** Find the statement path of `const <timelineVar> = gsap.timeline(...)`. */\nfunction findTimelineDeclarationPath(ast: AstNode, timelineVar: string): AstPath | null {\n let found: AstPath | null = null;\n recast.types.visit(ast, {\n visitVariableDeclaration(path: AstPath) {\n if (found) return false;\n for (const decl of path.node.declarations ?? []) {\n if (decl.id?.name === timelineVar && isGsapTimelineCall(decl.init)) {\n found = path;\n return false;\n }\n }\n this.traverse(path);\n },\n });\n return found;\n}\n\n/** Find the call that chains off `targetNode` (i.e. whose callee object IS it). */\nfunction findChainParentCall(stmtNode: AstNode, targetNode: AstNode): AstNode | null {\n let found: AstNode | null = null;\n recast.types.visit(stmtNode, {\n visitCallExpression(p: AstPath) {\n if (found) return false;\n if (p.node.callee?.type === \"MemberExpression\" && p.node.callee.object === targetNode) {\n found = p.node;\n return false;\n }\n this.traverse(p);\n },\n });\n return found;\n}\n\nexport function removeAnimationFromScript(script: string, animationId: string): string {\n let parsed: ParsedGsapAst;\n try {\n parsed = parseGsapAst(script);\n } catch (e) {\n console.warn(\"[gsap-parser] removeAnimationFromScript parse failed:\", e);\n return script;\n }\n let target = parsed.located.find((l) => l.id === animationId);\n if (!target) {\n const convertedId = animationId.replace(/-from-|-fromTo-/, \"-to-\");\n target = parsed.located.find((l) => l.id === convertedId);\n }\n if (!target) return script;\n const node = target.call.node;\n const stmtPath = findStatementPath(target.call.path);\n if (!stmtPath) return script;\n\n const parentCall = findChainParentCall(stmtPath.node, node);\n if (parentCall) {\n // Inner link of a chain — splice it out by re-pointing the next link.\n parentCall.callee.object = node.callee.object;\n } else if (node.callee?.object?.type === \"CallExpression\") {\n // Outermost link of a chain with earlier links — drop just this link.\n stmtPath.node.expression = node.callee.object;\n } else {\n // Standalone tween — remove the whole statement.\n stmtPath.prune();\n }\n return recast.print(parsed.ast).code;\n}\n\nfunction insertInheritedStateSet(\n script: string,\n selector: string,\n position: number,\n properties: Record<string, number | string>,\n): string {\n let parsed: ParsedGsapAst;\n try {\n parsed = parseGsapAst(script);\n } catch {\n return script;\n }\n const tlVar = parsed.timelineVar;\n const props = Object.entries(properties)\n .map(([k, v]) => `${k}: ${typeof v === \"string\" ? JSON.stringify(v) : v}`)\n .join(\", \");\n const code = `${tlVar}.set(${JSON.stringify(selector)}, { ${props} }, ${position});`;\n const newStatement = parseScript(code).program.body[0];\n const anchor = findTimelineDeclarationPath(parsed.ast, tlVar);\n if (anchor) {\n anchor.insertAfter(newStatement);\n } else if (parsed.located.length > 0) {\n const firstTween = parsed.located[0]!.call;\n const stmtPath = findStatementPath(firstTween.path);\n if (stmtPath) stmtPath.insertBefore(newStatement);\n else parsed.ast.program.body.unshift(newStatement);\n } else {\n parsed.ast.program.body.push(newStatement);\n }\n return recast.print(parsed.ast).code;\n}\n\n/** Marker on Studio-emitted pre-keyframe hold `set`s. `data` is a GSAP-reserved\n * config key (attached to the tween, never applied to the target), so it carries\n * the tag without triggering GSAP's \"Invalid property\" warning. */\nconst STUDIO_HOLD_MARKER = \"hf-hold\";\n\n/** True for a `tl.set(...)` this module emitted to hold a keyframe before its tween.\n * The Studio filters these out so they never appear as user keyframes/diamonds. */\nexport function isStudioHoldSet(anim: GsapAnimation): boolean {\n return anim.method === \"set\" && anim.properties?.data === STUDIO_HOLD_MARKER;\n}\n\n/**\n * Keep a `tl.set(selector, {x,y}, 0)` \"hold\" in front of every position-keyframed\n * tween that starts after t=0, so the element holds its first keyframe's position\n * BEFORE the tween plays instead of snapping to its CSS base (the universal NLE\n * \"hold before first keyframe\" behavior). The set is tagged with `data: \"hf-hold\"`\n * so this pass owns it: every call wipes the prior holds and recomputes from the\n * current keyframes, keeping them in sync as keyframes are added/moved/deleted.\n *\n * Idempotent. Only position props (x/y/xPercent/yPercent) are held — opacity/scale\n * keep their authored pre-tween behavior. A tween already starting at 0 needs no\n * hold (no gap before it).\n */\nexport function syncPositionHoldsBeforeKeyframes(script: string): string {\n let parsed: ParsedGsap;\n try {\n parsed = parseGsapScript(script);\n } catch {\n return script;\n }\n // 1. Drop every hold this pass previously emitted, so we recompute fresh.\n let result = script;\n const staleHoldIds = parsed.animations.filter(isStudioHoldSet).map((a) => a.id);\n for (const id of staleHoldIds) result = removeAnimationFromScript(result, id);\n\n // 2. Re-add a hold for each position-keyframed tween that starts after t=0.\n let reparsed: ParsedGsap;\n try {\n reparsed = parseGsapScript(result);\n } catch {\n return result;\n }\n for (const anim of reparsed.animations) {\n if (!anim.keyframes) continue;\n const start = anim.resolvedStart ?? (typeof anim.position === \"number\" ? anim.position : 0);\n if (!(start > 0.001)) continue;\n const firstKf = [...anim.keyframes.keyframes].sort((a, b) => a.percentage - b.percentage)[0];\n if (!firstKf) continue;\n const posProps: Record<string, number | string> = {};\n for (const [k, v] of Object.entries(firstKf.properties)) {\n if (classifyPropertyGroup(k) === \"position\" && typeof v === \"number\") posProps[k] = v;\n }\n if (Object.keys(posProps).length === 0) continue;\n result = insertInheritedStateSet(result, anim.targetSelector, 0, {\n ...posProps,\n data: STUDIO_HOLD_MARKER,\n });\n }\n return result;\n}\n\n// ── Split Animation Functions ─────────────────────────────────────────────\n\nexport interface SplitAnimationsOptions {\n originalId: string;\n newId: string;\n splitTime: number;\n elementStart: number;\n elementDuration: number;\n}\n\nexport interface SplitAnimationsResult {\n script: string;\n /** Non-ID-selector animations that the engine cannot safely retarget. */\n skippedSelectors: string[];\n}\n\n// fallow-ignore-next-line complexity\nexport function splitAnimationsInScript(\n script: string,\n opts: SplitAnimationsOptions,\n): SplitAnimationsResult {\n const parsed = parseGsapScript(script);\n const originalSelector = `#${opts.originalId}`;\n const newSelector = `#${opts.newId}`;\n\n const skippedSelectors: string[] = [];\n for (const a of parsed.animations) {\n if (a.targetSelector !== originalSelector && a.targetSelector.includes(opts.originalId)) {\n skippedSelectors.push(a.targetSelector);\n }\n }\n\n const matching = parsed.animations.filter((a) => a.targetSelector === originalSelector);\n if (matching.length === 0) return { script, skippedSelectors };\n\n let result = script;\n const newElementStart = opts.splitTime;\n const inheritedProps: Record<string, number | string> = {};\n\n // Reverse iteration: updateAnimationSelector mutates selectors in the source\n // string, which can shift count-based ID suffixes (e.g. \"#hero-1\" → \"#hero-2\")\n // for later animations. Processing last-to-first prevents stale ID collisions.\n for (let i = matching.length - 1; i >= 0; i--) {\n const anim = matching[i]!;\n const pos = typeof anim.position === \"number\" ? anim.position : 0;\n const dur = anim.duration ?? 0;\n const animEnd = pos + dur;\n\n if (anim.keyframes) {\n if (pos >= opts.splitTime) {\n result = updateAnimationSelector(result, anim.id, newSelector);\n } else if (animEnd > opts.splitTime) {\n // Spanning keyframes can't be correctly split without renormalizing\n // percentages and durations — leave on original, warn the caller.\n skippedSelectors.push(`${originalSelector} (keyframes spanning split)`);\n const kfs = anim.keyframes.keyframes;\n for (const kf of kfs) {\n const kfTime = pos + (kf.percentage / 100) * dur;\n if (kfTime <= opts.splitTime) {\n for (const [k, v] of Object.entries(kf.properties)) {\n inheritedProps[k] = v;\n }\n }\n }\n } else {\n // Entirely before split — extract final keyframe properties\n const kfs = anim.keyframes.keyframes;\n if (kfs.length > 0) {\n for (const [k, v] of Object.entries(kfs[kfs.length - 1]!.properties)) {\n inheritedProps[k] = v;\n }\n }\n }\n continue;\n }\n\n // `<=` (not `<`) is deliberate: a tween whose end coincides exactly with\n // the split boundary has fully played by splitTime, so it belongs to the\n // first half and contributes its resting state to the clone. The spanning\n // branch below handles only strictly-mid-flight tweens (pos < split < end).\n if (animEnd <= opts.splitTime) {\n // Only a completed .from() reverts the element to its natural state, so\n // its recorded properties are the HIDDEN start (e.g. opacity:0), not the\n // resting state — clearing them keeps the clone at its natural value\n // instead of pinning it to the from-values (which made it invisible).\n // .fromTo() and .to() both END at their to-values (no revert), so they\n // fall through to `else` and inherit `anim.properties` (the to-values) —\n // .fromTo() must NOT join the .from() clear-branch or the clone would\n // drop the very state the fromTo just established.\n if (anim.method === \"from\") {\n for (const k of Object.keys(anim.properties)) delete inheritedProps[k];\n } else {\n for (const [k, v] of Object.entries(anim.properties)) {\n inheritedProps[k] = v;\n }\n }\n continue;\n }\n\n if (pos >= opts.splitTime) {\n result = updateAnimationSelector(result, anim.id, newSelector);\n continue;\n }\n\n // Spans the split — use linear interpolation to compute mid-values,\n // then .fromTo() on the clone so both halves play the correct range.\n // For .fromTo() tweens we have explicit from-values; for .to() tweens\n // we use accumulated state from prior animations, defaulting to 0 for\n // unknown numeric properties (the standard GSAP transform initial state).\n const progress = dur > 0 ? (opts.splitTime - pos) / dur : 0;\n const fromSource = anim.fromProperties ?? inheritedProps;\n const midProps: Record<string, number | string> = {};\n for (const [k, v] of Object.entries(anim.properties)) {\n if (typeof v !== \"number\") {\n midProps[k] = v;\n continue;\n }\n const fromVal = typeof fromSource[k] === \"number\" ? (fromSource[k] as number) : 0;\n midProps[k] = fromVal + (v - fromVal) * progress;\n }\n\n const firstHalfDuration = opts.splitTime - pos;\n result = updateAnimationInScript(result, anim.id, {\n duration: firstHalfDuration,\n properties: midProps,\n });\n\n const secondHalfDuration = animEnd - opts.splitTime;\n const addResult = addAnimationToScript(result, {\n targetSelector: newSelector,\n method: \"fromTo\",\n position: newElementStart,\n duration: secondHalfDuration,\n properties: { ...anim.properties },\n fromProperties: { ...midProps },\n ease: anim.ease,\n extras: anim.extras,\n });\n result = addResult.script;\n\n for (const [k, v] of Object.entries(midProps)) {\n inheritedProps[k] = v;\n }\n }\n\n if (Object.keys(inheritedProps).length > 0) {\n result = insertInheritedStateSet(result, newSelector, newElementStart, inheritedProps);\n }\n\n return { script: result, skippedSelectors };\n}\n\n// ── Keyframe Mutation Functions ────────────────────────────────────────────\n\nfunction sortedKeyframes(\n kfs: Array<{ percentage: number; properties: Record<string, number | string>; ease?: string }>,\n) {\n return kfs.slice().sort((a, b) => a.percentage - b.percentage);\n}\n\nfunction keyframePropsToCode(kf: { properties: Record<string, number | string> }): string[] {\n return Object.entries(kf.properties).map(([k, v]) => `${safeKey(k)}: ${valueToCode(v)}`);\n}\n\nfunction buildKeyframeObjectCode(\n keyframes: Array<{\n percentage: number;\n properties: Record<string, number | string>;\n ease?: string;\n auto?: boolean;\n }>,\n options?: { easeEach?: string },\n): string {\n const entries = keyframes.map((kf) => {\n const props = keyframePropsToCode(kf);\n if (kf.ease) props.push(`ease: ${JSON.stringify(kf.ease)}`);\n if (kf.auto) props.push(`_auto: 1`);\n return `${JSON.stringify(`${kf.percentage}%`)}: { ${props.join(\", \")} }`;\n });\n if (options?.easeEach) entries.push(`easeEach: ${JSON.stringify(options.easeEach)}`);\n return `{ ${entries.join(\", \")} }`;\n}\n\n/** Remove a named property from an ObjectExpression's properties array. */\nfunction removeVarsKey(varsArg: AstNode, key: string): void {\n if (varsArg?.type !== \"ObjectExpression\") return;\n varsArg.properties = varsArg.properties.filter(\n (p: AstNode) => !(isObjectProperty(p) && propKeyName(p) === key),\n );\n}\n\n/** Extract the numeric percentage from a key like \"50%\". Returns NaN for non-percentage keys. */\nfunction percentageFromKey(key: string): number {\n const m = PERCENTAGE_KEY_RE.exec(key);\n return m ? Number.parseFloat(m[1]!) : Number.NaN;\n}\n\nconst PCT_TOLERANCE = 2;\n\nfunction findKeyframePropByPct(\n kfNode: AstNode,\n percentage: number,\n): { idx: number; prop: AstNode } | null {\n const props = kfNode.properties;\n for (let i = 0; i < props.length; i++) {\n if (!isObjectProperty(props[i])) continue;\n const key = propKeyName(props[i]);\n if (typeof key !== \"string\") continue;\n const parsed = percentageFromKey(key);\n if (Number.isNaN(parsed)) continue;\n if (Math.abs(parsed - percentage) <= PCT_TOLERANCE) return { idx: i, prop: props[i] };\n }\n return null;\n}\n\n/** Build a keyframe value AST node from properties and optional ease. */\nfunction buildKeyframeValueNode(\n properties: Record<string, number | string>,\n ease?: string,\n): AstNode {\n const entries = Object.entries(properties).map(([k, v]) => `${safeKey(k)}: ${valueToCode(v)}`);\n if (ease) entries.push(`ease: ${JSON.stringify(ease)}`);\n return parseExpr(`{ ${entries.join(\", \")} }`);\n}\n\n/** Parse + locate a target animation, returning null on failure. */\nfunction locateAnimation(\n script: string,\n animationId: string,\n): { parsed: ParsedGsapAst; target: ParsedGsapAst[\"located\"][number] } | null {\n let parsed: ParsedGsapAst;\n try {\n parsed = parseGsapAst(script);\n } catch {\n return null;\n }\n const target = parsed.located.find((l) => l.id === animationId);\n return target ? { parsed, target } : null;\n}\n\n// Animation ids encode the tween's timeline position in ms\n// (`#puck-a-to-1200-position`). A gesture/convert can re-emit a tween at a\n// different position, changing its id — so a client that cached the old id (its\n// selectedGsapAnimations hasn't refreshed) edits a now-nonexistent id and the op\n// no-ops. Parse `{selector}-{method}-{posMs}-{group}` so we can fall back to the\n// same selector+method+group tween nearest the requested position.\nconst ANIM_ID_RE = /^(.*)-(fromTo|from|to|set)-(\\d+)-([a-z]+)$/;\n\nfunction locateAnimationWithFallback(\n script: string,\n animationId: string,\n): ReturnType<typeof locateAnimation> {\n const loc = locateAnimation(script, animationId);\n if (loc) return loc;\n const convertedId = animationId.replace(/-from-|-fromTo-/, \"-to-\");\n if (convertedId !== animationId) {\n const converted = locateAnimation(script, convertedId);\n if (converted) return converted;\n }\n // Position-drift fallback: match by stable identity (selector+method+group),\n // disambiguating by the position closest to the one the caller asked for.\n const want = ANIM_ID_RE.exec(animationId);\n if (!want) return null;\n const [, sel, method, wantPosStr, group] = want;\n const wantPos = Number(wantPosStr);\n let parsed: ParsedGsapAst;\n try {\n parsed = parseGsapAst(script);\n } catch {\n return null;\n }\n let best: ParsedGsapAst[\"located\"][number] | null = null;\n let bestDist = Number.POSITIVE_INFINITY;\n for (const l of parsed.located) {\n const m = ANIM_ID_RE.exec(l.id);\n if (!m || m[1] !== sel || m[2] !== method || m[4] !== group) continue;\n const dist = Math.abs(Number(m[3]) - wantPos);\n if (dist < bestDist) {\n best = l;\n bestDist = dist;\n }\n }\n return best ? { parsed, target: best } : null;\n}\n\n/** Find the keyframes ObjectExpression node on a tween's varsArg, or null. */\nfunction findKeyframesObjectNode(varsArg: AstNode): AstNode | null {\n const node = findPropertyNode(varsArg, \"keyframes\");\n return node?.type === \"ObjectExpression\" ? node : null;\n}\n\n/**\n * Convert array-form keyframes (`keyframes: [{x,y}, …]`) to even-percentage object\n * form (`{ \"0%\": {…}, \"33.3%\": {…}, … }`) IN PLACE, returning the new object node\n * (or null if not array-form). GSAP distributes an array evenly, so this is\n * runtime-identical — but it gives the percentage-keyed write ops something to\n * target. Needed before INSERTING a keyframe at an arbitrary percentage, which an\n * even array can't host.\n */\nfunction convertArrayKeyframesToObjectNode(varsArg: AstNode): AstNode | null {\n if (varsArg?.type !== \"ObjectExpression\") return null;\n const prop = (varsArg.properties ?? []).find(\n (p: AstNode) => isObjectProperty(p) && propKeyName(p) === \"keyframes\",\n );\n if (!prop || prop.value?.type !== \"ArrayExpression\") return null;\n const els: AstNode[] = (prop.value.elements ?? []).filter(\n (e: AstNode | null): e is AstNode => !!e && e.type === \"ObjectExpression\",\n );\n const n = els.length;\n if (n === 0) return null;\n const entries = els.map((el: AstNode, i: number) => {\n const pct = n > 1 ? Math.round((i / (n - 1)) * 1000) / 10 : 0;\n return `${JSON.stringify(`${pct}%`)}: ${recast.print(el).code}`;\n });\n prop.value = parseExpr(`{ ${entries.join(\", \")} }`);\n return prop.value;\n}\n\n/** Filter percentage-keyed properties from a keyframes ObjectExpression. */\nfunction filterPercentageProps(kfNode: AstNode): AstNode[] {\n return kfNode.properties.filter((p: AstNode) => {\n if (!isObjectProperty(p)) return false;\n const key = propKeyName(p);\n return typeof key === \"string\" && PERCENTAGE_KEY_RE.test(key);\n });\n}\n\n/**\n * Collapse a keyframes node to flat tween: apply `record` entries as vars keys,\n * then remove `keyframes` and `easeEach` from varsArg. Skips the `ease` key\n * from the record (per-keyframe ease, not a tween ease).\n */\nfunction collapseKeyframesToFlat(varsArg: AstNode, record: Record<string, unknown>): void {\n for (const [k, v] of Object.entries(record)) {\n if (k === \"ease\") continue;\n if (typeof v === \"number\" || typeof v === \"string\") setVarsKey(varsArg, k, v);\n }\n removeVarsKey(varsArg, \"keyframes\");\n removeVarsKey(varsArg, \"easeEach\");\n}\n\n/**\n * Locate an animation's keyframes ObjectExpression and build the percentage key.\n * Shared preamble for addKeyframeToScript, removeKeyframeFromScript, and\n * updateKeyframeInScript.\n */\nfunction locateKeyframeCtx(script: string, animationId: string, percentage: number) {\n const loc = locateAnimationWithFallback(script, animationId);\n if (!loc) return null;\n const kfNode = findKeyframesObjectNode(loc.target.call.varsArg);\n if (!kfNode) return null;\n return { loc, kfNode, pctKey: `${percentage}%` };\n}\n\n/**\n * Insert a keyframe at the given percentage in an existing percentage-keyframes\n * object. If the percentage already exists, its value is replaced.\n */\nexport function addKeyframeToScript(\n script: string,\n animationId: string,\n percentage: number,\n properties: Record<string, number | string>,\n ease?: string,\n backfillDefaults?: Record<string, number | string>,\n): string {\n let loc = locateAnimationWithFallback(script, animationId);\n if (!loc) return script;\n let kfNode = findKeyframesObjectNode(loc.target.call.varsArg);\n\n // Array-form keyframes can't host an arbitrary new percentage — normalize to\n // object form in place first. (convertToKeyframesInScript below only converts\n // FLAT tweens; it early-returns when keyframes already exist.)\n if (!kfNode) kfNode = convertArrayKeyframesToObjectNode(loc.target.call.varsArg);\n\n if (!kfNode) {\n script = convertToKeyframesInScript(script, animationId);\n loc = locateAnimationWithFallback(script, animationId);\n if (!loc) return script;\n kfNode = findKeyframesObjectNode(loc.target.call.varsArg);\n if (!kfNode) return script;\n }\n const pctKey = `${percentage}%`;\n\n const newValueNode = buildKeyframeValueNode(properties, ease);\n\n // Merge into existing keyframe at this percentage, or insert new\n const existing = findKeyframePropByPct(kfNode, percentage);\n if (existing) {\n if (existing.prop.value?.type === \"ObjectExpression\") {\n const existingRecord = objectExpressionToRecord(existing.prop.value, loc.parsed.scope);\n const merged = { ...existingRecord };\n for (const [k, v] of Object.entries(properties)) merged[k] = v;\n existing.prop.value = buildKeyframeValueNode(\n merged as Record<string, number | string>,\n ease ?? (typeof existingRecord.ease === \"string\" ? existingRecord.ease : undefined),\n );\n } else {\n existing.prop.value = newValueNode;\n }\n } else {\n // Build the new property node with a quoted percentage key\n const newProp = parseExpr(`{ ${JSON.stringify(pctKey)}: {} }`).properties[0];\n newProp.value = newValueNode;\n\n // Insert in sorted order by percentage\n let insertIdx = kfNode.properties.length;\n for (let i = 0; i < kfNode.properties.length; i++) {\n const key = isObjectProperty(kfNode.properties[i])\n ? propKeyName(kfNode.properties[i])\n : undefined;\n if (typeof key === \"string\" && percentageFromKey(key) > percentage) {\n insertIdx = i;\n break;\n }\n }\n kfNode.properties.splice(insertIdx, 0, newProp);\n }\n\n // Auto-update adjacent endpoints: only update an `_auto` 0% or 100%\n // keyframe when the new keyframe is directly next to it (no other keyframe\n // between them). This prevents a keyframe at 74% from clobbering 100% when\n // 75% already exists, and a keyframe at 30% from clobbering 0% when 25%\n // already exists.\n if (percentage > 0 && percentage < 100) {\n const pctProps = filterPercentageProps(kfNode);\n const allPcts = pctProps\n .map((p: AstNode) => percentageFromKey(propKeyName(p) ?? \"\"))\n .filter((n: number) => !Number.isNaN(n) && n !== percentage)\n .sort((a: number, b: number) => a - b);\n const leftNeighbor = allPcts.filter((p: number) => p < percentage).pop();\n const rightNeighbor = allPcts.find((p: number) => p > percentage);\n for (const endPct of [0, 100]) {\n const isNeighbor = endPct === 0 ? leftNeighbor === 0 : rightNeighbor === 100;\n if (!isNeighbor) continue;\n const endProp = pctProps.find(\n (p: AstNode) => percentageFromKey(propKeyName(p) ?? \"\") === endPct,\n );\n if (!endProp?.value || endProp.value.type !== \"ObjectExpression\") continue;\n const hasAuto = endProp.value.properties.some(\n (p: AstNode) => isObjectProperty(p) && propKeyName(p) === \"_auto\",\n );\n if (!hasAuto) continue;\n const updatedProps = { ...properties, _auto: 1 as number | string };\n endProp.value = buildKeyframeValueNode(updatedProps, undefined);\n }\n }\n\n // Backfill: when the new keyframe introduces properties absent from other\n // keyframes, add default values so GSAP can interpolate them.\n if (backfillDefaults) {\n const newPropKeys = Object.keys(properties);\n const pctProps = filterPercentageProps(kfNode);\n for (const prop of pctProps) {\n const key = propKeyName(prop);\n if (key === pctKey) continue;\n const valObj = prop.value;\n if (!valObj || valObj.type !== \"ObjectExpression\") continue;\n const existingKeys = new Set(\n valObj.properties\n .filter((p: AstNode) => isObjectProperty(p))\n .map((p: AstNode) => propKeyName(p)),\n );\n for (const pk of newPropKeys) {\n if (existingKeys.has(pk)) continue;\n const defaultVal = backfillDefaults[pk];\n if (defaultVal == null) continue;\n const fillProp = parseExpr(`{ ${safeKey(pk)}: ${valueToCode(defaultVal)} }`).properties[0];\n valObj.properties.push(fillProp);\n }\n }\n }\n\n return recast.print(loc.parsed.ast).code;\n}\n\n/**\n * Remove a keyframe at the given percentage. If fewer than 2 keyframes remain\n * after removal, collapse the keyframes object to a flat tween using the\n * remaining keyframe's properties.\n */\nexport function removeKeyframeFromScript(\n script: string,\n animationId: string,\n percentage: number,\n): string {\n // Array-form keyframes (`keyframes: [{x,y}, …]`) have no explicit percentages —\n // GSAP distributes them evenly. The object-form path below can't see them\n // (findKeyframesObjectNode only matches ObjectExpression), so removing from an\n // array-form tween silently no-op'd. Resolve the element by its implicit\n // percentage and splice it; collapse to a flat tween when fewer than two remain.\n const arrLoc = locateAnimationWithFallback(script, animationId);\n // findPropertyNode here returns the property's VALUE node directly.\n const arrVal = arrLoc && findPropertyNode(arrLoc.target.call.varsArg, \"keyframes\");\n if (arrLoc && arrVal?.type === \"ArrayExpression\") {\n const elements: AstNode[] = (arrVal.elements ?? []).filter(\n (e: AstNode | null): e is AstNode => !!e && e.type === \"ObjectExpression\",\n );\n const n = elements.length;\n if (n === 0) return script;\n let matchIdx = -1;\n let bestDist = Number.POSITIVE_INFINITY;\n for (let i = 0; i < n; i++) {\n const pct = n > 1 ? (i / (n - 1)) * 100 : 0;\n const dist = Math.abs(pct - percentage);\n if (dist <= PCT_TOLERANCE && dist < bestDist) {\n matchIdx = i;\n bestDist = dist;\n }\n }\n if (matchIdx === -1) return script;\n const remaining = elements.filter((_, i) => i !== matchIdx);\n if (remaining.length < 2) {\n const sole = remaining[0];\n const record = sole ? objectExpressionToRecord(sole, arrLoc.parsed.scope) : {};\n collapseKeyframesToFlat(arrLoc.target.call.varsArg, record);\n } else {\n const realIdx = arrVal.elements.indexOf(elements[matchIdx]);\n arrVal.elements.splice(realIdx, 1);\n }\n return recast.print(arrLoc.parsed.ast).code;\n }\n\n const ctx = locateKeyframeCtx(script, animationId, percentage);\n if (!ctx) return script;\n const { loc, kfNode } = ctx;\n\n const match = findKeyframePropByPct(kfNode, percentage);\n if (!match) return script;\n const removeIdx = match.idx;\n\n kfNode.properties.splice(removeIdx, 1);\n\n const remainingKfs = filterPercentageProps(kfNode);\n if (remainingKfs.length < 2) {\n const record =\n remainingKfs.length === 1\n ? objectExpressionToRecord(remainingKfs[0]!.value, loc.parsed.scope)\n : {};\n collapseKeyframesToFlat(loc.target.call.varsArg, record);\n }\n\n return recast.print(loc.parsed.ast).code;\n}\n\n/**\n * Replace the properties (and optionally ease) at an existing keyframe percentage.\n */\nexport function updateKeyframeInScript(\n script: string,\n animationId: string,\n percentage: number,\n properties: Record<string, number | string>,\n ease?: string,\n): string {\n // Array-form keyframes (`keyframes: [{x,y}, …]`) have no explicit percentages —\n // GSAP distributes them evenly. The percentage-keyed object path below can't\n // match them (findKeyframesObjectNode only matches ObjectExpression), so dragging\n // a motion-path node on an array-authored tween silently no-op'd. Resolve the\n // element by its implicit percentage and replace it in place. Mirrors the array\n // branch in removeKeyframeFromScript.\n const arrLoc = locateAnimationWithFallback(script, animationId);\n const arrVal = arrLoc && findPropertyNode(arrLoc.target.call.varsArg, \"keyframes\");\n if (arrLoc && arrVal?.type === \"ArrayExpression\") {\n const elements: AstNode[] = (arrVal.elements ?? []).filter(\n (e: AstNode | null): e is AstNode => !!e && e.type === \"ObjectExpression\",\n );\n const n = elements.length;\n if (n === 0) return script;\n let matchIdx = -1;\n let bestDist = Number.POSITIVE_INFINITY;\n for (let i = 0; i < n; i++) {\n const pct = n > 1 ? (i / (n - 1)) * 100 : 0;\n const dist = Math.abs(pct - percentage);\n if (dist <= PCT_TOLERANCE && dist < bestDist) {\n matchIdx = i;\n bestDist = dist;\n }\n }\n if (matchIdx === -1) return script;\n const realIdx = arrVal.elements.indexOf(elements[matchIdx]);\n arrVal.elements[realIdx] = buildKeyframeValueNode(properties, ease);\n return recast.print(arrLoc.parsed.ast).code;\n }\n\n const ctx = locateKeyframeCtx(script, animationId, percentage);\n if (!ctx) return script;\n const { loc, kfNode } = ctx;\n\n const match = findKeyframePropByPct(kfNode, percentage);\n if (!match) return script;\n\n if (Object.keys(properties).length === 0 && ease) {\n // Ease-only update: preserve existing properties, just add/replace ease\n const existing = match.prop.value;\n if (existing?.type === \"ObjectExpression\") {\n const props = (existing.properties ?? []) as AstNode[];\n const easeIdx = props.findIndex(\n (p: AstNode) => isObjectProperty(p) && propKeyName(p) === \"ease\",\n );\n const easeNode = parseExpr(`({ ease: ${JSON.stringify(ease)} })`).properties[0];\n if (easeIdx >= 0) {\n props[easeIdx] = easeNode;\n } else {\n props.push(easeNode);\n }\n return recast.print(loc.parsed.ast).code;\n }\n // Non-object keyframe value (primitive shorthand, e.g. \"50%\": \"0.5\"): there\n // is no property bag to merge the ease into. Rebuilding from empty\n // `properties` would wipe the primitive — leave the keyframe untouched.\n return script;\n }\n // MERGE edited props into the existing keyframe, preserving props not in this edit\n // (z, transformPerspective, rotation, …). A whole-value rebuild drops them, so editing\n // one prop at the 0% keyframe strips z/transformPerspective and the element pops.\n // Mirrors acorn updateKeyframeInScript; parity-locked by gsapWriterParity.corpus.\n const existing = match.prop.value;\n if (existing?.type === \"ObjectExpression\") {\n const props = (existing.properties ?? []) as AstNode[];\n const upsert = (key: string, valueCode: string) => {\n const idx = props.findIndex((p: AstNode) => isObjectProperty(p) && propKeyName(p) === key);\n const node = parseExpr(`({ ${safeKey(key)}: ${valueCode} })`).properties[0];\n if (idx >= 0) props[idx] = node;\n else props.push(node);\n };\n for (const [k, v] of Object.entries(properties)) upsert(k, valueToCode(v));\n if (ease !== undefined) upsert(\"ease\", JSON.stringify(ease));\n return recast.print(loc.parsed.ast).code;\n }\n match.prop.value = buildKeyframeValueNode(properties, ease);\n return recast.print(loc.parsed.ast).code;\n}\n\n/** Strip editable properties and ease/keyframes keys from a varsArg. */\nfunction stripEditableAndEase(varsArg: AstNode): void {\n // ease is a BUILTIN_VAR_KEY (not editable), so filterEditableKeys won't remove it —\n // drop it explicitly before filtering, along with keyframes.\n if (varsArg?.type !== \"ObjectExpression\") return;\n varsArg.properties = varsArg.properties.filter((p: AstNode) => {\n if (!isObjectProperty(p)) return true;\n const key = propKeyName(p);\n return key !== \"ease\" && key !== \"keyframes\";\n });\n filterEditableKeys(varsArg, () => false);\n}\n\n/** Build and prepend a keyframes property node onto varsArg. */\nfunction insertKeyframesProp(\n varsArg: AstNode,\n fromProps: Record<string, number | string>,\n toProps: Record<string, number | string>,\n easeEach?: string,\n): void {\n const fromEntries = Object.entries(fromProps).map(([k, v]) => `${safeKey(k)}: ${valueToCode(v)}`);\n const toEntries = Object.entries(toProps).map(([k, v]) => `${safeKey(k)}: ${valueToCode(v)}`);\n const easeEntry = easeEach ? `, easeEach: ${JSON.stringify(easeEach)}` : \"\";\n const kfCode = `{ \"0%\": { ${fromEntries.join(\", \")} }, \"100%\": { ${toEntries.join(\", \")} }${easeEntry} }`;\n const kfProp = parseExpr(`{ keyframes: {} }`).properties[0];\n kfProp.value = parseExpr(kfCode);\n if (varsArg?.type === \"ObjectExpression\") varsArg.properties.unshift(kfProp);\n}\n\n/**\n * Convert a flat tween (to/from/fromTo) to percentage-keyframes format.\n * `resolvedFromValues` supplies the \"from\" state for `to()` tweens or\n * the \"to\" state for `from()` tweens (the values the DOM would resolve to).\n */\nexport function convertToKeyframesInScript(\n script: string,\n animationId: string,\n resolvedFromValues?: Record<string, number | string>,\n setDuration = 1,\n): string {\n let loc = locateAnimationWithFallback(script, animationId);\n if (!loc) return script;\n\n const anim = loc.target.animation;\n if (anim.keyframes) return script;\n\n const { fromProps, toProps } = resolveConversionProps(anim, resolvedFromValues);\n const varsArg = loc.target.call.varsArg;\n const originalEase = anim.ease;\n\n stripEditableAndEase(varsArg);\n insertKeyframesProp(varsArg, fromProps, toProps, originalEase || undefined);\n\n if (originalEase) {\n setVarsKey(varsArg, \"ease\", \"none\");\n }\n\n // For from() or fromTo(), convert to to()\n if (anim.method === \"from\" || anim.method === \"fromTo\") {\n loc.target.call.node.callee.property.name = \"to\";\n if (anim.method === \"fromTo\") loc.target.call.node.arguments.splice(1, 1);\n }\n\n // A static `set` becomes an animatable `to`: flip the method, drop the\n // immediateRender hold marker, and give it a real duration so the keyframes\n // span time. This is what makes a static 3D transform keyframeable.\n if (anim.method === \"set\") {\n // A GLOBAL `gsap.set(...)` is off-timeline; flipping only the method would\n // emit `gsap.to(...)`, which fires once at load and is NOT on the paused\n // master timeline (the engine can't seek/render it). Re-root it onto the\n // timeline var and add the position arg (a gsap.set has none) so the\n // converted tween is seekable. A `tl.set` already has the right object.\n const calleeObj = loc.target.call.node.callee.object;\n if (anim.global && calleeObj?.type === \"Identifier\") {\n calleeObj.name = loc.parsed.timelineVar;\n if (loc.target.call.node.arguments.length < 3) {\n loc.target.call.node.arguments.push(parseExpr(\"0\"));\n }\n }\n loc.target.call.node.callee.property.name = \"to\";\n removeVarsKey(varsArg, \"immediateRender\");\n setVarsKey(varsArg, \"duration\", Math.max(0.001, setDuration));\n }\n\n return recast.print(loc.parsed.ast).code;\n}\n\n/**\n * Remove all keyframes from a tween, collapsing to a flat tween with the\n * last keyframe's properties.\n */\nexport function removeAllKeyframesFromScript(script: string, animationId: string): string {\n let loc = locateAnimationWithFallback(script, animationId);\n if (!loc) return script;\n const kfNode = findKeyframesObjectNode(loc.target.call.varsArg);\n if (!kfNode) return script;\n\n const kfEntries = filterPercentageProps(kfNode)\n .map((p: AstNode) => ({ pct: percentageFromKey(propKeyName(p)!), prop: p }))\n .filter((e) => !Number.isNaN(e.pct))\n .sort((a, b) => a.pct - b.pct);\n if (kfEntries.length === 0) return script;\n\n // For to()/set(): collapse to last keyframe (the destination = visible state).\n // For from(): collapse to first keyframe (the starting state).\n const method = loc.target.call.method;\n const collapseEntry = method === \"from\" ? kfEntries[0]! : kfEntries[kfEntries.length - 1]!;\n const record = objectExpressionToRecord(collapseEntry.prop.value, loc.parsed.scope);\n collapseKeyframesToFlat(loc.target.call.varsArg, record);\n\n return recast.print(loc.parsed.ast).code;\n}\n\n/**\n * Replace a dynamic `keyframes: <expr>` with a static percentage-keyframes object.\n * Called when the user first edits a dynamically-generated keyframe in the studio.\n */\nexport function materializeKeyframesInScript(\n script: string,\n animationId: string,\n keyframes: Array<{\n percentage: number;\n properties: Record<string, number | string>;\n ease?: string;\n }>,\n easeEach?: string,\n resolvedSelector?: string,\n): string {\n let loc = locateAnimationWithFallback(script, animationId);\n if (!loc) return script;\n\n const varsArg = loc.target.call.varsArg;\n\n // Replace dynamic selector with resolved static string\n if (resolvedSelector && loc.target.call.node.arguments[0]) {\n loc.target.call.node.arguments[0] = parseExpr(JSON.stringify(resolvedSelector));\n }\n\n const kfObjCode = buildKeyframeObjectCode(sortedKeyframes(keyframes), { easeEach });\n const kfParent = varsArg.properties.find(\n (p: AstNode) => isObjectProperty(p) && propKeyName(p) === \"keyframes\",\n );\n if (kfParent) {\n kfParent.value = parseExpr(kfObjCode);\n } else {\n const kfProp = parseExpr(`{ keyframes: ${kfObjCode} }`).properties[0];\n varsArg.properties.unshift(kfProp);\n }\n\n removeVarsKey(varsArg, \"easeEach\");\n\n return recast.print(loc.parsed.ast).code;\n}\n\n// ── Arc Path (motionPath) AST Mutations ──────────────────────────────────\n\nfunction numericXY(props: Record<string, number | string>): { x: number; y: number } | null {\n const x = props.x;\n const y = props.y;\n return typeof x === \"number\" && typeof y === \"number\" ? { x, y } : null;\n}\n\nfunction extractArcWaypoints(anim: GsapAnimation): Array<{ x: number; y: number }> {\n const kfs = anim.keyframes?.keyframes ?? [];\n const waypoints = kfs.map((kf) => numericXY(kf.properties)).filter((p) => p !== null);\n if (waypoints.length >= 2) return waypoints;\n const px = anim.properties.x;\n const py = anim.properties.y;\n if (typeof px !== \"number\" && typeof py !== \"number\") return waypoints;\n return [\n { x: 0, y: 0 },\n { x: typeof px === \"number\" ? px : 0, y: typeof py === \"number\" ? py : 0 },\n ];\n}\n\nfunction buildMotionPathObjectCode(config: {\n waypoints: Array<{ x: number; y: number }>;\n segments: ArcPathSegment[];\n autoRotate: boolean | number;\n}): string {\n const { waypoints, segments, autoRotate } = config;\n const hasExplicitControlPoints = segments.some((s) => s.cp1 && s.cp2);\n // The simple `path` array supports only one scalar curviness for the whole\n // path, so per-segment curviness must use the cubic form (curviness baked into\n // each segment's control points). Without this, the simple branch serializes\n // only segments[0].curviness and silently drops every other segment's curve.\n const curvinessVaries = segments.some(\n (s) => (s.curviness ?? 1) !== (segments[0]?.curviness ?? 1),\n );\n\n let pathEntries: string[];\n if ((hasExplicitControlPoints || curvinessVaries) && waypoints.length >= 2) {\n // type: \"cubic\" — interleave control points: [anchor, cp1, cp2, anchor, ...]\n pathEntries = [`{x: ${waypoints[0]!.x}, y: ${waypoints[0]!.y}}`];\n for (let i = 0; i < segments.length; i++) {\n const seg = segments[i]!;\n const nextWp = waypoints[i + 1]!;\n if (seg.cp1 && seg.cp2) {\n pathEntries.push(`{x: ${seg.cp1.x}, y: ${seg.cp1.y}}`);\n pathEntries.push(`{x: ${seg.cp2.x}, y: ${seg.cp2.y}}`);\n } else {\n // Auto-generate simple midpoint control points from curviness\n const wp = waypoints[i]!;\n const dx = nextWp.x - wp.x;\n const dy = nextWp.y - wp.y;\n const c = seg.curviness ?? 1;\n pathEntries.push(\n `{x: ${wp.x + dx * 0.33}, y: ${wp.y + dy * 0.33 - c * Math.abs(dx) * 0.25}}`,\n );\n pathEntries.push(\n `{x: ${wp.x + dx * 0.66}, y: ${wp.y + dy * 0.66 - c * Math.abs(dx) * 0.25}}`,\n );\n }\n pathEntries.push(`{x: ${nextWp.x}, y: ${nextWp.y}}`);\n }\n const pathStr = pathEntries.join(\", \");\n const parts = [`path: [${pathStr}]`, `type: \"cubic\"`];\n if (autoRotate === true) parts.push(\"autoRotate: true\");\n else if (typeof autoRotate === \"number\") parts.push(`autoRotate: ${autoRotate}`);\n return `{ ${parts.join(\", \")} }`;\n }\n\n // Simple waypoint array with curviness\n pathEntries = waypoints.map((wp) => `{x: ${wp.x}, y: ${wp.y}}`);\n const curviness = segments[0]?.curviness ?? 1;\n const parts = [`path: [${pathEntries.join(\", \")}]`];\n if (curviness !== 1) parts.push(`curviness: ${curviness}`);\n if (autoRotate === true) parts.push(\"autoRotate: true\");\n else if (typeof autoRotate === \"number\") parts.push(`autoRotate: ${autoRotate}`);\n return `{ ${parts.join(\", \")} }`;\n}\n\nexport function setArcPathInScript(\n script: string,\n animationId: string,\n config: ArcPathConfig,\n): string {\n const loc = locateAnimation(script, animationId);\n if (!loc) return script;\n\n const varsArg = loc.target.call.varsArg;\n const anim = loc.target.animation;\n\n if (!config.enabled) {\n // Disable arc: restore x/y from motionPath's last waypoint, then remove motionPath\n const motionPathProp = varsArg.properties.find(\n (p: AstNode) => isObjectProperty(p) && propKeyName(p) === \"motionPath\",\n );\n if (motionPathProp) {\n const mpVal = motionPathProp.value;\n let pathArr: AstNode[] | undefined;\n if (mpVal?.type === \"ObjectExpression\") {\n const pathProp = mpVal.properties.find(\n (p: AstNode) => isObjectProperty(p) && propKeyName(p) === \"path\",\n );\n if (pathProp?.value?.type === \"ArrayExpression\") pathArr = pathProp.value.elements;\n }\n if (pathArr && pathArr.length > 0) {\n const last = pathArr[pathArr.length - 1];\n if (last?.type === \"ObjectExpression\") {\n for (const p of last.properties) {\n const k = propKeyName(p);\n if (k === \"x\" || k === \"y\") {\n const v = p.value?.value;\n if (typeof v === \"number\") setVarsKey(varsArg, k, v);\n }\n }\n }\n }\n }\n removeVarsKey(varsArg, \"motionPath\");\n return recast.print(loc.parsed.ast).code;\n }\n\n const waypoints = extractArcWaypoints(anim);\n if (waypoints.length < 2) return script;\n\n // Build segments — use provided segments or create defaults\n const segments: ArcPathSegment[] =\n config.segments.length === waypoints.length - 1\n ? config.segments\n : Array.from({ length: waypoints.length - 1 }, () => ({ curviness: 1 }));\n\n const motionPathCode = buildMotionPathObjectCode({\n waypoints,\n segments,\n autoRotate: config.autoRotate,\n });\n\n // Set motionPath on the vars\n const motionPathNode = parseExpr(motionPathCode);\n const existingProp = varsArg.properties.find(\n (p: AstNode) => isObjectProperty(p) && propKeyName(p) === \"motionPath\",\n );\n if (existingProp) {\n existingProp.value = motionPathNode;\n } else {\n const prop = parseExpr(`{ motionPath: ${motionPathCode} }`).properties[0];\n varsArg.properties.push(prop);\n }\n\n // Strip x/y from keyframes (they're now in motionPath)\n const kfNode = findKeyframesObjectNode(varsArg);\n if (kfNode) {\n for (const pctProp of filterPercentageProps(kfNode)) {\n if (pctProp.value?.type === \"ObjectExpression\") {\n pctProp.value.properties = pctProp.value.properties.filter((p: AstNode) => {\n const k = propKeyName(p);\n return k !== \"x\" && k !== \"y\";\n });\n }\n }\n }\n\n // Strip flat x/y from vars (they're now in motionPath)\n removeVarsKey(varsArg, \"x\");\n removeVarsKey(varsArg, \"y\");\n\n return recast.print(loc.parsed.ast).code;\n}\n\nexport function updateArcSegmentInScript(\n script: string,\n animationId: string,\n segmentIndex: number,\n update: Partial<ArcPathSegment>,\n): string {\n const loc = locateAnimation(script, animationId);\n if (!loc) return script;\n\n const anim = loc.target.animation;\n if (!anim.arcPath?.enabled) return script;\n\n const segments = [...anim.arcPath.segments];\n if (segmentIndex < 0 || segmentIndex >= segments.length) return script;\n\n segments[segmentIndex] = { ...segments[segmentIndex]!, ...update };\n\n const waypoints = extractArcWaypoints(anim);\n if (waypoints.length < 2) return script;\n\n const motionPathCode = buildMotionPathObjectCode({\n waypoints,\n segments,\n autoRotate: anim.arcPath.autoRotate,\n });\n\n const varsArg = loc.target.call.varsArg;\n const existingProp = varsArg.properties.find(\n (p: AstNode) => isObjectProperty(p) && propKeyName(p) === \"motionPath\",\n );\n if (existingProp) {\n existingProp.value = parseExpr(motionPathCode);\n }\n\n return recast.print(loc.parsed.ast).code;\n}\n\n/**\n * Move a single motionPath waypoint (anchor) to a new position. The waypoint\n * list is normalized to anchors for both straight and cubic paths, so\n * `pointIndex` matches the node order the studio overlay renders; cubic control\n * points are preserved. No-op when the animation/arc is missing or the index is\n * out of range.\n */\nexport function updateMotionPathPointInScript(\n script: string,\n animationId: string,\n pointIndex: number,\n point: { x: number; y: number },\n): string {\n const loc = locateAnimation(script, animationId);\n if (!loc) return script;\n\n const anim = loc.target.animation;\n if (!anim.arcPath?.enabled) return script;\n\n const waypoints = extractArcWaypoints(anim);\n if (pointIndex < 0 || pointIndex >= waypoints.length || waypoints.length < 2) return script;\n\n const nextWaypoints = waypoints.map((wp, i) =>\n i === pointIndex ? { x: point.x, y: point.y } : wp,\n );\n\n const motionPathCode = buildMotionPathObjectCode({\n waypoints: nextWaypoints,\n segments: anim.arcPath.segments,\n autoRotate: anim.arcPath.autoRotate,\n });\n\n const varsArg = loc.target.call.varsArg;\n const existingProp = varsArg.properties.find(\n (p: AstNode) => isObjectProperty(p) && propKeyName(p) === \"motionPath\",\n );\n if (existingProp) {\n existingProp.value = parseExpr(motionPathCode);\n }\n\n return recast.print(loc.parsed.ast).code;\n}\n\n/** True when any segment carries explicit cubic control points. Add/remove are\n * restricted to curviness (non-cubic) paths — synthesizing control points for\n * an inserted cubic anchor is out of scope. */\nfunction hasCubicSegments(segments: ArcPathSegment[]): boolean {\n return segments.some((s) => s.cp1 != null || s.cp2 != null);\n}\n\nfunction writeMotionPathValue(\n loc: NonNullable<ReturnType<typeof locateAnimation>>,\n waypoints: Array<{ x: number; y: number }>,\n segments: ArcPathSegment[],\n autoRotate: boolean | number,\n): string {\n const motionPathCode = buildMotionPathObjectCode({ waypoints, segments, autoRotate });\n const varsArg = loc.target.call.varsArg;\n const existingProp = varsArg.properties.find(\n (p: AstNode) => isObjectProperty(p) && propKeyName(p) === \"motionPath\",\n );\n if (existingProp) existingProp.value = parseExpr(motionPathCode);\n return recast.print(loc.parsed.ast).code;\n}\n\n/**\n * Insert a waypoint at `index` (between existing anchors), splitting the segment\n * it lands on so the new neighbor inherits its curviness. Non-cubic paths only.\n * No-op for missing animation/arc, out-of-range index, or cubic paths.\n */\nexport function addMotionPathPointInScript(\n script: string,\n animationId: string,\n index: number,\n point: { x: number; y: number },\n): string {\n const loc = locateAnimation(script, animationId);\n if (!loc) return script;\n const anim = loc.target.animation;\n if (!anim.arcPath?.enabled || hasCubicSegments(anim.arcPath.segments)) return script;\n\n const waypoints = extractArcWaypoints(anim);\n // Insert strictly between two anchors: index 1..length-1.\n if (index < 1 || index > waypoints.length - 1) return script;\n\n const segments = [...anim.arcPath.segments];\n waypoints.splice(index, 0, { x: point.x, y: point.y });\n const splitCurviness = segments[index - 1]?.curviness ?? 1;\n segments.splice(index - 1, 0, { curviness: splitCurviness });\n\n return writeMotionPathValue(loc, waypoints, segments, anim.arcPath.autoRotate);\n}\n\n/**\n * Remove the waypoint at `index`. Refuses to drop below two anchors (a path\n * can't have fewer). Non-cubic paths only. No-op for missing animation/arc,\n * out-of-range index, cubic paths, or a 2-point path.\n */\nexport function removeMotionPathPointInScript(\n script: string,\n animationId: string,\n index: number,\n): string {\n const loc = locateAnimation(script, animationId);\n if (!loc) return script;\n const anim = loc.target.animation;\n if (!anim.arcPath?.enabled || hasCubicSegments(anim.arcPath.segments)) return script;\n\n const waypoints = extractArcWaypoints(anim);\n if (waypoints.length <= 2 || index < 0 || index >= waypoints.length) return script;\n\n const segments = [...anim.arcPath.segments];\n waypoints.splice(index, 1);\n // Drop the segment on the side that still exists (last anchor → preceding segment).\n segments.splice(Math.min(index, segments.length - 1), 1);\n\n return writeMotionPathValue(loc, waypoints, segments, anim.arcPath.autoRotate);\n}\n\n/**\n * Author a fresh 2-anchor motionPath tween on a target element: a straight line\n * from the element's home (0,0) to `point`, gentle ease, ready for waypoint\n * editing. Mirrors `addAnimationWithKeyframesToScript`.\n */\nexport function addMotionPathToScript(\n script: string,\n targetSelector: string,\n position: number,\n duration: number,\n point: { x: number; y: number },\n ease = \"power1.inOut\",\n): { script: string; id: string | null } {\n // `id: null` on the failure paths is a deliberate sentinel: callers must\n // null-check before chaining (e.g. locating the new tween). An empty string\n // would silently flow into selector/locate calls and match nothing.\n let parsed: ParsedGsapAst;\n try {\n parsed = parseGsapAst(script);\n } catch (e) {\n console.warn(\"[gsap-parser] addMotionPathToScript parse failed:\", e);\n return { script, id: null };\n }\n if (parsed.located.length === 0 && parsed.detection.ref === null) {\n return { script, id: null };\n }\n\n const motionPathCode = buildMotionPathObjectCode({\n waypoints: [\n { x: 0, y: 0 },\n { x: point.x, y: point.y },\n ],\n segments: [{ curviness: 1 }],\n autoRotate: false,\n });\n const selector = JSON.stringify(targetSelector);\n const varEntries = [\n `motionPath: ${motionPathCode}`,\n `duration: ${valueToCode(duration)}`,\n `ease: ${JSON.stringify(ease)}`,\n ];\n const stmtCode = `${parsed.timelineVar}.to(${selector}, { ${varEntries.join(\", \")} }, ${valueToCode(position)});`;\n const newStatement = parseScript(stmtCode).program.body[0];\n insertAfterAnchor(parsed, newStatement);\n\n const result = recast.print(parsed.ast).code;\n const reParsed = parseGsapAst(result);\n const newId = reParsed.located[reParsed.located.length - 1]?.id ?? null;\n return { script: result, id: newId };\n}\n\nexport function removeArcPathFromScript(script: string, animationId: string): string {\n return setArcPathInScript(script, animationId, {\n enabled: false,\n autoRotate: false,\n segments: [],\n });\n}\n\n// ── Split Into Property Groups ────────────────────────────────────────────\n\n/**\n * Split a multi-group tween into separate per-group tweens. Each resulting\n * tween contains only properties belonging to one property group (position,\n * scale, rotation, visual, etc.). `transformOrigin` stays with the group that\n * has the most properties. If the tween already belongs to a single group,\n * returns the script unchanged with the original ID.\n */\n// fallow-ignore-next-line complexity\nexport function splitIntoPropertyGroups(\n script: string,\n animationId: string,\n): { script: string; ids: string[] } {\n let loc = locateAnimationWithFallback(script, animationId);\n if (!loc) return { script, ids: [animationId] };\n\n const anim = loc.target.animation;\n\n // Collect the properties to partition. For keyframed tweens, gather the\n // union of all properties across all keyframes. For flat tweens, use the\n // tween's own properties map.\n const allPropKeys = new Set<string>();\n if (anim.keyframes) {\n for (const kf of anim.keyframes.keyframes) {\n for (const k of Object.keys(kf.properties)) allPropKeys.add(k);\n }\n } else {\n for (const k of Object.keys(anim.properties)) allPropKeys.add(k);\n }\n\n // Partition properties into groups (excluding transformOrigin — handled below).\n const groupProps = new Map<PropertyGroupName, string[]>();\n for (const key of allPropKeys) {\n if (key === \"transformOrigin\") continue;\n const group = classifyPropertyGroup(key);\n let arr = groupProps.get(group);\n if (!arr) {\n arr = [];\n groupProps.set(group, arr);\n }\n arr.push(key);\n }\n\n // Only one group (or zero) — no split needed.\n if (groupProps.size <= 1) return { script, ids: [anim.id] };\n\n // Assign transformOrigin to the group with the most properties.\n if (allPropKeys.has(\"transformOrigin\")) {\n let largestGroup: PropertyGroupName | undefined;\n let largestCount = 0;\n for (const [group, props] of groupProps) {\n if (props.length > largestCount) {\n largestCount = props.length;\n largestGroup = group;\n }\n }\n if (largestGroup) {\n groupProps.get(largestGroup)!.push(\"transformOrigin\");\n }\n }\n\n // Build per-group tweens and insert them, then remove the original.\n let result = script;\n\n // Remove the original tween first.\n result = removeAnimationFromScript(result, anim.id);\n\n // Insert one tween per group. Iteration order of the Map follows insertion\n // order, which mirrors the order properties were encountered.\n for (const [, props] of groupProps) {\n const propSet = new Set(props);\n\n if (anim.keyframes) {\n // Build keyframes containing only this group's properties per keyframe.\n const groupKeyframes: Array<{\n percentage: number;\n properties: Record<string, number | string>;\n ease?: string;\n auto?: boolean;\n }> = [];\n\n for (const kf of anim.keyframes.keyframes) {\n const filtered: Record<string, number | string> = {};\n for (const [k, v] of Object.entries(kf.properties)) {\n if (propSet.has(k)) filtered[k] = v;\n }\n // Skip keyframes where this group has zero properties.\n if (Object.keys(filtered).length === 0) continue;\n groupKeyframes.push({\n percentage: kf.percentage,\n properties: filtered,\n ...(kf.ease ? { ease: kf.ease } : {}),\n });\n }\n\n if (groupKeyframes.length === 0) continue;\n\n const addResult = addAnimationWithKeyframesToScript(\n result,\n anim.targetSelector,\n typeof anim.position === \"number\" ? anim.position : 0,\n anim.duration ?? 0.5,\n groupKeyframes,\n anim.keyframes.easeEach ?? anim.ease,\n );\n result = addResult.script;\n } else {\n // Flat tween — filter properties to this group.\n const groupProperties: Record<string, number | string> = {};\n for (const [k, v] of Object.entries(anim.properties)) {\n if (propSet.has(k)) groupProperties[k] = v;\n }\n if (Object.keys(groupProperties).length === 0) continue;\n\n let fromProperties: Record<string, number | string> | undefined;\n if (anim.method === \"fromTo\" && anim.fromProperties) {\n fromProperties = {};\n for (const [k, v] of Object.entries(anim.fromProperties)) {\n if (propSet.has(k)) fromProperties[k] = v;\n }\n }\n\n const addResult = addAnimationToScript(result, {\n targetSelector: anim.targetSelector,\n method: anim.method,\n position: anim.position,\n duration: anim.duration,\n ease: anim.ease,\n properties: groupProperties,\n fromProperties,\n extras: anim.extras,\n });\n result = addResult.script;\n }\n }\n\n // Re-parse to collect the new IDs.\n const reParsed = parseGsapAst(result);\n const newIds = reParsed.located\n .filter((l) => l.animation.targetSelector === anim.targetSelector)\n .map((l) => l.id);\n\n return { script: result, ids: newIds };\n}\n\n/**\n * Replace a dynamic loop that generates multiple tween calls with individual\n * static `tl.to()` calls — one per element. Finds the loop containing the\n * animation and replaces the entire loop body with unrolled static calls.\n */\nexport function unrollDynamicAnimations(\n script: string,\n animationId: string,\n elements: Array<{\n selector: string;\n keyframes: Array<{ percentage: number; properties: Record<string, number | string> }>;\n easeEach?: string;\n }>,\n): string {\n const loc = locateAnimation(script, animationId);\n if (!loc) return script;\n\n const varsArg = loc.target.call.varsArg;\n\n // Read duration and ease from the original tween vars\n const durationVal = extractLiteralValue(findPropertyNode(varsArg, \"duration\"), loc.parsed.scope);\n const easeVal = extractLiteralValue(findPropertyNode(varsArg, \"ease\"), loc.parsed.scope);\n const duration = typeof durationVal === \"number\" ? durationVal : 8;\n const ease = typeof easeVal === \"string\" ? easeVal : \"none\";\n const posArg = loc.target.call.positionArg;\n const position = posArg ? extractLiteralValue(posArg, loc.parsed.scope) : 0;\n const posCode =\n typeof position === \"number\"\n ? String(position)\n : typeof position === \"string\"\n ? JSON.stringify(position)\n : \"0\";\n\n // Find the enclosing loop (for/forEach) by walking up the AST path\n let loopNode: AstNode | null = null;\n let current = loc.target.call.path;\n while (current) {\n const node = current.node ?? current.value;\n if (\n node?.type === \"ForStatement\" ||\n node?.type === \"ForInStatement\" ||\n node?.type === \"ForOfStatement\" ||\n node?.type === \"WhileStatement\"\n ) {\n loopNode = node;\n break;\n }\n if (\n node?.type === \"ExpressionStatement\" &&\n node.expression?.type === \"CallExpression\" &&\n node.expression.callee?.property?.name === \"forEach\"\n ) {\n loopNode = node;\n break;\n }\n current = current.parent ?? current.parentPath;\n }\n\n // Build replacement code: individual tl.to() calls for each element\n const calls: string[] = [];\n for (const el of elements) {\n const kfCode = buildKeyframeObjectCode(sortedKeyframes(el.keyframes), {\n easeEach: el.easeEach,\n });\n calls.push(\n `${loc.parsed.timelineVar}.to(${JSON.stringify(el.selector)}, { keyframes: ${kfCode}, duration: ${duration}, ease: ${JSON.stringify(ease)} }, ${posCode});`,\n );\n }\n\n const replacement = calls.join(\"\\n \");\n\n if (loopNode) {\n // Replace the entire loop with the unrolled calls\n const start = loopNode.start ?? loopNode.range?.[0];\n const end = loopNode.end ?? loopNode.range?.[1];\n if (typeof start === \"number\" && typeof end === \"number\") {\n return script.slice(0, start) + replacement + script.slice(end);\n }\n }\n\n // Fallback: replace just the tween call's enclosing expression statement\n const stmtNode = loc.target.call.path?.parent?.node ?? loc.target.call.path?.parentPath?.node;\n if (stmtNode?.type === \"ExpressionStatement\") {\n const start = stmtNode.start ?? stmtNode.range?.[0];\n const end = stmtNode.end ?? stmtNode.range?.[1];\n if (typeof start === \"number\" && typeof end === \"number\") {\n return script.slice(0, start) + replacement + script.slice(end);\n }\n }\n\n return script;\n}\n","/**\n * Damped harmonic oscillator solver for GSAP CustomEase spring curves.\n *\n * Generates an SVG path data string compatible with `CustomEase.create(id, data)`.\n * The solver supports underdamped (bouncy), critically damped, and overdamped\n * spring configurations. Output is normalized to x ∈ [0,1] with y starting at 0\n * and settling to 1.\n */\n\nexport interface SpringPreset {\n name: string;\n label: string;\n mass: number;\n stiffness: number;\n damping: number;\n}\n\nexport const SPRING_PRESETS: SpringPreset[] = [\n { name: \"spring-gentle\", label: \"Gentle\", mass: 1, stiffness: 100, damping: 15 },\n { name: \"spring-bouncy\", label: \"Bouncy\", mass: 1, stiffness: 180, damping: 12 },\n { name: \"spring-stiff\", label: \"Stiff\", mass: 1, stiffness: 300, damping: 20 },\n { name: \"spring-wobbly\", label: \"Wobbly\", mass: 1, stiffness: 120, damping: 8 },\n { name: \"spring-heavy\", label: \"Heavy\", mass: 3, stiffness: 200, damping: 20 },\n];\n\n/**\n * Solve a damped harmonic oscillator and return a GSAP CustomEase data string.\n *\n * The output is an SVG path (`M0,0 L... L...`) that CustomEase.create() accepts.\n * The curve is normalized so x spans [0,1] and the spring settles at y = 1.\n *\n * @param mass - Spring mass (> 0)\n * @param stiffness - Spring stiffness constant (> 0)\n * @param damping - Damping coefficient (> 0)\n * @param steps - Number of sample points (default 120)\n */\nexport function generateSpringEaseData(\n mass: number,\n stiffness: number,\n damping: number,\n steps = 120,\n): string {\n const w0 = Math.sqrt(stiffness / mass);\n const zeta = damping / (2 * Math.sqrt(stiffness * mass));\n\n // Determine simulation duration: time until oscillation settles within threshold of 1.0.\n // Underdamped: ~5 time constants. Critically/overdamped: characteristic decay time.\n let settleDuration: number;\n if (zeta < 1) {\n settleDuration = Math.min(5 / (zeta * w0), 10);\n } else {\n const decayRate = zeta * w0 - w0 * Math.sqrt(zeta * zeta - 1);\n settleDuration = Math.min(4 / Math.max(decayRate, 0.01), 10);\n }\n const simDuration = Math.max(settleDuration, 1);\n\n const segments: string[] = [\"M0,0\"];\n\n for (let i = 1; i <= steps; i++) {\n const t = i / steps;\n const simT = t * simDuration;\n let value: number;\n\n if (zeta < 1) {\n // Underdamped — oscillates before settling\n const wd = w0 * Math.sqrt(1 - zeta * zeta);\n value =\n 1 -\n Math.exp(-zeta * w0 * simT) *\n (Math.cos(wd * simT) + ((zeta * w0) / wd) * Math.sin(wd * simT));\n } else if (zeta === 1) {\n // Critically damped — fastest approach without oscillation\n value = 1 - (1 + w0 * simT) * Math.exp(-w0 * simT);\n } else {\n // Overdamped — slow exponential approach\n const s1 = -w0 * (zeta - Math.sqrt(zeta * zeta - 1));\n const s2 = -w0 * (zeta + Math.sqrt(zeta * zeta - 1));\n value = 1 + (s1 * Math.exp(s2 * simT) - s2 * Math.exp(s1 * simT)) / (s2 - s1);\n }\n\n segments.push(`${t.toFixed(4)},${value.toFixed(4)}`);\n }\n\n // Force exact endpoint\n segments[segments.length - 1] = \"1,1\";\n\n return `${segments[0]} L${segments.slice(1).join(\" \")}`;\n}\n","// fallow-ignore-file code-duplication\n/**\n * Browser-safe GSAP read path — acorn + acorn-walk.\n *\n * T6b oracle: produces identical ParsedGsap output to gsapParser.ts (recast).\n * Replaces recast as the shared implementation once T6d passes.\n *\n * Write path (T6c) will add magic-string splice once read parity is confirmed.\n * No Node globals, no fs, no require — safe to bundle for browser use.\n */\nimport * as acorn from \"acorn\";\nimport * as acornWalk from \"acorn-walk\";\nimport type {\n ArcPathConfig,\n GsapAnimation,\n GsapKeyframesData,\n GsapMethod,\n GsapPercentageKeyframe,\n ParsedGsap,\n} from \"./gsapSerialize.js\";\nimport { classifyTweenPropertyGroup } from \"./gsapConstants.js\";\nimport { buildArcPath } from \"./gsapSerialize.js\";\nimport { inlineComputedTimelines, readProvenance } from \"./gsapInline.js\";\n\n// Browser-safe re-exports so studio code can build arc config without importing\n// the recast parser (this acorn module is the browser-safe gsap subpath).\nexport { buildArcPath, editabilityForProvenance } from \"./gsapSerialize.js\";\nexport type {\n ArcPathConfig,\n ArcPathSegment,\n MotionPathShape,\n GsapProvenance,\n GsapProvenanceKind,\n KeyframeEditability,\n} from \"./gsapSerialize.js\";\n\nconst GSAP_METHODS = new Set<string>([\"set\", \"to\", \"from\", \"fromTo\"]);\nconst QUERY_METHODS = new Set([\"querySelector\", \"querySelectorAll\"]);\nconst ITERATION_METHODS = new Set([\"forEach\", \"map\"]);\nconst SCOPE_NODE_TYPES = new Set([\n \"Program\",\n \"FunctionDeclaration\",\n \"FunctionExpression\",\n \"ArrowFunctionExpression\",\n]);\n\n// ── Types ────────────────────────────────────────────────────────────────────\n\ntype ScopeBindings = ReadonlyMap<string, number | string | boolean>;\n/** Per-scope element bindings: scopeNode → (variable name → selector). */\ntype TargetBindings = Map<any, Map<string, string>>;\n\n// ── Value resolution ─────────────────────────────────────────────────────────\n\n// fallow-ignore-next-line complexity\nfunction resolveNode(\n node: any,\n scope: ReadonlyMap<string, number | string | boolean>,\n): number | string | boolean | undefined {\n if (!node) return undefined;\n if (node.type === \"NumericLiteral\" || (node.type === \"Literal\" && typeof node.value === \"number\"))\n return node.value;\n if (node.type === \"StringLiteral\" || (node.type === \"Literal\" && typeof node.value === \"string\"))\n return node.value;\n if (\n node.type === \"BooleanLiteral\" ||\n (node.type === \"Literal\" && typeof node.value === \"boolean\")\n )\n return node.value;\n if (node.type === \"UnaryExpression\" && node.operator === \"-\" && node.argument) {\n const val = resolveNode(node.argument, scope);\n return typeof val === \"number\" ? -val : undefined;\n }\n if (node.type === \"BinaryExpression\") {\n const left = resolveNode(node.left, scope);\n const right = resolveNode(node.right, scope);\n if (typeof left === \"number\" && typeof right === \"number\") {\n switch (node.operator) {\n case \"+\":\n return left + right;\n case \"-\":\n return left - right;\n case \"*\":\n return left * right;\n case \"/\":\n return right !== 0 ? left / right : undefined;\n }\n }\n if (typeof left === \"string\" && node.operator === \"+\") return left + String(right ?? \"\");\n if (typeof right === \"string\" && node.operator === \"+\") return String(left ?? \"\") + right;\n }\n if (node.type === \"Identifier\" && scope.has(node.name)) {\n return scope.get(node.name);\n }\n if (node.type === \"TemplateLiteral\" && node.expressions?.length === 0) {\n return node.quasis?.[0]?.value?.cooked ?? undefined;\n }\n return undefined;\n}\n\nfunction extractLiteralValue(node: any, scope: ScopeBindings): unknown {\n return resolveNode(node, scope);\n}\n\n// ── DOM selector resolution ───────────────────────────────────────────────────\n\n// fallow-ignore-next-line complexity\nfunction selectorFromQueryCall(node: any, scope: ScopeBindings): string | null {\n if (node?.type !== \"CallExpression\") return null;\n const callee = node.callee;\n if (callee?.type !== \"MemberExpression\" || callee.property?.type !== \"Identifier\") return null;\n const method = callee.property.name;\n const argValue = resolveNode(node.arguments?.[0], scope);\n if (typeof argValue !== \"string\" || argValue.length === 0) return null;\n if (QUERY_METHODS.has(method) || method === \"toArray\") return argValue;\n if (method === \"getElementById\") return `#${argValue}`;\n return null;\n}\n\n// ── Ancestor-based scope helpers (replaces NodePath walking) ──────────────────\n\n/**\n * Return the nearest ancestor node whose type is in SCOPE_NODE_TYPES.\n * `ancestors` is the acorn-walk ancestor array (root→current, current is last).\n */\nfunction enclosingScopeNodeFromAncestors(ancestors: any[]): any {\n for (let i = ancestors.length - 2; i >= 0; i--) {\n const node = ancestors[i];\n if (node && SCOPE_NODE_TYPES.has(node.type)) return node;\n }\n return null;\n}\n\n/** Scope chain innermost-first, derived from the acorn-walk ancestors array. */\nfunction scopeChainFromAncestors(ancestors: any[]): any[] {\n const chain: any[] = [];\n for (let i = ancestors.length - 1; i >= 0; i--) {\n const node = ancestors[i];\n if (node && SCOPE_NODE_TYPES.has(node.type)) chain.push(node);\n }\n return chain;\n}\n\n// ── Target bindings ───────────────────────────────────────────────────────────\n\nfunction addBinding(\n bindings: TargetBindings,\n scopeNode: any,\n name: string,\n selector: string,\n): void {\n let scoped = bindings.get(scopeNode);\n if (!scoped) {\n scoped = new Map();\n bindings.set(scopeNode, scoped);\n }\n if (!scoped.has(name)) scoped.set(name, selector);\n}\n\nfunction lookupBindingFromAncestors(\n name: string,\n ancestors: any[],\n bindings: TargetBindings,\n): string | null {\n for (const scopeNode of scopeChainFromAncestors(ancestors)) {\n const selector = bindings.get(scopeNode)?.get(name);\n if (selector !== undefined) return selector;\n }\n // Program-scope bindings are stored under null (enclosingScopeNodeFromAncestors\n // returns null when no function wrapper exists — the common case in HF scripts).\n return bindings.get(null)?.get(name) ?? null;\n}\n\nfunction isFunctionNode(node: any): boolean {\n return (\n node?.type === \"ArrowFunctionExpression\" ||\n node?.type === \"FunctionExpression\" ||\n node?.type === \"FunctionDeclaration\"\n );\n}\n\nfunction resolveCollectionSelector(\n node: any,\n ancestors: any[],\n scope: ScopeBindings,\n bindings: TargetBindings,\n): string | null {\n if (node?.type === \"Identifier\")\n return lookupBindingFromAncestors(node.name, ancestors, bindings);\n if (node?.type === \"CallExpression\") return selectorFromQueryCall(node, scope);\n return null;\n}\n\nfunction collectScopeBindings(ast: any): ScopeBindings {\n const bindings = new Map<string, number | string | boolean>();\n acornWalk.simple(ast, {\n VariableDeclarator(node: any) {\n const name = node.id?.name;\n const init = node.init;\n if (name && init) {\n const val = resolveNode(init, bindings);\n if (val !== undefined) bindings.set(name, val);\n }\n },\n });\n return bindings;\n}\n\n/**\n * Build a lexically-scoped index of element variables → selector.\n * Pass 1: direct DOM-lookup assignments.\n * Pass 2: forEach/map callback params whose collection's selector is known.\n */\nfunction collectTargetBindings(ast: any, scope: ScopeBindings): TargetBindings {\n const bindings: TargetBindings = new Map();\n\n acornWalk.ancestor(ast, {\n VariableDeclarator(node: any, _: unknown, ancestors: any[]) {\n const name = node.id?.name;\n const selector = selectorFromQueryCall(node.init, scope);\n if (name && selector !== null) {\n addBinding(bindings, enclosingScopeNodeFromAncestors(ancestors), name, selector);\n }\n },\n AssignmentExpression(node: any, _: unknown, ancestors: any[]) {\n const left = node.left;\n const selector = selectorFromQueryCall(node.right, scope);\n if (left?.type === \"Identifier\" && selector !== null) {\n addBinding(bindings, enclosingScopeNodeFromAncestors(ancestors), left.name, selector);\n }\n },\n } as any);\n\n // Pass 2: forEach/map callback params take the collection's selector.\n acornWalk.ancestor(ast, {\n // fallow-ignore-next-line complexity\n CallExpression(node: any, _: unknown, ancestors: any[]) {\n const callee = node.callee;\n if (\n callee?.type === \"MemberExpression\" &&\n callee.property?.type === \"Identifier\" &&\n ITERATION_METHODS.has(callee.property.name)\n ) {\n const collectionSelector = resolveCollectionSelector(\n callee.object,\n ancestors,\n scope,\n bindings,\n );\n const fn = node.arguments?.[0];\n const param = fn?.params?.[0];\n if (collectionSelector && param?.type === \"Identifier\" && isFunctionNode(fn)) {\n addBinding(bindings, fn, param.name, collectionSelector);\n }\n }\n },\n } as any);\n\n return bindings;\n}\n\n// fallow-ignore-next-line complexity\nfunction resolveTargetSelector(\n node: any,\n ancestors: any[],\n scope: ScopeBindings,\n bindings: TargetBindings,\n): string | null {\n if (!node) return null;\n if (node.type === \"StringLiteral\" || node.type === \"Literal\") {\n return typeof node.value === \"string\" ? node.value : null;\n }\n if (node.type === \"Identifier\") {\n return lookupBindingFromAncestors(node.name, ancestors, bindings);\n }\n if (node.type === \"CallExpression\") {\n return selectorFromQueryCall(node, scope);\n }\n if (node.type === \"ArrayExpression\") {\n const parts = node.elements\n .map((el: any) => resolveTargetSelector(el, ancestors, scope, bindings))\n .filter((s: string | null): s is string => typeof s === \"string\" && s.length > 0);\n return parts.length > 0 ? parts.join(\", \") : null;\n }\n if (node.type === \"MemberExpression\" && node.object?.type === \"Identifier\") {\n return lookupBindingFromAncestors(node.object.name, ancestors, bindings);\n }\n return null;\n}\n\n// ── ObjectExpression utilities ────────────────────────────────────────────────\n\nfunction isObjectProperty(prop: any): boolean {\n return prop?.type === \"ObjectProperty\" || prop?.type === \"Property\";\n}\n\nfunction propKeyName(prop: any): string | undefined {\n return prop?.key?.name ?? prop?.key?.value;\n}\n\nfunction findPropertyNode(varsArgNode: any, key: string): any | undefined {\n if (varsArgNode?.type !== \"ObjectExpression\") return undefined;\n for (const prop of varsArgNode.properties ?? []) {\n if (!isObjectProperty(prop)) continue;\n if (propKeyName(prop) === key) return prop.value;\n }\n return undefined;\n}\n\n/**\n * Extract raw source text for a property value — the offset-splice primitive.\n * Equivalent to `recast.print(node).code` for unmodified nodes.\n */\nfunction extractRawPropertySource(\n varsArgNode: any,\n key: string,\n source: string,\n): string | undefined {\n const node = findPropertyNode(varsArgNode, key);\n return node ? source.slice(node.start, node.end) : undefined;\n}\n\n// fallow-ignore-next-line complexity\nfunction objectExpressionToRecord(\n node: any,\n scope: ScopeBindings,\n source: string,\n): Record<string, unknown> {\n const result: Record<string, unknown> = {};\n if (node?.type !== \"ObjectExpression\") return result;\n for (const prop of node.properties ?? []) {\n if (!isObjectProperty(prop)) continue;\n const key = prop.key?.name ?? prop.key?.value;\n if (!key) continue;\n const resolved = resolveNode(prop.value, scope);\n if (resolved !== undefined) {\n result[key] = resolved;\n } else {\n result[key] = `__raw:${source.slice(prop.value.start, prop.value.end)}`;\n }\n }\n return result;\n}\n\n// ── Timeline detection ────────────────────────────────────────────────────────\n\nfunction isGsapTimelineCall(node: any): boolean {\n return (\n node?.type === \"CallExpression\" &&\n node.callee?.type === \"MemberExpression\" &&\n node.callee.object?.name === \"gsap\" &&\n node.callee.property?.name === \"timeline\"\n );\n}\n\ninterface TimelineDefaults {\n ease?: string;\n duration?: number;\n}\n\n// How the timeline is referred to in source. `identifier` is the canonical\n// `const tl = …` form; `member` is the inline `window.__timelines[\"scene\"] = …`\n// form, where the timeline IS the member expression (no variable name).\ntype TimelineRef = { kind: \"identifier\"; name: string } | { kind: \"member\"; node: any };\n\ninterface TimelineDetection {\n /** Identifier name for the canonical form, else null (member or none). */\n timelineVar: string | null;\n /** Structural reference: identifier OR member expression. Null when none found. */\n ref: TimelineRef | null;\n timelineCount: number;\n defaults?: TimelineDefaults;\n}\n\n/** The static string key of a member access (`window.__timelines[\"scene\"]` → \"scene\"), else null. */\nfunction staticMemberKey(node: any): string | null {\n if (!node || node.type !== \"MemberExpression\") return null;\n if (node.computed) {\n const p = node.property;\n if (p?.type === \"Literal\" && typeof p.value === \"string\") return p.value;\n return null; // computed non-string-literal key → not statically resolvable\n }\n return node.property?.type === \"Identifier\" ? node.property.name : null;\n}\n\n/** True when a member expression refers to a statically-resolvable timeline slot. */\nfunction isStaticMemberRef(node: any): boolean {\n return node?.type === \"MemberExpression\" && staticMemberKey(node) !== null;\n}\n\n/** Structural equality of two member-access nodes (object chain + static key), quote-insensitive. */\nfunction sameMemberAccess(a: any, b: any): boolean {\n if (a?.type !== \"MemberExpression\" || b?.type !== \"MemberExpression\") return false;\n if (staticMemberKey(a) !== staticMemberKey(b) || staticMemberKey(a) === null) return false;\n const ao = a.object;\n const bo = b.object;\n if (ao?.type === \"Identifier\" && bo?.type === \"Identifier\") return ao.name === bo.name;\n if (ao?.type === \"MemberExpression\" && bo?.type === \"MemberExpression\")\n return sameMemberAccess(ao, bo);\n return false;\n}\n\n/** The source string a tween call is rooted at: identifier name, or the member source as written. */\nfunction timelineRootSource(ref: TimelineRef, script: string): string {\n return ref.kind === \"identifier\" ? ref.name : script.slice(ref.node.start, ref.node.end);\n}\n\nfunction escapeRegExp(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\n// fallow-ignore-next-line complexity\nfunction extractTimelineDefaults(\n callNode: any,\n scope: ScopeBindings,\n): TimelineDefaults | undefined {\n const arg = callNode.arguments?.[0];\n if (!arg || arg.type !== \"ObjectExpression\") return undefined;\n const defaultsProp = arg.properties?.find(\n (p: any) => isObjectProperty(p) && propKeyName(p) === \"defaults\",\n );\n if (!defaultsProp?.value || defaultsProp.value.type !== \"ObjectExpression\") return undefined;\n const result: TimelineDefaults = {};\n for (const prop of defaultsProp.value.properties ?? []) {\n if (!isObjectProperty(prop)) continue;\n const key = propKeyName(prop);\n const val = resolveNode(prop.value, scope);\n if (key === \"ease\" && typeof val === \"string\") result.ease = val;\n if (key === \"duration\" && typeof val === \"number\") result.duration = val;\n }\n return Object.keys(result).length > 0 ? result : undefined;\n}\n\nfunction findTimelineVar(ast: any, scope?: ScopeBindings): TimelineDetection {\n let timelineVar: string | null = null;\n let ref: TimelineRef | null = null;\n let timelineCount = 0;\n let defaults: TimelineDefaults | undefined;\n const emptyScope: ScopeBindings = scope ?? new Map();\n\n acornWalk.simple(ast, {\n VariableDeclarator(node: any) {\n if (isGsapTimelineCall(node.init)) {\n timelineCount += 1;\n if (!ref && node.id?.type === \"Identifier\") {\n timelineVar = node.id.name;\n ref = { kind: \"identifier\", name: node.id.name };\n defaults = extractTimelineDefaults(node.init, emptyScope);\n }\n }\n },\n AssignmentExpression(node: any) {\n if (isGsapTimelineCall(node.right)) {\n timelineCount += 1;\n if (!ref) {\n const left = node.left;\n if (left?.type === \"Identifier\") {\n timelineVar = left.name;\n ref = { kind: \"identifier\", name: left.name };\n defaults = extractTimelineDefaults(node.right, emptyScope);\n } else if (isStaticMemberRef(left)) {\n // Inline form: `window.__timelines[\"scene\"] = gsap.timeline(...)`.\n ref = { kind: \"member\", node: left };\n defaults = extractTimelineDefaults(node.right, emptyScope);\n }\n }\n }\n },\n });\n\n return { timelineVar, ref, timelineCount, defaults };\n}\n\n// ── Tween call collection ─────────────────────────────────────────────────────\n\n/** Keys stored on dedicated GsapAnimation fields (not in properties/extras). */\nconst BUILTIN_VAR_KEYS = new Set([\"duration\", \"ease\", \"delay\"]);\n/** Keys never preserved (callbacks / advanced patterns). */\nconst DROPPED_VAR_KEYS = new Set([\"onComplete\", \"onStart\", \"onUpdate\", \"onRepeat\"]);\n/** Keys that go in `extras` — non-editable GSAP config that must survive round-trips. */\nconst EXTRAS_KEYS = new Set([\n \"stagger\",\n \"yoyo\",\n \"repeat\",\n \"repeatDelay\",\n \"snap\",\n \"overwrite\",\n \"immediateRender\",\n]);\n\nexport interface TweenCallInfo {\n node: any;\n /** acorn-walk ancestor array at the call site (root→call, call is last). */\n ancestors: any[];\n method: GsapMethod;\n selector: string;\n varsArg: any;\n fromArg?: any;\n positionArg?: any;\n /** True for a base `gsap.set(...)` (off-timeline) rather than `tl.set(...)`. */\n global?: boolean;\n}\n\n/** True when the callee chain is rooted at the timeline reference (identifier or member). */\nfunction isTimelineRootedCall(callNode: any, ref: TimelineRef): boolean {\n let obj = callNode.callee?.object;\n while (obj?.type === \"CallExpression\") {\n obj = obj.callee?.object;\n }\n if (ref.kind === \"identifier\") return obj?.type === \"Identifier\" && obj.name === ref.name;\n return sameMemberAccess(obj, ref.node);\n}\n\n/**\n * Pre-order recursive walk for tween collection.\n *\n * acorn-walk is POST-order (visitor fires after children), which reverses\n * chained calls vs recast.types.visit (PRE-order). We need pre-order to\n * match the golden ordering where the outermost chained call appears first.\n */\nfunction findAllTweenCalls(\n ast: any,\n ref: TimelineRef,\n scope: ScopeBindings,\n targetBindings: TargetBindings,\n): TweenCallInfo[] {\n const results: TweenCallInfo[] = [];\n\n // fallow-ignore-next-line complexity\n function visit(node: any, ancestors: readonly any[]): void {\n if (!node || typeof node !== \"object\") return;\n const nodeAncestors = [...ancestors, node];\n\n // Fire BEFORE children (pre-order) so chained outer calls come first.\n if (node.type === \"CallExpression\") {\n const callee = node.callee;\n // A base `gsap.set(\"#sel\", props)` is an off-timeline static hold — parse it as\n // an editable global `set` so a static value round-trips and re-edits in place.\n // STRING-LITERAL selectors only: variable-target holds stay surrounding source.\n const gsapSetArg = node.arguments?.[0];\n const isGlobalSet =\n callee?.type === \"MemberExpression\" &&\n callee.object?.type === \"Identifier\" &&\n callee.object.name === \"gsap\" &&\n callee.property?.type === \"Identifier\" &&\n callee.property.name === \"set\" &&\n (gsapSetArg?.type === \"StringLiteral\" ||\n (gsapSetArg?.type === \"Literal\" && typeof gsapSetArg.value === \"string\"));\n if (\n callee?.type === \"MemberExpression\" &&\n callee.property?.type === \"Identifier\" &&\n (isTimelineRootedCall(node, ref) || isGlobalSet) &&\n GSAP_METHODS.has(callee.property.name)\n ) {\n const method = callee.property.name;\n const args = node.arguments;\n const selectorValue =\n args.length >= 1\n ? (resolveTargetSelector(args[0], nodeAncestors, scope, targetBindings) ??\n \"__unresolved__\")\n : \"__unresolved__\";\n\n if (method === \"fromTo\" && args.length >= 3) {\n results.push({\n node,\n ancestors: nodeAncestors,\n method: \"fromTo\",\n selector: selectorValue,\n fromArg: args[1],\n varsArg: args[2],\n positionArg: args[3],\n });\n } else if (method !== \"fromTo\" && args.length >= 2) {\n results.push({\n node,\n ancestors: nodeAncestors,\n method: method as GsapMethod,\n selector: selectorValue,\n varsArg: args[1],\n positionArg: args[2],\n ...(isGlobalSet ? { global: true } : {}),\n });\n }\n }\n }\n\n // Traverse children. Object.keys preserves insertion order, so callee\n // comes before arguments in acorn's CallExpression nodes.\n for (const key of Object.keys(node)) {\n if (key === \"type\" || key === \"start\" || key === \"end\" || key === \"loc\") continue;\n const child = (node as any)[key];\n if (Array.isArray(child)) {\n for (const item of child) {\n if (item && typeof item === \"object\" && item.type) visit(item, nodeAncestors);\n }\n } else if (child && typeof child === \"object\" && (child as any).type) {\n visit(child, nodeAncestors);\n }\n }\n }\n\n visit(ast, []);\n return results;\n}\n\n// ── Keyframes parsing ─────────────────────────────────────────────────────────\n\nconst PERCENTAGE_KEY_RE = /^(\\d+(?:\\.\\d+)?)%$/;\n\nfunction tryResolveStringProp(propValue: any, scope: ScopeBindings): string | undefined {\n const val = resolveNode(propValue, scope);\n return typeof val === \"string\" ? val : undefined;\n}\n\n// fallow-ignore-next-line complexity\nfunction parsePercentageKeyframes(\n node: any,\n scope: ScopeBindings,\n source: string,\n): GsapKeyframesData {\n const keyframes: GsapPercentageKeyframe[] = [];\n let ease: string | undefined;\n let easeEach: string | undefined;\n\n for (const prop of node.properties ?? []) {\n if (prop.type !== \"ObjectProperty\" && prop.type !== \"Property\") continue;\n const key = prop.key?.value ?? prop.key?.name;\n if (typeof key !== \"string\") continue;\n\n const pctMatch = PERCENTAGE_KEY_RE.exec(key);\n if (pctMatch) {\n const percentage = Number.parseFloat(pctMatch[1] ?? \"0\");\n const record = objectExpressionToRecord(prop.value, scope, source);\n const properties: Record<string, number | string> = {};\n let kfEase: string | undefined;\n for (const [k, v] of Object.entries(record)) {\n if (k === \"ease\" && typeof v === \"string\") {\n kfEase = v;\n } else if (typeof v === \"number\" || typeof v === \"string\") {\n properties[k] = v;\n }\n }\n keyframes.push({ percentage, properties, ...(kfEase ? { ease: kfEase } : {}) });\n } else if (key === \"ease\") {\n ease = tryResolveStringProp(prop.value, scope) ?? ease;\n } else if (key === \"easeEach\") {\n easeEach = tryResolveStringProp(prop.value, scope) ?? easeEach;\n }\n }\n\n keyframes.sort((a, b) => a.percentage - b.percentage);\n\n return {\n format: \"percentage\",\n keyframes,\n ...(ease ? { ease } : {}),\n ...(easeEach ? { easeEach } : {}),\n };\n}\n\n// fallow-ignore-next-line complexity\nfunction computeKeyframesTotalDuration(\n varsNode: any,\n scope: ScopeBindings,\n source: string,\n): number | undefined {\n const kfNode = (varsNode.properties ?? []).find(\n (p: any) => (p.key?.name ?? p.key?.value) === \"keyframes\",\n )?.value;\n if (!kfNode || kfNode.type !== \"ArrayExpression\") return undefined;\n let total = 0;\n for (const el of kfNode.elements ?? []) {\n if (!el || el.type !== \"ObjectExpression\") continue;\n const r = objectExpressionToRecord(el, scope, source);\n if (typeof r.duration === \"number\") total += r.duration;\n }\n return total > 0 ? total : undefined;\n}\n\n// fallow-ignore-next-line complexity\nfunction parseObjectArrayKeyframes(\n node: any,\n scope: ScopeBindings,\n source: string,\n): GsapKeyframesData {\n const elements = node.elements ?? [];\n const raw: Array<{\n properties: Record<string, number | string>;\n duration?: number;\n ease?: string;\n }> = [];\n\n for (const el of elements) {\n if (!el || el.type !== \"ObjectExpression\") continue;\n const record = objectExpressionToRecord(el, scope, source);\n const properties: Record<string, number | string> = {};\n let duration: number | undefined;\n let ease: string | undefined;\n for (const [k, v] of Object.entries(record)) {\n if (k === \"duration\" && typeof v === \"number\") {\n duration = v;\n } else if (k === \"ease\" && typeof v === \"string\") {\n ease = v;\n } else if (typeof v === \"number\" || typeof v === \"string\") {\n properties[k] = v;\n }\n }\n raw.push({ properties, duration, ease });\n }\n\n const totalDuration = raw.reduce((sum, r) => sum + (r.duration ?? 0), 0);\n const keyframes: GsapPercentageKeyframe[] = [];\n\n if (totalDuration > 0) {\n let cumulative = 0;\n for (const entry of raw) {\n cumulative += entry.duration ?? 0;\n const percentage = Math.round((cumulative / totalDuration) * 100);\n keyframes.push({\n percentage,\n properties: entry.properties,\n ...(entry.ease ? { ease: entry.ease } : {}),\n });\n }\n } else {\n for (let i = 0; i < raw.length; i++) {\n const entry = raw[i];\n if (!entry) continue;\n const percentage = raw.length > 1 ? Math.round((i / (raw.length - 1)) * 100) : 0;\n keyframes.push({\n percentage,\n properties: entry.properties,\n ...(entry.ease ? { ease: entry.ease } : {}),\n });\n }\n }\n\n return { format: \"object-array\", keyframes };\n}\n\n// fallow-ignore-next-line complexity\nfunction parseSimpleArrayKeyframes(node: any, scope: ScopeBindings): GsapKeyframesData {\n const arrayProps: Map<string, (number | string)[]> = new Map();\n let ease: string | undefined;\n let easeEach: string | undefined;\n\n for (const prop of node.properties ?? []) {\n if (prop.type !== \"ObjectProperty\" && prop.type !== \"Property\") continue;\n const key = prop.key?.name ?? prop.key?.value;\n if (typeof key !== \"string\") continue;\n\n if (prop.value?.type === \"ArrayExpression\") {\n const values: (number | string)[] = [];\n for (const el of prop.value.elements ?? []) {\n const val = resolveNode(el, scope);\n if (typeof val === \"number\" || typeof val === \"string\") {\n values.push(val);\n }\n }\n if (values.length > 0) arrayProps.set(key, values);\n } else if (key === \"ease\") {\n ease = tryResolveStringProp(prop.value, scope) ?? ease;\n } else if (key === \"easeEach\") {\n easeEach = tryResolveStringProp(prop.value, scope) ?? easeEach;\n }\n }\n\n const maxLen = Math.max(...[...arrayProps.values()].map((a) => a.length), 0);\n const keyframes: GsapPercentageKeyframe[] = [];\n\n for (let i = 0; i < maxLen; i++) {\n const percentage = maxLen > 1 ? Math.round((i / (maxLen - 1)) * 100) : 0;\n const properties: Record<string, number | string> = {};\n for (const [key, values] of arrayProps) {\n if (i < values.length) properties[key] = values[i] as number | string;\n }\n keyframes.push({ percentage, properties });\n }\n\n return {\n format: \"simple-array\",\n keyframes,\n ...(ease ? { ease } : {}),\n ...(easeEach ? { easeEach } : {}),\n };\n}\n\n// fallow-ignore-next-line complexity\nfunction parseKeyframesNode(\n node: any,\n scope: ScopeBindings,\n source: string,\n): GsapKeyframesData | undefined {\n if (!node) return undefined;\n\n if (node.type === \"ArrayExpression\") {\n return parseObjectArrayKeyframes(node, scope, source);\n }\n\n if (node.type !== \"ObjectExpression\") return undefined;\n\n const props = node.properties ?? [];\n let hasPercentageKey = false;\n let hasArrayValue = false;\n\n for (const prop of props) {\n if (prop.type !== \"ObjectProperty\" && prop.type !== \"Property\") continue;\n const key = prop.key?.value ?? prop.key?.name;\n if (typeof key === \"string\" && PERCENTAGE_KEY_RE.test(key)) {\n hasPercentageKey = true;\n break;\n }\n if (prop.value?.type === \"ArrayExpression\") {\n hasArrayValue = true;\n }\n }\n\n if (hasPercentageKey) return parsePercentageKeyframes(node, scope, source);\n if (hasArrayValue) return parseSimpleArrayKeyframes(node, scope);\n\n return undefined;\n}\n\n// ── MotionPath parsing ────────────────────────────────────────────────────────\n\ninterface MotionPathParseResult {\n arcPath: ArcPathConfig;\n waypoints: Array<{ x: number; y: number }>;\n}\n\n// fallow-ignore-next-line complexity\nfunction parseMotionPathNode(\n node: any,\n scope: ScopeBindings,\n source: string,\n): MotionPathParseResult | undefined {\n if (!node) return undefined;\n\n let pathNode: any;\n let autoRotate: boolean | number = false;\n let curviness = 1;\n let isCubic = false;\n\n if (node.type === \"ObjectExpression\") {\n for (const prop of node.properties ?? []) {\n if (!isObjectProperty(prop)) continue;\n const key = propKeyName(prop);\n if (key === \"path\") pathNode = prop.value;\n else if (key === \"autoRotate\") {\n const val = resolveNode(prop.value, scope);\n autoRotate = typeof val === \"number\" ? val : val === true;\n } else if (key === \"curviness\") {\n const val = resolveNode(prop.value, scope);\n if (typeof val === \"number\") curviness = val;\n } else if (key === \"type\") {\n const val = resolveNode(prop.value, scope);\n if (val === \"cubic\") isCubic = true;\n }\n }\n } else if (node.type === \"ArrayExpression\") {\n pathNode = node;\n }\n\n if (!pathNode || pathNode.type !== \"ArrayExpression\") return undefined;\n\n const elements = pathNode.elements ?? [];\n const coords: Array<{ x: number; y: number }> = [];\n for (const elem of elements) {\n if (!elem || elem.type !== \"ObjectExpression\") continue;\n const rec = objectExpressionToRecord(elem, scope, source);\n const x = typeof rec.x === \"number\" ? rec.x : undefined;\n const y = typeof rec.y === \"number\" ? rec.y : undefined;\n if (x !== undefined && y !== undefined) coords.push({ x, y });\n }\n\n return buildArcPath(coords, curviness, autoRotate, isCubic);\n}\n\n// ── Animation assembly ────────────────────────────────────────────────────────\n\n// fallow-ignore-next-line complexity\nfunction tweenCallToAnimation(\n call: TweenCallInfo,\n scope: ScopeBindings,\n source: string,\n): Omit<GsapAnimation, \"id\"> {\n const vars = objectExpressionToRecord(call.varsArg, scope, source);\n const properties: Record<string, number | string> = {};\n const extras: Record<string, unknown> = {};\n let keyframesData: GsapKeyframesData | undefined;\n let hasUnresolvedKeyframes = false;\n let motionPathResult: MotionPathParseResult | undefined;\n\n for (const [key, val] of Object.entries(vars)) {\n if (BUILTIN_VAR_KEYS.has(key)) continue;\n if (DROPPED_VAR_KEYS.has(key)) continue;\n\n if (key === \"keyframes\") {\n const kfNode = findPropertyNode(call.varsArg, \"keyframes\");\n keyframesData = parseKeyframesNode(kfNode, scope, source);\n if (!keyframesData && kfNode) hasUnresolvedKeyframes = true;\n continue;\n }\n\n if (key === \"motionPath\") {\n const mpNode = findPropertyNode(call.varsArg, \"motionPath\");\n motionPathResult = parseMotionPathNode(mpNode, scope, source);\n continue;\n }\n\n if (key === \"easeEach\") continue;\n\n if (EXTRAS_KEYS.has(key)) {\n const rawSource = extractRawPropertySource(call.varsArg, key, source);\n if (rawSource !== undefined) {\n extras[key] = `__raw:${rawSource}`;\n } else if (val !== undefined) {\n extras[key] = val;\n }\n continue;\n }\n\n if (typeof val === \"number\" || typeof val === \"string\") {\n properties[key] = val;\n }\n }\n\n if (keyframesData && typeof vars.easeEach === \"string\") {\n keyframesData.easeEach = vars.easeEach as string;\n }\n\n if (motionPathResult) {\n const { waypoints } = motionPathResult;\n if (!keyframesData) {\n const kf: GsapPercentageKeyframe[] = waypoints.map((wp, i) => ({\n percentage: waypoints.length > 1 ? Math.round((i / (waypoints.length - 1)) * 100) : 0,\n properties: { x: wp.x, y: wp.y },\n }));\n keyframesData = { format: \"percentage\", keyframes: kf };\n } else {\n const kfs = keyframesData.keyframes;\n if (kfs.length === waypoints.length) {\n for (let i = 0; i < kfs.length; i++) {\n const kf = kfs[i];\n const wp = waypoints[i];\n if (kf && wp) {\n kf.properties.x = wp.x;\n kf.properties.y = wp.y;\n }\n }\n }\n }\n }\n\n let fromProperties: Record<string, number | string> | undefined;\n if (call.method === \"fromTo\" && call.fromArg) {\n fromProperties = {};\n const fromVars = objectExpressionToRecord(call.fromArg, scope, source);\n for (const [key, val] of Object.entries(fromVars)) {\n if (typeof val === \"number\" || typeof val === \"string\") {\n fromProperties[key] = val;\n }\n }\n }\n\n const hasPositionArg = !!call.positionArg;\n const posVal = hasPositionArg ? extractLiteralValue(call.positionArg, scope) : 0;\n const position: number | string =\n typeof posVal === \"number\" ? posVal : typeof posVal === \"string\" ? posVal : 0;\n let duration = typeof vars.duration === \"number\" ? vars.duration : undefined;\n const ease = typeof vars.ease === \"string\" ? vars.ease : undefined;\n\n if (duration === undefined && keyframesData) {\n duration = computeKeyframesTotalDuration(call.varsArg, scope, source);\n }\n\n const anim: Omit<GsapAnimation, \"id\"> = {\n targetSelector: call.selector,\n method: call.method,\n position,\n properties,\n fromProperties,\n duration,\n ease,\n };\n if (!hasPositionArg) anim.implicitPosition = true;\n let group = classifyTweenPropertyGroup(properties);\n if (!group && keyframesData) {\n const kfProps: Record<string, unknown> = {};\n for (const kf of keyframesData.keyframes) {\n for (const k of Object.keys(kf.properties)) kfProps[k] = true;\n }\n group = classifyTweenPropertyGroup(kfProps);\n }\n if (group) anim.propertyGroup = group;\n if (call.global) anim.global = true;\n if (Object.keys(extras).length > 0) anim.extras = extras;\n if (keyframesData) anim.keyframes = keyframesData;\n if (motionPathResult) anim.arcPath = motionPathResult.arcPath;\n if (hasUnresolvedKeyframes) anim.hasUnresolvedKeyframes = true;\n if (call.selector === \"__unresolved__\") anim.hasUnresolvedSelector = true;\n const provenance = readProvenance(call.node);\n if (provenance) anim.provenance = provenance;\n return anim;\n}\n\n// ── Timeline position resolution ─────────────────────────────────────────────\n\nconst GSAP_DEFAULT_DURATION = 0.5;\n\n// fallow-ignore-next-line complexity\nfunction resolvePositionString(pos: string, cursor: number, prevStart: number): number | null {\n const trimmed = pos.trim();\n if (trimmed === \"\") return cursor;\n if (trimmed.startsWith(\"+=\")) {\n const n = Number.parseFloat(trimmed.slice(2));\n return Number.isFinite(n) ? cursor + n : null;\n }\n if (trimmed.startsWith(\"-=\")) {\n const n = Number.parseFloat(trimmed.slice(2));\n return Number.isFinite(n) ? cursor - n : null;\n }\n if (trimmed === \"<\") return prevStart;\n if (trimmed === \">\") return cursor;\n if (trimmed.startsWith(\"<\")) {\n const n = Number.parseFloat(trimmed.slice(1));\n return Number.isFinite(n) ? prevStart + n : null;\n }\n if (trimmed.startsWith(\">\")) {\n const n = Number.parseFloat(trimmed.slice(1));\n return Number.isFinite(n) ? cursor + n : null;\n }\n const n = Number.parseFloat(trimmed);\n return Number.isFinite(n) ? n : null;\n}\n\nfunction applyTimelineDefaults(\n anims: Omit<GsapAnimation, \"id\">[],\n defaults?: TimelineDefaults,\n): void {\n if (!defaults) return;\n for (const anim of anims) {\n if (anim.method === \"set\") continue;\n if (anim.duration === undefined && defaults.duration !== undefined) {\n anim.duration = defaults.duration;\n }\n if (anim.ease === undefined && defaults.ease !== undefined) {\n anim.ease = defaults.ease;\n }\n }\n}\n\n// fallow-ignore-next-line complexity\nfunction resolveTimelinePositions(anims: Omit<GsapAnimation, \"id\">[]): void {\n let cursor = 0;\n let prevStart = 0;\n for (const anim of anims) {\n // A global `gsap.set(...)` is off-timeline — applied once at load, not\n // sequenced on the master timeline. It carries no position arg, so the\n // cursor fallback would otherwise hand it the comp-end time. Pin it to 0\n // (its load-time start) and don't advance the cursor/prevStart.\n if (anim.method === \"set\" && anim.global) {\n anim.resolvedStart = 0;\n continue;\n }\n const duration = anim.method === \"set\" ? 0 : (anim.duration ?? GSAP_DEFAULT_DURATION);\n let start: number | null;\n\n if (anim.implicitPosition) {\n start = cursor;\n } else if (typeof anim.position === \"number\") {\n start = anim.position;\n } else if (typeof anim.position === \"string\") {\n start = resolvePositionString(anim.position, cursor, prevStart);\n } else {\n start = cursor;\n }\n\n if (start != null) {\n anim.resolvedStart = Math.max(0, start);\n prevStart = anim.resolvedStart;\n cursor = Math.max(cursor, anim.resolvedStart + duration);\n }\n }\n}\n\nfunction compareByLoc(a: TweenCallInfo, b: TweenCallInfo): number {\n const aLoc = a.node.callee?.property?.loc?.start;\n const bLoc = b.node.callee?.property?.loc?.start;\n if (!aLoc || !bLoc) return 0;\n return aLoc.line - bLoc.line || aLoc.column - bLoc.column;\n}\n\n// Inlined tweens carry a monotonic __hfOrder (clones share source loc, so loc\n// can't order them); they sort by that, after all literal (loc-ordered) tweens.\nfunction compareCallOrder(a: TweenCallInfo, b: TweenCallInfo): number {\n const ao = a.node.__hfOrder;\n const bo = b.node.__hfOrder;\n if (ao === undefined && bo === undefined) return compareByLoc(a, b);\n if (ao === undefined) return -1;\n if (bo === undefined) return 1;\n return ao - bo;\n}\n\nfunction sortBySourcePosition(calls: TweenCallInfo[]): void {\n calls.sort(compareCallOrder);\n}\n\n// ── Stable ID generation ──────────────────────────────────────────────────────\n\nfunction assignStableIds(anims: Omit<GsapAnimation, \"id\">[]): GsapAnimation[] {\n const counts = new Map<string, number>();\n return anims.map((anim) => {\n const posKey =\n typeof anim.position === \"number\"\n ? String(Math.round(anim.position * 1000))\n : String(anim.position);\n const groupSuffix = anim.propertyGroup ? `-${anim.propertyGroup}` : \"\";\n const base = `${anim.targetSelector}-${anim.method}-${posKey}${groupSuffix}`;\n const count = (counts.get(base) ?? 0) + 1;\n counts.set(base, count);\n const id = count === 1 ? base : `${base}-${count}`;\n return { ...anim, id };\n });\n}\n\n// ── Write-path internal parse ─────────────────────────────────────────────────\n\nexport interface ParsedGsapAcornForWrite {\n ast: any;\n timelineVar: string;\n hasTimeline: boolean;\n located: Array<{ id: string; call: TweenCallInfo; animation: GsapAnimation }>;\n}\n\n/**\n * Parse a GSAP script and return internal AST + call nodes for the write path.\n * Consumed by gsapWriterAcorn.ts (magic-string offset-splice).\n */\nexport function parseGsapScriptAcornForWrite(script: string): ParsedGsapAcornForWrite | null {\n try {\n const ast = acorn.parse(script, {\n ecmaVersion: \"latest\",\n sourceType: \"script\",\n locations: true,\n });\n const scope = collectScopeBindings(ast);\n const targetBindings = collectTargetBindings(ast, scope);\n const detection = findTimelineVar(ast, scope);\n const ref: TimelineRef = detection.ref ?? { kind: \"identifier\", name: \"tl\" };\n const timelineVar = timelineRootSource(ref, script);\n const calls = findAllTweenCalls(ast, ref, scope, targetBindings);\n sortBySourcePosition(calls);\n const rawAnims = calls.map((call) => tweenCallToAnimation(call, scope, script));\n applyTimelineDefaults(rawAnims, detection.defaults);\n resolveTimelinePositions(rawAnims);\n const animations = assignStableIds(rawAnims);\n const located = calls.map((call, i) => ({\n id: animations[i]!.id,\n call,\n animation: animations[i]!,\n }));\n return { ast, timelineVar, hasTimeline: detection.ref !== null, located };\n } catch {\n return null;\n }\n}\n\n// ── Public API ────────────────────────────────────────────────────────────────\n\n/**\n * Browser-safe equivalent of `parseGsapScript` (gsapParser.ts).\n * Uses acorn + acorn-walk instead of recast + @babel/parser.\n */\nexport function parseGsapScriptAcorn(script: string): ParsedGsap {\n try {\n const ast = acorn.parse(script, {\n ecmaVersion: \"latest\",\n sourceType: \"script\",\n locations: true,\n });\n const scope = collectScopeBindings(ast);\n const detection = findTimelineVar(ast, scope);\n const ref: TimelineRef = detection.ref ?? { kind: \"identifier\", name: \"tl\" };\n const timelineVar = timelineRootSource(ref, script);\n // Expand helper-built / bounded-loop timelines before analysis so their\n // tweens resolve at true positions (read path only — the write path keeps\n // original source nodes). Degrades to the un-inlined AST on any failure.\n // Only the identifier form uses the helper-built pattern; inline member\n // timelines have nothing to inline, so skip (avoids mis-rooting on the member).\n if (ref.kind === \"identifier\") {\n try {\n inlineComputedTimelines(ast, timelineVar, (node) => resolveNode(node, scope));\n } catch {\n /* fall back to current behavior */\n }\n }\n const targetBindings = collectTargetBindings(ast, scope);\n const calls = findAllTweenCalls(ast, ref, scope, targetBindings);\n sortBySourcePosition(calls);\n const rawAnims = calls.map((call) => tweenCallToAnimation(call, scope, script));\n applyTimelineDefaults(rawAnims, detection.defaults);\n resolveTimelinePositions(rawAnims);\n const animations = assignStableIds(rawAnims);\n\n // Preamble = source up to and including the timeline declaration/assignment.\n // Identifier keeps the original `const|let|var <name> = …` regex (byte-stable);\n // member matches `<member source> = …`.\n const declPattern =\n ref.kind === \"identifier\"\n ? `(?:const|let|var)\\\\s+${timelineVar}\\\\s*=\\\\s*gsap\\\\.timeline\\\\s*\\\\([^)]*\\\\)\\\\s*;?`\n : `${escapeRegExp(timelineVar)}\\\\s*=\\\\s*gsap\\\\.timeline\\\\s*\\\\([^)]*\\\\)\\\\s*;?`;\n const timelineMatch = script.match(new RegExp(`^[\\\\s\\\\S]*?${declPattern}`));\n const fallbackPreamble =\n ref.kind === \"identifier\"\n ? `const ${timelineVar} = gsap.timeline({ paused: true });`\n : `${timelineVar} = gsap.timeline({ paused: true });`;\n const preamble = timelineMatch?.[0] ?? fallbackPreamble;\n\n const lastCallIdx = script.lastIndexOf(`${timelineVar}.`);\n let postamble = \"\";\n if (lastCallIdx !== -1) {\n const afterLast = script.slice(lastCallIdx);\n const endOfCall = afterLast.indexOf(\";\");\n if (endOfCall !== -1) {\n postamble = script.slice(lastCallIdx + endOfCall + 1).trim();\n }\n }\n\n const result: ParsedGsap = { animations, timelineVar, preamble, postamble };\n if (detection.timelineCount > 1) result.multipleTimelines = true;\n if (detection.timelineCount > 0 && detection.ref === null)\n result.unsupportedTimelinePattern = true;\n return result;\n } catch {\n return { animations: [], timelineVar: \"tl\", preamble: \"\", postamble: \"\" };\n }\n}\n\n// ── Label extraction (WS-C) ──────────────────────────────────────────────────\n\nexport interface GsapLabelEntry {\n name: string;\n position: number;\n}\n\n/**\n * Extract all `tl.addLabel(\"name\", position)` calls from a GSAP script.\n *\n * Returns labels in source order. Position must be a numeric literal; labels\n * with non-numeric positions (e.g. label-relative offsets) are skipped.\n *\n * Pure — no side effects, no DOM, no Date.now.\n */\nexport function extractGsapLabels(script: string): GsapLabelEntry[] {\n try {\n const ast = acorn.parse(script, {\n ecmaVersion: \"latest\",\n sourceType: \"script\",\n locations: true,\n });\n const scope = collectScopeBindings(ast);\n const detection = findTimelineVar(ast, scope);\n const ref: TimelineRef = detection.ref ?? { kind: \"identifier\", name: \"tl\" };\n\n const labels: GsapLabelEntry[] = [];\n\n acornWalk.simple(ast, {\n // fallow-ignore-next-line complexity\n ExpressionStatement(node: any) {\n const expr = node.expression;\n if (!expr || expr.type !== \"CallExpression\") return;\n const callee = expr.callee;\n // Match <timeline>.addLabel(...) for identifier or member timeline refs.\n const objMatches =\n ref.kind === \"identifier\"\n ? callee.object?.type === \"Identifier\" && callee.object.name === ref.name\n : sameMemberAccess(callee.object, ref.node);\n if (\n callee?.type !== \"MemberExpression\" ||\n !objMatches ||\n callee.property?.name !== \"addLabel\"\n )\n return;\n const args = expr.arguments ?? [];\n const nameNode = args[0];\n const posNode = args[1];\n if (nameNode?.type !== \"Literal\" || typeof nameNode.value !== \"string\") return;\n if (!posNode) return;\n const pos = resolveNode(posNode, scope);\n if (typeof pos !== \"number\" || !Number.isFinite(pos)) return;\n labels.push({ name: nameNode.value, position: pos });\n },\n });\n\n return labels;\n } catch {\n // Labels are best-effort/supplementary, not load-bearing — a malformed or\n // unparseable script yields no labels rather than failing the caller.\n return [];\n }\n}\n","/**\n * Static evaluation for computed GSAP timelines (browser-safe, acorn/ESTree).\n *\n * The read parser resolves only literals and top-level consts, so timelines\n * built by a helper called N times or by a bounded loop collapse to position 0.\n * This module expands those constructs into a synthetic analysis AST: each\n * helper invocation and each loop iteration becomes its own concrete set of\n * `tl.*` calls, with parameters/loop-vars substituted by the call's argument\n * (or element/index) AST nodes — after which the existing parse pipeline\n * resolves positions and `motionPath` arcs unchanged.\n *\n * Substituted nodes keep their original source offsets, so downstream\n * source-slicing (raw extras, keyframes) stays correct. The substitution\n * primitives never mutate their input; `inlineComputedTimelines` rewrites the\n * Program body of the freshly-parsed AST it is handed (owned by the caller).\n */\nimport type { GsapProvenance } from \"./gsapSerialize.js\";\n\n// acorn ESTree nodes are structurally untyped; mirror gsapParserAcorn.ts.\ntype Node = any;\n\n/** Node keys that are metadata, not child AST to traverse/substitute. */\nconst SKIP_KEYS = new Set([\"type\", \"start\", \"end\", \"loc\", \"range\", \"__hfProvenance\", \"__hfOrder\"]);\n\nconst FUNCTION_TYPES = new Set([\n \"ArrowFunctionExpression\",\n \"FunctionExpression\",\n \"FunctionDeclaration\",\n]);\nconst GSAP_METHODS = new Set([\"set\", \"to\", \"from\", \"fromTo\"]);\n\n// Bounds on synthetic expansion (recursion + iteration runaway guards).\nconst MAX_DEPTH = 8;\nconst MAX_ITERS = 512;\n\nfunction isFunctionNode(node: Node): boolean {\n return !!node && FUNCTION_TYPES.has(node.type);\n}\n\nfunction isNode(x: Node): boolean {\n return !!x && typeof x === \"object\" && typeof x.type === \"string\";\n}\n\n/**\n * Apply `fn` to each child AST node, writing back its return value. Skips\n * metadata keys and key/member slots that must not be treated as values.\n * The one place array-vs-single child traversal lives, so walkers stay flat.\n */\nfunction transformChildren(node: Node, fn: (child: Node) => Node): void {\n for (const key of Object.keys(node)) {\n if (SKIP_KEYS.has(key) || isNonValueIdentifierSlot(node, key)) continue;\n const child = node[key];\n if (Array.isArray(child)) {\n for (let i = 0; i < child.length; i++) child[i] = fn(child[i]);\n } else {\n node[key] = fn(child);\n }\n }\n}\n\n/** Deep structural clone preserving `start`/`end`/`loc` (needed for source slicing). */\nexport function cloneNode<T extends Node>(node: T): T {\n return structuredClone(node);\n}\n\n// ponytail: Identifier + default + rest only. Destructured bindings (`{x}`, `[x]`)\n// aren't inlined (U2 inlines Identifier-param helpers / loop vars only), so a\n// destructuring shadow is a double-rare miss that just falls back. Add the\n// pattern cases here if that ever bites.\nfunction collectPatternNames(pattern: Node, out: Set<string>): void {\n if (pattern?.type === \"Identifier\") out.add(pattern.name);\n else if (pattern?.type === \"AssignmentPattern\") collectPatternNames(pattern.left, out);\n else if (pattern?.type === \"RestElement\") collectPatternNames(pattern.argument, out);\n}\n\n/** Every identifier name bound anywhere inside the subtree (fn params, declared vars, catch params). */\nfunction collectBoundNames(root: Node): Set<string> {\n const names = new Set<string>();\n const visit = (node: Node): Node => {\n if (!isNode(node)) return node;\n if (isFunctionNode(node)) for (const p of node.params ?? []) collectPatternNames(p, names);\n else if (node.type === \"VariableDeclarator\") collectPatternNames(node.id, names);\n else if (node.type === \"CatchClause\") collectPatternNames(node.param, names);\n transformChildren(node, visit);\n return node;\n };\n visit(root);\n return names;\n}\n\n/** A child in key/property position that must not be treated as a value identifier. */\nfunction isNonValueIdentifierSlot(node: Node, key: string): boolean {\n if (node.computed) return false;\n return (\n (node.type === \"MemberExpression\" && key === \"property\") ||\n (node.type === \"Property\" && key === \"key\")\n );\n}\n\n/**\n * Substitute bound identifiers in an already-cloned subtree, returning the\n * (possibly replaced) root. Names shadowed anywhere inside (nested function\n * params, declared vars) are dropped up front rather than tracked per scope —\n * worst case we under-substitute and the caller falls back to current behavior.\n * Never substitutes identifiers in key/member positions. Mutates the passed\n * clone in place — callers pass `cloneNode(...)`.\n */\nexport function substituteParams(node: Node, bindings: ReadonlyMap<string, Node>): Node {\n const shadowed = collectBoundNames(node);\n let effective = bindings;\n if (shadowed.size > 0) {\n effective = new Map(bindings);\n for (const name of shadowed) (effective as Map<string, Node>).delete(name);\n }\n if (effective.size === 0) return node;\n return replace(node, effective);\n}\n\nfunction replace(node: Node, bindings: ReadonlyMap<string, Node>): Node {\n if (!isNode(node)) return node;\n if (node.type === \"Identifier\" && bindings.has(node.name)) {\n return cloneNode(bindings.get(node.name));\n }\n transformChildren(node, (child) => replace(child, bindings));\n return node;\n}\n\n/** Tag a node (typically a `tl.*` CallExpression) with its construction provenance. */\nexport function tagProvenance(node: Node, provenance: GsapProvenance): Node {\n if (node && typeof node === \"object\") node.__hfProvenance = provenance;\n return node;\n}\n\n/** Read a provenance tag previously set by `tagProvenance`, if any. */\nexport function readProvenance(node: Node): GsapProvenance | undefined {\n return node?.__hfProvenance;\n}\n\n/** Synthesize a numeric `Literal` node (for loop indices, which have no source node). */\nexport function numericLiteral(value: number): Node {\n return { type: \"Literal\", value, raw: String(value) };\n}\n\n// ── Expansion engine (U2) ─────────────────────────────────────────────────────\n\n/** Resolve an expression to a literal value (top-level consts in scope, arithmetic). */\ntype LiteralResolver = (node: Node) => number | string | boolean | undefined;\n\ninterface ExpandCtx {\n helpers: Map<string, Node>;\n timelineVar: string;\n resolve: LiteralResolver;\n depth: number;\n /** Mutable source-order counter for provenance call-site ordinals. */\n site: { n: number };\n /** Mutable counter stamping expansion order onto tweens (clones share source loc). */\n order: { n: number };\n}\n\nfunction walkNodes(node: Node, fn: (n: Node) => void): void {\n if (!isNode(node)) return;\n fn(node);\n for (const key of Object.keys(node)) {\n if (SKIP_KEYS.has(key)) continue;\n const child = node[key];\n if (Array.isArray(child)) for (const c of child) walkNodes(c, fn);\n else walkNodes(child, fn);\n }\n}\n\n/** The identifier a (possibly chained) call's member expression is rooted at. */\nfunction timelineRootName(call: Node): string | null {\n let obj = call.callee?.object;\n while (obj?.type === \"CallExpression\") obj = obj.callee?.object;\n return obj?.type === \"Identifier\" ? obj.name : null;\n}\n\nfunction isTimelineRooted(call: Node, timelineVar: string): boolean {\n if (timelineRootName(call) !== timelineVar) return false;\n return (\n call.callee?.property?.type === \"Identifier\" && GSAP_METHODS.has(call.callee.property.name)\n );\n}\n\nfunction containsTimelineCall(node: Node, timelineVar: string): boolean {\n let found = false;\n walkNodes(node, (n) => {\n if (n.type === \"CallExpression\" && isTimelineRooted(n, timelineVar)) found = true;\n });\n return found;\n}\n\nfunction rangeOf(node: Node): [number, number] | undefined {\n return typeof node.start === \"number\" && typeof node.end === \"number\"\n ? [node.start, node.end]\n : undefined;\n}\n\n/** Plain identifier params + block body (shape we can inline). Timeline content checked separately. */\nfunction isShapeEligible(fn: Node): boolean {\n return (\n isFunctionNode(fn) &&\n fn.body?.type === \"BlockStatement\" &&\n !(fn.params ?? []).some((p: Node) => p.type !== \"Identifier\")\n );\n}\n\n/** True if the subtree calls any function named in `names`. */\nfunction callsAny(node: Node, names: Set<string>): boolean {\n let hit = false;\n walkNodes(node, (n) => {\n if (\n n.type === \"CallExpression\" &&\n n.callee?.type === \"Identifier\" &&\n names.has(n.callee.name)\n ) {\n hit = true;\n }\n });\n return hit;\n}\n\n/** `[name, fnNode]` if a single-declarator `const f = fn` is an inlinable-shaped helper. */\nfunction varDeclHelper(stmt: Node): [string, Node] | null {\n if (stmt.declarations?.length !== 1) return null;\n const d = stmt.declarations[0];\n return d.id?.type === \"Identifier\" && isShapeEligible(d.init) ? [d.id.name, d.init] : null;\n}\n\n/** `[name, fnNode]` if `stmt` declares an inlinable-shaped helper, else null. */\nfunction helperFromStatement(stmt: Node): [string, Node] | null {\n if (stmt.type === \"FunctionDeclaration\") {\n return stmt.id && isShapeEligible(stmt) ? [stmt.id.name, stmt] : null;\n }\n if (stmt.type === \"VariableDeclaration\") return varDeclHelper(stmt);\n return null;\n}\n\n/** Top-level functions whose shape we can inline (Identifier params + block body). */\nfunction gatherHelperCandidates(program: Node): Map<string, Node> {\n const candidates = new Map<string, Node>();\n for (const stmt of program.body ?? []) {\n const helper = helperFromStatement(stmt);\n if (helper) candidates.set(helper[0], helper[1]);\n }\n return candidates;\n}\n\n/** Names that build the timeline directly or by calling another builder (transitive closure). */\nfunction timelineBuildingNames(candidates: Map<string, Node>, timelineVar: string): Set<string> {\n const building = new Set<string>();\n for (const [name, fn] of candidates) {\n if (containsTimelineCall(fn.body, timelineVar)) building.add(name);\n }\n for (let changed = true; changed; ) {\n changed = false;\n for (const [name, fn] of candidates) {\n if (!building.has(name) && callsAny(fn.body, building)) {\n building.add(name);\n changed = true;\n }\n }\n }\n return building;\n}\n\nfunction bump(counts: Map<string, number>, key: string): void {\n counts.set(key, (counts.get(key) ?? 0) + 1);\n}\n\n/**\n * Keep only candidates safe to drop: every reference to the name is its\n * declaration or a statement-level call. (1 decl id + 1 callee id per\n * statement-level call ⇒ total occurrences with no stray uses.)\n */\nfunction safelyDroppable(program: Node, candidates: Map<string, Node>): Map<string, Node> {\n const names = new Set(candidates.keys());\n const totalIds = new Map<string, number>();\n const stmtCalls = new Map<string, number>();\n walkNodes(program, (n) => {\n if (n.type === \"Identifier\" && names.has(n.name)) bump(totalIds, n.name);\n const e = n.type === \"ExpressionStatement\" ? n.expression : undefined;\n if (\n e?.type === \"CallExpression\" &&\n e.callee?.type === \"Identifier\" &&\n names.has(e.callee.name)\n ) {\n bump(stmtCalls, e.callee.name);\n }\n });\n const safe = new Map<string, Node>();\n for (const [name, fn] of candidates) {\n if ((totalIds.get(name) ?? 0) === 1 + (stmtCalls.get(name) ?? 0)) safe.set(name, fn);\n }\n return safe;\n}\n\n/** Top-level timeline-building helpers that are safe to inline-and-drop. */\nfunction collectInlinableHelpers(program: Node, timelineVar: string): Map<string, Node> {\n const candidates = gatherHelperCandidates(program);\n if (candidates.size === 0) return candidates;\n const building = timelineBuildingNames(candidates, timelineVar);\n for (const name of [...candidates.keys()]) if (!building.has(name)) candidates.delete(name);\n if (candidates.size === 0) return candidates;\n return safelyDroppable(program, candidates);\n}\n\nfunction isHelperDecl(stmt: Node, helpers: Map<string, Node>): boolean {\n if (stmt.type === \"FunctionDeclaration\") return !!stmt.id && helpers.get(stmt.id.name) === stmt;\n if (stmt.type === \"VariableDeclaration\" && stmt.declarations?.length === 1) {\n const d = stmt.declarations[0];\n return d.id?.type === \"Identifier\" && helpers.get(d.id.name) === d.init;\n }\n return false;\n}\n\nfunction bodyStatements(node: Node): Node[] {\n if (node?.type === \"BlockStatement\") return node.body ?? [];\n return node ? [{ type: \"ExpressionStatement\", expression: node }] : [];\n}\n\n/** Tag this body's direct timeline tweens with provenance + a monotonic expansion-order stamp. */\nfunction tagTimelineCalls(stmts: Node[], prov: GsapProvenance, ctx: ExpandCtx): void {\n for (const stmt of stmts) {\n walkNodes(stmt, (n) => {\n if (n.type === \"CallExpression\" && isTimelineRooted(n, ctx.timelineVar)) {\n tagProvenance(n, { ...prov });\n n.__hfOrder = ctx.order.n++;\n }\n });\n }\n}\n\n/** Clone a body as one scope, substitute the bindings, tag provenance, recurse. */\nfunction expandBody(\n bodyStmts: Node[],\n bindings: Map<string, Node>,\n prov: GsapProvenance,\n ctx: ExpandCtx,\n): Node[] {\n const block = substituteParams(cloneNode({ type: \"BlockStatement\", body: bodyStmts }), bindings);\n tagTimelineCalls(block.body, prov, ctx);\n return expandStatements(block.body, { ...ctx, depth: ctx.depth + 1 });\n}\n\nfunction inlineHelper(call: Node, ctx: ExpandCtx): Node[] {\n const fn = ctx.helpers.get(call.callee.name);\n const bindings = new Map<string, Node>();\n (fn.params ?? []).forEach((p: Node, i: number) => {\n const arg = call.arguments?.[i];\n if (arg) bindings.set(p.name, arg);\n });\n const prov: GsapProvenance = {\n kind: \"helper\",\n fn: call.callee.name,\n callSite: ++ctx.site.n,\n sourceRange: rangeOf(call),\n };\n return expandBody(fn.body.body, bindings, prov, ctx);\n}\n\nfunction assignStep(update: Node, resolve: LiteralResolver): number | undefined {\n if (update.operator === \"+=\") return asNum(resolve(update.right));\n if (update.operator === \"-=\") {\n const s = asNum(resolve(update.right));\n return s === undefined ? undefined : -s;\n }\n // `i = i + S` — the step is the right operand of the addition.\n if (update.operator === \"=\" && update.right?.type === \"BinaryExpression\") {\n return asNum(resolve(update.right.right));\n }\n return undefined;\n}\n\n/** The loop variable a `for` update clause mutates (`i++` or `i += S`), or null. */\nfunction updatedVarName(update: Node): string | null {\n if (update?.type === \"UpdateExpression\") return update.argument?.name ?? null;\n if (update?.type === \"AssignmentExpression\") return update.left?.name ?? null;\n return null;\n}\n\nfunction loopStep(update: Node, varName: string, resolve: LiteralResolver): number | undefined {\n if (updatedVarName(update) !== varName) return undefined;\n if (update.type === \"UpdateExpression\") return update.operator === \"++\" ? 1 : -1;\n return assignStep(update, resolve);\n}\n\nfunction asNum(v: unknown): number | undefined {\n return typeof v === \"number\" && Number.isFinite(v) ? v : undefined;\n}\n\nfunction loopSatisfied(op: string, x: number, end: number): boolean {\n if (op === \"<\") return x < end;\n if (op === \"<=\") return x <= end;\n if (op === \">\") return x > end;\n if (op === \">=\") return x >= end;\n return false;\n}\n\ninterface ForHeader {\n v: string;\n start: number;\n end: number;\n op: string;\n step: number;\n}\n\n/** The single `let v = <init>` of a for-loop init clause, or null. */\nfunction forInitVar(init: Node): { name: string; initExpr: Node } | null {\n if (init?.type !== \"VariableDeclaration\" || init.declarations?.length !== 1) return null;\n const d = init.declarations[0];\n return d.id?.type === \"Identifier\" ? { name: d.id.name, initExpr: d.init } : null;\n}\n\n/** Parse `for (let v = A; v <op> B; v += S)` into resolved bounds, or null if not statically bounded. */\nfunction parseForHeader(stmt: Node, resolve: LiteralResolver): ForHeader | null {\n const iv = forInitVar(stmt.init);\n const test = stmt.test;\n if (!iv || test?.type !== \"BinaryExpression\" || test.left?.name !== iv.name) return null;\n const start = asNum(resolve(iv.initExpr));\n const end = asNum(resolve(test.right));\n const step = loopStep(stmt.update, iv.name, resolve);\n if (start === undefined || end === undefined || !step) return null;\n return { v: iv.name, start, end, op: test.operator, step };\n}\n\nfunction unrollFor(stmt: Node, ctx: ExpandCtx): Node[] | null {\n const h = parseForHeader(stmt, ctx.resolve);\n if (!h) return null;\n const body = bodyStatements(stmt.body);\n const out: Node[] = [];\n const site = ++ctx.site.n;\n let iteration = 0;\n for (let x = h.start; loopSatisfied(h.op, x, h.end); x += h.step) {\n if (iteration >= MAX_ITERS) return null;\n const prov: GsapProvenance = {\n kind: \"loop\",\n callSite: site,\n iteration,\n sourceRange: rangeOf(stmt),\n };\n out.push(...expandBody(body, new Map([[h.v, numericLiteral(x)]]), prov, ctx));\n iteration++;\n }\n return out;\n}\n\nfunction forOfVarName(left: Node): string | null {\n if (left?.type === \"VariableDeclaration\") {\n const id = left.declarations?.[0]?.id;\n return id?.type === \"Identifier\" ? id.name : null;\n }\n return left?.type === \"Identifier\" ? left.name : null;\n}\n\n/** Expand `for (const el of [literal array]) {...}` and `[literal array].forEach((el, i) => {...})`. */\nfunction unrollOverArray(\n elements: Node[],\n body: Node[],\n elName: string | null,\n idxName: string | null,\n range: [number, number] | undefined,\n ctx: ExpandCtx,\n): Node[] {\n const out: Node[] = [];\n const site = ++ctx.site.n;\n elements.forEach((el, i) => {\n if (!el) return;\n const bindings = new Map<string, Node>();\n if (elName) bindings.set(elName, el);\n if (idxName) bindings.set(idxName, numericLiteral(i));\n const prov: GsapProvenance = { kind: \"loop\", callSite: site, iteration: i, sourceRange: range };\n out.push(...expandBody(body, bindings, prov, ctx));\n });\n return out;\n}\n\nfunction unrollForOf(stmt: Node, ctx: ExpandCtx): Node[] | null {\n if (stmt.right?.type !== \"ArrayExpression\") return null;\n const elName = forOfVarName(stmt.left);\n if (!elName) return null;\n return unrollOverArray(\n stmt.right.elements ?? [],\n bodyStatements(stmt.body),\n elName,\n null,\n rangeOf(stmt),\n ctx,\n );\n}\n\n/** The (element, index) param names of a callback, or null if either is non-Identifier. */\nfunction callbackParamNames(cb: Node): { el: string | null; idx: string | null } | null {\n const names: Array<string | null> = [];\n for (const p of [cb.params?.[0], cb.params?.[1]]) {\n if (!p) names.push(null);\n else if (p.type !== \"Identifier\") return null;\n else names.push(p.name);\n }\n return { el: names[0]!, idx: names[1]! };\n}\n\n/** True for `[arrayLiteral].forEach` member callees. */\nfunction isForEachCall(callee: Node): boolean {\n return (\n callee?.type === \"MemberExpression\" &&\n callee.property?.name === \"forEach\" &&\n callee.object?.type === \"ArrayExpression\"\n );\n}\n\n/** The element array + callback of `[...].forEach(cb)`, or null. */\nfunction forEachTarget(call: Node): { elements: Node[]; cb: Node } | null {\n if (!isForEachCall(call.callee)) return null;\n const cb = call.arguments?.[0];\n return isFunctionNode(cb) ? { elements: call.callee.object.elements ?? [], cb } : null;\n}\n\nfunction unrollForEach(call: Node, ctx: ExpandCtx): Node[] | null {\n const target = forEachTarget(call);\n if (!target) return null;\n const params = callbackParamNames(target.cb);\n if (!params) return null;\n return unrollOverArray(\n target.elements,\n bodyStatements(target.cb.body),\n params.el,\n params.idx,\n rangeOf(call),\n ctx,\n );\n}\n\nfunction expandCall(call: Node, ctx: ExpandCtx): Node[] | null {\n if (call.callee?.type === \"Identifier\" && ctx.helpers.has(call.callee.name)) {\n return inlineHelper(call, ctx);\n }\n return unrollForEach(call, ctx);\n}\n\nfunction expandStatement(stmt: Node, ctx: ExpandCtx): Node[] | null {\n if (ctx.depth >= MAX_DEPTH) return null;\n if (stmt.type === \"ForStatement\") return unrollFor(stmt, ctx);\n if (stmt.type === \"ForOfStatement\") return unrollForOf(stmt, ctx);\n if (stmt.type === \"ExpressionStatement\" && stmt.expression?.type === \"CallExpression\") {\n return expandCall(stmt.expression, ctx);\n }\n return null;\n}\n\nfunction expandStatements(stmts: Node[], ctx: ExpandCtx): Node[] {\n const out: Node[] = [];\n for (const stmt of stmts) {\n const expanded = expandStatement(stmt, ctx);\n if (expanded) out.push(...expanded);\n else out.push(stmt);\n }\n return out;\n}\n\n/**\n * Rewrite the Program body so helper invocations and bounded loops that build\n * the timeline are expanded into concrete per-call / per-iteration `tl.*`\n * statements, each tagged with provenance. Mutates `ast` in place (caller owns\n * the freshly-parsed tree). Constructs it can't statically resolve are left\n * untouched, so the parser falls back to current behavior for them.\n */\nexport function inlineComputedTimelines(\n ast: Node,\n timelineVar: string,\n resolve: LiteralResolver,\n): void {\n const helpers = collectInlinableHelpers(ast, timelineVar);\n const ctx: ExpandCtx = {\n helpers,\n timelineVar,\n resolve,\n depth: 0,\n site: { n: 0 },\n order: { n: 0 },\n };\n const body = (ast.body ?? []).filter((stmt: Node) => !isHelperDecl(stmt, helpers));\n ast.body = expandStatements(body, ctx);\n}\n","/**\n * Stable hf- element id minting (R1). Node-safe (linkedom only, not browser DOM).\n *\n * Two surfaces share these helpers:\n * - ensureHfIds(html): node-id surface — mints data-hf-id on every element.\n * - mintHfId(el, assigned): shared by htmlParser for clip ids.\n *\n * Hash is CONTENT ONLY (tag + sorted attrs + own text) — no sibling position,\n * so inserting a non-identical sibling never shifts another element's id.\n */\nimport { parseHTML } from \"linkedom\";\n\n// Non-editable / non-visual elements that should never receive a stable id.\nexport const EXCLUDED_TAGS = new Set([\n \"script\",\n \"style\",\n \"template\",\n \"meta\",\n \"link\",\n \"noscript\",\n \"base\",\n]);\n\n// 32-bit FNV-1a. Pure, deterministic, no crypto, no Math.random.\nfunction fnv1a(str: string): number {\n let h = 0x811c9dc5;\n for (let i = 0; i < str.length; i++) {\n h ^= str.charCodeAt(i);\n h = Math.imul(h, 0x01000193);\n }\n return h >>> 0;\n}\n\n// 4 base-36 chars · 36^4 ≈ 1.68M ids per document. Birthday-paradox collision\n// ≈ N²/(2·36^4): well under 1% per document after dup rehash at realistic\n// clip-model sizes (≤ a few hundred elements). The dup-rehash in mintHfId\n// resolves the rare collision; width is deliberately small for readable ids.\nfunction toHfId(hash: number): string {\n const s = (hash >>> 0).toString(36);\n // Use suffix (most-avalanched bits) for better distribution within the 4-char window.\n const four = s.length >= 4 ? s.slice(-4) : s.padStart(4, \"0\");\n return `hf-${four}`;\n}\n\n// Element's own direct text (TEXT_NODE children), not descendants'.\nfunction ownText(el: Element): string {\n let text = \"\";\n el.childNodes.forEach((n) => {\n if (n.nodeType === 3) text += (n as Text).nodeValue ?? \"\";\n });\n return text.trim();\n}\n\nfunction contentKey(el: Element): string {\n // Exclude all data-hf-* attrs (ids, studio state) — they must not influence the hash.\n // Use \\x00 / \\x01 separators (invalid in HTML attrs) to prevent ambiguous serialization.\n const attrs = Array.from(el.attributes)\n .filter((a) => !a.name.startsWith(\"data-hf-\"))\n .map((a) => `${a.name}\\x00${a.value}`)\n .sort()\n .join(\"\\x01\");\n return `${el.tagName.toLowerCase()}|${attrs}|${ownText(el)}`;\n}\n\n/**\n * Collision tiebreak for byte-identical siblings: document-order dup counter\n * (`hash(key#N)`). This IS order-dependent — two identical `<span></span>`\n * get different ids based on which comes first in the DOM. This is unavoidable:\n * unique ids for byte-identical elements require a positional signal.\n *\n * Why this is safe in practice: once `ensureHfIds` write-back persists\n * `data-hf-id` to source the attribute is physically bound to its element.\n * Reordering identical siblings carries the attribute along → zero\n * order-dependence post-persist. `ensureHfIds` skips pinned elements\n * (`if (el.getAttribute(\"data-hf-id\")) continue`), so normal operation\n * never re-exposes the ordering after first persist.\n */\n// WIRE CONTRACT: id minting is content-keyed (FNV1a of innerHTML + tag). R7's\n// preview route relies on mintHfId producing identical ids across mint contexts\n// (disk-persist pass vs. in-memory bundle pass) — see preview.test.ts\n// \"bundle returning untagged HTML gets same ids as disk\". Any change that adds\n// positional, session, or random input to the hash breaks that invariant and\n// makes hf- ids diverge between disk and served HTML, silently corrupting\n// drag-to-edit targeting.\nexport function mintHfId(el: Element, assigned: Set<string>): string {\n const key = contentKey(el);\n let id = toHfId(fnv1a(key));\n let dup = 0;\n while (assigned.has(id)) {\n dup += 1;\n // Graceful fallback instead of a hard throw: rehashing only fails to find a\n // free 4-char slot in a pathological document (~1.6M identical elements).\n // Rather than crash the whole parse, widen the id with the dup counter —\n // still deterministic and unique, just longer than the 4-char norm.\n if (dup > 10000) {\n id = `hf-${(fnv1a(key) >>> 0).toString(36)}-${dup}`;\n break;\n }\n id = toHfId(fnv1a(`${key}#${dup}`));\n }\n assigned.add(id);\n return id;\n}\n\nexport function ensureHfIds(html: string): string {\n // Mirror parseSourceDocument's fragment-wrapping so bare fragments don't land\n // outside <body> in linkedom, which would cause body.querySelectorAll to return [].\n const hasDocumentShell = /<!doctype|<html[\\s>]/i.test(html);\n const wrapped = !hasDocumentShell;\n const { document } = wrapped\n ? parseHTML(`<!DOCTYPE html><html><head></head><body>${html}</body></html>`)\n : parseHTML(html);\n const body = document.body;\n if (!body) return html;\n\n const assigned = new Set<string>();\n // Seed with already-present ids (pin) so fresh mints never collide with them.\n // Scope to <body> to match the mint walk below — a stray data-hf-id in <head>\n // must not pin an id into the set that a body element would then be bumped off.\n for (const el of Array.from(body.querySelectorAll(\"[data-hf-id]\"))) {\n const existing = el.getAttribute(\"data-hf-id\");\n if (existing) assigned.add(existing);\n }\n\n for (const el of Array.from(body.querySelectorAll(\"*\"))) {\n if (EXCLUDED_TAGS.has(el.tagName.toLowerCase())) continue;\n if (el.getAttribute(\"data-hf-id\")) continue; // pinned\n el.setAttribute(\"data-hf-id\", mintHfId(el, assigned));\n }\n\n return wrapped ? document.body.innerHTML || \"\" : document.toString();\n}\n","// ponytail: queries DOM by exact attribute match without interpolating\n// the value into a selector string — zero injection surface.\nexport function queryByAttr(\n root: ParentNode,\n attr: string,\n value: string,\n tag?: string,\n): Element | null {\n const selector = tag ? `${tag}[${attr}]` : `[${attr}]`;\n for (const el of root.querySelectorAll(selector)) {\n if (el.getAttribute(attr) === value) return el;\n }\n return null;\n}\n","// fallow-ignore-file code-duplication\n/**\n * Browser-safe GSAP write path — magic-string offset-splice.\n *\n * T6c: edits GSAP scripts by overwriting/removing byte ranges in the original\n * source. Every byte outside the edited span is preserved verbatim — no\n * pretty-printer churn. Consumes ParsedGsapAcornForWrite from gsapParserAcorn.ts.\n */\nimport MagicString from \"magic-string\";\nimport type {\n GsapAnimation,\n GsapPercentageKeyframe,\n ArcPathConfig,\n ArcPathSegment,\n} from \"./gsapSerialize.js\";\nimport {\n resolveConversionProps,\n extractArcWaypoints,\n buildMotionPathObjectCode,\n} from \"./gsapSerialize.js\";\nimport {\n parseGsapScriptAcornForWrite,\n type ParsedGsapAcornForWrite,\n type TweenCallInfo,\n} from \"./gsapParserAcorn.js\";\nimport { classifyPropertyGroup } from \"./gsapConstants.js\";\nimport type { PropertyGroupName } from \"./gsapConstants.js\";\nimport type { SplitAnimationsOptions, SplitAnimationsResult } from \"./gsapSerialize.js\";\nimport * as acornWalk from \"acorn-walk\";\n\n// acorn ESTree nodes are structurally untyped here; mirror gsapParserAcorn.ts /\n// gsapInline.ts rather than re-deriving the full ESTree union for every access.\ntype Node = any;\n\n// ── Code generation helpers ──────────────────────────────────────────────────\n\n// Local serializer for the tween-statement path, which may carry boolean/object\n// extras (stagger config). serializeValue stringifies objects to \"[object\n// Object]\", so keep this richer JSON fallback for that path. Keyframe values are\n// always number|string and use the shared serializeValue (recast parity).\nfunction valueToCode(value: unknown): string {\n if (typeof value === \"string\" && value.startsWith(\"__raw:\")) return value.slice(6);\n if (typeof value === \"string\") return JSON.stringify(value);\n if (typeof value === \"number\") return Number.isNaN(value) ? \"0\" : String(value);\n if (typeof value === \"boolean\") return String(value);\n return JSON.stringify(value);\n}\n\nfunction safeKey(key: string): string {\n return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key) ? key : JSON.stringify(key);\n}\n\n// fallow-ignore-next-line complexity\nfunction buildTweenStatementCode(timelineVar: string, anim: Omit<GsapAnimation, \"id\">): string {\n const selector = JSON.stringify(anim.targetSelector);\n const props: Record<string, number | string> = { ...anim.properties };\n if (anim.method !== \"set\" && anim.duration !== undefined) props.duration = anim.duration;\n if (anim.ease) props.ease = anim.ease;\n const entries = Object.entries(props).map(([k, v]) => `${safeKey(k)}: ${valueToCode(v)}`);\n if (anim.extras) {\n for (const [k, v] of Object.entries(anim.extras)) {\n entries.push(`${safeKey(k)}: ${valueToCode(v)}`);\n }\n }\n const objCode = `{ ${entries.join(\", \")} }`;\n const posCode = valueToCode(\n typeof anim.position === \"number\" ? anim.position : (anim.position ?? 0),\n );\n if (anim.method === \"fromTo\") {\n const fromEntries = Object.entries(anim.fromProperties ?? {}).map(\n ([k, v]) => `${safeKey(k)}: ${valueToCode(v)}`,\n );\n return `${timelineVar}.fromTo(${selector}, { ${fromEntries.join(\", \")} }, ${objCode}, ${posCode});`;\n }\n // A base `gsap.set` is off the timeline: no timeline var, no position arg.\n if (anim.method === \"set\" && anim.global) {\n return `gsap.set(${selector}, ${objCode});`;\n }\n return `${timelineVar}.${anim.method}(${selector}, ${objCode}, ${posCode});`;\n}\n\n// ── AST node helpers ─────────────────────────────────────────────────────────\n\nfunction isObjectProperty(prop: Node): boolean {\n return prop?.type === \"ObjectProperty\" || prop?.type === \"Property\";\n}\n\nfunction propKeyName(prop: Node): string | undefined {\n return prop?.key?.name ?? prop?.key?.value;\n}\n\nfunction findPropertyNode(varsArgNode: Node, key: string): Node | undefined {\n if (varsArgNode?.type !== \"ObjectExpression\") return undefined;\n for (const prop of varsArgNode.properties ?? []) {\n if (!isObjectProperty(prop)) continue;\n if (propKeyName(prop) === key) return prop;\n }\n return undefined;\n}\n\n/** The `keyframes` property's ObjectExpression value, or null when not a keyframe tween. */\nfunction keyframesObjectNode(varsNode: Node): Node | null {\n const kfProp = findPropertyNode(varsNode, \"keyframes\");\n return kfProp?.value?.type === \"ObjectExpression\" ? kfProp.value : null;\n}\n\nfunction findEnclosingExpressionStatement(ancestors: Node[]): Node | null {\n for (let i = ancestors.length - 2; i >= 0; i--) {\n if (ancestors[i]?.type === \"ExpressionStatement\") return ancestors[i];\n }\n return null;\n}\n\n/** Find the VariableDeclaration statement for `tl = gsap.timeline(...)`. */\nfunction findTimelineDeclarationStatement(ast: Node, timelineVar: string): Node | null {\n let found: Node = null;\n acornWalk.simple(ast, {\n // fallow-ignore-next-line complexity\n VariableDeclaration(node: Node) {\n if (found) return;\n for (const decl of node.declarations ?? []) {\n if (\n decl.id?.name === timelineVar &&\n decl.init?.type === \"CallExpression\" &&\n decl.init.callee?.type === \"MemberExpression\" &&\n decl.init.callee.object?.name === \"gsap\" &&\n decl.init.callee.property?.name === \"timeline\"\n ) {\n found = node;\n }\n }\n },\n });\n return found;\n}\n\n// ── Property splice helpers ───────────────────────────────────────────────────\n\n/**\n * Remove a property from a properties array, handling its comma.\n * `editableProps` must be the isObjectProperty-filtered subset in source order.\n */\nfunction removeProp(ms: MagicString, propNode: Node, editableProps: Node[]): void {\n const idx = editableProps.indexOf(propNode);\n if (idx === -1) return;\n if (editableProps.length === 1) {\n ms.remove(propNode.start, propNode.end);\n } else if (idx === 0) {\n // First prop: remove from its start to next prop start (drops trailing \", \")\n ms.remove(editableProps[0].start, editableProps[1].start);\n } else {\n // Non-first: remove from prev prop end to this prop end (drops leading \", \")\n ms.remove(editableProps[idx - 1].end, propNode.end);\n }\n}\n\n/** Serialize a vars record to an object-literal source: `{ k: v, ... }`. */\nfunction buildVarsObjectCode(record: Record<string, number | string>): string {\n const entries = Object.entries(record).map(([k, v]) => `${safeKey(k)}: ${valueToCode(v)}`);\n return entries.length > 0 ? `{ ${entries.join(\", \")} }` : \"{}\";\n}\n\n/** Overwrite a tween call's vars ObjectExpression with freshly-built source. */\nfunction overwriteVarsArg(ms: MagicString, call: TweenCallInfo, objCode: string): void {\n if (!call.varsArg) return;\n ms.overwrite(call.varsArg.start, call.varsArg.end, objCode);\n}\n\n/**\n * Update a property value if it exists, or append a new key: val before the\n * closing `}`. Call with the full ObjectExpression node.\n */\nfunction upsertProp(ms: MagicString, objNode: Node, key: string, value: unknown): void {\n if (objNode?.type !== \"ObjectExpression\") return;\n const existing = findPropertyNode(objNode, key);\n if (existing) {\n ms.overwrite(existing.value.start, existing.value.end, valueToCode(value));\n } else {\n const sep = objNode.properties.length > 0 ? \", \" : \"\";\n ms.appendLeft(objNode.end - 1, `${sep}${safeKey(key)}: ${valueToCode(value)}`);\n }\n}\n\n/**\n * Vars keys that are NOT editable transform/style props: builtins\n * (duration/ease/delay), dropped callbacks, and extras (stagger/yoyo/repeat/…).\n * The exact union of recast's BUILTIN_VAR_KEYS + DROPPED_VAR_KEYS + EXTRAS_KEYS,\n * so both writers classify vars keys identically. (Distinct from the keyframe-\n * conversion NON_EDITABLE_VAR_KEYS below, which intentionally omits `ease`\n * because that path re-emits ease separately.)\n */\nconst NON_EDITABLE_PROP_KEYS = new Set([\n \"duration\",\n \"ease\",\n \"delay\",\n \"onComplete\",\n \"onStart\",\n \"onUpdate\",\n \"onRepeat\",\n \"stagger\",\n \"yoyo\",\n \"repeat\",\n \"repeatDelay\",\n \"snap\",\n \"overwrite\",\n \"immediateRender\",\n]);\n\n/**\n * Editable transform/style key test: anything NOT a builtin, dropped callback, or\n * extras key. Mirrors recast's isEditablePropertyKey so both writers classify\n * vars keys identically.\n */\nfunction isEditableVarKey(key: string): boolean {\n return !NON_EDITABLE_PROP_KEYS.has(key);\n}\n\n/**\n * Collect verbatim `key: value` entries to PRESERVE from a vars/keyframe\n * ObjectExpression: every property whose key `drop` does not reject, sliced from\n * source — except keys present in `overrides`, whose value is replaced. Returns\n * the entries plus the set of keys it kept, so callers can append new keys.\n */\nfunction preservedEntries(\n objNode: Node,\n source: string,\n drop: (key: string) => boolean,\n overrides: Record<string, unknown>,\n): { entries: string[]; keys: Set<string> } {\n const entries: string[] = [];\n const keys = new Set<string>();\n for (const prop of objNode.properties ?? []) {\n if (!isObjectProperty(prop)) continue;\n const key = propKeyName(prop);\n if (typeof key !== \"string\" || drop(key)) continue;\n keys.add(key);\n const code =\n key in overrides\n ? valueToCode(overrides[key])\n : source.slice(prop.value.start, prop.value.end);\n entries.push(`${safeKey(key)}: ${code}`);\n }\n return { entries, keys };\n}\n\n/**\n * Replace the editable-property keys on a vars ObjectExpression with exactly\n * `newProps`, leaving non-editable keys (duration/ease/stagger/callbacks/…)\n * untouched unless overridden in `nonEditableOverrides`. Mirrors recast's\n * reconcileEditableProperties: editable keys absent from `newProps` are DROPPED,\n * not merged. Rebuilt in a single ms.overwrite so the splice can never overlap a\n * sibling edit — non-editable updates that also target this node (duration/ease/\n * extras) are folded into the same rebuild rather than spliced separately.\n */\nfunction reconcileEditableProps(\n ms: MagicString,\n objNode: Node,\n source: string,\n newProps: Record<string, number | string>,\n nonEditableOverrides?: Record<string, unknown>,\n): void {\n if (objNode?.type !== \"ObjectExpression\") return;\n const overrides = nonEditableOverrides ?? {};\n const { entries, keys } = preservedEntries(objNode, source, isEditableVarKey, overrides);\n for (const [key, value] of Object.entries(overrides)) {\n if (!keys.has(key)) entries.push(`${safeKey(key)}: ${valueToCode(value)}`);\n }\n for (const [key, value] of Object.entries(newProps)) {\n entries.push(`${safeKey(key)}: ${valueToCode(value)}`);\n }\n ms.overwrite(objNode.start, objNode.end, `{ ${entries.join(\", \")} }`);\n}\n\n// ── Insertion helpers ─────────────────────────────────────────────────────────\n\n/** Traverse callee.object chain to check if a call ultimately roots at timelineVar. */\nfunction isTimelineRooted(node: Node, timelineVar: string, script: string): boolean {\n if (node?.type === \"Identifier\") return node.name === timelineVar;\n // Inline/member timelines: `timelineVar` is the source slice (e.g.\n // `window.__timelines[\"scene\"]`); match a MemberExpression callee by its source.\n if (node?.type === \"MemberExpression\") return script.slice(node.start, node.end) === timelineVar;\n if (node?.type === \"CallExpression\")\n return isTimelineRooted(node.callee?.object, timelineVar, script);\n return false;\n}\n\n/**\n * Find the byte offset after which to insert a new statement (tween or label).\n * Returns null when no timeline declaration exists in the script — callers must\n * not emit `tl.xxx()` calls in that case as `tl` would be undefined at render.\n */\nfunction findInsertionPoint(parsed: ParsedGsapAcornForWrite): number | null {\n const lastLocated = parsed.located[parsed.located.length - 1];\n if (lastLocated) {\n const lastCall = lastLocated.call;\n const exprStmt = findEnclosingExpressionStatement(lastCall.ancestors);\n return exprStmt?.end ?? lastCall.node.end;\n }\n if (!parsed.hasTimeline) return null;\n const tlDecl = findTimelineDeclarationStatement(parsed.ast, parsed.timelineVar);\n return tlDecl?.end ?? (parsed.ast.end as number);\n}\n\n// ── Public write API ─────────────────────────────────────────────────────────\n\n// fallow-ignore-next-line complexity\nexport function updateAnimationInScript(\n script: string,\n animationId: string,\n updates: Partial<GsapAnimation> & { easeEach?: string; resetKeyframeEases?: boolean },\n): string {\n if (!Object.keys(updates).length) return script;\n const parsed = parseGsapScriptAcornForWrite(script);\n if (!parsed) return script;\n const target = parsed.located.find((l) => l.id === animationId);\n if (!target) return script;\n\n const ms = new MagicString(script);\n const { call }: { call: TweenCallInfo } = target;\n\n // When `properties` is present we REPLACE the editable set (recast parity:\n // editable keys absent from the update are dropped). Fold any concurrent\n // non-editable updates (duration/ease/extras) into the single varsArg rebuild\n // so their splices can't overlap the rebuild's overwrite of the whole node.\n if (updates.properties) {\n const overrides: Record<string, unknown> = {};\n if (updates.duration !== undefined) overrides.duration = updates.duration;\n if (updates.ease !== undefined) overrides.ease = updates.ease;\n if (updates.extras) Object.assign(overrides, updates.extras);\n reconcileEditableProps(ms, call.varsArg, script, updates.properties, overrides);\n } else {\n if (updates.duration !== undefined) {\n upsertProp(ms, call.varsArg, \"duration\", updates.duration);\n }\n const easeValue = updates.easeEach ?? updates.ease;\n if (easeValue !== undefined) {\n const kfNode = keyframesObjectNode(call.varsArg);\n if (kfNode) {\n upsertProp(ms, kfNode, \"easeEach\", easeValue);\n // \"Apply to all segments\": drop every per-keyframe `ease` override so the\n // single easeEach governs all segments uniformly (AE select-all + F9).\n if (updates.resetKeyframeEases) {\n for (const kfEntry of kfNode.properties ?? []) {\n if (!isObjectProperty(kfEntry)) continue;\n const val = kfEntry.value;\n if (val?.type !== \"ObjectExpression\") continue;\n const easeNode = findPropertyNode(val, \"ease\");\n if (easeNode) removeProp(ms, easeNode, val.properties);\n }\n }\n } else {\n upsertProp(ms, call.varsArg, \"ease\", easeValue);\n }\n }\n if (updates.extras) {\n for (const [key, value] of Object.entries(updates.extras)) {\n upsertProp(ms, call.varsArg, key, value);\n }\n }\n }\n\n if (updates.fromProperties && call.method === \"fromTo\" && call.fromArg) {\n // fromTo's from-vars carry only editable props — REPLACE them too (recast\n // parity). fromArg is a distinct node from varsArg, so this rebuild never\n // overlaps the varsArg edits above.\n reconcileEditableProps(ms, call.fromArg, script, updates.fromProperties);\n }\n\n if (updates.position !== undefined) {\n overwritePosition(ms, call, updates.position);\n }\n\n return ms.toString();\n}\n\n/**\n * Overwrite a tween call's numeric position argument (the positionArg the parser\n * located: 3rd arg for fromTo, else 2nd), or append one when the call has no\n * explicit position. Shared by updateAnimationInScript and the\n * shift/scalePositionsInScript timeline ops.\n */\nfunction overwritePosition(ms: MagicString, call: TweenCallInfo, position: number | string): void {\n if (call.positionArg) {\n ms.overwrite(call.positionArg.start, call.positionArg.end, valueToCode(position));\n } else {\n ms.appendLeft(call.node.end - 1, `, ${valueToCode(position)}`);\n }\n}\n\n/**\n * Shift every tween targeting `targetSelector` by `delta` seconds (clamped ≥0),\n * rewriting each call's position argument. Mirrors recast's shiftPositionsInScript\n * (used by timeline clip-move to keep GSAP positions in sync with the clip start).\n */\nexport function shiftPositionsInScript(\n script: string,\n targetSelector: string,\n delta: number,\n): string {\n const parsed = parseGsapScriptAcornForWrite(script);\n if (!parsed) return script;\n const ms = new MagicString(script);\n let changed = false;\n for (const entry of parsed.located) {\n if (entry.animation.targetSelector !== targetSelector) continue;\n if (typeof entry.animation.position !== \"number\") continue;\n const newPos = Math.max(0, Math.round((entry.animation.position + delta) * 1000) / 1000);\n overwritePosition(ms, entry.call, newPos);\n changed = true;\n }\n return changed ? ms.toString() : script;\n}\n\n/**\n * Linearly remap every tween targeting `targetSelector` from the old clip\n * [oldStart, oldDuration] onto the new [newStart, newDuration] (position and,\n * when present, duration scaled by the duration ratio). Mirrors recast's\n * scalePositionsInScript (used by timeline clip-resize).\n */\nexport function scalePositionsInScript(\n script: string,\n targetSelector: string,\n oldStart: number,\n oldDuration: number,\n newStart: number,\n newDuration: number,\n): string {\n if (oldDuration <= 0 || newDuration <= 0) return script;\n const ratio = newDuration / oldDuration;\n const parsed = parseGsapScriptAcornForWrite(script);\n if (!parsed) return script;\n const ms = new MagicString(script);\n let changed = false;\n for (const entry of parsed.located) {\n if (entry.animation.targetSelector !== targetSelector) continue;\n if (typeof entry.animation.position !== \"number\") continue;\n const newPos = Math.max(\n 0,\n Math.round((newStart + (entry.animation.position - oldStart) * ratio) * 1000) / 1000,\n );\n overwritePosition(ms, entry.call, newPos);\n if (typeof entry.animation.duration === \"number\" && entry.animation.duration > 0) {\n const newDur = Math.max(0.001, Math.round(entry.animation.duration * ratio * 1000) / 1000);\n upsertProp(ms, entry.call.varsArg, \"duration\", newDur);\n }\n changed = true;\n }\n return changed ? ms.toString() : script;\n}\n\nexport function addAnimationToScript(\n script: string,\n animation: Omit<GsapAnimation, \"id\">,\n): { script: string; id: string } {\n const parsed = parseGsapScriptAcornForWrite(script);\n if (!parsed) return { script, id: \"\" };\n\n const insertionPoint = findInsertionPoint(parsed);\n if (insertionPoint === null) return { script, id: \"\" };\n\n const ms = new MagicString(script);\n const statementCode = buildTweenStatementCode(parsed.timelineVar, animation);\n ms.appendLeft(insertionPoint, \"\\n\" + statementCode);\n\n const result = ms.toString();\n const reParsed = parseGsapScriptAcornForWrite(result);\n const newId = reParsed?.located[reParsed.located.length - 1]?.id ?? \"\";\n return { script: result, id: newId };\n}\n\nexport function removeAnimationFromScript(script: string, animationId: string): string {\n const parsed = parseGsapScriptAcornForWrite(script);\n if (!parsed) return script;\n const target = parsed.located.find((l) => l.id === animationId);\n if (!target) return script;\n\n const ms = new MagicString(script);\n const N = target.call.node;\n const exprStmt = findEnclosingExpressionStatement(target.call.ancestors);\n\n if (N.callee?.object?.type !== \"CallExpression\" && exprStmt?.expression === N) {\n // Standalone `tl.method(...)` — remove the whole ExpressionStatement\n const end =\n exprStmt.end < script.length && script[exprStmt.end] === \"\\n\"\n ? exprStmt.end + 1\n : exprStmt.end;\n ms.remove(exprStmt.start, end);\n } else {\n // Chain link — splice out `.method(args)` from N.callee.object.end to N.end\n ms.remove(N.callee.object.end, N.end);\n }\n\n return ms.toString();\n}\n\n// ── Flat-tween → keyframes conversion ──────────────────────────────────────────\n//\n// Mirror recast's convertToKeyframesInScript: when the first keyframe op lands\n// on a flat to()/from()/fromTo() tween, rewrite its vars object to\n// `{ keyframes: { \"0%\": {from}, \"100%\": {to} }, <preserved non-editable keys>,\n// ease: \"none\"? }` and convert from()/fromTo() to to(). We rebuild the whole\n// vars ObjectExpression in one ms.overwrite (single-edit-per-node), so the next\n// keyframe-add re-parses cleanly.\n\n// Identity value for an editable transform/style prop (recast's CSS_IDENTITY).\nconst CSS_IDENTITY: Record<string, number> = {\n opacity: 1,\n autoAlpha: 1,\n scale: 1,\n scaleX: 1,\n scaleY: 1,\n};\n\nfunction cssIdentityValue(prop: string): number {\n return CSS_IDENTITY[prop] ?? 0;\n}\n\n// Keys NOT in the editable set — preserved verbatim on the converted vars object\n// (matches the parser's classification: builtin/dropped/extras keys).\nconst NON_EDITABLE_VAR_KEYS = new Set([\n \"duration\",\n \"delay\",\n \"onComplete\",\n \"onStart\",\n \"onUpdate\",\n \"onRepeat\",\n \"stagger\",\n \"yoyo\",\n \"repeat\",\n \"repeatDelay\",\n \"snap\",\n \"overwrite\",\n \"immediateRender\",\n]);\n\n/** The CSS-identity counterpart of a props record (numbers → identity value). */\nfunction identityProps(\n properties: Record<string, number | string>,\n): Record<string, number | string> {\n const identity: Record<string, number | string> = {};\n for (const [k, v] of Object.entries(properties)) {\n if (v != null) identity[k] = typeof v === \"number\" ? cssIdentityValue(k) : v;\n }\n return identity;\n}\n\n/** Resolve the 0%/100% endpoint records for a tween being converted. */\nfunction conversionEndpoints(animation: GsapAnimation): {\n fromProps: Record<string, number | string>;\n toProps: Record<string, number | string>;\n} {\n if (animation.method === \"from\") {\n return { fromProps: { ...animation.properties }, toProps: identityProps(animation.properties) };\n }\n if (animation.method === \"fromTo\") {\n return {\n fromProps: { ...(animation.fromProperties ?? {}) },\n toProps: { ...animation.properties },\n };\n }\n // to(): 0% is the CSS identity state, 100% is the authored props.\n return { fromProps: identityProps(animation.properties), toProps: { ...animation.properties } };\n}\n\n/** Collect preserved (non-editable) `key: value` entries from the original vars node. */\nfunction preservedVarsEntries(varsNode: Node, source: string): string[] {\n const entries: string[] = [];\n if (varsNode?.type !== \"ObjectExpression\") return entries;\n for (const prop of varsNode.properties ?? []) {\n if (!isObjectProperty(prop)) continue;\n const key = propKeyName(prop);\n if (typeof key !== \"string\" || !NON_EDITABLE_VAR_KEYS.has(key)) continue;\n entries.push(`${safeKey(key)}: ${source.slice(prop.value.start, prop.value.end)}`);\n }\n return entries;\n}\n\n/** Build the rebuilt vars-object code for a converted flat tween. */\nfunction buildConvertedVarsCode(animation: GsapAnimation, varsNode: Node, source: string): string {\n const { fromProps, toProps } = conversionEndpoints(animation);\n const easeEach = animation.ease;\n const easeEachEntry = easeEach ? `, easeEach: ${JSON.stringify(easeEach)}` : \"\";\n const kfCode = `{ \"0%\": ${recordToCode(fromProps)}, \"100%\": ${recordToCode(toProps)}${easeEachEntry} }`;\n const entries = [`keyframes: ${kfCode}`, ...preservedVarsEntries(varsNode, source)];\n if (easeEach) entries.push(`ease: \"none\"`);\n return `{ ${entries.join(\", \")} }`;\n}\n\n/** Rename a from()/fromTo() call to to(), dropping fromTo's leading from-vars arg. */\nfunction convertMethodToTo(\n ms: MagicString,\n animation: GsapAnimation,\n call: Node,\n varsNode: Node,\n): void {\n if (animation.method !== \"from\" && animation.method !== \"fromTo\") return;\n const calleeProp = call.node.callee?.property;\n if (calleeProp) ms.overwrite(calleeProp.start, calleeProp.end, \"to\");\n // Remove the from-vars arg and its trailing separator up to the to-vars arg.\n if (animation.method === \"fromTo\" && call.fromArg) ms.remove(call.fromArg.start, varsNode.start);\n}\n\nfunction convertFlatTweenToKeyframes(script: string, target: Node): string {\n const animation: GsapAnimation = target.animation;\n if (animation.keyframes || animation.method === \"set\") return script;\n const call = target.call;\n const varsNode = call.varsArg;\n if (varsNode?.type !== \"ObjectExpression\") return script;\n\n const ms = new MagicString(script);\n ms.overwrite(varsNode.start, varsNode.end, buildConvertedVarsCode(animation, varsNode, script));\n convertMethodToTo(ms, animation, call, varsNode);\n return ms.toString();\n}\n\n// ── Keyframe write ops ────────────────────────────────────────────────────────\n//\n// Design: mirror the recast writer's rebuild-the-node model. The recast writer\n// mutates AST nodes in place and re-prints, so it never has an offset-overlap\n// problem. Here we instead compute the FINAL property record for every keyframe\n// value node that must change (the target merge, `_auto` endpoint sync, and\n// backfilled siblings) against the ORIGINAL parsed AST, then emit exactly ONE\n// `ms.overwrite(valueNode.start, valueNode.end, code)` per changed node (and a\n// single insert for a brand-new key). No node is ever both overwritten and\n// appended into, so the splices can never overlap.\n\nconst PERCENTAGE_KEY_RE = /^(\\d+(?:\\.\\d+)?)%$/;\n\n// Matches recast's PCT_TOLERANCE: percentages within 2 of an existing key are\n// treated as the same keyframe (merge), not a new insert.\nconst PCT_TOLERANCE = 2;\n\nfunction percentageFromKey(key: string): number {\n const m = PERCENTAGE_KEY_RE.exec(key);\n return m ? Number.parseFloat(m[1] ?? \"0\") : Number.NaN;\n}\n\n/** Serialize a final keyframe property record (number|string values) to code. */\nfunction recordToCode(record: Record<string, number | string>): string {\n const entries = Object.entries(record).map(([k, v]) => `${safeKey(k)}: ${valueToCode(v)}`);\n return `{ ${entries.join(\", \")} }`;\n}\n\n/** Percentage-keyed property nodes of a keyframes ObjectExpression, in source order. */\nfunction percentagePropsOf(kfNode: Node): Node[] {\n return (kfNode.properties ?? []).filter((p: Node) => {\n if (!isObjectProperty(p)) return false;\n const key = propKeyName(p);\n return typeof key === \"string\" && PERCENTAGE_KEY_RE.test(key);\n });\n}\n\nconst LITERAL_NODE_TYPES = new Set([\"Literal\", \"NumericLiteral\", \"StringLiteral\"]);\n\n/** Read one value node: a number/string literal, a negative number, or raw source. */\n// fallow-ignore-next-line complexity\nfunction readValueNode(v: Node, source: string): number | string {\n if (\n LITERAL_NODE_TYPES.has(v?.type) &&\n (typeof v.value === \"number\" || typeof v.value === \"string\")\n ) {\n return v.value;\n }\n if (\n v?.type === \"UnaryExpression\" &&\n v.operator === \"-\" &&\n typeof v.argument?.value === \"number\"\n ) {\n return -v.argument.value;\n }\n return `__raw:${source.slice(v.start, v.end)}`;\n}\n\n/**\n * Read a keyframe value ObjectExpression into a record, mirroring the parser's\n * `objectExpressionToRecord`: literals resolve to their value; anything else is\n * preserved as `__raw:<source>` so serializeValue round-trips it verbatim.\n * Keyframe values are literals in practice, so the raw fallback is rarely hit.\n */\nfunction valueNodeToRecord(valueNode: Node, source: string): Record<string, number | string> {\n const record: Record<string, number | string> = {};\n if (valueNode?.type !== \"ObjectExpression\") return record;\n for (const prop of valueNode.properties ?? []) {\n if (!isObjectProperty(prop)) continue;\n const key = propKeyName(prop);\n if (typeof key !== \"string\") continue;\n record[key] = readValueNode(prop.value, source);\n }\n return record;\n}\n\n/** True when a keyframe value record carries the synthetic `_auto` marker. */\nfunction recordHasAuto(record: Record<string, number | string>): boolean {\n return \"_auto\" in record;\n}\n\n/**\n * Compute `_auto` endpoint overwrites: when the new keyframe is the immediate\n * neighbor of an `_auto` 0% or 100% endpoint, that endpoint is rewritten to\n * `{ ...newProps, _auto: 1 }`. Only fires for interior keyframes. Returns the\n * percentage→overwrite map so the caller can fold these into the per-node final\n * records (never a separate splice).\n */\nfunction autoEndpointOverwrites(\n kfNode: Node,\n source: string,\n percentage: number,\n properties: Record<string, number | string>,\n): Map<any, Record<string, number | string>> {\n const result = new Map<any, Record<string, number | string>>();\n if (percentage <= 0 || percentage >= 100) return result;\n const pctProps = percentagePropsOf(kfNode);\n const allPcts = pctProps\n .map((p: Node) => percentageFromKey(propKeyName(p) ?? \"\"))\n .filter((n: number) => !Number.isNaN(n) && n !== percentage)\n .sort((a: number, b: number) => a - b);\n const leftNeighbor = allPcts.filter((p: number) => p < percentage).pop();\n const rightNeighbor = allPcts.find((p: number) => p > percentage);\n for (const endPct of [0, 100]) {\n const isNeighbor = endPct === 0 ? leftNeighbor === 0 : rightNeighbor === 100;\n if (!isNeighbor) continue;\n const endProp = pctProps.find((p: Node) => percentageFromKey(propKeyName(p) ?? \"\") === endPct);\n if (!endProp) continue;\n const rec = valueNodeToRecord(endProp.value, source);\n if (!recordHasAuto(rec)) continue;\n result.set(endProp, { ...properties, _auto: 1 });\n }\n return result;\n}\n\nfunction findKfPropByPct(kfNode: Node, percentage: number): { prop: Node; idx: number } | null {\n // Match the CLOSEST keyframe within tolerance, not the first one within range.\n // Keyframes at e.g. 0/49/50/100 are all valid (the SDK dedups to a unique\n // match at TOLERANCE=0.001 upstream); picking the first-within-PCT_TOLERANCE=2\n // would hit 49% when the caller meant 50%. Tie-break on the earliest index so\n // the choice stays deterministic.\n const props = kfNode.properties ?? [];\n let best: { prop: Node; idx: number } | null = null;\n let bestDist = Number.POSITIVE_INFINITY;\n for (let i = 0; i < props.length; i++) {\n const prop = props[i];\n if (!isObjectProperty(prop)) continue;\n const key = propKeyName(prop);\n if (typeof key !== \"string\") continue;\n const dist = Math.abs(percentageFromKey(key) - percentage);\n if (dist <= PCT_TOLERANCE && dist < bestDist) {\n best = { prop, idx: i };\n bestDist = dist;\n }\n }\n return best;\n}\n\nexport function updateKeyframeInScript(\n script: string,\n animationId: string,\n percentage: number,\n properties: Record<string, number | string>,\n ease?: string,\n): string {\n const parsed = parseGsapScriptAcornForWrite(script);\n if (!parsed) return script;\n const target = parsed.located.find((l) => l.id === animationId);\n if (!target) return script;\n\n const kfPropNode = findPropertyNode(target.call.varsArg, \"keyframes\");\n if (!kfPropNode) return script;\n\n // Array-form keyframes (`keyframes: [{x,y}, ...]`) carry no explicit percentages\n // — GSAP distributes them evenly, and the runtime read assigns even percentages\n // (0, 100/(n-1), …). Map the percentage back to an array index and overwrite that\n // element in place (preserving the array form). Without this the function bailed\n // on the ObjectExpression check, so dragging a motion-path node on an array-form\n // tween committed nothing (server no-op).\n if (kfPropNode.value?.type === \"ArrayExpression\") {\n return updateArrayKeyframeByPct(script, kfPropNode.value, percentage, properties, ease);\n }\n if (kfPropNode.value?.type !== \"ObjectExpression\") return script;\n\n const match = findKfPropByPct(kfPropNode.value, percentage);\n if (!match) return script;\n\n const ms = new MagicString(script);\n // MERGE the edited props into the existing keyframe, preserving properties already\n // keyframed at this percentage (z, transformPerspective, rotation, …). A whole-value\n // overwrite DROPS every prop not in this edit — e.g. editing rotationY at the 0%\n // keyframe would strip z / transformPerspective, so the lens then animates from 0 and\n // the element pops. Mirrors addKeyframeToScript's merge-into-existing branch.\n if (match.prop.value?.type === \"ObjectExpression\") {\n for (const [k, v] of Object.entries(properties)) {\n upsertProp(ms, match.prop.value, k, v);\n }\n if (ease !== undefined) upsertProp(ms, match.prop.value, \"ease\", ease);\n } else {\n const record: Record<string, number | string> = { ...properties };\n if (ease) record.ease = ease;\n ms.overwrite(match.prop.value.start, match.prop.value.end, recordToCode(record));\n }\n return ms.toString();\n}\n\n// ponytail: even-spacing index map; if array keyframes ever carry per-element\n// `duration`, switch to matching the closest cumulative position.\nfunction updateArrayKeyframeByPct(\n script: string,\n arrayNode: Node,\n percentage: number,\n properties: Record<string, number | string>,\n ease?: string,\n): string {\n const elements = ((arrayNode.elements ?? []) as Array<Node | null>).filter(\n (el): el is Node => !!el && el.type === \"ObjectExpression\",\n );\n const n = elements.length;\n if (n === 0) return script;\n const idx = n > 1 ? Math.round((percentage / 100) * (n - 1)) : 0;\n const el = elements[Math.max(0, Math.min(n - 1, idx))];\n if (!el) return script;\n const merged: Record<string, number | string> = {\n ...valueNodeToRecord(el, script),\n ...properties,\n };\n if (ease) merged.ease = ease;\n const ms = new MagicString(script);\n ms.overwrite(el.start, el.end, recordToCode(merged));\n return ms.toString();\n}\n\n/**\n * Build the final property record for the keyframe at `percentage`. If a\n * keyframe already exists there, MERGE the new props over the existing record\n * (preserve untouched props, preserve `_auto`, preserve the existing per-keyframe\n * ease when the op omits one); otherwise it's just the new props.\n */\nfunction buildTargetRecord(\n existing: { prop: Node; idx: number } | null,\n source: string,\n properties: Record<string, number | string>,\n ease: string | undefined,\n): Record<string, number | string> {\n if (!existing || existing.prop.value?.type !== \"ObjectExpression\") {\n const record: Record<string, number | string> = { ...properties };\n if (ease) record.ease = ease;\n return record;\n }\n const existingRecord = valueNodeToRecord(existing.prop.value, source);\n const existingEase = typeof existingRecord.ease === \"string\" ? existingRecord.ease : undefined;\n const merged: Record<string, number | string> = { ...existingRecord };\n for (const [k, v] of Object.entries(properties)) merged[k] = v;\n const finalEase = ease ?? existingEase;\n if (finalEase) merged.ease = finalEase;\n else delete merged.ease;\n return merged;\n}\n\n/**\n * Compute the backfilled final record for one sibling keyframe: append any of\n * `newPropKeys` it's missing, using the backfill default. Returns null when\n * nothing changes (so the caller emits no overwrite for it).\n */\nfunction backfilledSiblingRecord(\n valueNode: Node,\n source: string,\n newPropKeys: string[],\n backfillDefaults: Record<string, number | string>,\n): Record<string, number | string> | null {\n if (valueNode?.type !== \"ObjectExpression\") return null;\n const record = valueNodeToRecord(valueNode, source);\n let changed = false;\n for (const pk of newPropKeys) {\n const defaultVal = backfillDefaults[pk];\n if (pk in record || defaultVal == null) continue;\n record[pk] = defaultVal;\n changed = true;\n }\n return changed ? record : null;\n}\n\n/** A located tween whose varsArg has a static keyframes ObjectExpression, or null. */\nfunction locateWithKeyframes(\n script: string,\n animationId: string,\n): { script: string; parsed: ParsedGsapAcornForWrite; target: Node; kfNode: Node } | null {\n const parsed = parseGsapScriptAcornForWrite(script);\n if (!parsed) return null;\n // Converting from()/fromTo() to to() rewrites the content-derived id; match\n // recast's locateAnimationWithFallback by remapping the method segment.\n const convertedId = animationId.replace(/-from-|-fromTo-/, \"-to-\");\n const target =\n parsed.located.find((l) => l.id === animationId) ??\n parsed.located.find((l) => l.id === convertedId);\n if (!target) return null;\n const kfPropNode = findPropertyNode(target.call.varsArg, \"keyframes\");\n if (!kfPropNode || kfPropNode.value?.type !== \"ObjectExpression\") return null;\n return { script, parsed, target, kfNode: kfPropNode.value };\n}\n\n/** Locate a tween's keyframes object, converting a flat tween first if absent. */\n// Array-form keyframes (`keyframes: [{x,y}, …]`) → even-percentage object form\n// (`{ \"0%\": {…}, \"33.3%\": {…}, … }`). Inserting a keyframe needs percentage keys,\n// which an even array can't host. Runtime-identical; mirrors the recast path.\nfunction convertArrayKeyframesToObject(script: string, target: Node): string {\n const kfPropNode = findPropertyNode(target.call.varsArg, \"keyframes\");\n if (!kfPropNode || kfPropNode.value?.type !== \"ArrayExpression\") return script;\n const els = ((kfPropNode.value.elements ?? []) as Array<Node | null>).filter(\n (el): el is Node => !!el && el.type === \"ObjectExpression\",\n );\n const n = els.length;\n if (n === 0) return script;\n const entries = els.map((el, i) => {\n const pct = n > 1 ? Math.round((i / (n - 1)) * 1000) / 10 : 0;\n return `${JSON.stringify(`${pct}%`)}: ${script.slice(el.start, el.end)}`;\n });\n const ms = new MagicString(script);\n ms.overwrite(kfPropNode.value.start, kfPropNode.value.end, `{ ${entries.join(\", \")} }`);\n return ms.toString();\n}\n\nfunction ensureKeyframesNode(\n script: string,\n animationId: string,\n): { script: string; parsed: ParsedGsapAcornForWrite; target: Node; kfNode: Node } | null {\n const direct = locateWithKeyframes(script, animationId);\n if (direct) return direct;\n\n const parsed = parseGsapScriptAcornForWrite(script);\n const target = parsed?.located.find((l) => l.id === animationId);\n if (!target) return null;\n\n // Array-form keyframes → normalize to object form, then re-locate.\n const kfProp = findPropertyNode(target.call.varsArg, \"keyframes\");\n if (kfProp?.value?.type === \"ArrayExpression\") {\n const normalized = convertArrayKeyframesToObject(script, target);\n if (normalized !== script) return locateWithKeyframes(normalized, animationId);\n return null;\n }\n\n // No static keyframes object — convert the flat tween, then re-locate.\n const converted = convertFlatTweenToKeyframes(script, target);\n if (converted === script) return null;\n return locateWithKeyframes(converted, animationId);\n}\n\n/**\n * Compute the sibling keyframe nodes that need a backfilled prop, excluding the\n * target keyframe and any node already being overwritten as an `_auto` endpoint.\n */\nfunction collectBackfillOverwrites(\n kfNode: Node,\n src: string,\n properties: Record<string, number | string>,\n backfillDefaults: Record<string, number | string> | undefined,\n skip: { existingProp: Node; endpoints: Map<any, unknown> },\n): Map<any, Record<string, number | string>> {\n const result = new Map<any, Record<string, number | string>>();\n if (!backfillDefaults) return result;\n const newPropKeys = Object.keys(properties);\n for (const prop of percentagePropsOf(kfNode)) {\n if (prop === skip.existingProp || skip.endpoints.has(prop)) continue;\n const rec = backfilledSiblingRecord(prop.value, src, newPropKeys, backfillDefaults);\n if (rec) result.set(prop, rec);\n }\n return result;\n}\n\nexport function addKeyframeToScript(\n script: string,\n animationId: string,\n percentage: number,\n properties: Record<string, number | string>,\n ease?: string,\n backfillDefaults?: Record<string, number | string>,\n): string {\n const located = ensureKeyframesNode(script, animationId);\n if (!located) return script;\n const { script: src, kfNode } = located;\n\n const existing = findKfPropByPct(kfNode, percentage);\n\n // Final record for the target keyframe (merge if it already exists).\n const targetRecord = buildTargetRecord(existing, src, properties, ease);\n // `_auto` endpoint syncs fire only on new inserts; a merge landing ON an\n // endpoint already preserves `_auto` via buildTargetRecord.\n const endpointOverwrites = existing\n ? new Map<any, Record<string, number | string>>()\n : autoEndpointOverwrites(kfNode, src, percentage, properties);\n // Backfilled siblings (each node changes at most once).\n const backfillOverwrites = collectBackfillOverwrites(kfNode, src, properties, backfillDefaults, {\n existingProp: existing?.prop,\n endpoints: endpointOverwrites,\n });\n\n // Emit exactly one overwrite per changed node, plus one insert for a new key.\n const ms = new MagicString(src);\n if (existing) {\n // Merge into the existing keyframe at this percentage, preserving sibling\n // properties — overwrite only the given keys. (A whole-value overwrite here\n // would silently drop other properties already keyframed at this percent.)\n if (existing.prop.value?.type === \"ObjectExpression\") {\n for (const [k, v] of Object.entries(properties)) {\n upsertProp(ms, existing.prop.value, k, v);\n }\n if (ease !== undefined) upsertProp(ms, existing.prop.value, \"ease\", ease);\n } else {\n ms.overwrite(existing.prop.value.start, existing.prop.value.end, recordToCode(targetRecord));\n }\n } else {\n insertNewKeyframe(ms, kfNode, percentage, `${percentage}%`, recordToCode(targetRecord));\n }\n for (const [prop, rec] of [...endpointOverwrites, ...backfillOverwrites]) {\n ms.overwrite(prop.value.start, prop.value.end, recordToCode(rec));\n }\n\n return ms.toString();\n}\n\n/** Insert a brand-new `\"pct%\": {...}` property in sorted order. */\nfunction insertNewKeyframe(\n ms: MagicString,\n kfNode: Node,\n percentage: number,\n pctKey: string,\n valueCode: string,\n): void {\n const allProps = (kfNode.properties ?? []).filter((p: Node) => isObjectProperty(p));\n let insertBeforeProp: Node = null;\n for (const prop of allProps) {\n const key = propKeyName(prop);\n if (typeof key === \"string\" && percentageFromKey(key) > percentage) {\n insertBeforeProp = prop;\n break;\n }\n }\n if (insertBeforeProp) {\n ms.appendLeft(insertBeforeProp.start, `${JSON.stringify(pctKey)}: ${valueCode}, `);\n } else {\n const sep = allProps.length > 0 ? \", \" : \"\";\n ms.appendLeft(kfNode.end - 1, `${sep}${JSON.stringify(pctKey)}: ${valueCode}`);\n }\n}\n\n/**\n * Rebuild a vars ObjectExpression that has just dropped below two keyframes,\n * collapsing `keyframes: {…}` back to a flat tween. Mirrors recast's\n * collapseKeyframesToFlat: drop the `keyframes` + `easeEach` keys, preserve every\n * other vars key verbatim, and splice the remaining keyframe's properties (minus\n * its per-keyframe `ease`) in as flat vars keys. Single ms.overwrite of the whole\n * vars node so the splice can't overlap the keyframe removal.\n */\nfunction collapseKeyframesToFlat(\n ms: MagicString,\n varsNode: Node,\n source: string,\n remainingRecord: Record<string, number | string>,\n): void {\n if (varsNode?.type !== \"ObjectExpression\") return;\n const dropKeyframeKeys = (key: string) => key === \"keyframes\" || key === \"easeEach\";\n const { entries } = preservedEntries(varsNode, source, dropKeyframeKeys, {});\n for (const [k, v] of Object.entries(remainingRecord)) {\n if (k !== \"ease\") entries.push(`${safeKey(k)}: ${valueToCode(v)}`);\n }\n ms.overwrite(varsNode.start, varsNode.end, `{ ${entries.join(\", \")} }`);\n}\n\n/** Implicit tween-relative percentage of array-form keyframe index `i` of `n`\n * (GSAP distributes array keyframes evenly: 0%, 1/(n-1), …, 100%). */\nfunction arrayKeyframePct(i: number, n: number): number {\n return n > 1 ? (i / (n - 1)) * 100 : 0;\n}\n\n// Array-form keyframes (`keyframes: [{x,y}, …]`) carry no explicit percentages —\n// GSAP distributes them evenly. removeKeyframeFromScript only handled the\n// object-form (`keyframes: { \"50%\": {…} }`), so removing from an array-form tween\n// was a silent no-op (and the downstream hold-sync then stranded an `hf-hold`).\n// Resolve the element by its implicit percentage and splice it out; collapse to a\n// flat tween when fewer than two remain (parity with the object-form path).\nfunction removeArrayKeyframe(\n ms: MagicString,\n varsArg: Node,\n arrNode: Node,\n script: string,\n percentage: number,\n): boolean {\n const elements: Node[] = (arrNode.elements ?? []).filter(\n (e: Node | null): e is Node => !!e && e.type === \"ObjectExpression\",\n );\n const n = elements.length;\n if (n === 0) return false;\n\n let matchIdx = -1;\n let bestDist = Number.POSITIVE_INFINITY;\n for (let i = 0; i < n; i++) {\n const dist = Math.abs(arrayKeyframePct(i, n) - percentage);\n if (dist <= PCT_TOLERANCE && dist < bestDist) {\n matchIdx = i;\n bestDist = dist;\n }\n }\n if (matchIdx === -1) return false;\n\n const remaining = elements.filter((_, i) => i !== matchIdx);\n if (remaining.length < 2) {\n const sole = remaining[0];\n const record = sole ? valueNodeToRecord(sole, script) : {};\n collapseKeyframesToFlat(ms, varsArg, script, record);\n return true;\n }\n removeProp(ms, elements[matchIdx], elements);\n return true;\n}\n\nexport function removeKeyframeFromScript(\n script: string,\n animationId: string,\n percentage: number,\n): string {\n const parsed = parseGsapScriptAcornForWrite(script);\n if (!parsed) return script;\n const target = parsed.located.find((l) => l.id === animationId);\n if (!target) return script;\n\n const kfPropNode = findPropertyNode(target.call.varsArg, \"keyframes\");\n if (!kfPropNode) return script;\n\n if (kfPropNode.value?.type === \"ArrayExpression\") {\n const ms = new MagicString(script);\n return removeArrayKeyframe(ms, target.call.varsArg, kfPropNode.value, script, percentage)\n ? ms.toString()\n : script;\n }\n\n if (kfPropNode.value?.type !== \"ObjectExpression\") return script;\n const kfNode = kfPropNode.value;\n\n const match = findKfPropByPct(kfNode, percentage);\n if (!match) return script;\n\n const ms = new MagicString(script);\n\n // If removing this keyframe leaves fewer than two, collapse the keyframes\n // object back to a flat tween (recast parity) instead of leaving a lone\n // keyframe. We rebuild the whole vars node, so we never also splice the kf\n // node — the two edits would overlap.\n const remaining = percentagePropsOf(kfNode).filter((p) => p !== match.prop);\n if (remaining.length < 2) {\n const sole = remaining[0];\n const record = sole ? valueNodeToRecord(sole.value, script) : {};\n collapseKeyframesToFlat(ms, target.call.varsArg, script, record);\n return ms.toString();\n }\n\n const allProps = (kfNode.properties ?? []).filter((p: Node) => isObjectProperty(p));\n removeProp(ms, match.prop, allProps);\n return ms.toString();\n}\n\nexport function removePropertyFromAnimation(\n script: string,\n animationId: string,\n property: string,\n from = false,\n): string {\n const parsed = parseGsapScriptAcornForWrite(script);\n if (!parsed) return script;\n const target = parsed.located.find((l) => l.id === animationId);\n if (!target) return script;\n const { call } = target;\n const objNode = from ? (call.method === \"fromTo\" ? call.fromArg : null) : call.varsArg;\n if (!objNode) return script;\n const propNode = findPropertyNode(objNode, property);\n if (!propNode) return script;\n const allProps = (objNode.properties ?? []).filter((p: Node) => isObjectProperty(p));\n const ms = new MagicString(script);\n removeProp(ms, propNode, allProps);\n return ms.toString();\n}\n\n/**\n * Remove all keyframes from a tween, collapsing to a flat tween with one\n * keyframe's properties: the first for `from()`, the last otherwise (the\n * destination = the visible resting state).\n */\nexport function removeAllKeyframesFromScript(script: string, animationId: string): string {\n const parsed = parseGsapScriptAcornForWrite(script);\n if (!parsed) return script;\n const target = parsed.located.find((l) => l.id === animationId);\n if (!target) return script;\n const kfs = target.animation.keyframes?.keyframes;\n if (!kfs || kfs.length === 0) return script;\n\n const sorted = [...kfs].sort((a, b) => a.percentage - b.percentage);\n const collapse = target.call.method === \"from\" ? sorted[0] : sorted[sorted.length - 1];\n if (!collapse) return script;\n\n const ms = new MagicString(script);\n overwriteVarsArg(\n ms,\n target.call,\n buildVarsObjectCode(buildCollapsedFlatVars(target.animation, collapse)),\n );\n return ms.toString();\n}\n\n// Flat vars for a tween collapsing its keyframes onto one stop: existing\n// top-level props, then the collapse keyframe's props (skip per-keyframe\n// `ease`), then duration/ease/extras. Drops keyframes + easeEach by omission.\nfunction buildCollapsedFlatVars(\n animation: GsapAnimation,\n collapse: { properties: Record<string, number | string> },\n): Record<string, number | string> {\n const flat: Record<string, number | string> = { ...animation.properties };\n for (const [k, v] of Object.entries(collapse.properties)) {\n if (k !== \"ease\") flat[k] = v;\n }\n if (animation.duration !== undefined) flat.duration = animation.duration;\n if (animation.ease) flat.ease = animation.ease;\n for (const [k, v] of Object.entries(animation.extras ?? {})) {\n if (typeof v === \"number\" || typeof v === \"string\") flat[k] = v;\n }\n return flat;\n}\n\n/** Build the full replacement vars object for a tween being converted to keyframes. */\nfunction buildKeyframesVarsCode(\n animation: GsapAnimation,\n fromProps: Record<string, number | string>,\n toProps: Record<string, number | string>,\n varsNode: Node,\n source: string,\n setDuration?: number,\n): string {\n const fromEntries = Object.entries(fromProps).map(([k, v]) => `${safeKey(k)}: ${valueToCode(v)}`);\n const toEntries = Object.entries(toProps).map(([k, v]) => `${safeKey(k)}: ${valueToCode(v)}`);\n const easeEntry = animation.ease ? `, easeEach: ${JSON.stringify(animation.ease)}` : \"\";\n const kfCode = `{ \"0%\": { ${fromEntries.join(\", \")} }, \"100%\": { ${toEntries.join(\", \")} }${easeEntry} }`;\n // Preserve every non-editable key (duration/delay/callbacks/stagger/yoyo/…)\n // verbatim from source — rebuilding from the animation object alone dropped\n // `delay` (not a GsapAnimation field), shifting the tween's start time.\n let preserved = preservedVarsEntries(varsNode, source);\n // Converting a static `set` → drop its hold markers and give it a real duration\n // so the keyframes span time.\n if (setDuration !== undefined) {\n preserved = preserved.filter((e) => !/^\\s*(immediateRender|data|duration)\\s*:/.test(e));\n }\n const parts: string[] = [`keyframes: ${kfCode}`, ...preserved];\n if (setDuration !== undefined) parts.push(`duration: ${Math.max(0.001, setDuration)}`);\n if (animation.ease) parts.push(`ease: \"none\"`);\n return `{ ${parts.join(\", \")} }`;\n}\n\n/**\n * Convert a flat tween (to/from/fromTo) to percentage-keyframes format.\n * `resolvedFromValues` supplies the current DOM state: overrides the 0% endpoint\n * for `to()`, the 100% endpoint for `from()`, or merges into toProps for `fromTo()`.\n */\nexport function convertToKeyframesFromScript(\n script: string,\n animationId: string,\n resolvedFromValues?: Record<string, number | string>,\n setDuration = 1,\n): string {\n const parsed = parseGsapScriptAcornForWrite(script);\n if (!parsed) return script;\n const target = parsed.located.find((l) => l.id === animationId);\n if (!target) return script;\n const { animation, call } = target;\n if (animation.keyframes) return script;\n const isSet = call.method === \"set\";\n\n const { fromProps, toProps } = resolveConversionProps(animation, resolvedFromValues);\n const ms = new MagicString(script);\n\n // A GLOBAL `gsap.set(...)` is off-timeline; rewriting only the method emits\n // `gsap.to(...)`, which fires once at load and isn't on the paused master\n // timeline (the engine can't seek/render it). Re-root onto the timeline var\n // and add the position arg the set lacks so the converted tween is seekable.\n if (isSet && animation.global) {\n const calleeObj = call.node.callee.object;\n if (calleeObj?.type === \"Identifier\") {\n ms.overwrite(calleeObj.start, calleeObj.end, parsed.timelineVar);\n }\n const args = call.node.arguments;\n if (args.length > 0 && args.length < 3) {\n ms.appendLeft(args[args.length - 1].end, \", 0\");\n }\n }\n\n // set/from/fromTo all become `to`; fromTo also drops its `from` argument.\n if (call.method === \"from\" || call.method === \"fromTo\" || isSet) {\n ms.overwrite(call.node.callee.property.start, call.node.callee.property.end, \"to\");\n }\n if (call.method === \"fromTo\" && call.fromArg) {\n ms.remove(call.fromArg.start, call.varsArg.start);\n }\n overwriteVarsArg(\n ms,\n call,\n buildKeyframesVarsCode(\n animation,\n fromProps,\n toProps,\n call.varsArg,\n script,\n isSet ? setDuration : undefined,\n ),\n );\n\n return ms.toString();\n}\n\n// ── Keyframe-object code builder ─────────────────────────────────────────────\n\n/** Build a percentage-keyframes object literal: `{ \"0%\": { x: 0 }, \"100%\": { x: 100 } }`. */\nfunction buildKeyframeObjectCode(\n keyframes: Array<{\n percentage: number;\n properties: Record<string, number | string>;\n ease?: string;\n auto?: boolean;\n }>,\n easeEach?: string,\n): string {\n const entries = keyframes.map((kf) => {\n const props = Object.entries(kf.properties).map(([k, v]) => `${safeKey(k)}: ${valueToCode(v)}`);\n if (kf.ease) props.push(`ease: ${JSON.stringify(kf.ease)}`);\n if (kf.auto) props.push(`_auto: 1`);\n return `${JSON.stringify(`${kf.percentage}%`)}: { ${props.join(\", \")} }`;\n });\n if (easeEach) entries.push(`easeEach: ${JSON.stringify(easeEach)}`);\n return `{ ${entries.join(\", \")} }`;\n}\n\n// ── Materialize keyframes ────────────────────────────────────────────────────\n\n/**\n * Replace a dynamic or static keyframes expression with a fully-resolved\n * percentage-keyframes object. Called when a user first edits a dynamically-\n * generated keyframe in the studio so it becomes statically editable.\n */\nexport function materializeKeyframesFromScript(\n script: string,\n animationId: string,\n keyframes: Array<{\n percentage: number;\n properties: Record<string, number | string>;\n ease?: string;\n }>,\n easeEach?: string,\n resolvedSelector?: string,\n): string {\n // An empty keyframe list has no materialized form — rebuilding vars with an\n // empty keyframes object would empty the animation. No-op instead.\n if (keyframes.length === 0) return script;\n const parsed = parseGsapScriptAcornForWrite(script);\n if (!parsed) return script;\n const target = parsed.located.find((l) => l.id === animationId);\n if (!target) return script;\n\n const { call } = target;\n const sorted = [...keyframes].sort((a, b) => a.percentage - b.percentage);\n const kfObjCode = buildKeyframeObjectCode(sorted, easeEach);\n const ms = new MagicString(script);\n\n if (resolvedSelector) {\n const selectorArg = call.node.arguments[0];\n if (selectorArg)\n ms.overwrite(selectorArg.start, selectorArg.end, JSON.stringify(resolvedSelector));\n }\n\n const kfProp = findPropertyNode(call.varsArg, \"keyframes\");\n if (kfProp) {\n ms.overwrite(kfProp.value.start, kfProp.value.end, kfObjCode);\n } else if (call.varsArg?.type === \"ObjectExpression\") {\n const vars = call.varsArg;\n if (vars.properties.length > 0) {\n ms.prependLeft(vars.properties[0].start, `keyframes: ${kfObjCode}, `);\n } else {\n ms.appendLeft(vars.end - 1, `keyframes: ${kfObjCode}`);\n }\n }\n\n const eachProp = findPropertyNode(call.varsArg, \"easeEach\");\n if (eachProp) {\n const allProps = (call.varsArg.properties ?? []).filter((p: Node) => isObjectProperty(p));\n removeProp(ms, eachProp, allProps);\n }\n\n return ms.toString();\n}\n\n// ── Add animation with keyframes ──────────────────────────────────────────────\n\n/** Insert a new keyframed `to()` call and return the new animation ID. */\nexport function addAnimationWithKeyframesToScript(\n script: string,\n targetSelector: string,\n position: number,\n duration: number,\n keyframes: Array<{\n percentage: number;\n properties: Record<string, number | string>;\n ease?: string;\n auto?: boolean;\n }>,\n ease?: string,\n easeEach?: string,\n): { script: string; id: string } {\n const parsed = parseGsapScriptAcornForWrite(script);\n if (!parsed) return { script, id: \"\" };\n const insertionPoint = findInsertionPoint(parsed);\n if (insertionPoint === null) return { script, id: \"\" };\n\n const sorted = [...keyframes].sort((a, b) => a.percentage - b.percentage);\n const kfObjCode = buildKeyframeObjectCode(sorted, easeEach);\n const varParts = [`keyframes: ${kfObjCode}`, `duration: ${valueToCode(duration)}`];\n if (ease) varParts.push(`ease: ${JSON.stringify(ease)}`);\n const stmtCode = `${parsed.timelineVar}.to(${JSON.stringify(targetSelector)}, { ${varParts.join(\", \")} }, ${valueToCode(position)});`;\n\n const ms = new MagicString(script);\n ms.appendLeft(insertionPoint, \"\\n\" + stmtCode);\n\n const result = ms.toString();\n const reParsed = parseGsapScriptAcornForWrite(result);\n const newId = reParsed?.located[reParsed.located.length - 1]?.id ?? \"\";\n return { script: result, id: newId };\n}\n\n// ── Split into property groups ────────────────────────────────────────────────\n\nfunction collectPropertyKeys(anim: GsapAnimation): Set<string> {\n const keys = new Set<string>();\n if (anim.keyframes) {\n for (const kf of anim.keyframes.keyframes) {\n for (const k of Object.keys(kf.properties)) keys.add(k);\n }\n } else {\n for (const k of Object.keys(anim.properties)) keys.add(k);\n }\n return keys;\n}\n\nfunction partitionPropertyGroups(keys: Set<string>): Map<PropertyGroupName, string[]> {\n const groups = new Map<PropertyGroupName, string[]>();\n for (const key of keys) {\n if (key === \"transformOrigin\") continue;\n const group = classifyPropertyGroup(key);\n let arr = groups.get(group);\n if (!arr) {\n arr = [];\n groups.set(group, arr);\n }\n arr.push(key);\n }\n return groups;\n}\n\nfunction assignTransformOrigin(groupProps: Map<PropertyGroupName, string[]>): void {\n let largestGroup: PropertyGroupName | undefined;\n let largestCount = 0;\n for (const [group, props] of groupProps) {\n if (props.length > largestCount) {\n largestCount = props.length;\n largestGroup = group;\n }\n }\n const largest = largestGroup ? groupProps.get(largestGroup) : undefined;\n if (largest) largest.push(\"transformOrigin\");\n}\n\nfunction filterGroupKeyframes(\n kfs: GsapPercentageKeyframe[],\n propSet: Set<string>,\n): Array<{ percentage: number; properties: Record<string, number | string>; ease?: string }> {\n const result: Array<{\n percentage: number;\n properties: Record<string, number | string>;\n ease?: string;\n }> = [];\n for (const kf of kfs) {\n const filtered: Record<string, number | string> = {};\n for (const [k, v] of Object.entries(kf.properties)) {\n if (propSet.has(k)) filtered[k] = v;\n }\n if (Object.keys(filtered).length > 0) {\n result.push({\n percentage: kf.percentage,\n properties: filtered,\n ...(kf.ease ? { ease: kf.ease } : {}),\n });\n }\n }\n return result;\n}\n\nfunction filterGroupProperties(\n properties: Record<string, number | string>,\n propSet: Set<string>,\n): Record<string, number | string> {\n const result: Record<string, number | string> = {};\n for (const [k, v] of Object.entries(properties)) {\n if (propSet.has(k)) result[k] = v;\n }\n return result;\n}\n\nfunction addGroupAnimToScript(\n script: string,\n anim: GsapAnimation,\n propSet: Set<string>,\n): { script: string; id: string } {\n if (anim.keyframes) {\n const groupKeyframes = filterGroupKeyframes(anim.keyframes.keyframes, propSet);\n if (groupKeyframes.length === 0) return { script, id: \"\" };\n const pos = typeof anim.position === \"number\" ? anim.position : 0;\n return addAnimationWithKeyframesToScript(\n script,\n anim.targetSelector,\n pos,\n anim.duration ?? 0.5,\n groupKeyframes,\n anim.keyframes.easeEach ?? anim.ease,\n );\n }\n const groupProperties = filterGroupProperties(anim.properties, propSet);\n if (Object.keys(groupProperties).length === 0) return { script, id: \"\" };\n const fromProperties =\n anim.method === \"fromTo\" && anim.fromProperties\n ? filterGroupProperties(anim.fromProperties, propSet)\n : undefined;\n return addAnimationToScript(script, {\n targetSelector: anim.targetSelector,\n method: anim.method,\n position: anim.position,\n duration: anim.duration,\n ease: anim.ease,\n properties: groupProperties,\n fromProperties,\n extras: anim.extras,\n });\n}\n\n/**\n * Split a mixed-property tween into one tween per property group (position,\n * scale, visual, etc.) so each group can be edited independently.\n * Returns the updated script and the IDs of the newly-created tweens.\n */\nexport function splitIntoPropertyGroupsFromScript(\n script: string,\n animationId: string,\n): { script: string; ids: string[] } {\n const parsed = parseGsapScriptAcornForWrite(script);\n if (!parsed) return { script, ids: [animationId] };\n const target = parsed.located.find((l) => l.id === animationId);\n if (!target) return { script, ids: [animationId] };\n const { animation } = target;\n\n const allPropKeys = collectPropertyKeys(animation);\n const groupProps = partitionPropertyGroups(allPropKeys);\n if (groupProps.size <= 1) return { script, ids: [animationId] };\n if (allPropKeys.has(\"transformOrigin\")) assignTransformOrigin(groupProps);\n\n let result = removeAnimationFromScript(script, animationId);\n for (const [, props] of groupProps) {\n const { script: next, id } = addGroupAnimToScript(result, animation, new Set(props));\n if (id) result = next;\n }\n\n const reParsed = parseGsapScriptAcornForWrite(result);\n const newIds = (reParsed?.located ?? [])\n .filter((l) => l.animation.targetSelector === animation.targetSelector)\n .map((l) => l.id);\n return { script: result, ids: newIds };\n}\n\n// ── Label write ops ───────────────────────────────────────────────────────────\n\n/** True when `expr` is `tl.<method>(…)` rooted at the timeline var. */\nfunction isTimelineMethodCall(\n expr: Node,\n timelineVar: string,\n method: string,\n script: string,\n): boolean {\n return (\n expr?.type === \"CallExpression\" &&\n expr.callee?.type === \"MemberExpression\" &&\n isTimelineRooted(expr.callee.object, timelineVar, script) &&\n expr.callee.property?.name === method\n );\n}\n\n/** True when `expr` is `tl.addLabel(\"<name>\", …)` rooted at the timeline var. */\nfunction isAddLabelCall(expr: Node, timelineVar: string, name: string, script: string): boolean {\n const firstArg = expr?.arguments?.[0];\n return (\n isTimelineMethodCall(expr, timelineVar, \"addLabel\", script) &&\n firstArg?.type === \"Literal\" &&\n firstArg.value === name\n );\n}\n\n/** Every `tl.addLabel(\"<name>\", …)` ExpressionStatement in the script. */\nfunction findLabelStatements(\n parsed: ParsedGsapAcornForWrite,\n name: string,\n script: string,\n): Node[] {\n const targets: Node[] = [];\n acornWalk.simple(parsed.ast, {\n ExpressionStatement(node: Node) {\n if (isAddLabelCall(node.expression, parsed.timelineVar, name, script)) targets.push(node);\n },\n });\n return targets;\n}\n\nexport function addLabelToScript(script: string, name: string, position: number): string {\n const parsed = parseGsapScriptAcornForWrite(script);\n if (!parsed) return script;\n\n // If the label already exists, MOVE it (overwrite its position) rather than\n // appending a duplicate. Two same-named addLabel statements make removeLabel\n // over-remove — it deletes every match, including a pre-existing label the\n // user never touched.\n const existing = findLabelStatements(parsed, name, script)[0];\n if (existing) {\n const ms = new MagicString(script);\n const posArg = existing.expression.arguments?.[1];\n if (posArg) ms.overwrite(posArg.start, posArg.end, valueToCode(position));\n else ms.appendLeft(existing.expression.end - 1, `, ${valueToCode(position)}`);\n return ms.toString();\n }\n\n const insertionPoint = findInsertionPoint(parsed);\n if (insertionPoint === null) return script;\n\n const ms = new MagicString(script);\n const labelCode = `${parsed.timelineVar}.addLabel(${JSON.stringify(name)}, ${valueToCode(position)});`;\n ms.appendLeft(insertionPoint, \"\\n\" + labelCode);\n return ms.toString();\n}\n\nexport function removeLabelFromScript(script: string, name: string): string {\n const parsed = parseGsapScriptAcornForWrite(script);\n if (!parsed) return script;\n\n const targets = findLabelStatements(parsed, name, script);\n if (!targets.length) return script;\n\n const ms = new MagicString(script);\n for (const target of targets) {\n const end =\n target.end < script.length && script[target.end] === \"\\n\" ? target.end + 1 : target.end;\n ms.remove(target.start, end);\n }\n return ms.toString();\n}\n\n// ── Arc path helpers ─────────────────────────────────────────────────────────\n\n/**\n * Remove a set of properties from an ObjectExpression in a single pass.\n * Groups consecutive marked props into blocks to avoid overlapping remove ranges.\n */\nfunction removePropsByKey(ms: MagicString, objNode: Node, keys: Set<string>): void {\n if (objNode?.type !== \"ObjectExpression\") return;\n const allProps = (objNode.properties ?? []).filter(isObjectProperty);\n const marked = allProps.map((p: Node) => keys.has(propKeyName(p) ?? \"\"));\n let i = 0;\n while (i < allProps.length) {\n if (!marked[i]) {\n i++;\n continue;\n }\n const blockStart = i;\n while (i < allProps.length && marked[i]) i++;\n ms.remove(...blockRemoveRange(allProps, blockStart, i));\n }\n}\n\nfunction blockRemoveRange(\n allProps: Node[],\n blockStart: number,\n blockEnd: number,\n): [number, number] {\n if (blockStart === 0 && blockEnd === allProps.length)\n return [allProps[0].start, allProps[allProps.length - 1].end];\n if (blockStart === 0) return [allProps[0].start, allProps[blockEnd].start];\n return [allProps[blockStart - 1].end, allProps[blockEnd - 1].end];\n}\n\n// fallow-ignore-next-line complexity\nfunction readLastWaypointXY(mpVal: Node): { x: number | null; y: number | null } {\n if (mpVal?.type !== \"ObjectExpression\") return { x: null, y: null };\n const pathProp = findPropertyNode(mpVal, \"path\");\n if (pathProp?.value?.type !== \"ArrayExpression\") return { x: null, y: null };\n const elems: Node[] = pathProp.value.elements ?? [];\n const last = elems[elems.length - 1];\n if (last?.type !== \"ObjectExpression\") return { x: null, y: null };\n return {\n x: readNumericLiteralNode(findPropertyNode(last, \"x\")?.value),\n y: readNumericLiteralNode(findPropertyNode(last, \"y\")?.value),\n };\n}\n\n/**\n * Read a numeric value node — a plain numeric literal or a unary-minus negative\n * literal (e.g. `-120`). Returns null for anything non-numeric. Without the\n * UnaryExpression branch, negative waypoint coords (parsed as a UnaryExpression\n * with no `.value`) would be lost when disabling an arc path.\n */\nfunction readNumericLiteralNode(v: Node): number | null {\n if (LITERAL_NODE_TYPES.has(v?.type) && typeof v.value === \"number\") return v.value;\n if (\n v?.type === \"UnaryExpression\" &&\n v.operator === \"-\" &&\n typeof v.argument?.value === \"number\"\n ) {\n return -v.argument.value;\n }\n return null;\n}\n\nfunction disableArcPath(ms: MagicString, call: TweenCallInfo): boolean {\n const mpProp = findPropertyNode(call.varsArg, \"motionPath\");\n if (!mpProp) return false;\n const { x, y } = readLastWaypointXY(mpProp.value);\n if (x === null && y === null) {\n const allProps = (call.varsArg.properties ?? []).filter(isObjectProperty);\n removeProp(ms, mpProp, allProps);\n return true;\n }\n // Overwrite the entire motionPath property with the recovered x/y pair — avoids\n // the appendLeft+remove range-boundary issue in MagicString.\n const parts: string[] = [];\n if (x !== null) parts.push(`x: ${x}`);\n if (y !== null) parts.push(`y: ${y}`);\n ms.overwrite(mpProp.start, mpProp.end, parts.join(\", \"));\n return true;\n}\n\nfunction stripXYFromKeyframes(ms: MagicString, kfPropNode: Node): void {\n if (kfPropNode?.value?.type !== \"ObjectExpression\") return;\n const xyKeys = new Set([\"x\", \"y\"]);\n for (const pctProp of (kfPropNode.value.properties ?? []).filter(isObjectProperty)) {\n const k = propKeyName(pctProp);\n if (typeof k === \"string\" && k.endsWith(\"%\") && pctProp.value?.type === \"ObjectExpression\") {\n removePropsByKey(ms, pctProp.value, xyKeys);\n }\n }\n}\n\nfunction enableArcPath(\n ms: MagicString,\n call: TweenCallInfo,\n animation: GsapAnimation,\n config: ArcPathConfig,\n): boolean {\n const waypoints = extractArcWaypoints(animation);\n if (waypoints.length < 2) return false;\n const segments: ArcPathSegment[] =\n config.segments.length === waypoints.length - 1\n ? config.segments\n : Array.from({ length: waypoints.length - 1 }, () => ({ curviness: 1 }));\n const motionPathCode = buildMotionPathObjectCode({\n waypoints,\n segments,\n autoRotate: config.autoRotate,\n });\n const vars = call.varsArg;\n if (vars?.type !== \"ObjectExpression\") return false;\n // Insert motionPath right after the opening `{` (appendRight at start+1) so the\n // insertion point can never coincide with the end boundary of the x/y removal\n // range. upsertProp would appendLeft at `end - 1`, which collides with a\n // remove-range that ends at the same offset when x/y are the only props —\n // MagicString then discards the append and the output loses everything.\n const editable = (vars.properties ?? []).filter(isObjectProperty);\n const survivesRemoval = editable.some((p: Node) => {\n const k = propKeyName(p);\n return k !== \"x\" && k !== \"y\";\n });\n const sep = survivesRemoval ? \", \" : \"\";\n ms.appendRight(vars.start + 1, ` motionPath: ${motionPathCode}${sep}`);\n stripXYFromKeyframes(ms, findPropertyNode(call.varsArg, \"keyframes\"));\n removePropsByKey(ms, call.varsArg, new Set([\"x\", \"y\"]));\n return true;\n}\n\nexport function setArcPathInScript(\n script: string,\n animationId: string,\n config: ArcPathConfig,\n): string {\n const parsed = parseGsapScriptAcornForWrite(script);\n if (!parsed) return script;\n const target = parsed.located.find((l) => l.id === animationId);\n if (!target) return script;\n const ms = new MagicString(script);\n const handled = config.enabled\n ? enableArcPath(ms, target.call, target.animation, config)\n : disableArcPath(ms, target.call);\n return handled ? ms.toString() : script;\n}\n\nexport function updateArcSegmentInScript(\n script: string,\n animationId: string,\n segmentIndex: number,\n update: Partial<ArcPathSegment>,\n): string {\n const parsed = parseGsapScriptAcornForWrite(script);\n if (!parsed) return script;\n const target = parsed.located.find((l) => l.id === animationId);\n if (!target) return script;\n\n const { call, animation } = target;\n if (!animation.arcPath?.enabled) return script;\n\n const segments = [...animation.arcPath.segments];\n const existingSeg = segments[segmentIndex];\n if (segmentIndex < 0 || segmentIndex >= segments.length || !existingSeg) return script;\n\n segments[segmentIndex] = { ...existingSeg, ...update };\n\n const waypoints = extractArcWaypoints(animation);\n if (waypoints.length < 2) return script;\n\n const motionPathCode = buildMotionPathObjectCode({\n waypoints,\n segments,\n autoRotate: animation.arcPath.autoRotate,\n });\n\n const mpProp = findPropertyNode(call.varsArg, \"motionPath\");\n if (!mpProp) return script;\n\n const ms = new MagicString(script);\n ms.overwrite(mpProp.value.start, mpProp.value.end, motionPathCode);\n return ms.toString();\n}\n\nexport function removeArcPathFromScript(script: string, animationId: string): string {\n return setArcPathInScript(script, animationId, {\n enabled: false,\n autoRotate: false,\n segments: [],\n });\n}\n\n// ── splitAnimationsInScript helpers ──────────────────────────────────────────\n\n/** Overwrite the selector (first arg) of a tween call. */\nfunction updateAnimationSelectorInScript(\n script: string,\n animationId: string,\n newSelector: string,\n): string {\n const parsed = parseGsapScriptAcornForWrite(script);\n if (!parsed) return script;\n const target = parsed.located.find((l) => l.id === animationId);\n if (!target) return script;\n const selectorArg = target.call.node.arguments?.[0];\n if (!selectorArg) return script;\n const ms = new MagicString(script);\n ms.overwrite(selectorArg.start, selectorArg.end, JSON.stringify(newSelector));\n return ms.toString();\n}\n\n/**\n * Insert a `tl.set()` call immediately after the timeline declaration\n * (before existing tweens) to establish inherited state on a new element.\n */\nfunction insertInheritedStateSetInScript(\n script: string,\n selector: string,\n position: number,\n properties: Record<string, number | string>,\n): string {\n const parsed = parseGsapScriptAcornForWrite(script);\n if (!parsed) return script;\n const props = Object.entries(properties)\n .map(([k, v]) => `${safeKey(k)}: ${valueToCode(v)}`)\n .join(\", \");\n const code = `${parsed.timelineVar}.set(${JSON.stringify(selector)}, { ${props} }, ${position});`;\n const ms = new MagicString(script);\n const tlDecl = findTimelineDeclarationStatement(parsed.ast, parsed.timelineVar);\n const firstLocated = parsed.located[0];\n if (tlDecl) {\n ms.appendLeft(tlDecl.end, \"\\n\" + code);\n } else if (firstLocated) {\n const firstCall = firstLocated.call;\n const exprStmt = findEnclosingExpressionStatement(firstCall.ancestors);\n const insertAt = exprStmt?.start ?? firstCall.node.start;\n ms.prependLeft(insertAt, code + \"\\n\");\n } else {\n ms.append(\"\\n\" + code);\n }\n return ms.toString();\n}\n\n/**\n * Compute, in forward (timeline) order, the inherited-props baseline available\n * BEFORE each matching tween, plus the final cumulative state at the split point.\n * A tween contributes to later baselines when it ends at/before the split (full\n * props or last keyframe), spans the split via keyframes (kfs at/before split),\n * or spans the split as a flat tween (its interpolated midpoint). Decoupled from\n * the reverse write loop so the spanning-tween midpoint reads earlier tweens.\n */\n// fallow-ignore-next-line complexity\nfunction computeForwardBaselines(\n matching: GsapAnimation[],\n splitTime: number,\n): { before: Array<Record<string, number | string>>; final: Record<string, number | string> } {\n const before: Array<Record<string, number | string>> = [];\n const acc: Record<string, number | string> = {};\n for (const anim of matching) {\n before.push({ ...acc });\n const pos = typeof anim.position === \"number\" ? anim.position : 0;\n const dur = anim.duration ?? 0;\n const animEnd = pos + dur;\n\n if (anim.keyframes) {\n const kfs = anim.keyframes.keyframes;\n if (pos >= splitTime) {\n // Moves wholly to the new element — contributes nothing to the baseline.\n } else if (animEnd > splitTime) {\n for (const kf of kfs) {\n const kfTime = pos + (kf.percentage / 100) * dur;\n if (kfTime <= splitTime) {\n for (const [k, v] of Object.entries(kf.properties)) acc[k] = v;\n }\n }\n } else {\n const lastKf = kfs[kfs.length - 1];\n if (lastKf) {\n for (const [k, v] of Object.entries(lastKf.properties)) acc[k] = v;\n }\n }\n continue;\n }\n\n if (animEnd <= splitTime) {\n for (const [k, v] of Object.entries(anim.properties)) acc[k] = v;\n continue;\n }\n\n if (pos >= splitTime) continue;\n\n // Flat tween spanning the split — its midpoint becomes the inherited value.\n const progress = dur > 0 ? (splitTime - pos) / dur : 0;\n const fromSource = anim.fromProperties ?? acc;\n for (const [k, v] of Object.entries(anim.properties)) {\n if (typeof v !== \"number\") {\n acc[k] = v;\n continue;\n }\n const fromVal = typeof fromSource[k] === \"number\" ? (fromSource[k] as number) : 0;\n acc[k] = fromVal + (v - fromVal) * progress;\n }\n }\n return { before, final: { ...acc } };\n}\n\n// Split one tween that straddles the split point: trim the original to the\n// first half (interpolated midpoint as its new end) and add a fromTo for the\n// second half on the new element. `fromSource` is the forward baseline.\nfunction buildSpanningSplit(\n result: string,\n anim: GsapAnimation,\n pos: number,\n dur: number,\n fromSource: Record<string, number | string>,\n ctx: { splitTime: number; newSelector: string; newElementStart: number },\n): string {\n const progress = dur > 0 ? (ctx.splitTime - pos) / dur : 0;\n const midProps: Record<string, number | string> = {};\n for (const [k, v] of Object.entries(anim.properties)) {\n if (typeof v !== \"number\") {\n midProps[k] = v;\n continue;\n }\n const fromVal = typeof fromSource[k] === \"number\" ? (fromSource[k] as number) : 0;\n midProps[k] = fromVal + (v - fromVal) * progress;\n }\n const trimmed = updateAnimationInScript(result, anim.id, {\n duration: ctx.splitTime - pos,\n properties: midProps,\n });\n return addAnimationToScript(trimmed, {\n targetSelector: ctx.newSelector,\n method: \"fromTo\",\n position: ctx.newElementStart,\n duration: pos + dur - ctx.splitTime,\n properties: { ...anim.properties },\n fromProperties: { ...midProps },\n ease: anim.ease,\n extras: anim.extras,\n }).script;\n}\n\ntype SplitCtx = {\n splitTime: number;\n originalSelector: string;\n newSelector: string;\n newElementStart: number;\n};\n\n// Decide what one matching tween does at the split point: move to the new\n// element (wholly after), stay (wholly before / keyframes before), get skipped\n// (keyframes spanning), or get interpolated in half (spanning). Returns the\n// updated script; pushes any skip reason into `skippedSelectors`.\nfunction applyTweenSplit(\n result: string,\n anim: GsapAnimation,\n baselineBefore: Record<string, number | string>,\n ctx: SplitCtx,\n skippedSelectors: string[],\n): string {\n const pos = typeof anim.position === \"number\" ? anim.position : 0;\n const dur = anim.duration ?? 0;\n const animEnd = pos + dur;\n\n if (anim.keyframes) {\n if (pos >= ctx.splitTime)\n return updateAnimationSelectorInScript(result, anim.id, ctx.newSelector);\n if (animEnd > ctx.splitTime) {\n skippedSelectors.push(`${ctx.originalSelector} (keyframes spanning split)`);\n }\n // Inherited-state for kf tweens is handled by computeForwardBaselines.\n return result;\n }\n // Wholly before the split — kept on the original element.\n if (animEnd <= ctx.splitTime) return result;\n // Wholly after — move to the new element.\n if (pos >= ctx.splitTime)\n return updateAnimationSelectorInScript(result, anim.id, ctx.newSelector);\n // Spans the split — interpolate the midpoint from the FORWARD baseline.\n const fromSource = anim.fromProperties ?? baselineBefore;\n return buildSpanningSplit(result, anim, pos, dur, fromSource, ctx);\n}\n\nexport function splitAnimationsInScript(\n script: string,\n opts: SplitAnimationsOptions,\n): SplitAnimationsResult {\n const parsed = parseGsapScriptAcornForWrite(script);\n if (!parsed) return { script, skippedSelectors: [] };\n\n const originalSelector = `#${opts.originalId}`;\n const newSelector = `#${opts.newId}`;\n\n const animations = parsed.located.map((l) => l.animation);\n const skippedSelectors: string[] = [];\n\n for (const a of animations) {\n if (a.targetSelector !== originalSelector && a.targetSelector.includes(opts.originalId)) {\n skippedSelectors.push(a.targetSelector);\n }\n }\n\n const matching = animations.filter((a) => a.targetSelector === originalSelector);\n if (matching.length === 0) return { script, skippedSelectors };\n\n let result = script;\n const newElementStart = opts.splitTime;\n\n // Forward pre-pass: compute the inherited-props baseline available BEFORE each\n // matching tween, in source/timeline order. The write loop below runs in\n // REVERSE (so updateAnimationSelectorInScript's selector edits can't shift the\n // count-based IDs of not-yet-processed tweens), but the spanning-tween midpoint\n // interpolation needs the baseline from EARLIER tweens — which a reverse\n // accumulator hasn't seen yet. Decoupling the two fixes the wrong midpoint.\n const { before: baselineBefore, final: finalInheritedProps } = computeForwardBaselines(\n matching,\n opts.splitTime,\n );\n\n // Reverse iteration: updateAnimationSelectorInScript mutates selectors which\n // can shift count-based ID suffixes for later animations.\n const ctx = { splitTime: opts.splitTime, originalSelector, newSelector, newElementStart };\n for (let i = matching.length - 1; i >= 0; i--) {\n const anim = matching[i];\n if (!anim) continue;\n result = applyTweenSplit(result, anim, baselineBefore[i] ?? {}, ctx, skippedSelectors);\n }\n\n if (Object.keys(finalInheritedProps).length > 0) {\n result = insertInheritedStateSetInScript(\n result,\n newSelector,\n newElementStart,\n finalInheritedProps,\n );\n }\n\n return { script: result, skippedSelectors };\n}\n\n// ── Unroll dynamic animations ────────────────────────────────────────────────\n\nfunction isLoopNode(node: Node): boolean {\n const t = node?.type;\n return (\n t === \"ForStatement\" ||\n t === \"ForInStatement\" ||\n t === \"ForOfStatement\" ||\n t === \"WhileStatement\"\n );\n}\n\nfunction isForEachStatement(node: Node): boolean {\n return (\n node?.type === \"ExpressionStatement\" &&\n node.expression?.type === \"CallExpression\" &&\n node.expression.callee?.property?.name === \"forEach\"\n );\n}\n\n/** The nearest enclosing loop / forEach AST node (not just its byte range). */\nfunction findEnclosingLoopNode(ancestors: Node[]): Node | null {\n for (let i = ancestors.length - 2; i >= 0; i--) {\n const node = ancestors[i];\n if (isLoopNode(node) || isForEachStatement(node)) return node;\n }\n return null;\n}\n\n/** Statements making up a loop's body block, or null when not a simple block. */\nfunction loopBodyStatements(loopNode: Node): Node[] | null {\n let body: Node;\n if (loopNode?.type === \"ExpressionStatement\") {\n // forEach(cb): body is the callback's block.\n const cb = loopNode.expression?.arguments?.[0];\n body = cb?.body;\n } else {\n body = loopNode?.body;\n }\n if (body?.type !== \"BlockStatement\") return null;\n return (body.body ?? []).filter((s: Node) => s?.type === \"ExpressionStatement\");\n}\n\n/** The loop's index identifier name (`for (let i …)`), used for per-iteration substitution. */\nfunction loopIndexVarName(loopNode: Node): string | null {\n if (loopNode?.type === \"ForStatement\") {\n const decl = loopNode.init?.declarations?.[0];\n return typeof decl?.id?.name === \"string\" ? decl.id.name : null;\n }\n return null;\n}\n\n/**\n * Rewrite one body statement's source for iteration `idx`: replace USES of the\n * loop index variable (AST Identifier nodes) with the literal index. AST-based,\n * not a text regex, so the index name appearing inside a string literal (e.g. a\n * selector \".row-i\") or as a non-computed member/key (`obj.i`, `{ i: … }`) is\n * left untouched — only real references to the variable are substituted.\n */\n// An identifier in \"binding position\" is a name, not a value reference: a\n// non-computed member property (`obj.i`) or object-literal key (`{ i: … }`).\n// Those must NOT be substituted with the iteration index.\nfunction isIndexBindingPosition(node: Node, parent: Node): boolean {\n if (parent?.type === \"MemberExpression\") return parent.property === node && !parent.computed;\n if (parent?.type === \"Property\" || parent?.type === \"ObjectProperty\") {\n return parent.key === node && !parent.computed;\n }\n return false;\n}\n\nfunction substituteLoopIndex(stmt: Node, indexVar: string, idx: number, script: string): string {\n const base = stmt.start as number;\n const src = script.slice(base, stmt.end as number);\n const ranges: Array<[number, number]> = [];\n acornWalk.ancestor(stmt, {\n Identifier(node: Node, _state: unknown, ancestors: Node[]) {\n if (node.name !== indexVar) return;\n if (isIndexBindingPosition(node, ancestors[ancestors.length - 2])) return;\n ranges.push([(node.start as number) - base, (node.end as number) - base]);\n },\n });\n if (ranges.length === 0) return src;\n ranges.sort((a, b) => b[0] - a[0]);\n let out = src;\n for (const [s, e] of ranges) out = out.slice(0, s) + String(idx) + out.slice(e);\n return out;\n}\n\nfunction buildUnrollReplacement(\n timelineVar: string,\n animation: GsapAnimation,\n elements: Array<{\n selector: string;\n keyframes: Array<{ percentage: number; properties: Record<string, number | string> }>;\n easeEach?: string;\n }>,\n): string {\n const duration = typeof animation.duration === \"number\" ? animation.duration : 8;\n const ease = typeof animation.ease === \"string\" ? animation.ease : \"none\";\n const pos = animation.position ?? 0;\n const posCode = typeof pos === \"number\" ? String(pos) : JSON.stringify(pos);\n const calls = elements.map((el) => {\n const sorted = [...el.keyframes].sort((a, b) => a.percentage - b.percentage);\n const kfCode = buildKeyframeObjectCode(sorted, el.easeEach);\n return `${timelineVar}.to(${JSON.stringify(el.selector)}, { keyframes: ${kfCode}, duration: ${duration}, ease: ${JSON.stringify(ease)} }, ${posCode});`;\n });\n return calls.join(\"\\n \");\n}\n\nexport type UnrollElement = {\n selector: string;\n keyframes: Array<{ percentage: number; properties: Record<string, number | string> }>;\n easeEach?: string;\n};\n\n/** Build one element's unrolled `tl.to(...)` call from the target animation. */\nfunction buildUnrollCallForElement(\n timelineVar: string,\n animation: GsapAnimation,\n el: UnrollElement,\n): string {\n const duration = typeof animation.duration === \"number\" ? animation.duration : 8;\n const ease = typeof animation.ease === \"string\" ? animation.ease : \"none\";\n const pos = animation.position ?? 0;\n const posCode = typeof pos === \"number\" ? String(pos) : JSON.stringify(pos);\n const sorted = [...el.keyframes].sort((a, b) => a.percentage - b.percentage);\n const kfCode = buildKeyframeObjectCode(sorted, el.easeEach);\n return `${timelineVar}.to(${JSON.stringify(el.selector)}, { keyframes: ${kfCode}, duration: ${duration}, ease: ${JSON.stringify(ease)} }, ${posCode});`;\n}\n\n/** Sentinel: the unroll cannot safely reproduce the loop body — caller no-ops. */\nconst REFUSE_UNROLL = Symbol(\"refuse-unroll\");\n\n/** Every statement in a loop's body block (unfiltered), or [] when not a block. */\nfunction loopBodyRawStatements(loopNode: Node): Node[] {\n const body =\n loopNode?.type === \"ExpressionStatement\"\n ? loopNode.expression?.arguments?.[0]?.body\n : loopNode?.body;\n return body?.type === \"BlockStatement\" ? (body.body ?? []) : [];\n}\n\n/** A node that re-binds `indexVar`: a re-declaration or a function param. */\nfunction rebindsIndex(node: Node, indexVar: string): boolean {\n if (node.type === \"VariableDeclarator\") return node.id?.name === indexVar;\n if (\n node.type === \"FunctionExpression\" ||\n node.type === \"FunctionDeclaration\" ||\n node.type === \"ArrowFunctionExpression\"\n ) {\n return (node.params ?? []).some((p: Node) => p?.name === indexVar);\n }\n return false;\n}\n\n/** Object shorthand `{ i }` — substituting the value would yield invalid `{ 0 }`. */\nfunction isShorthandIndexUse(node: Node, indexVar: string): boolean {\n return (\n (node.type === \"Property\" || node.type === \"ObjectProperty\") &&\n node.shorthand === true &&\n propKeyName(node) === indexVar\n );\n}\n\n/**\n * A sibling statement can't be safely index-substituted when it re-binds the\n * loop index (shadowing — a nested `for (let i …)`, a callback param `i`) or\n * uses it in object shorthand (`{ i }`, which would splice to the invalid\n * `{ 0 }`). substituteLoopIndex has no scope analysis, so in these cases it\n * would emit broken or wrong code — the unroll must refuse instead.\n */\nfunction hasUnsafeLoopIndexUse(stmt: Node, indexVar: string): boolean {\n let unsafe = false;\n acornWalk.full(stmt, (node: Node) => {\n if (!unsafe && (isShorthandIndexUse(node, indexVar) || rebindsIndex(node, indexVar))) {\n unsafe = true;\n }\n });\n return unsafe;\n}\n\n/** How to handle the loop body's non-target siblings when unrolling. */\nfunction unrollSiblingStrategy(\n loopNode: Node,\n targetStmt: Node,\n stmts: Node[],\n indexVar: string | null,\n): \"blanket\" | \"refuse\" | \"preserve\" {\n const siblings = stmts.filter((s) => s !== targetStmt);\n // A sibling the filtered statement list doesn't model (non-ExpressionStatement)\n // would be silently lost by either path — refuse if any exists.\n const hasUnmodeledSibling = loopBodyRawStatements(loopNode).some(\n (s) => s !== targetStmt && !stmts.includes(s),\n );\n if (siblings.length === 0 && !hasUnmodeledSibling) return \"blanket\";\n if (hasUnmodeledSibling || !indexVar) return \"refuse\";\n return siblings.some((s) => hasUnsafeLoopIndexUse(s, indexVar)) ? \"refuse\" : \"preserve\";\n}\n\n/** Emit the per-iteration unrolled lines (target → static tl.to, siblings → index-substituted). */\nfunction emitUnrolledLines(\n stmts: Node[],\n targetStmt: Node,\n elements: UnrollElement[],\n timelineVar: string,\n animation: GsapAnimation,\n indexVar: string,\n script: string,\n): string {\n const lines: string[] = [];\n for (let idx = 0; idx < elements.length; idx++) {\n const el = elements[idx];\n if (!el) continue;\n for (const stmt of stmts) {\n lines.push(\n stmt === targetStmt\n ? buildUnrollCallForElement(timelineVar, animation, el)\n : substituteLoopIndex(stmt, indexVar, idx, script),\n );\n }\n }\n return lines.join(\"\\n \");\n}\n\n/**\n * Unroll the loop body, preserving every statement that is NOT the target tween.\n * For each iteration, emit each non-target statement with the loop index\n * substituted (e.g. `tl.set(items[i], …)` → `tl.set(items[0], …)`), and replace\n * the target tween statement with that element's static `tl.to()` call.\n *\n * Returns null when a blanket overwrite is lossless (no sibling statements), and\n * REFUSE_UNROLL when siblings exist but can't be safely reproduced — a non-`for`\n * loop (no numeric index to splice), a statement we don't model, or an unsafe\n * index use (shadowing / shorthand). Refusing no-ops the unroll, which is safe:\n * the dynamic loop keeps rendering correctly, just un-flattened.\n */\nfunction buildLoopUnrollPreserving(\n script: string,\n timelineVar: string,\n animation: GsapAnimation,\n elements: UnrollElement[],\n loopNode: Node,\n targetStmt: Node,\n): string | null | typeof REFUSE_UNROLL {\n const stmts = loopBodyStatements(loopNode);\n if (!stmts || !stmts.includes(targetStmt)) return null;\n const indexVar = loopIndexVarName(loopNode);\n const strategy = unrollSiblingStrategy(loopNode, targetStmt, stmts, indexVar);\n if (strategy === \"blanket\") return null;\n if (strategy === \"refuse\" || !indexVar) return REFUSE_UNROLL;\n return emitUnrolledLines(stmts, targetStmt, elements, timelineVar, animation, indexVar, script);\n}\n\n/**\n * Replace a dynamic loop that generates multiple tween calls with individual\n * static `tl.to()` calls — one per element. Finds the loop containing the\n * animation and replaces the loop with unrolled static calls, preserving every\n * non-target statement in the loop body per iteration.\n */\nexport function unrollDynamicAnimations(\n script: string,\n animationId: string,\n elements: UnrollElement[],\n): string {\n // An empty element list has no unrolled form — replacing the loop/statement\n // with zero calls would silently delete the animation. No-op instead.\n if (elements.length === 0) return script;\n const parsed = parseGsapScriptAcornForWrite(script);\n if (!parsed) return script;\n const target = parsed.located.find((l) => l.id === animationId);\n if (!target) return script;\n\n const ms = new MagicString(script);\n const loopNode = findEnclosingLoopNode(target.call.ancestors);\n if (loopNode) {\n const targetStmt = findEnclosingExpressionStatement(target.call.ancestors);\n const preserving = targetStmt\n ? buildLoopUnrollPreserving(\n script,\n parsed.timelineVar,\n target.animation,\n elements,\n loopNode,\n targetStmt,\n )\n : null;\n // Siblings exist but can't be safely reproduced — leave the loop untouched\n // rather than drop or corrupt them. The op no-ops (before === after).\n if (preserving === REFUSE_UNROLL) return script;\n // Fall back to the simple whole-body replacement when the body isn't a plain\n // block of statements we can preserve.\n const replacement =\n preserving ?? buildUnrollReplacement(parsed.timelineVar, target.animation, elements);\n ms.overwrite(loopNode.start as number, loopNode.end as number, replacement);\n } else {\n const stmt = findEnclosingExpressionStatement(target.call.ancestors);\n if (!stmt) return script;\n const replacement = buildUnrollReplacement(parsed.timelineVar, target.animation, elements);\n ms.overwrite(stmt.start as number, stmt.end as number, replacement);\n }\n return ms.toString();\n}\n","import type {\n TimelineElement,\n TimelineElementType,\n TimelineMediaElement,\n TimelineTextElement,\n TimelineCompositionElement,\n CanvasResolution,\n Keyframe,\n KeyframeProperties,\n StageZoomKeyframe,\n CompositionVariable,\n ValidationResult,\n} from \"./types.js\";\nimport { validateCompositionGsap } from \"./gsapSerialize\";\nimport { ensureHfIds } from \"./hfIds.js\";\nimport { parseGsapScriptAcornForWrite } from \"./gsapParserAcorn.js\";\nimport { queryByAttr } from \"./utils/cssSelector.js\";\nimport { removeAnimationFromScript } from \"./gsapWriterAcorn.js\";\n\nconst MEDIA_TYPES = new Set<string>([\"video\", \"image\", \"audio\"]);\n\nexport interface ParsedHtml {\n elements: TimelineElement[];\n gsapScript: string | null;\n styles: string | null;\n resolution: CanvasResolution;\n keyframes: Record<string, Keyframe[]>;\n stageZoomKeyframes: StageZoomKeyframe[];\n}\n\nfunction getElementType(el: Element): TimelineElementType | null {\n const tag = el.tagName.toLowerCase();\n if (tag === \"video\") return \"video\";\n if (tag === \"img\") return \"image\";\n if (tag === \"audio\") return \"audio\";\n // Check for explicit data-type attribute first\n const dataType = el.getAttribute(\"data-type\");\n if (dataType === \"composition\") return \"composition\";\n if (dataType === \"text\") return \"text\";\n // Fall back to tag-based detection for backwards compatibility\n if (\n tag === \"div\" ||\n tag === \"p\" ||\n tag === \"h1\" ||\n tag === \"h2\" ||\n tag === \"h3\" ||\n tag === \"span\"\n ) {\n return \"text\";\n }\n return null;\n}\n\nfunction getElementName(el: Element): string {\n const dataName = el.getAttribute(\"data-name\");\n if (dataName) return dataName;\n\n const type = getElementType(el);\n if (type === \"text\") {\n const text = el.textContent?.trim().slice(0, 30) || \"Text\";\n return text.length === 30 ? text + \"...\" : text;\n }\n\n const src = el.getAttribute(\"src\");\n if (src) {\n const filename = src.split(\"/\").pop() || src;\n return filename.split(\"?\")[0] ?? filename;\n }\n\n return el.id || el.className?.toString().split(\" \")[0] || \"Element\";\n}\n\nfunction getZIndex(el: Element): number {\n const dataLayer = el.getAttribute(\"data-layer\");\n if (dataLayer) return parseInt(dataLayer, 10) || 0;\n\n const style = (el as HTMLElement).style?.zIndex;\n if (style) return parseInt(style, 10) || 0;\n\n return 0;\n}\n\nfunction parseResolutionFromCss(doc: Document, cssText: string | null): CanvasResolution {\n const stage = doc.getElementById(\"stage\") || doc.querySelector(\"#stage\");\n if (stage) {\n const inlineStyle = (stage as HTMLElement).style;\n if (inlineStyle?.width && inlineStyle?.height) {\n const w = parseInt(inlineStyle.width, 10);\n const h = parseInt(inlineStyle.height, 10);\n if (w && h) {\n return resolveResolutionFromDimensions(w, h);\n }\n }\n }\n\n if (cssText) {\n const stageMatch = cssText.match(\n /#stage\\s*\\{[^}]*width:\\s*(\\d+)px[^}]*height:\\s*(\\d+)px[^}]*\\}/,\n );\n if (stageMatch) {\n const w = parseInt(stageMatch[1] ?? \"\", 10);\n const h = parseInt(stageMatch[2] ?? \"\", 10);\n return resolveResolutionFromDimensions(w, h);\n }\n const stageMatchReverse = cssText.match(\n /#stage\\s*\\{[^}]*height:\\s*(\\d+)px[^}]*width:\\s*(\\d+)px[^}]*\\}/,\n );\n if (stageMatchReverse) {\n const h = parseInt(stageMatchReverse[1] ?? \"\", 10);\n const w = parseInt(stageMatchReverse[2] ?? \"\", 10);\n return resolveResolutionFromDimensions(w, h);\n }\n }\n\n return \"portrait\";\n}\n\nfunction parseResolutionFromHtml(doc: Document): CanvasResolution | null {\n const htmlEl = doc.documentElement;\n const resolutionAttr = htmlEl.getAttribute(\"data-resolution\");\n if (\n resolutionAttr === \"landscape\" ||\n resolutionAttr === \"portrait\" ||\n resolutionAttr === \"landscape-4k\" ||\n resolutionAttr === \"portrait-4k\" ||\n resolutionAttr === \"square\" ||\n resolutionAttr === \"square-4k\"\n ) {\n return resolutionAttr;\n }\n\n const widthAttr = htmlEl.getAttribute(\"data-composition-width\");\n const heightAttr = htmlEl.getAttribute(\"data-composition-height\");\n if (widthAttr && heightAttr) {\n const width = parseInt(widthAttr, 10);\n const height = parseInt(heightAttr, 10);\n if (width && height) {\n return resolveResolutionFromDimensions(width, height);\n }\n }\n\n return null;\n}\n\nconst UHD_SQUARE_MIN = 2160;\nconst UHD_RECT_MIN = 3840;\n\nfunction resolveResolutionFromDimensions(width: number, height: number): CanvasResolution {\n const longSide = Math.max(width, height);\n if (width === height) {\n return longSide >= UHD_SQUARE_MIN ? \"square-4k\" : \"square\";\n }\n const isLandscape = width > height;\n const isUhd = longSide >= UHD_RECT_MIN;\n if (isLandscape) return isUhd ? \"landscape-4k\" : \"landscape\";\n return isUhd ? \"portrait-4k\" : \"portrait\";\n}\n\nexport function parseHtml(html: string): ParsedHtml {\n const withIds = ensureHfIds(html);\n const parser = new DOMParser();\n const doc = parser.parseFromString(withIds, \"text/html\");\n\n const elements: TimelineElement[] = [];\n const keyframes: Record<string, Keyframe[]> = {};\n let idCounter = 0;\n\n const htmlEl = doc.documentElement;\n const customStylesAttr = htmlEl.getAttribute(\"data-custom-styles\");\n let customStyles: string | null = null;\n if (customStylesAttr) {\n try {\n customStyles = JSON.parse(customStylesAttr);\n } catch {\n customStyles = customStylesAttr;\n }\n }\n\n const timedElements = doc.querySelectorAll(\"[data-start]\");\n\n timedElements.forEach((el) => {\n const type = getElementType(el);\n if (!type) return;\n\n const start = parseFloat(el.getAttribute(\"data-start\") || \"0\");\n const dataEnd = el.getAttribute(\"data-end\");\n\n let duration: number;\n if (dataEnd) {\n duration = Math.max(0, parseFloat(dataEnd) - start);\n } else {\n duration = 5;\n }\n\n // R1: stable hf- id minted by ensureHfIds above; clips just read it.\n // Legacy/migration note: ensureHfIds pins a pre-existing `data-hf-id`, and\n // the generator emits `data-hf-id=\"${element.id}\"`. So a clip authored\n // before R1 with `id=\"my-title\"` round-trips as `data-hf-id=\"my-title\"` —\n // a non-`hf-`-shaped but still stable, exact-match handle. This is safe\n // indefinitely: targeting uses exact `[data-hf-id=\"…\"]` match (it does not\n // require the hf- prefix). ensureHfIds skips elements that already carry\n // data-hf-id, so legacy values are NOT re-minted automatically — they\n // persist until the user re-saves the composition through Studio. Not a bug.\n const id = el.getAttribute(\"data-hf-id\") || el.id || `element-${++idCounter}`;\n const name = getElementName(el);\n const zIndex = getZIndex(el);\n\n // Parse data-keyframes attribute if present\n const keyframesAttr = el.getAttribute(\"data-keyframes\");\n if (keyframesAttr) {\n try {\n const parsedKeyframes = JSON.parse(keyframesAttr);\n if (Array.isArray(parsedKeyframes) && parsedKeyframes.length > 0) {\n keyframes[id] = parsedKeyframes;\n }\n } catch {\n // skip invalid keyframes\n }\n }\n\n // Parse transform properties (x, y, scale, opacity)\n const xAttr = el.getAttribute(\"data-x\");\n const yAttr = el.getAttribute(\"data-y\");\n const scaleAttr = el.getAttribute(\"data-scale\");\n const opacityAttr = el.getAttribute(\"data-opacity\");\n const x = xAttr ? parseFloat(xAttr) : undefined;\n const y = yAttr ? parseFloat(yAttr) : undefined;\n const scale = scaleAttr ? parseFloat(scaleAttr) : undefined;\n const opacity = opacityAttr ? parseFloat(opacityAttr) : undefined;\n\n if (type === \"text\") {\n const textEl = el.firstElementChild;\n const content = textEl?.textContent || name;\n const color = el.getAttribute(\"data-color\") || undefined;\n const fontSizeAttr = el.getAttribute(\"data-font-size\");\n const fontSize = fontSizeAttr ? parseInt(fontSizeAttr, 10) : undefined;\n const fontWeightAttr = el.getAttribute(\"data-font-weight\");\n const fontWeight = fontWeightAttr ? parseInt(fontWeightAttr, 10) : undefined;\n const fontFamily = el.getAttribute(\"data-font-family\") || undefined;\n const textShadowAttr = el.getAttribute(\"data-text-shadow\");\n const textShadow = textShadowAttr === \"false\" ? false : undefined;\n\n // Parse outline properties\n const textOutlineAttr = el.getAttribute(\"data-text-outline\");\n const textOutline = textOutlineAttr === \"true\" ? true : undefined;\n const textOutlineColor = el.getAttribute(\"data-text-outline-color\") || undefined;\n const textOutlineWidthAttr = el.getAttribute(\"data-text-outline-width\");\n const textOutlineWidth = textOutlineWidthAttr\n ? parseInt(textOutlineWidthAttr, 10)\n : undefined;\n\n // Parse highlight properties\n const textHighlightAttr = el.getAttribute(\"data-text-highlight\");\n const textHighlight = textHighlightAttr === \"true\" ? true : undefined;\n const textHighlightColor = el.getAttribute(\"data-text-highlight-color\") || undefined;\n const textHighlightPaddingAttr = el.getAttribute(\"data-text-highlight-padding\");\n const textHighlightPadding = textHighlightPaddingAttr\n ? parseInt(textHighlightPaddingAttr, 10)\n : undefined;\n const textHighlightRadiusAttr = el.getAttribute(\"data-text-highlight-radius\");\n const textHighlightRadius = textHighlightRadiusAttr\n ? parseInt(textHighlightRadiusAttr, 10)\n : undefined;\n\n const textElement: TimelineTextElement = {\n id,\n type: \"text\",\n name,\n content,\n startTime: start,\n duration,\n zIndex,\n x,\n y,\n scale,\n opacity,\n color,\n fontSize,\n fontWeight,\n fontFamily,\n textShadow,\n textOutline,\n textOutlineColor,\n textOutlineWidth,\n textHighlight,\n textHighlightColor,\n textHighlightPadding,\n textHighlightRadius,\n };\n elements.push(textElement);\n } else if (type === \"composition\") {\n // Composition is a div container with iframe inside\n const iframe = el.querySelector(\"iframe\");\n const src = iframe?.getAttribute(\"src\") || el.getAttribute(\"src\") || \"\";\n const compositionId = el.getAttribute(\"data-composition-id\") || \"\";\n const sourceDurationAttr = el.getAttribute(\"data-source-duration\");\n const sourceDuration = sourceDurationAttr ? parseFloat(sourceDurationAttr) : undefined;\n const sourceWidthAttr = el.getAttribute(\"data-source-width\");\n const sourceWidth = sourceWidthAttr ? parseInt(sourceWidthAttr, 10) : undefined;\n const sourceHeightAttr = el.getAttribute(\"data-source-height\");\n const sourceHeight = sourceHeightAttr ? parseInt(sourceHeightAttr, 10) : undefined;\n\n // Parse variable values if present\n const variableValuesAttr = el.getAttribute(\"data-variable-values\");\n let variableValues: Record<string, string | number | boolean> | undefined;\n if (variableValuesAttr) {\n try {\n variableValues = JSON.parse(variableValuesAttr);\n } catch {\n // skip invalid variable values\n }\n }\n\n const compositionElement: TimelineCompositionElement = {\n id,\n type: \"composition\",\n name,\n src,\n compositionId,\n startTime: start,\n duration,\n zIndex,\n x,\n y,\n scale,\n opacity,\n sourceDuration,\n sourceWidth,\n sourceHeight,\n variableValues,\n };\n elements.push(compositionElement);\n } else {\n if (!MEDIA_TYPES.has(type)) return;\n\n const src = el.getAttribute(\"src\") || \"\";\n const mediaStartTimeAttr = el.getAttribute(\"data-media-start\");\n const mediaStartTime = mediaStartTimeAttr ? parseFloat(mediaStartTimeAttr) : undefined;\n const sourceDurationAttr = el.getAttribute(\"data-source-duration\");\n const sourceDuration = sourceDurationAttr ? parseFloat(sourceDurationAttr) : undefined;\n const isArollAttr = el.getAttribute(\"data-aroll\");\n const isAroll = isArollAttr === \"true\" ? true : undefined;\n const volumeAttr = el.getAttribute(\"data-volume\");\n const volume = volumeAttr ? parseFloat(volumeAttr) : undefined;\n const hasAudioAttr = el.getAttribute(\"data-has-audio\");\n const hasAudio = hasAudioAttr === \"true\" ? true : undefined;\n\n const mediaElement: TimelineMediaElement = {\n id,\n type: type as \"video\" | \"image\" | \"audio\",\n name,\n src,\n startTime: start,\n duration,\n zIndex,\n x,\n y,\n scale,\n opacity,\n mediaStartTime,\n sourceDuration,\n isAroll,\n volume,\n hasAudio,\n };\n elements.push(mediaElement);\n }\n });\n\n const scriptTags = doc.querySelectorAll(\"script\");\n let gsapScript: string | null = null;\n\n for (const script of scriptTags) {\n const src = script.getAttribute(\"src\");\n if (src && src.includes(\"gsap\")) continue;\n\n const content = script.textContent?.trim();\n if (content && (content.includes(\"gsap\") || content.includes(\"timeline\"))) {\n gsapScript = content;\n break;\n }\n }\n\n // Normalize keyframes (clamp negative time, convert absolute -> relative if detected)\n for (const element of elements) {\n const elementKeyframes = keyframes[element.id];\n if (!elementKeyframes || elementKeyframes.length === 0) continue;\n\n const baseX = element.x ?? 0;\n const baseY = element.y ?? 0;\n const baseScale =\n element.type === \"video\" || element.type === \"image\" || element.type === \"composition\"\n ? ((element as TimelineMediaElement | TimelineCompositionElement).scale ?? 1)\n : 1;\n\n keyframes[element.id] = normalizeKeyframes(elementKeyframes, baseX, baseY, baseScale);\n }\n\n const styleTags = doc.querySelectorAll(\"style\");\n const allStyles =\n Array.from(styleTags)\n .map((s) => s.textContent?.trim())\n .filter(Boolean)\n .join(\"\\n\\n\") || null;\n\n const customStyleTags = Array.from(styleTags).filter(\n (s) => s.getAttribute(\"data-hf-custom\") === \"true\",\n );\n const customStylesFromTags =\n customStyleTags\n .map((s) => s.textContent?.trim())\n .filter(Boolean)\n .join(\"\\n\\n\") || null;\n\n const styles = customStyles ?? customStylesFromTags ?? null;\n\n const resolution = parseResolutionFromHtml(doc) ?? parseResolutionFromCss(doc, allStyles);\n\n // Parse stage zoom keyframes from zoom container\n const stageZoomKeyframes = parseStageZoomKeyframes(doc);\n\n return {\n elements,\n gsapScript,\n styles,\n resolution,\n keyframes,\n stageZoomKeyframes,\n };\n}\n\nfunction parseStageZoomKeyframes(doc: Document): StageZoomKeyframe[] {\n const zoomContainer = doc.getElementById(\"stage-zoom-container\");\n if (!zoomContainer) {\n return [];\n }\n\n const zoomKeyframesAttr = zoomContainer.getAttribute(\"data-zoom-keyframes\");\n if (!zoomKeyframesAttr) {\n return [];\n }\n\n try {\n const parsed = JSON.parse(zoomKeyframesAttr);\n if (Array.isArray(parsed)) {\n return parsed.filter(\n (kf): kf is StageZoomKeyframe =>\n typeof kf === \"object\" &&\n kf !== null &&\n typeof kf.id === \"string\" &&\n typeof kf.time === \"number\" &&\n typeof kf.zoom === \"object\" &&\n kf.zoom !== null &&\n typeof kf.zoom.scale === \"number\" &&\n typeof kf.zoom.focusX === \"number\" &&\n typeof kf.zoom.focusY === \"number\",\n );\n }\n } catch {\n // skip invalid zoom keyframes\n }\n\n return [];\n}\n\nfunction normalizeKeyframes(\n keyframes: Keyframe[],\n baseX: number,\n baseY: number,\n baseScale: number,\n): Keyframe[] {\n const timeEpsilon = 0.001;\n const valueEpsilon = 0.00001;\n\n const hasBaseCheck = (value: number | undefined, base: number): boolean =>\n value !== undefined && Math.abs(value - base) <= valueEpsilon && Math.abs(base) > valueEpsilon;\n\n const timeZeroKeyframes = keyframes.filter((kf) => Math.abs(kf.time) <= timeEpsilon);\n\n const treatAsAbsolute = timeZeroKeyframes.some((kf) => {\n const props = kf.properties || {};\n if (\n hasBaseCheck(props.x, baseX) ||\n hasBaseCheck(props.y, baseY) ||\n (baseScale !== 1 && hasBaseCheck(props.scale, baseScale))\n ) {\n return true;\n }\n return false;\n });\n\n return keyframes.map((kf) => {\n const normalizedProps: Partial<KeyframeProperties> = {};\n for (const [key, value] of Object.entries(kf.properties || {})) {\n if (typeof value !== \"number\") continue;\n if (treatAsAbsolute && key === \"x\") {\n normalizedProps.x = value - baseX;\n } else if (treatAsAbsolute && key === \"y\") {\n normalizedProps.y = value - baseY;\n } else if (treatAsAbsolute && key === \"scale\") {\n normalizedProps.scale = baseScale !== 0 ? value / baseScale : value;\n } else {\n (normalizedProps as Record<string, number>)[key] = value;\n }\n }\n\n return {\n ...kf,\n time: Math.max(0, kf.time),\n properties: normalizedProps,\n };\n });\n}\n\nexport function updateElementInHtml(\n html: string,\n elementId: string,\n updates: Partial<TimelineElement>,\n): string {\n const parser = new DOMParser();\n const doc = parser.parseFromString(html, \"text/html\");\n\n const el = doc.getElementById(elementId) || queryByAttr(doc, \"data-name\", elementId);\n if (!el) return html;\n\n if (updates.startTime !== undefined) {\n el.setAttribute(\"data-start\", String(updates.startTime));\n if (el.hasAttribute(\"data-end\") && updates.duration !== undefined) {\n el.setAttribute(\"data-end\", String(updates.startTime + updates.duration));\n }\n }\n\n if (updates.duration !== undefined) {\n const start = parseFloat(el.getAttribute(\"data-start\") || \"0\");\n el.setAttribute(\"data-end\", String(start + updates.duration));\n el.removeAttribute(\"data-duration\"); // Clean up legacy\n }\n\n if (updates.name !== undefined) {\n el.setAttribute(\"data-name\", updates.name);\n }\n\n if (updates.zIndex !== undefined) {\n el.setAttribute(\"data-layer\", String(updates.zIndex));\n }\n\n // Handle media-specific property\n if (\"src\" in updates && updates.src !== undefined) {\n el.setAttribute(\"src\", updates.src);\n }\n\n // Handle text-specific properties\n if (\"content\" in updates && updates.content !== undefined) {\n const textEl = el.firstElementChild;\n if (textEl) {\n textEl.textContent = updates.content;\n }\n }\n\n if (\"color\" in updates && updates.color !== undefined) {\n el.setAttribute(\"data-color\", updates.color);\n }\n\n if (\"fontSize\" in updates && updates.fontSize !== undefined) {\n el.setAttribute(\"data-font-size\", String(updates.fontSize));\n }\n\n if (\"textShadow\" in updates) {\n if (updates.textShadow === false) {\n el.setAttribute(\"data-text-shadow\", \"false\");\n } else {\n el.removeAttribute(\"data-text-shadow\");\n }\n }\n\n // Handle volume property for audio/video\n if (\"volume\" in updates) {\n if (updates.volume !== undefined && updates.volume !== 1) {\n el.setAttribute(\"data-volume\", String(updates.volume));\n } else {\n el.removeAttribute(\"data-volume\");\n }\n }\n\n // Handle hasAudio property for videos\n if (\"hasAudio\" in updates) {\n if (updates.hasAudio === true) {\n el.setAttribute(\"data-has-audio\", \"true\");\n } else {\n el.removeAttribute(\"data-has-audio\");\n }\n }\n\n return \"<!DOCTYPE html>\\n\" + doc.documentElement.outerHTML;\n}\n\nexport function addElementToHtml(\n html: string,\n element: Omit<TimelineElement, \"id\"> & { id?: string },\n): { html: string; id: string } {\n const parser = new DOMParser();\n const doc = parser.parseFromString(html, \"text/html\");\n\n // Prefer zoom container, fall back to stage, then container, then body\n const container =\n doc.querySelector(\"#stage-zoom-container\") ||\n doc.querySelector(\".container\") ||\n doc.querySelector(\"#stage\") ||\n doc.body;\n\n const id = element.id || `element-${Date.now()}`;\n\n let newEl: Element;\n\n function applyMediaAttrs(el: Element, mediaEl: TimelineMediaElement): void {\n if (mediaEl.src) el.setAttribute(\"src\", mediaEl.src);\n if (mediaEl.volume !== undefined && mediaEl.volume !== 1) {\n el.setAttribute(\"data-volume\", String(mediaEl.volume));\n }\n }\n\n switch (element.type) {\n case \"video\": {\n const mediaEl = element as TimelineMediaElement;\n newEl = doc.createElement(\"video\");\n newEl.setAttribute(\"muted\", \"\");\n newEl.setAttribute(\"playsinline\", \"\");\n applyMediaAttrs(newEl, mediaEl);\n if (mediaEl.hasAudio) {\n newEl.setAttribute(\"data-has-audio\", \"true\");\n }\n break;\n }\n case \"image\": {\n const mediaEl = element as TimelineMediaElement;\n newEl = doc.createElement(\"img\");\n if (mediaEl.src) newEl.setAttribute(\"src\", mediaEl.src);\n newEl.setAttribute(\"alt\", element.name);\n break;\n }\n case \"audio\": {\n const mediaEl = element as TimelineMediaElement;\n newEl = doc.createElement(\"audio\");\n applyMediaAttrs(newEl, mediaEl);\n break;\n }\n case \"text\":\n default: {\n const textEl = element as TimelineTextElement;\n newEl = doc.createElement(\"div\");\n const textContent = doc.createElement(\"div\");\n textContent.textContent = textEl.content || element.name;\n newEl.appendChild(textContent);\n if (textEl.color) {\n newEl.setAttribute(\"data-color\", textEl.color);\n }\n if (textEl.fontSize) {\n newEl.setAttribute(\"data-font-size\", String(textEl.fontSize));\n }\n break;\n }\n }\n\n newEl.id = id;\n newEl.setAttribute(\"data-start\", String(element.startTime));\n newEl.setAttribute(\"data-end\", String(element.startTime + element.duration));\n newEl.setAttribute(\"data-layer\", String(element.zIndex));\n newEl.setAttribute(\"data-name\", element.name);\n\n container.appendChild(newEl);\n\n return {\n html: \"<!DOCTYPE html>\\n\" + doc.documentElement.outerHTML,\n id,\n };\n}\n\nfunction selectorTargetsId(selector: string, id: string): boolean {\n return (\n selector === `#${id}` ||\n selector === `[data-hf-id=\"${id}\"]` ||\n selector === `[data-hf-id='${id}']`\n );\n}\n\nfunction stripGsapForId(script: string, elementId: string): string {\n // Re-parse after every removal. Animation ids are count-based (positional), so\n // removing one tween renumbers the survivors — ids captured from a single\n // up-front parse go stale and silently no-op, orphaning later tweens on the\n // now-deleted element. Always remove the FIRST still-matching animation in a\n // freshly-parsed script until none remain.\n let current = script;\n for (;;) {\n const parsed = parseGsapScriptAcornForWrite(current);\n if (!parsed) return current;\n const match = parsed.located.find((l) =>\n selectorTargetsId(l.animation.targetSelector, elementId),\n );\n if (!match) return current;\n const updated = removeAnimationFromScript(current, match.id);\n // Guard against a non-removing match (would otherwise loop forever).\n if (updated === current) return current;\n current = updated;\n }\n}\n\nfunction cascadeRemoveGsapById(doc: Document, elementId: string): void {\n for (const script of Array.from(doc.querySelectorAll(\"script\"))) {\n const text = script.textContent ?? \"\";\n if (!text.includes(\"gsap\") && !text.includes(\"ScrollTrigger\")) continue;\n const updated = stripGsapForId(text, elementId);\n if (updated !== text) script.textContent = updated;\n }\n}\n\nexport function removeElementFromHtml(html: string, elementId: string): string {\n const parser = new DOMParser();\n const doc = parser.parseFromString(html, \"text/html\");\n doc.getElementById(elementId)?.remove();\n cascadeRemoveGsapById(doc, elementId);\n return \"<!DOCTYPE html>\\n\" + doc.documentElement.outerHTML;\n}\n\nexport interface CompositionMetadata {\n compositionId: string | null;\n compositionDuration: number | null;\n variables: CompositionVariable[];\n}\n\nexport function extractCompositionMetadata(html: string): CompositionMetadata {\n const parser = new DOMParser();\n const doc = parser.parseFromString(html, \"text/html\");\n const htmlEl = doc.documentElement;\n\n const compositionId = htmlEl.getAttribute(\"data-composition-id\");\n const durationStr = htmlEl.getAttribute(\"data-composition-duration\");\n const compositionDuration = durationStr ? parseFloat(durationStr) : null;\n\n const variables = parseCompositionVariables(htmlEl);\n\n return {\n compositionId,\n compositionDuration:\n compositionDuration && isFinite(compositionDuration) ? compositionDuration : null,\n variables,\n };\n}\n\nfunction parseCompositionVariables(htmlEl: Element): CompositionVariable[] {\n const variablesAttr = htmlEl.getAttribute(\"data-composition-variables\");\n if (!variablesAttr) {\n return [];\n }\n\n try {\n const parsed = JSON.parse(variablesAttr);\n if (!Array.isArray(parsed)) {\n return [];\n }\n\n return parsed.filter((v): v is CompositionVariable => {\n if (typeof v !== \"object\" || v === null) return false;\n if (typeof v.id !== \"string\" || typeof v.label !== \"string\") return false;\n if (![\"string\", \"number\", \"color\", \"boolean\", \"enum\", \"font\", \"image\"].includes(v.type))\n return false;\n\n switch (v.type) {\n case \"string\":\n return typeof v.default === \"string\";\n case \"number\":\n return typeof v.default === \"number\";\n case \"color\":\n return typeof v.default === \"string\";\n case \"boolean\":\n return typeof v.default === \"boolean\";\n case \"enum\":\n return typeof v.default === \"string\" && Array.isArray(v.options);\n case \"font\":\n // default is the font-family name string; extra metadata fields are optional\n return typeof v.default === \"string\";\n case \"image\":\n // default is the fallback image URL string; extra metadata fields are optional\n return typeof v.default === \"string\";\n default:\n return false;\n }\n });\n } catch {\n return [];\n }\n}\n\nexport function validateCompositionHtml(html: string): ValidationResult {\n const errors: string[] = [];\n const warnings: string[] = [];\n\n const parser = new DOMParser();\n const doc = parser.parseFromString(html, \"text/html\");\n const htmlEl = doc.documentElement;\n\n const compositionId = htmlEl.getAttribute(\"data-composition-id\");\n if (!compositionId) {\n errors.push(\"Missing data-composition-id attribute on <html> element\");\n }\n\n const durationStr = htmlEl.getAttribute(\"data-composition-duration\");\n if (!durationStr) {\n errors.push(\"Missing data-composition-duration attribute on <html> element\");\n } else {\n const duration = parseFloat(durationStr);\n if (!isFinite(duration) || duration <= 0) {\n errors.push(\"data-composition-duration must be a positive finite number\");\n }\n }\n\n const stage = doc.getElementById(\"stage\");\n if (!stage) {\n errors.push(\"Missing #stage element\");\n }\n\n if (/\\son\\w+\\s*=/i.test(html)) {\n errors.push(\"Inline event handlers (onclick, onload, etc.) not allowed\");\n }\n\n if (/javascript\\s*:/i.test(html)) {\n errors.push(\"javascript: URLs not allowed\");\n }\n\n const scripts = doc.querySelectorAll(\"script\");\n if (scripts.length > 2) {\n warnings.push(\"Multiple script tags detected - only GSAP CDN and main script expected\");\n }\n\n const gsapScript = extractGsapScript(doc);\n if (gsapScript) {\n const gsapValidation = validateCompositionGsap(gsapScript);\n errors.push(...gsapValidation.errors);\n warnings.push(...gsapValidation.warnings);\n }\n\n return {\n valid: errors.length === 0,\n errors,\n warnings,\n };\n}\n\nfunction extractGsapScript(doc: Document): string | null {\n const scripts = doc.querySelectorAll(\"script\");\n for (const script of scripts) {\n const content = script.textContent || \"\";\n if (\n content.includes(\"gsap.timeline\") ||\n content.includes(\".set(\") ||\n content.includes(\".to(\")\n ) {\n return content;\n }\n }\n return null;\n}\n","/**\n * Unroll computed GSAP timelines (helpers / bounded loops) into explicit literal\n * tweens — the source-rewrite behind the Studio \"Unroll to edit\" action.\n *\n * Strategy: the read parser already resolves each computed tween (positions,\n * motionPath arcs, keyframes, provenance). We serialize those resolved\n * animations back to literal `tl.*` statements and surgically replace the\n * top-level helper-call / loop statements that produced them (and drop the now\n * dead helper declarations) via magic-string, leaving the rest of the source —\n * literal tweens, comments, formatting — untouched. The result is a visual\n * no-op: re-parsing it yields the same animations, now all literal.\n *\n * Scope: top-level helper calls and loops (the common authoring shape). Tweens\n * whose origin can't be mapped to a top-level statement (e.g. helpers nested\n * inside other helpers) are left as-is rather than guessed at.\n */\nimport * as acorn from \"acorn\";\nimport MagicString from \"magic-string\";\nimport type { GsapAnimation } from \"./gsapSerialize.js\";\nimport { serializeValue as valueToCode, safeJsKey as safeKey } from \"./gsapSerialize.js\";\nimport { parseGsapScriptAcorn } from \"./gsapParserAcorn.js\";\n\n// acorn nodes are structurally untyped here.\ntype Node = any;\n\nfunction propEntries(props: Record<string, number | string>): string[] {\n return Object.entries(props).map(([k, v]) => `${safeKey(k)}: ${valueToCode(v)}`);\n}\n\nfunction motionPathEntry(anim: GsapAnimation): string {\n const waypoints = (anim.keyframes?.keyframes ?? [])\n .filter((k) => typeof k.properties.x === \"number\" && typeof k.properties.y === \"number\")\n .map((k) => `{ x: ${valueToCode(k.properties.x!)}, y: ${valueToCode(k.properties.y!)} }`);\n const curviness = anim.arcPath?.segments[0]?.curviness ?? 1;\n const autoRotate = anim.arcPath?.autoRotate;\n const extra = autoRotate ? `, autoRotate: ${valueToCode(autoRotate as number | string)}` : \"\";\n return `motionPath: { path: [${waypoints.join(\", \")}], curviness: ${curviness}${extra} }`;\n}\n\nfunction keyframesEntry(anim: GsapAnimation): string {\n const kfs = (anim.keyframes?.keyframes ?? []).map((k) => {\n const body = propEntries(k.properties);\n if (k.ease) body.push(`ease: ${valueToCode(k.ease)}`);\n return `\"${k.percentage}%\": { ${body.join(\", \")} }`;\n });\n if (anim.keyframes?.easeEach) kfs.push(`easeEach: ${valueToCode(anim.keyframes.easeEach)}`);\n return `keyframes: { ${kfs.join(\", \")} }`;\n}\n\n/** The vars-object entries for a tween: motionPath/keyframes block, props, duration, ease, extras. */\nfunction buildVarsParts(anim: GsapAnimation): string[] {\n const parts: string[] = [];\n if (anim.arcPath?.enabled) parts.push(motionPathEntry(anim));\n else if (anim.keyframes) parts.push(keyframesEntry(anim));\n parts.push(...propEntries(anim.properties));\n if (anim.method !== \"set\" && anim.duration !== undefined) {\n parts.push(`duration: ${valueToCode(anim.duration)}`);\n }\n if (anim.ease) parts.push(`ease: ${valueToCode(anim.ease)}`);\n for (const [k, v] of Object.entries(anim.extras ?? {})) {\n parts.push(`${safeKey(k)}: ${valueToCode(v as number | string)}`);\n }\n return parts;\n}\n\n/** Serialize one resolved animation to a literal `tl.*` statement (arc/keyframe-aware). */\nfunction serializeTweenStatement(timelineVar: string, anim: GsapAnimation): string {\n const obj = `{ ${buildVarsParts(anim).join(\", \")} }`;\n const pos = valueToCode(\n anim.resolvedStart ?? (typeof anim.position === \"number\" ? anim.position : 0),\n );\n const sel = valueToCode(anim.targetSelector);\n if (anim.method === \"fromTo\") {\n const from = `{ ${propEntries(anim.fromProperties ?? {}).join(\", \")} }`;\n return `${timelineVar}.fromTo(${sel}, ${from}, ${obj}, ${pos});`;\n }\n return `${timelineVar}.${anim.method}(${sel}, ${obj}, ${pos});`;\n}\n\n/** A computed animation is one expanded from a helper or loop (not literal/dynamic). */\nfunction isComputed(anim: GsapAnimation): boolean {\n return anim.provenance?.kind === \"helper\" || anim.provenance?.kind === \"loop\";\n}\n\n/** Top-level statements of the parsed program. */\nfunction topLevelStatements(script: string): Node[] {\n return acorn.parse(script, { ecmaVersion: \"latest\", sourceType: \"script\" }).body ?? [];\n}\n\n/** The top-level statement whose source span contains [start, end], or null. */\nfunction enclosingTopLevel(statements: Node[], start: number, end: number): Node | null {\n for (const stmt of statements) {\n if (stmt.start <= start && stmt.end >= end) return stmt;\n }\n return null;\n}\n\nfunction isHelperDeclNamed(stmt: Node, names: Set<string>): boolean {\n if (stmt.type === \"FunctionDeclaration\") return names.has(stmt.id?.name);\n if (stmt.type === \"VariableDeclaration\") {\n return (stmt.declarations ?? []).some((d: Node) => names.has(d.id?.name));\n }\n return false;\n}\n\n/**\n * Rewrite `script` so top-level helper calls / loops that build the timeline\n * become explicit literal tweens. Returns the original script unchanged when\n * there is nothing statically-resolvable to unroll.\n */\nexport function unrollComputedTimeline(script: string): string {\n const parsed = parseGsapScriptAcorn(script);\n const computed = parsed.animations.filter((a) => isComputed(a) && a.provenance?.sourceRange);\n if (computed.length === 0) return script;\n\n const statements = topLevelStatements(script);\n\n // Group computed animations by the top-level statement that produced them,\n // preserving source order within each group.\n const byStatement = new Map<Node, GsapAnimation[]>();\n const helperNames = new Set<string>();\n for (const anim of computed) {\n if (anim.provenance?.fn) helperNames.add(anim.provenance.fn);\n const [s, e] = anim.provenance!.sourceRange!;\n const stmt = enclosingTopLevel(statements, s, e);\n if (!stmt) continue; // nested origin — leave it; can't map to a top-level edit\n const list = byStatement.get(stmt) ?? [];\n list.push(anim);\n byStatement.set(stmt, list);\n }\n if (byStatement.size === 0) return script;\n\n const ms = new MagicString(script);\n for (const [stmt, anims] of byStatement) {\n const literals = anims.map((a) => serializeTweenStatement(parsed.timelineVar, a)).join(\"\\n\");\n ms.overwrite(stmt.start, stmt.end, literals);\n }\n // Drop the now-dead helper declarations.\n for (const stmt of statements) {\n if (isHelperDeclNamed(stmt, helperNames)) ms.remove(stmt.start, stmt.end);\n }\n return ms.toString();\n}\n"],"mappings":";AAkBO,IAAM,oBAAoB;AAAA,EAC/B,WAAW,EAAE,OAAO,MAAM,QAAQ,KAAK;AAAA,EACvC,UAAU,EAAE,OAAO,MAAM,QAAQ,KAAK;AAAA,EACtC,gBAAgB,EAAE,OAAO,MAAM,QAAQ,KAAK;AAAA,EAC5C,eAAe,EAAE,OAAO,MAAM,QAAQ,KAAK;AAAA,EAC3C,QAAQ,EAAE,OAAO,MAAM,QAAQ,KAAK;AAAA,EACpC,aAAa,EAAE,OAAO,MAAM,QAAQ,KAAK;AAC3C;AAWO,IAAM,2BAA2B,OAAO;AAAA,EAC7C;AACF;AAEA,IAAM,qBAAuD;AAAA,EAC3D,SAAS;AAAA,EACT,IAAI;AAAA,EACJ,kBAAkB;AAAA,EAClB,kBAAkB;AAAA,EAClB,MAAM;AAAA,EACN,KAAK;AAAA,EACL,eAAe;AAAA,EACf,gBAAgB;AAAA,EAChB,gBAAgB;AAAA,EAChB,aAAa;AACf;AAOO,SAAS,wBAAwB,OAAyD;AAC/F,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,UAAU,MAAM,YAAY;AAClC,MAAK,yBAA+C,SAAS,OAAO,GAAG;AACrE,WAAO;AAAA,EACT;AACA,SAAO,mBAAmB,OAAO;AACnC;AA6EO,IAAM,6BAA6B;AAAA,EACxC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AA+FO,SAAS,cAAc,IAAgD;AAC5E,SAAO,GAAG,SAAS;AACrB;AAEO,SAAS,eAAe,IAAiD;AAC9E,SAAO,GAAG,SAAS,WAAW,GAAG,SAAS,WAAW,GAAG,SAAS;AACnE;AAEO,SAAS,qBAAqB,IAAuD;AAC1F,SAAO,GAAG,SAAS;AACrB;AAcO,IAAM,kBAAuD;AAAA,EAClE,OAAO;AAAA,EACP,OAAO;AAAA,EACP,MAAM;AAAA,EACN,OAAO;AAAA,EACP,aAAa;AACf;AAEO,IAAM,oBAAyD;AAAA,EACpE,OAAO;AAAA,EACP,OAAO;AAAA,EACP,MAAM;AAAA,EACN,OAAO;AAAA,EACP,aAAa;AACf;AAgLO,SAAS,oBAAoB,YAAyC;AAC3E,QAAM,EAAE,OAAO,OAAO,IAAI,kBAAkB,UAAU;AACtD,SAAO;AAAA,IACL,OAAO;AAAA,IACP,QAAQ,QAAQ;AAAA,IAChB,QAAQ,SAAS;AAAA,EACnB;AACF;;;AC3cO,IAAM,kBAAkB;AAAA;AAAA,EAE7B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA;AAAA,EAEA;AACF;AAQO,IAAM,kBAAkE;AAAA,EAC7E,UAAU,oBAAI,IAAI,CAAC,KAAK,KAAK,YAAY,UAAU,CAAC;AAAA,EACpD,OAAO,oBAAI,IAAI,CAAC,SAAS,UAAU,QAAQ,CAAC;AAAA,EAC5C,MAAM,oBAAI,IAAI,CAAC,SAAS,QAAQ,CAAC;AAAA,EACjC,UAAU,oBAAI,IAAI,CAAC,YAAY,SAAS,OAAO,CAAC;AAAA,EAChD,QAAQ,oBAAI,IAAI,CAAC,WAAW,WAAW,CAAC;AAAA,EACxC,OAAO,oBAAI,IAAY;AACzB;AAEA,IAAM,gBAAgB,oBAAI,IAA+B;AACzD,WAAW,CAAC,OAAO,KAAK,KAAK,OAAO,QAAQ,eAAe,GAGtD;AACH,aAAW,KAAK,MAAO,eAAc,IAAI,GAAG,KAAK;AACnD;AAEO,SAAS,sBAAsB,MAAiC;AACrE,SAAO,cAAc,IAAI,IAAI,KAAK;AACpC;AAEO,SAAS,2BACd,YAC+B;AAC/B,QAAM,SAAS,oBAAI,IAAuB;AAC1C,aAAW,OAAO,OAAO,KAAK,UAAU,GAAG;AAIzC,QAAI,QAAQ,qBAAqB,QAAQ,WAAW,QAAQ,OAAQ;AACpE,UAAM,IAAI,sBAAsB,GAAG;AACnC,WAAO,IAAI,CAAC;AAAA,EACd;AACA,MAAI,OAAO,SAAS,EAAG,QAAO,OAAO,OAAO,EAAE,KAAK,EAAE;AACrD,SAAO;AACT;AAEO,IAAM,kBAAkB;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;;;AChFO,SAAS,yBAAyB,YAAkD;AACzF,MAAI,CAAC,cAAc,WAAW,SAAS,UAAW,QAAO;AACzD,MAAI,WAAW,SAAS,kBAAmB,QAAO;AAClD,SAAO;AACT;AA0EO,SAAS,aACd,QACA,WACA,YACA,SAC6B;AAC7B,QAAM,QAAQ,OAAO,CAAC;AACtB,MAAI,OAAO,SAAS,KAAK,CAAC,MAAO,QAAO;AACxC,QAAM,WAA6B,CAAC;AACpC,MAAI;AACJ,MAAI,WAAW,OAAO,UAAU,GAAG;AAEjC,gBAAY,CAAC,KAAK;AAClB,aAAS,IAAI,GAAG,IAAI,IAAI,OAAO,QAAQ,KAAK,GAAG;AAC7C,YAAM,MAAM,OAAO,CAAC;AACpB,YAAM,MAAM,OAAO,IAAI,CAAC;AACxB,YAAM,SAAS,OAAO,IAAI,CAAC;AAC3B,UAAI,CAAC,OAAO,CAAC,OAAO,CAAC,OAAQ;AAC7B,gBAAU,KAAK,MAAM;AACrB,eAAS,KAAK,EAAE,WAAW,KAAK,IAAI,CAAC;AAAA,IACvC;AAAA,EACF,OAAO;AACL,gBAAY;AACZ,aAAS,IAAI,GAAG,IAAI,UAAU,SAAS,GAAG,IAAK,UAAS,KAAK,EAAE,UAAU,CAAC;AAAA,EAC5E;AACA,SAAO,EAAE,SAAS,EAAE,SAAS,MAAM,YAAY,SAAS,GAAG,UAAU;AACvE;AA+BO,SAAS,wBACd,YACA,cAAc,MACd,SACQ;AACR,QAAM,SAAS,CAAC,GAAG,UAAU,EAAE,KAAK,CAAC,GAAG,MAAM;AAC5C,UAAM,OACJ,EAAE,kBAAkB,OAAO,EAAE,aAAa,WAAW,EAAE,WAAW,OAAO;AAC3E,UAAM,OACJ,EAAE,kBAAkB,OAAO,EAAE,aAAa,WAAW,EAAE,WAAW,OAAO;AAC3E,WAAO,OAAO;AAAA,EAChB,CAAC;AAED,QAAM,QAAQ,OAAO,IAAI,CAAC,SAAS;AACjC,UAAM,WAAW,IAAI,KAAK,cAAc;AACxC,UAAM,QAAyC,EAAE,GAAG,KAAK,WAAW;AACpE,QAAI,KAAK,aAAa,OAAW,OAAM,WAAW,KAAK;AACvD,QAAI,KAAK,KAAM,OAAM,OAAO,KAAK;AACjC,QAAI,WAAW,gBAAgB,KAAK;AACpC,QAAI,KAAK,UAAU,OAAO,KAAK,KAAK,MAAM,EAAE,SAAS,GAAG;AACtD,YAAM,YAAY,gBAAgB,KAAK,MAAM;AAC7C,UAAI,OAAO,KAAK,KAAK,EAAE,WAAW,GAAG;AACnC,mBAAW,KAAK,SAAS;AAAA,MAC3B,OAAO;AAEL,mBAAW,SAAS,MAAM,GAAG,EAAE,IAAI,KAAK,SAAS;AAAA,MACnD;AAAA,IACF;AACA,UAAM,SAAS,OAAO,KAAK,aAAa,WAAW,IAAI,KAAK,QAAQ,MAAM,KAAK;AAC/E,YAAQ,KAAK,QAAQ;AAAA,MACnB,KAAK;AAEH,eAAO,KAAK,SACR,gBAAgB,QAAQ,KAAK,QAAQ,OACrC,OAAO,WAAW,QAAQ,QAAQ,KAAK,QAAQ,KAAK,MAAM;AAAA,MAChE,KAAK;AACH,eAAO,OAAO,WAAW,OAAO,QAAQ,KAAK,QAAQ,KAAK,MAAM;AAAA,MAClE,KAAK;AACH,eAAO,OAAO,WAAW,SAAS,QAAQ,KAAK,QAAQ,KAAK,MAAM;AAAA,MACpE,KAAK,UAAU;AACb,cAAM,UAAU,gBAAgB,KAAK,kBAAkB,CAAC,CAAC;AACzD,eAAO,OAAO,WAAW,WAAW,QAAQ,KAAK,OAAO,KAAK,QAAQ,KAAK,MAAM;AAAA,MAClF;AAAA,IACF;AAAA,EACF,CAAC;AAED,MAAI,YAAY;AAChB,MAAI,SAAS,kBAAkB;AAC7B,gBAAY;AAAA,MACV,WAAW;AAAA,qBACI,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,iCASC,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQ1C;AAEA,QAAM,WAAW,SAAS,YAAY,SAAS,WAAW;AAC1D,QAAM,YAAY,SAAS,YAAY;AAAA,MAAS,QAAQ,SAAS,KAAK;AAEtE,SAAO;AAAA,MACH,QAAQ;AAAA,EACZ,MAAM,KAAK,IAAI,CAAC,GAAG,SAAS,GAAG,SAAS;AAAA;AAE1C;AAEO,SAAS,eAAe,OAAwB;AACrD,MAAI,OAAO,UAAU,YAAY,MAAM,WAAW,QAAQ,GAAG;AAC3D,WAAO,MAAM,MAAM,CAAC;AAAA,EACtB;AACA,MAAI,OAAO,UAAU,SAAU,QAAO,KAAK,UAAU,KAAK;AAC1D,SAAO,OAAO,KAAK;AACrB;AAEO,SAAS,UAAU,KAAqB;AAC7C,SAAO,6BAA6B,KAAK,GAAG,IAAI,MAAM,KAAK,UAAU,GAAG;AAC1E;AAEA,SAAS,gBAAgB,KAA8C;AACrE,QAAM,UAAU,OAAO,QAAQ,GAAG,EAAE,IAAI,CAAC,CAAC,KAAK,KAAK,MAAM;AACxD,WAAO,GAAG,UAAU,GAAG,CAAC,KAAK,eAAe,KAAK,CAAC;AAAA,EACpD,CAAC;AACD,SAAO,KAAK,QAAQ,KAAK,IAAI,CAAC;AAChC;AAEA,SAAS,gBAAgB,QAAyC;AAChE,SAAO,OAAO,QAAQ,MAAM,EACzB,IAAI,CAAC,CAAC,KAAK,KAAK,MAAM;AACrB,WAAO,GAAG,UAAU,GAAG,CAAC,KAAK,eAAe,KAAK,CAAC;AAAA,EACpD,CAAC,EACA,KAAK,IAAI;AACd;AASO,SAAS,0BACd,YACA,WACiB;AACjB,QAAM,WAAW,IAAI,SAAS;AAC9B,SAAO,WAAW,OAAO,CAAC,MAAM,EAAE,mBAAmB,QAAQ;AAC/D;AAIA,IAAM,0BAAuE;AAAA,EAC3E,EAAE,SAAS,eAAe,SAAS,4BAA4B;AAAA,EAC/D,EAAE,SAAS,cAAc,SAAS,2BAA2B;AAAA,EAC7D,EAAE,SAAS,mBAAmB,SAAS,gCAAgC;AAAA,EACvE,EAAE,SAAS,6BAA6B,SAAS,+BAA+B;AAAA,EAChF,EAAE,SAAS,iBAAiB,SAAS,4BAA4B;AAAA,EACjE,EAAE,SAAS,kBAAkB,SAAS,kCAAkC;AAAA,EACxE,EAAE,SAAS,gBAAgB,SAAS,gCAAgC;AAAA,EACpE,EAAE,SAAS,eAAe,SAAS,+BAA+B;AAAA,EAClE,EAAE,SAAS,gBAAgB,SAAS,gCAAgC;AAAA,EACpE,EAAE,SAAS,yBAAyB,SAAS,yCAAyC;AAAA,EACtF,EAAE,SAAS,mBAAmB,SAAS,2CAA2C;AAAA,EAClF,EAAE,SAAS,qBAAqB,SAAS,0CAA0C;AAAA,EACnF,EAAE,SAAS,kBAAkB,SAAS,+CAA+C;AAAA,EACrF,EAAE,SAAS,mBAAmB,SAAS,+BAA+B;AAAA,EACtE,EAAE,SAAS,mBAAmB,SAAS,yBAAyB;AAAA,EAChE,EAAE,SAAS,oBAAoB,SAAS,0BAA0B;AAAA,EAClE,EAAE,SAAS,8BAA8B,SAAS,oCAAoC;AACxF;AAEO,SAAS,wBAAwB,QAAkC;AACxE,QAAM,SAAmB,CAAC;AAC1B,QAAM,WAAqB,CAAC;AAC5B,aAAW,EAAE,SAAS,QAAQ,KAAK,yBAAyB;AAC1D,QAAI,QAAQ,KAAK,MAAM,EAAG,QAAO,KAAK,OAAO;AAAA,EAC/C;AACA,MAAI,kBAAkB,KAAK,MAAM,GAAG;AAClC,aAAS,KAAK,wDAAwD;AAAA,EACxE;AACA,MAAI,cAAc,KAAK,MAAM,GAAG;AAC9B,aAAS,KAAK,gDAAgD;AAAA,EAChE;AACA,SAAO,EAAE,OAAO,OAAO,WAAW,GAAG,QAAQ,SAAS;AACxD;AAIO,SAAS,0BACd,WACA,WACA,kBACA,MACiB;AACjB,QAAM,SAAS,CAAC,GAAG,SAAS,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,OAAO,EAAE,IAAI;AAC5D,QAAM,aAA8B,CAAC;AACrC,QAAM,QAAQ,MAAM,KAAK;AACzB,QAAM,QAAQ,MAAM,KAAK;AACzB,QAAM,YAAY,MAAM,SAAS;AAGjC,SAAO,QAAQ,CAAC,IAAI,MAAM;AACxB,UAAM,eAAe,mBAAmB,GAAG;AAC3C,UAAM,UAAU,MAAM;AACtB,UAAM,SAAS,IAAI,IAAI,OAAO,IAAI,CAAC,IAAI;AACvC,UAAM,WAAW,SAAS,GAAG,OAAO,OAAO,OAAO;AAClD,UAAM,WAAW,SAAS,mBAAmB,OAAO,OAAO;AAE3D,UAAM,aAA8C,CAAC;AACrD,eAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,GAAG,UAAU,GAAG;AACxD,UAAI,OAAO,UAAU,SAAU;AAC/B,UAAI,QAAQ,IAAK,YAAW,IAAI,QAAQ;AAAA,eAC/B,QAAQ,IAAK,YAAW,IAAI,QAAQ;AAAA,eACpC,QAAQ,QAAS,YAAW,QAAQ,YAAY;AAAA,UACpD,YAAW,GAAG,IAAI;AAAA,IACzB;AAEA,eAAW,KAAK;AAAA,MACd,IAAI,GAAG,SAAS,OAAO,GAAG,EAAE;AAAA,MAC5B,gBAAgB,IAAI,SAAS;AAAA,MAC7B,QAAQ,UAAU,QAAQ;AAAA,MAC1B;AAAA,MACA;AAAA,MACA,UAAU,UAAU,SAAY;AAAA,MAChC,MAAM,GAAG;AAAA,IACX,CAAC;AAAA,EACH,CAAC;AAED,SAAO;AACT;AAEO,SAAS,0BACd,YACA,kBACA,SAOY;AACZ,QAAM,eAA6B,CAAC,OAAO,MAAM,QAAQ,QAAQ;AACjE,QAAM,QAAQ,SAAS,SAAS;AAChC,QAAM,QAAQ,SAAS,SAAS;AAChC,QAAM,YAAY,SAAS,aAAa;AACxC,QAAM,kBAAkB,SAAS,mBAAmB;AACpD,QAAM,cAAc,SAAS,eAAe;AAC5C,QAAM,kBAAkB;AACxB,QAAM,mBAAmB;AAEzB,SACE,WACG;AAAA,IACC,CAAC,MACC,aAAa,SAAS,EAAE,MAAM,KAAK,OAAO,EAAE,aAAa;AAAA,EAC7D,EAEC,IAAI,CAAC,MAAM;AACV,UAAM,kBAAkB,EAAE,WAAW;AACrC,UAAM,OAAO,kBAAkB,KAAK,IAAI,GAAG,eAAe,IAAI;AAE9D,UAAM,aAA0C,CAAC;AACjD,eAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,EAAE,UAAU,GAAG;AACvD,UAAI,OAAO,UAAU,SAAU;AAC/B,UAAI,QAAQ,IAAK,YAAW,IAAI,QAAQ;AAAA,eAC/B,QAAQ,IAAK,YAAW,IAAI,QAAQ;AAAA,eACpC,QAAQ,SAAS;AACxB,mBAAW,QAAQ,cAAc,IAAI,QAAQ,YAAY;AAAA,MAC3D,OAAO;AACL,QAAC,WAAsC,GAAG,IAAI;AAAA,MAChD;AAAA,IACF;AAEA,QACE,eACA,EAAE,WAAW,SACb,OAAO,mBACP,OAAO,OAAO,UAAU,EAAE;AAAA,MACxB,CAAC,MAAM,OAAO,MAAM,YAAY,KAAK,IAAI,CAAC,IAAI;AAAA,IAChD,GACA;AACA,aAAO;AAAA,IACT;AAEA,WAAO;AAAA,MACL,IAAI,EAAE,GAAG,QAAQ,WAAW,EAAE;AAAA,MAC9B;AAAA,MACA;AAAA,MACA,MAAM,EAAE;AAAA,IACV;AAAA,EACF,CAAC,EACA,OAAO,CAAC,OAAqC,OAAO,IAAI;AAE/D;;;ACjbA,YAAY,YAAY;AACxB,SAAS,SAAS,kBAAkB;;;ACM7B,IAAM,iBAAiC;AAAA,EAC5C,EAAE,MAAM,iBAAiB,OAAO,UAAU,MAAM,GAAG,WAAW,KAAK,SAAS,GAAG;AAAA,EAC/E,EAAE,MAAM,iBAAiB,OAAO,UAAU,MAAM,GAAG,WAAW,KAAK,SAAS,GAAG;AAAA,EAC/E,EAAE,MAAM,gBAAgB,OAAO,SAAS,MAAM,GAAG,WAAW,KAAK,SAAS,GAAG;AAAA,EAC7E,EAAE,MAAM,iBAAiB,OAAO,UAAU,MAAM,GAAG,WAAW,KAAK,SAAS,EAAE;AAAA,EAC9E,EAAE,MAAM,gBAAgB,OAAO,SAAS,MAAM,GAAG,WAAW,KAAK,SAAS,GAAG;AAC/E;AAaO,SAAS,uBACd,MACA,WACA,SACA,QAAQ,KACA;AACR,QAAM,KAAK,KAAK,KAAK,YAAY,IAAI;AACrC,QAAM,OAAO,WAAW,IAAI,KAAK,KAAK,YAAY,IAAI;AAItD,MAAI;AACJ,MAAI,OAAO,GAAG;AACZ,qBAAiB,KAAK,IAAI,KAAK,OAAO,KAAK,EAAE;AAAA,EAC/C,OAAO;AACL,UAAM,YAAY,OAAO,KAAK,KAAK,KAAK,KAAK,OAAO,OAAO,CAAC;AAC5D,qBAAiB,KAAK,IAAI,IAAI,KAAK,IAAI,WAAW,IAAI,GAAG,EAAE;AAAA,EAC7D;AACA,QAAM,cAAc,KAAK,IAAI,gBAAgB,CAAC;AAE9C,QAAM,WAAqB,CAAC,MAAM;AAElC,WAAS,IAAI,GAAG,KAAK,OAAO,KAAK;AAC/B,UAAM,IAAI,IAAI;AACd,UAAM,OAAO,IAAI;AACjB,QAAI;AAEJ,QAAI,OAAO,GAAG;AAEZ,YAAM,KAAK,KAAK,KAAK,KAAK,IAAI,OAAO,IAAI;AACzC,cACE,IACA,KAAK,IAAI,CAAC,OAAO,KAAK,IAAI,KACvB,KAAK,IAAI,KAAK,IAAI,IAAM,OAAO,KAAM,KAAM,KAAK,IAAI,KAAK,IAAI;AAAA,IACpE,WAAW,SAAS,GAAG;AAErB,cAAQ,KAAK,IAAI,KAAK,QAAQ,KAAK,IAAI,CAAC,KAAK,IAAI;AAAA,IACnD,OAAO;AAEL,YAAM,KAAK,CAAC,MAAM,OAAO,KAAK,KAAK,OAAO,OAAO,CAAC;AAClD,YAAM,KAAK,CAAC,MAAM,OAAO,KAAK,KAAK,OAAO,OAAO,CAAC;AAClD,cAAQ,KAAK,KAAK,KAAK,IAAI,KAAK,IAAI,IAAI,KAAK,KAAK,IAAI,KAAK,IAAI,MAAM,KAAK;AAAA,IAC5E;AAEA,aAAS,KAAK,GAAG,EAAE,QAAQ,CAAC,CAAC,IAAI,MAAM,QAAQ,CAAC,CAAC,EAAE;AAAA,EACrD;AAGA,WAAS,SAAS,SAAS,CAAC,IAAI;AAEhC,SAAO,GAAG,SAAS,CAAC,CAAC,KAAK,SAAS,MAAM,CAAC,EAAE,KAAK,GAAG,CAAC;AACvD;;;ADwjDA,IAAM,qBAAqB;AAIpB,SAAS,gBAAgB,MAA8B;AAC5D,SAAO,KAAK,WAAW,SAAS,KAAK,YAAY,SAAS;AAC5D;;;AE3oDA,YAAY,WAAW;AACvB,YAAY,eAAe;;;ACW3B,IAAM,YAAY,oBAAI,IAAI,CAAC,QAAQ,SAAS,OAAO,OAAO,SAAS,kBAAkB,WAAW,CAAC;AAEjG,IAAM,iBAAiB,oBAAI,IAAI;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AACF,CAAC;AACD,IAAM,eAAe,oBAAI,IAAI,CAAC,OAAO,MAAM,QAAQ,QAAQ,CAAC;AAG5D,IAAM,YAAY;AAClB,IAAM,YAAY;AAElB,SAAS,eAAe,MAAqB;AAC3C,SAAO,CAAC,CAAC,QAAQ,eAAe,IAAI,KAAK,IAAI;AAC/C;AAEA,SAAS,OAAO,GAAkB;AAChC,SAAO,CAAC,CAAC,KAAK,OAAO,MAAM,YAAY,OAAO,EAAE,SAAS;AAC3D;AAOA,SAAS,kBAAkB,MAAY,IAAiC;AACtE,aAAW,OAAO,OAAO,KAAK,IAAI,GAAG;AACnC,QAAI,UAAU,IAAI,GAAG,KAAK,yBAAyB,MAAM,GAAG,EAAG;AAC/D,UAAM,QAAQ,KAAK,GAAG;AACtB,QAAI,MAAM,QAAQ,KAAK,GAAG;AACxB,eAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,IAAK,OAAM,CAAC,IAAI,GAAG,MAAM,CAAC,CAAC;AAAA,IAC/D,OAAO;AACL,WAAK,GAAG,IAAI,GAAG,KAAK;AAAA,IACtB;AAAA,EACF;AACF;AAGO,SAAS,UAA0B,MAAY;AACpD,SAAO,gBAAgB,IAAI;AAC7B;AAMA,SAAS,oBAAoB,SAAe,KAAwB;AAClE,MAAI,SAAS,SAAS,aAAc,KAAI,IAAI,QAAQ,IAAI;AAAA,WAC/C,SAAS,SAAS,oBAAqB,qBAAoB,QAAQ,MAAM,GAAG;AAAA,WAC5E,SAAS,SAAS,cAAe,qBAAoB,QAAQ,UAAU,GAAG;AACrF;AAGA,SAAS,kBAAkB,MAAyB;AAClD,QAAM,QAAQ,oBAAI,IAAY;AAC9B,QAAM,QAAQ,CAAC,SAAqB;AAClC,QAAI,CAAC,OAAO,IAAI,EAAG,QAAO;AAC1B,QAAI,eAAe,IAAI,EAAG,YAAW,KAAK,KAAK,UAAU,CAAC,EAAG,qBAAoB,GAAG,KAAK;AAAA,aAChF,KAAK,SAAS,qBAAsB,qBAAoB,KAAK,IAAI,KAAK;AAAA,aACtE,KAAK,SAAS,cAAe,qBAAoB,KAAK,OAAO,KAAK;AAC3E,sBAAkB,MAAM,KAAK;AAC7B,WAAO;AAAA,EACT;AACA,QAAM,IAAI;AACV,SAAO;AACT;AAGA,SAAS,yBAAyB,MAAY,KAAsB;AAClE,MAAI,KAAK,SAAU,QAAO;AAC1B,SACG,KAAK,SAAS,sBAAsB,QAAQ,cAC5C,KAAK,SAAS,cAAc,QAAQ;AAEzC;AAUO,SAAS,iBAAiB,MAAY,UAA2C;AACtF,QAAM,WAAW,kBAAkB,IAAI;AACvC,MAAI,YAAY;AAChB,MAAI,SAAS,OAAO,GAAG;AACrB,gBAAY,IAAI,IAAI,QAAQ;AAC5B,eAAW,QAAQ,SAAU,CAAC,UAAgC,OAAO,IAAI;AAAA,EAC3E;AACA,MAAI,UAAU,SAAS,EAAG,QAAO;AACjC,SAAO,QAAQ,MAAM,SAAS;AAChC;AAEA,SAAS,QAAQ,MAAY,UAA2C;AACtE,MAAI,CAAC,OAAO,IAAI,EAAG,QAAO;AAC1B,MAAI,KAAK,SAAS,gBAAgB,SAAS,IAAI,KAAK,IAAI,GAAG;AACzD,WAAO,UAAU,SAAS,IAAI,KAAK,IAAI,CAAC;AAAA,EAC1C;AACA,oBAAkB,MAAM,CAAC,UAAU,QAAQ,OAAO,QAAQ,CAAC;AAC3D,SAAO;AACT;AAGO,SAAS,cAAc,MAAY,YAAkC;AAC1E,MAAI,QAAQ,OAAO,SAAS,SAAU,MAAK,iBAAiB;AAC5D,SAAO;AACT;AAGO,SAAS,eAAe,MAAwC;AACrE,SAAO,MAAM;AACf;AAGO,SAAS,eAAe,OAAqB;AAClD,SAAO,EAAE,MAAM,WAAW,OAAO,KAAK,OAAO,KAAK,EAAE;AACtD;AAkBA,SAAS,UAAU,MAAY,IAA6B;AAC1D,MAAI,CAAC,OAAO,IAAI,EAAG;AACnB,KAAG,IAAI;AACP,aAAW,OAAO,OAAO,KAAK,IAAI,GAAG;AACnC,QAAI,UAAU,IAAI,GAAG,EAAG;AACxB,UAAM,QAAQ,KAAK,GAAG;AACtB,QAAI,MAAM,QAAQ,KAAK,EAAG,YAAW,KAAK,MAAO,WAAU,GAAG,EAAE;AAAA,QAC3D,WAAU,OAAO,EAAE;AAAA,EAC1B;AACF;AAGA,SAAS,iBAAiB,MAA2B;AACnD,MAAI,MAAM,KAAK,QAAQ;AACvB,SAAO,KAAK,SAAS,iBAAkB,OAAM,IAAI,QAAQ;AACzD,SAAO,KAAK,SAAS,eAAe,IAAI,OAAO;AACjD;AAEA,SAAS,iBAAiB,MAAY,aAA8B;AAClE,MAAI,iBAAiB,IAAI,MAAM,YAAa,QAAO;AACnD,SACE,KAAK,QAAQ,UAAU,SAAS,gBAAgB,aAAa,IAAI,KAAK,OAAO,SAAS,IAAI;AAE9F;AAEA,SAAS,qBAAqB,MAAY,aAA8B;AACtE,MAAI,QAAQ;AACZ,YAAU,MAAM,CAAC,MAAM;AACrB,QAAI,EAAE,SAAS,oBAAoB,iBAAiB,GAAG,WAAW,EAAG,SAAQ;AAAA,EAC/E,CAAC;AACD,SAAO;AACT;AAEA,SAAS,QAAQ,MAA0C;AACzD,SAAO,OAAO,KAAK,UAAU,YAAY,OAAO,KAAK,QAAQ,WACzD,CAAC,KAAK,OAAO,KAAK,GAAG,IACrB;AACN;AAGA,SAAS,gBAAgB,IAAmB;AAC1C,SACE,eAAe,EAAE,KACjB,GAAG,MAAM,SAAS,oBAClB,EAAE,GAAG,UAAU,CAAC,GAAG,KAAK,CAAC,MAAY,EAAE,SAAS,YAAY;AAEhE;AAGA,SAAS,SAAS,MAAY,OAA6B;AACzD,MAAI,MAAM;AACV,YAAU,MAAM,CAAC,MAAM;AACrB,QACE,EAAE,SAAS,oBACX,EAAE,QAAQ,SAAS,gBACnB,MAAM,IAAI,EAAE,OAAO,IAAI,GACvB;AACA,YAAM;AAAA,IACR;AAAA,EACF,CAAC;AACD,SAAO;AACT;AAGA,SAAS,cAAc,MAAmC;AACxD,MAAI,KAAK,cAAc,WAAW,EAAG,QAAO;AAC5C,QAAM,IAAI,KAAK,aAAa,CAAC;AAC7B,SAAO,EAAE,IAAI,SAAS,gBAAgB,gBAAgB,EAAE,IAAI,IAAI,CAAC,EAAE,GAAG,MAAM,EAAE,IAAI,IAAI;AACxF;AAGA,SAAS,oBAAoB,MAAmC;AAC9D,MAAI,KAAK,SAAS,uBAAuB;AACvC,WAAO,KAAK,MAAM,gBAAgB,IAAI,IAAI,CAAC,KAAK,GAAG,MAAM,IAAI,IAAI;AAAA,EACnE;AACA,MAAI,KAAK,SAAS,sBAAuB,QAAO,cAAc,IAAI;AAClE,SAAO;AACT;AAGA,SAAS,uBAAuB,SAAkC;AAChE,QAAM,aAAa,oBAAI,IAAkB;AACzC,aAAW,QAAQ,QAAQ,QAAQ,CAAC,GAAG;AACrC,UAAM,SAAS,oBAAoB,IAAI;AACvC,QAAI,OAAQ,YAAW,IAAI,OAAO,CAAC,GAAG,OAAO,CAAC,CAAC;AAAA,EACjD;AACA,SAAO;AACT;AAGA,SAAS,sBAAsB,YAA+B,aAAkC;AAC9F,QAAM,WAAW,oBAAI,IAAY;AACjC,aAAW,CAAC,MAAM,EAAE,KAAK,YAAY;AACnC,QAAI,qBAAqB,GAAG,MAAM,WAAW,EAAG,UAAS,IAAI,IAAI;AAAA,EACnE;AACA,WAAS,UAAU,MAAM,WAAW;AAClC,cAAU;AACV,eAAW,CAAC,MAAM,EAAE,KAAK,YAAY;AACnC,UAAI,CAAC,SAAS,IAAI,IAAI,KAAK,SAAS,GAAG,MAAM,QAAQ,GAAG;AACtD,iBAAS,IAAI,IAAI;AACjB,kBAAU;AAAA,MACZ;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,KAAK,QAA6B,KAAmB;AAC5D,SAAO,IAAI,MAAM,OAAO,IAAI,GAAG,KAAK,KAAK,CAAC;AAC5C;AAOA,SAAS,gBAAgB,SAAe,YAAkD;AACxF,QAAM,QAAQ,IAAI,IAAI,WAAW,KAAK,CAAC;AACvC,QAAM,WAAW,oBAAI,IAAoB;AACzC,QAAM,YAAY,oBAAI,IAAoB;AAC1C,YAAU,SAAS,CAAC,MAAM;AACxB,QAAI,EAAE,SAAS,gBAAgB,MAAM,IAAI,EAAE,IAAI,EAAG,MAAK,UAAU,EAAE,IAAI;AACvE,UAAM,IAAI,EAAE,SAAS,wBAAwB,EAAE,aAAa;AAC5D,QACE,GAAG,SAAS,oBACZ,EAAE,QAAQ,SAAS,gBACnB,MAAM,IAAI,EAAE,OAAO,IAAI,GACvB;AACA,WAAK,WAAW,EAAE,OAAO,IAAI;AAAA,IAC/B;AAAA,EACF,CAAC;AACD,QAAM,OAAO,oBAAI,IAAkB;AACnC,aAAW,CAAC,MAAM,EAAE,KAAK,YAAY;AACnC,SAAK,SAAS,IAAI,IAAI,KAAK,OAAO,KAAK,UAAU,IAAI,IAAI,KAAK,GAAI,MAAK,IAAI,MAAM,EAAE;AAAA,EACrF;AACA,SAAO;AACT;AAGA,SAAS,wBAAwB,SAAe,aAAwC;AACtF,QAAM,aAAa,uBAAuB,OAAO;AACjD,MAAI,WAAW,SAAS,EAAG,QAAO;AAClC,QAAM,WAAW,sBAAsB,YAAY,WAAW;AAC9D,aAAW,QAAQ,CAAC,GAAG,WAAW,KAAK,CAAC,EAAG,KAAI,CAAC,SAAS,IAAI,IAAI,EAAG,YAAW,OAAO,IAAI;AAC1F,MAAI,WAAW,SAAS,EAAG,QAAO;AAClC,SAAO,gBAAgB,SAAS,UAAU;AAC5C;AAEA,SAAS,aAAa,MAAY,SAAqC;AACrE,MAAI,KAAK,SAAS,sBAAuB,QAAO,CAAC,CAAC,KAAK,MAAM,QAAQ,IAAI,KAAK,GAAG,IAAI,MAAM;AAC3F,MAAI,KAAK,SAAS,yBAAyB,KAAK,cAAc,WAAW,GAAG;AAC1E,UAAM,IAAI,KAAK,aAAa,CAAC;AAC7B,WAAO,EAAE,IAAI,SAAS,gBAAgB,QAAQ,IAAI,EAAE,GAAG,IAAI,MAAM,EAAE;AAAA,EACrE;AACA,SAAO;AACT;AAEA,SAAS,eAAe,MAAoB;AAC1C,MAAI,MAAM,SAAS,iBAAkB,QAAO,KAAK,QAAQ,CAAC;AAC1D,SAAO,OAAO,CAAC,EAAE,MAAM,uBAAuB,YAAY,KAAK,CAAC,IAAI,CAAC;AACvE;AAGA,SAAS,iBAAiB,OAAe,MAAsB,KAAsB;AACnF,aAAW,QAAQ,OAAO;AACxB,cAAU,MAAM,CAAC,MAAM;AACrB,UAAI,EAAE,SAAS,oBAAoB,iBAAiB,GAAG,IAAI,WAAW,GAAG;AACvE,sBAAc,GAAG,EAAE,GAAG,KAAK,CAAC;AAC5B,UAAE,YAAY,IAAI,MAAM;AAAA,MAC1B;AAAA,IACF,CAAC;AAAA,EACH;AACF;AAGA,SAAS,WACP,WACA,UACA,MACA,KACQ;AACR,QAAM,QAAQ,iBAAiB,UAAU,EAAE,MAAM,kBAAkB,MAAM,UAAU,CAAC,GAAG,QAAQ;AAC/F,mBAAiB,MAAM,MAAM,MAAM,GAAG;AACtC,SAAO,iBAAiB,MAAM,MAAM,EAAE,GAAG,KAAK,OAAO,IAAI,QAAQ,EAAE,CAAC;AACtE;AAEA,SAAS,aAAa,MAAY,KAAwB;AACxD,QAAM,KAAK,IAAI,QAAQ,IAAI,KAAK,OAAO,IAAI;AAC3C,QAAM,WAAW,oBAAI,IAAkB;AACvC,GAAC,GAAG,UAAU,CAAC,GAAG,QAAQ,CAAC,GAAS,MAAc;AAChD,UAAM,MAAM,KAAK,YAAY,CAAC;AAC9B,QAAI,IAAK,UAAS,IAAI,EAAE,MAAM,GAAG;AAAA,EACnC,CAAC;AACD,QAAM,OAAuB;AAAA,IAC3B,MAAM;AAAA,IACN,IAAI,KAAK,OAAO;AAAA,IAChB,UAAU,EAAE,IAAI,KAAK;AAAA,IACrB,aAAa,QAAQ,IAAI;AAAA,EAC3B;AACA,SAAO,WAAW,GAAG,KAAK,MAAM,UAAU,MAAM,GAAG;AACrD;AAEA,SAAS,WAAW,QAAc,SAA8C;AAC9E,MAAI,OAAO,aAAa,KAAM,QAAO,MAAM,QAAQ,OAAO,KAAK,CAAC;AAChE,MAAI,OAAO,aAAa,MAAM;AAC5B,UAAM,IAAI,MAAM,QAAQ,OAAO,KAAK,CAAC;AACrC,WAAO,MAAM,SAAY,SAAY,CAAC;AAAA,EACxC;AAEA,MAAI,OAAO,aAAa,OAAO,OAAO,OAAO,SAAS,oBAAoB;AACxE,WAAO,MAAM,QAAQ,OAAO,MAAM,KAAK,CAAC;AAAA,EAC1C;AACA,SAAO;AACT;AAGA,SAAS,eAAe,QAA6B;AACnD,MAAI,QAAQ,SAAS,mBAAoB,QAAO,OAAO,UAAU,QAAQ;AACzE,MAAI,QAAQ,SAAS,uBAAwB,QAAO,OAAO,MAAM,QAAQ;AACzE,SAAO;AACT;AAEA,SAAS,SAAS,QAAc,SAAiB,SAA8C;AAC7F,MAAI,eAAe,MAAM,MAAM,QAAS,QAAO;AAC/C,MAAI,OAAO,SAAS,mBAAoB,QAAO,OAAO,aAAa,OAAO,IAAI;AAC9E,SAAO,WAAW,QAAQ,OAAO;AACnC;AAEA,SAAS,MAAM,GAAgC;AAC7C,SAAO,OAAO,MAAM,YAAY,OAAO,SAAS,CAAC,IAAI,IAAI;AAC3D;AAEA,SAAS,cAAc,IAAY,GAAW,KAAsB;AAClE,MAAI,OAAO,IAAK,QAAO,IAAI;AAC3B,MAAI,OAAO,KAAM,QAAO,KAAK;AAC7B,MAAI,OAAO,IAAK,QAAO,IAAI;AAC3B,MAAI,OAAO,KAAM,QAAO,KAAK;AAC7B,SAAO;AACT;AAWA,SAAS,WAAW,MAAqD;AACvE,MAAI,MAAM,SAAS,yBAAyB,KAAK,cAAc,WAAW,EAAG,QAAO;AACpF,QAAM,IAAI,KAAK,aAAa,CAAC;AAC7B,SAAO,EAAE,IAAI,SAAS,eAAe,EAAE,MAAM,EAAE,GAAG,MAAM,UAAU,EAAE,KAAK,IAAI;AAC/E;AAGA,SAAS,eAAe,MAAY,SAA4C;AAC9E,QAAM,KAAK,WAAW,KAAK,IAAI;AAC/B,QAAM,OAAO,KAAK;AAClB,MAAI,CAAC,MAAM,MAAM,SAAS,sBAAsB,KAAK,MAAM,SAAS,GAAG,KAAM,QAAO;AACpF,QAAM,QAAQ,MAAM,QAAQ,GAAG,QAAQ,CAAC;AACxC,QAAM,MAAM,MAAM,QAAQ,KAAK,KAAK,CAAC;AACrC,QAAM,OAAO,SAAS,KAAK,QAAQ,GAAG,MAAM,OAAO;AACnD,MAAI,UAAU,UAAa,QAAQ,UAAa,CAAC,KAAM,QAAO;AAC9D,SAAO,EAAE,GAAG,GAAG,MAAM,OAAO,KAAK,IAAI,KAAK,UAAU,KAAK;AAC3D;AAEA,SAAS,UAAU,MAAY,KAA+B;AAC5D,QAAM,IAAI,eAAe,MAAM,IAAI,OAAO;AAC1C,MAAI,CAAC,EAAG,QAAO;AACf,QAAM,OAAO,eAAe,KAAK,IAAI;AACrC,QAAM,MAAc,CAAC;AACrB,QAAM,OAAO,EAAE,IAAI,KAAK;AACxB,MAAI,YAAY;AAChB,WAAS,IAAI,EAAE,OAAO,cAAc,EAAE,IAAI,GAAG,EAAE,GAAG,GAAG,KAAK,EAAE,MAAM;AAChE,QAAI,aAAa,UAAW,QAAO;AACnC,UAAM,OAAuB;AAAA,MAC3B,MAAM;AAAA,MACN,UAAU;AAAA,MACV;AAAA,MACA,aAAa,QAAQ,IAAI;AAAA,IAC3B;AACA,QAAI,KAAK,GAAG,WAAW,MAAM,oBAAI,IAAI,CAAC,CAAC,EAAE,GAAG,eAAe,CAAC,CAAC,CAAC,CAAC,GAAG,MAAM,GAAG,CAAC;AAC5E;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,aAAa,MAA2B;AAC/C,MAAI,MAAM,SAAS,uBAAuB;AACxC,UAAM,KAAK,KAAK,eAAe,CAAC,GAAG;AACnC,WAAO,IAAI,SAAS,eAAe,GAAG,OAAO;AAAA,EAC/C;AACA,SAAO,MAAM,SAAS,eAAe,KAAK,OAAO;AACnD;AAGA,SAAS,gBACP,UACA,MACA,QACA,SACA,OACA,KACQ;AACR,QAAM,MAAc,CAAC;AACrB,QAAM,OAAO,EAAE,IAAI,KAAK;AACxB,WAAS,QAAQ,CAAC,IAAI,MAAM;AAC1B,QAAI,CAAC,GAAI;AACT,UAAM,WAAW,oBAAI,IAAkB;AACvC,QAAI,OAAQ,UAAS,IAAI,QAAQ,EAAE;AACnC,QAAI,QAAS,UAAS,IAAI,SAAS,eAAe,CAAC,CAAC;AACpD,UAAM,OAAuB,EAAE,MAAM,QAAQ,UAAU,MAAM,WAAW,GAAG,aAAa,MAAM;AAC9F,QAAI,KAAK,GAAG,WAAW,MAAM,UAAU,MAAM,GAAG,CAAC;AAAA,EACnD,CAAC;AACD,SAAO;AACT;AAEA,SAAS,YAAY,MAAY,KAA+B;AAC9D,MAAI,KAAK,OAAO,SAAS,kBAAmB,QAAO;AACnD,QAAM,SAAS,aAAa,KAAK,IAAI;AACrC,MAAI,CAAC,OAAQ,QAAO;AACpB,SAAO;AAAA,IACL,KAAK,MAAM,YAAY,CAAC;AAAA,IACxB,eAAe,KAAK,IAAI;AAAA,IACxB;AAAA,IACA;AAAA,IACA,QAAQ,IAAI;AAAA,IACZ;AAAA,EACF;AACF;AAGA,SAAS,mBAAmB,IAA4D;AACtF,QAAM,QAA8B,CAAC;AACrC,aAAW,KAAK,CAAC,GAAG,SAAS,CAAC,GAAG,GAAG,SAAS,CAAC,CAAC,GAAG;AAChD,QAAI,CAAC,EAAG,OAAM,KAAK,IAAI;AAAA,aACd,EAAE,SAAS,aAAc,QAAO;AAAA,QACpC,OAAM,KAAK,EAAE,IAAI;AAAA,EACxB;AACA,SAAO,EAAE,IAAI,MAAM,CAAC,GAAI,KAAK,MAAM,CAAC,EAAG;AACzC;AAGA,SAAS,cAAc,QAAuB;AAC5C,SACE,QAAQ,SAAS,sBACjB,OAAO,UAAU,SAAS,aAC1B,OAAO,QAAQ,SAAS;AAE5B;AAGA,SAAS,cAAc,MAAmD;AACxE,MAAI,CAAC,cAAc,KAAK,MAAM,EAAG,QAAO;AACxC,QAAM,KAAK,KAAK,YAAY,CAAC;AAC7B,SAAO,eAAe,EAAE,IAAI,EAAE,UAAU,KAAK,OAAO,OAAO,YAAY,CAAC,GAAG,GAAG,IAAI;AACpF;AAEA,SAAS,cAAc,MAAY,KAA+B;AAChE,QAAM,SAAS,cAAc,IAAI;AACjC,MAAI,CAAC,OAAQ,QAAO;AACpB,QAAM,SAAS,mBAAmB,OAAO,EAAE;AAC3C,MAAI,CAAC,OAAQ,QAAO;AACpB,SAAO;AAAA,IACL,OAAO;AAAA,IACP,eAAe,OAAO,GAAG,IAAI;AAAA,IAC7B,OAAO;AAAA,IACP,OAAO;AAAA,IACP,QAAQ,IAAI;AAAA,IACZ;AAAA,EACF;AACF;AAEA,SAAS,WAAW,MAAY,KAA+B;AAC7D,MAAI,KAAK,QAAQ,SAAS,gBAAgB,IAAI,QAAQ,IAAI,KAAK,OAAO,IAAI,GAAG;AAC3E,WAAO,aAAa,MAAM,GAAG;AAAA,EAC/B;AACA,SAAO,cAAc,MAAM,GAAG;AAChC;AAEA,SAAS,gBAAgB,MAAY,KAA+B;AAClE,MAAI,IAAI,SAAS,UAAW,QAAO;AACnC,MAAI,KAAK,SAAS,eAAgB,QAAO,UAAU,MAAM,GAAG;AAC5D,MAAI,KAAK,SAAS,iBAAkB,QAAO,YAAY,MAAM,GAAG;AAChE,MAAI,KAAK,SAAS,yBAAyB,KAAK,YAAY,SAAS,kBAAkB;AACrF,WAAO,WAAW,KAAK,YAAY,GAAG;AAAA,EACxC;AACA,SAAO;AACT;AAEA,SAAS,iBAAiB,OAAe,KAAwB;AAC/D,QAAM,MAAc,CAAC;AACrB,aAAW,QAAQ,OAAO;AACxB,UAAM,WAAW,gBAAgB,MAAM,GAAG;AAC1C,QAAI,SAAU,KAAI,KAAK,GAAG,QAAQ;AAAA,QAC7B,KAAI,KAAK,IAAI;AAAA,EACpB;AACA,SAAO;AACT;AASO,SAAS,wBACd,KACA,aACA,SACM;AACN,QAAM,UAAU,wBAAwB,KAAK,WAAW;AACxD,QAAM,MAAiB;AAAA,IACrB;AAAA,IACA;AAAA,IACA;AAAA,IACA,OAAO;AAAA,IACP,MAAM,EAAE,GAAG,EAAE;AAAA,IACb,OAAO,EAAE,GAAG,EAAE;AAAA,EAChB;AACA,QAAM,QAAQ,IAAI,QAAQ,CAAC,GAAG,OAAO,CAAC,SAAe,CAAC,aAAa,MAAM,OAAO,CAAC;AACjF,MAAI,OAAO,iBAAiB,MAAM,GAAG;AACvC;;;ADniBA,IAAMA,gBAAe,oBAAI,IAAY,CAAC,OAAO,MAAM,QAAQ,QAAQ,CAAC;AACpE,IAAM,gBAAgB,oBAAI,IAAI,CAAC,iBAAiB,kBAAkB,CAAC;AACnE,IAAM,oBAAoB,oBAAI,IAAI,CAAC,WAAW,KAAK,CAAC;AACpD,IAAM,mBAAmB,oBAAI,IAAI;AAAA,EAC/B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAWD,SAAS,YACP,MACA,OACuC;AACvC,MAAI,CAAC,KAAM,QAAO;AAClB,MAAI,KAAK,SAAS,oBAAqB,KAAK,SAAS,aAAa,OAAO,KAAK,UAAU;AACtF,WAAO,KAAK;AACd,MAAI,KAAK,SAAS,mBAAoB,KAAK,SAAS,aAAa,OAAO,KAAK,UAAU;AACrF,WAAO,KAAK;AACd,MACE,KAAK,SAAS,oBACb,KAAK,SAAS,aAAa,OAAO,KAAK,UAAU;AAElD,WAAO,KAAK;AACd,MAAI,KAAK,SAAS,qBAAqB,KAAK,aAAa,OAAO,KAAK,UAAU;AAC7E,UAAM,MAAM,YAAY,KAAK,UAAU,KAAK;AAC5C,WAAO,OAAO,QAAQ,WAAW,CAAC,MAAM;AAAA,EAC1C;AACA,MAAI,KAAK,SAAS,oBAAoB;AACpC,UAAM,OAAO,YAAY,KAAK,MAAM,KAAK;AACzC,UAAM,QAAQ,YAAY,KAAK,OAAO,KAAK;AAC3C,QAAI,OAAO,SAAS,YAAY,OAAO,UAAU,UAAU;AACzD,cAAQ,KAAK,UAAU;AAAA,QACrB,KAAK;AACH,iBAAO,OAAO;AAAA,QAChB,KAAK;AACH,iBAAO,OAAO;AAAA,QAChB,KAAK;AACH,iBAAO,OAAO;AAAA,QAChB,KAAK;AACH,iBAAO,UAAU,IAAI,OAAO,QAAQ;AAAA,MACxC;AAAA,IACF;AACA,QAAI,OAAO,SAAS,YAAY,KAAK,aAAa,IAAK,QAAO,OAAO,OAAO,SAAS,EAAE;AACvF,QAAI,OAAO,UAAU,YAAY,KAAK,aAAa,IAAK,QAAO,OAAO,QAAQ,EAAE,IAAI;AAAA,EACtF;AACA,MAAI,KAAK,SAAS,gBAAgB,MAAM,IAAI,KAAK,IAAI,GAAG;AACtD,WAAO,MAAM,IAAI,KAAK,IAAI;AAAA,EAC5B;AACA,MAAI,KAAK,SAAS,qBAAqB,KAAK,aAAa,WAAW,GAAG;AACrE,WAAO,KAAK,SAAS,CAAC,GAAG,OAAO,UAAU;AAAA,EAC5C;AACA,SAAO;AACT;AAEA,SAAS,oBAAoB,MAAW,OAA+B;AACrE,SAAO,YAAY,MAAM,KAAK;AAChC;AAKA,SAAS,sBAAsB,MAAW,OAAqC;AAC7E,MAAI,MAAM,SAAS,iBAAkB,QAAO;AAC5C,QAAM,SAAS,KAAK;AACpB,MAAI,QAAQ,SAAS,sBAAsB,OAAO,UAAU,SAAS,aAAc,QAAO;AAC1F,QAAM,SAAS,OAAO,SAAS;AAC/B,QAAM,WAAW,YAAY,KAAK,YAAY,CAAC,GAAG,KAAK;AACvD,MAAI,OAAO,aAAa,YAAY,SAAS,WAAW,EAAG,QAAO;AAClE,MAAI,cAAc,IAAI,MAAM,KAAK,WAAW,UAAW,QAAO;AAC9D,MAAI,WAAW,iBAAkB,QAAO,IAAI,QAAQ;AACpD,SAAO;AACT;AAQA,SAAS,gCAAgC,WAAuB;AAC9D,WAAS,IAAI,UAAU,SAAS,GAAG,KAAK,GAAG,KAAK;AAC9C,UAAM,OAAO,UAAU,CAAC;AACxB,QAAI,QAAQ,iBAAiB,IAAI,KAAK,IAAI,EAAG,QAAO;AAAA,EACtD;AACA,SAAO;AACT;AAGA,SAAS,wBAAwB,WAAyB;AACxD,QAAM,QAAe,CAAC;AACtB,WAAS,IAAI,UAAU,SAAS,GAAG,KAAK,GAAG,KAAK;AAC9C,UAAM,OAAO,UAAU,CAAC;AACxB,QAAI,QAAQ,iBAAiB,IAAI,KAAK,IAAI,EAAG,OAAM,KAAK,IAAI;AAAA,EAC9D;AACA,SAAO;AACT;AAIA,SAAS,WACP,UACA,WACA,MACA,UACM;AACN,MAAI,SAAS,SAAS,IAAI,SAAS;AACnC,MAAI,CAAC,QAAQ;AACX,aAAS,oBAAI,IAAI;AACjB,aAAS,IAAI,WAAW,MAAM;AAAA,EAChC;AACA,MAAI,CAAC,OAAO,IAAI,IAAI,EAAG,QAAO,IAAI,MAAM,QAAQ;AAClD;AAEA,SAAS,2BACP,MACA,WACA,UACe;AACf,aAAW,aAAa,wBAAwB,SAAS,GAAG;AAC1D,UAAM,WAAW,SAAS,IAAI,SAAS,GAAG,IAAI,IAAI;AAClD,QAAI,aAAa,OAAW,QAAO;AAAA,EACrC;AAGA,SAAO,SAAS,IAAI,IAAI,GAAG,IAAI,IAAI,KAAK;AAC1C;AAEA,SAASC,gBAAe,MAAoB;AAC1C,SACE,MAAM,SAAS,6BACf,MAAM,SAAS,wBACf,MAAM,SAAS;AAEnB;AAEA,SAAS,0BACP,MACA,WACA,OACA,UACe;AACf,MAAI,MAAM,SAAS;AACjB,WAAO,2BAA2B,KAAK,MAAM,WAAW,QAAQ;AAClE,MAAI,MAAM,SAAS,iBAAkB,QAAO,sBAAsB,MAAM,KAAK;AAC7E,SAAO;AACT;AAEA,SAAS,qBAAqB,KAAyB;AACrD,QAAM,WAAW,oBAAI,IAAuC;AAC5D,EAAU,iBAAO,KAAK;AAAA,IACpB,mBAAmB,MAAW;AAC5B,YAAM,OAAO,KAAK,IAAI;AACtB,YAAM,OAAO,KAAK;AAClB,UAAI,QAAQ,MAAM;AAChB,cAAM,MAAM,YAAY,MAAM,QAAQ;AACtC,YAAI,QAAQ,OAAW,UAAS,IAAI,MAAM,GAAG;AAAA,MAC/C;AAAA,IACF;AAAA,EACF,CAAC;AACD,SAAO;AACT;AAOA,SAAS,sBAAsB,KAAU,OAAsC;AAC7E,QAAM,WAA2B,oBAAI,IAAI;AAEzC,EAAU,mBAAS,KAAK;AAAA,IACtB,mBAAmB,MAAW,GAAY,WAAkB;AAC1D,YAAM,OAAO,KAAK,IAAI;AACtB,YAAM,WAAW,sBAAsB,KAAK,MAAM,KAAK;AACvD,UAAI,QAAQ,aAAa,MAAM;AAC7B,mBAAW,UAAU,gCAAgC,SAAS,GAAG,MAAM,QAAQ;AAAA,MACjF;AAAA,IACF;AAAA,IACA,qBAAqB,MAAW,GAAY,WAAkB;AAC5D,YAAM,OAAO,KAAK;AAClB,YAAM,WAAW,sBAAsB,KAAK,OAAO,KAAK;AACxD,UAAI,MAAM,SAAS,gBAAgB,aAAa,MAAM;AACpD,mBAAW,UAAU,gCAAgC,SAAS,GAAG,KAAK,MAAM,QAAQ;AAAA,MACtF;AAAA,IACF;AAAA,EACF,CAAQ;AAGR,EAAU,mBAAS,KAAK;AAAA;AAAA,IAEtB,eAAe,MAAW,GAAY,WAAkB;AACtD,YAAM,SAAS,KAAK;AACpB,UACE,QAAQ,SAAS,sBACjB,OAAO,UAAU,SAAS,gBAC1B,kBAAkB,IAAI,OAAO,SAAS,IAAI,GAC1C;AACA,cAAM,qBAAqB;AAAA,UACzB,OAAO;AAAA,UACP;AAAA,UACA;AAAA,UACA;AAAA,QACF;AACA,cAAM,KAAK,KAAK,YAAY,CAAC;AAC7B,cAAM,QAAQ,IAAI,SAAS,CAAC;AAC5B,YAAI,sBAAsB,OAAO,SAAS,gBAAgBA,gBAAe,EAAE,GAAG;AAC5E,qBAAW,UAAU,IAAI,MAAM,MAAM,kBAAkB;AAAA,QACzD;AAAA,MACF;AAAA,IACF;AAAA,EACF,CAAQ;AAER,SAAO;AACT;AAGA,SAAS,sBACP,MACA,WACA,OACA,UACe;AACf,MAAI,CAAC,KAAM,QAAO;AAClB,MAAI,KAAK,SAAS,mBAAmB,KAAK,SAAS,WAAW;AAC5D,WAAO,OAAO,KAAK,UAAU,WAAW,KAAK,QAAQ;AAAA,EACvD;AACA,MAAI,KAAK,SAAS,cAAc;AAC9B,WAAO,2BAA2B,KAAK,MAAM,WAAW,QAAQ;AAAA,EAClE;AACA,MAAI,KAAK,SAAS,kBAAkB;AAClC,WAAO,sBAAsB,MAAM,KAAK;AAAA,EAC1C;AACA,MAAI,KAAK,SAAS,mBAAmB;AACnC,UAAM,QAAQ,KAAK,SAChB,IAAI,CAAC,OAAY,sBAAsB,IAAI,WAAW,OAAO,QAAQ,CAAC,EACtE,OAAO,CAAC,MAAkC,OAAO,MAAM,YAAY,EAAE,SAAS,CAAC;AAClF,WAAO,MAAM,SAAS,IAAI,MAAM,KAAK,IAAI,IAAI;AAAA,EAC/C;AACA,MAAI,KAAK,SAAS,sBAAsB,KAAK,QAAQ,SAAS,cAAc;AAC1E,WAAO,2BAA2B,KAAK,OAAO,MAAM,WAAW,QAAQ;AAAA,EACzE;AACA,SAAO;AACT;AAIA,SAAS,iBAAiB,MAAoB;AAC5C,SAAO,MAAM,SAAS,oBAAoB,MAAM,SAAS;AAC3D;AAEA,SAAS,YAAY,MAA+B;AAClD,SAAO,MAAM,KAAK,QAAQ,MAAM,KAAK;AACvC;AAEA,SAAS,iBAAiB,aAAkB,KAA8B;AACxE,MAAI,aAAa,SAAS,mBAAoB,QAAO;AACrD,aAAW,QAAQ,YAAY,cAAc,CAAC,GAAG;AAC/C,QAAI,CAAC,iBAAiB,IAAI,EAAG;AAC7B,QAAI,YAAY,IAAI,MAAM,IAAK,QAAO,KAAK;AAAA,EAC7C;AACA,SAAO;AACT;AAMA,SAAS,yBACP,aACA,KACA,QACoB;AACpB,QAAM,OAAO,iBAAiB,aAAa,GAAG;AAC9C,SAAO,OAAO,OAAO,MAAM,KAAK,OAAO,KAAK,GAAG,IAAI;AACrD;AAGA,SAAS,yBACP,MACA,OACA,QACyB;AACzB,QAAM,SAAkC,CAAC;AACzC,MAAI,MAAM,SAAS,mBAAoB,QAAO;AAC9C,aAAW,QAAQ,KAAK,cAAc,CAAC,GAAG;AACxC,QAAI,CAAC,iBAAiB,IAAI,EAAG;AAC7B,UAAM,MAAM,KAAK,KAAK,QAAQ,KAAK,KAAK;AACxC,QAAI,CAAC,IAAK;AACV,UAAM,WAAW,YAAY,KAAK,OAAO,KAAK;AAC9C,QAAI,aAAa,QAAW;AAC1B,aAAO,GAAG,IAAI;AAAA,IAChB,OAAO;AACL,aAAO,GAAG,IAAI,SAAS,OAAO,MAAM,KAAK,MAAM,OAAO,KAAK,MAAM,GAAG,CAAC;AAAA,IACvE;AAAA,EACF;AACA,SAAO;AACT;AAIA,SAAS,mBAAmB,MAAoB;AAC9C,SACE,MAAM,SAAS,oBACf,KAAK,QAAQ,SAAS,sBACtB,KAAK,OAAO,QAAQ,SAAS,UAC7B,KAAK,OAAO,UAAU,SAAS;AAEnC;AAsBA,SAAS,gBAAgB,MAA0B;AACjD,MAAI,CAAC,QAAQ,KAAK,SAAS,mBAAoB,QAAO;AACtD,MAAI,KAAK,UAAU;AACjB,UAAM,IAAI,KAAK;AACf,QAAI,GAAG,SAAS,aAAa,OAAO,EAAE,UAAU,SAAU,QAAO,EAAE;AACnE,WAAO;AAAA,EACT;AACA,SAAO,KAAK,UAAU,SAAS,eAAe,KAAK,SAAS,OAAO;AACrE;AAGA,SAAS,kBAAkB,MAAoB;AAC7C,SAAO,MAAM,SAAS,sBAAsB,gBAAgB,IAAI,MAAM;AACxE;AAGA,SAAS,iBAAiB,GAAQ,GAAiB;AACjD,MAAI,GAAG,SAAS,sBAAsB,GAAG,SAAS,mBAAoB,QAAO;AAC7E,MAAI,gBAAgB,CAAC,MAAM,gBAAgB,CAAC,KAAK,gBAAgB,CAAC,MAAM,KAAM,QAAO;AACrF,QAAM,KAAK,EAAE;AACb,QAAM,KAAK,EAAE;AACb,MAAI,IAAI,SAAS,gBAAgB,IAAI,SAAS,aAAc,QAAO,GAAG,SAAS,GAAG;AAClF,MAAI,IAAI,SAAS,sBAAsB,IAAI,SAAS;AAClD,WAAO,iBAAiB,IAAI,EAAE;AAChC,SAAO;AACT;AAGA,SAAS,mBAAmB,KAAkB,QAAwB;AACpE,SAAO,IAAI,SAAS,eAAe,IAAI,OAAO,OAAO,MAAM,IAAI,KAAK,OAAO,IAAI,KAAK,GAAG;AACzF;AAEA,SAAS,aAAa,GAAmB;AACvC,SAAO,EAAE,QAAQ,uBAAuB,MAAM;AAChD;AAGA,SAAS,wBACP,UACA,OAC8B;AAC9B,QAAM,MAAM,SAAS,YAAY,CAAC;AAClC,MAAI,CAAC,OAAO,IAAI,SAAS,mBAAoB,QAAO;AACpD,QAAM,eAAe,IAAI,YAAY;AAAA,IACnC,CAAC,MAAW,iBAAiB,CAAC,KAAK,YAAY,CAAC,MAAM;AAAA,EACxD;AACA,MAAI,CAAC,cAAc,SAAS,aAAa,MAAM,SAAS,mBAAoB,QAAO;AACnF,QAAM,SAA2B,CAAC;AAClC,aAAW,QAAQ,aAAa,MAAM,cAAc,CAAC,GAAG;AACtD,QAAI,CAAC,iBAAiB,IAAI,EAAG;AAC7B,UAAM,MAAM,YAAY,IAAI;AAC5B,UAAM,MAAM,YAAY,KAAK,OAAO,KAAK;AACzC,QAAI,QAAQ,UAAU,OAAO,QAAQ,SAAU,QAAO,OAAO;AAC7D,QAAI,QAAQ,cAAc,OAAO,QAAQ,SAAU,QAAO,WAAW;AAAA,EACvE;AACA,SAAO,OAAO,KAAK,MAAM,EAAE,SAAS,IAAI,SAAS;AACnD;AAEA,SAAS,gBAAgB,KAAU,OAA0C;AAC3E,MAAI,cAA6B;AACjC,MAAI,MAA0B;AAC9B,MAAI,gBAAgB;AACpB,MAAI;AACJ,QAAM,aAA4B,SAAS,oBAAI,IAAI;AAEnD,EAAU,iBAAO,KAAK;AAAA,IACpB,mBAAmB,MAAW;AAC5B,UAAI,mBAAmB,KAAK,IAAI,GAAG;AACjC,yBAAiB;AACjB,YAAI,CAAC,OAAO,KAAK,IAAI,SAAS,cAAc;AAC1C,wBAAc,KAAK,GAAG;AACtB,gBAAM,EAAE,MAAM,cAAc,MAAM,KAAK,GAAG,KAAK;AAC/C,qBAAW,wBAAwB,KAAK,MAAM,UAAU;AAAA,QAC1D;AAAA,MACF;AAAA,IACF;AAAA,IACA,qBAAqB,MAAW;AAC9B,UAAI,mBAAmB,KAAK,KAAK,GAAG;AAClC,yBAAiB;AACjB,YAAI,CAAC,KAAK;AACR,gBAAM,OAAO,KAAK;AAClB,cAAI,MAAM,SAAS,cAAc;AAC/B,0BAAc,KAAK;AACnB,kBAAM,EAAE,MAAM,cAAc,MAAM,KAAK,KAAK;AAC5C,uBAAW,wBAAwB,KAAK,OAAO,UAAU;AAAA,UAC3D,WAAW,kBAAkB,IAAI,GAAG;AAElC,kBAAM,EAAE,MAAM,UAAU,MAAM,KAAK;AACnC,uBAAW,wBAAwB,KAAK,OAAO,UAAU;AAAA,UAC3D;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF,CAAC;AAED,SAAO,EAAE,aAAa,KAAK,eAAe,SAAS;AACrD;AAKA,IAAM,mBAAmB,oBAAI,IAAI,CAAC,YAAY,QAAQ,OAAO,CAAC;AAE9D,IAAM,mBAAmB,oBAAI,IAAI,CAAC,cAAc,WAAW,YAAY,UAAU,CAAC;AAElF,IAAM,cAAc,oBAAI,IAAI;AAAA,EAC1B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAgBD,SAAS,qBAAqB,UAAe,KAA2B;AACtE,MAAI,MAAM,SAAS,QAAQ;AAC3B,SAAO,KAAK,SAAS,kBAAkB;AACrC,UAAM,IAAI,QAAQ;AAAA,EACpB;AACA,MAAI,IAAI,SAAS,aAAc,QAAO,KAAK,SAAS,gBAAgB,IAAI,SAAS,IAAI;AACrF,SAAO,iBAAiB,KAAK,IAAI,IAAI;AACvC;AASA,SAAS,kBACP,KACA,KACA,OACA,gBACiB;AACjB,QAAM,UAA2B,CAAC;AAGlC,WAAS,MAAM,MAAW,WAAiC;AACzD,QAAI,CAAC,QAAQ,OAAO,SAAS,SAAU;AACvC,UAAM,gBAAgB,CAAC,GAAG,WAAW,IAAI;AAGzC,QAAI,KAAK,SAAS,kBAAkB;AAClC,YAAM,SAAS,KAAK;AAIpB,YAAM,aAAa,KAAK,YAAY,CAAC;AACrC,YAAM,cACJ,QAAQ,SAAS,sBACjB,OAAO,QAAQ,SAAS,gBACxB,OAAO,OAAO,SAAS,UACvB,OAAO,UAAU,SAAS,gBAC1B,OAAO,SAAS,SAAS,UACxB,YAAY,SAAS,mBACnB,YAAY,SAAS,aAAa,OAAO,WAAW,UAAU;AACnE,UACE,QAAQ,SAAS,sBACjB,OAAO,UAAU,SAAS,iBACzB,qBAAqB,MAAM,GAAG,KAAK,gBACpCD,cAAa,IAAI,OAAO,SAAS,IAAI,GACrC;AACA,cAAM,SAAS,OAAO,SAAS;AAC/B,cAAM,OAAO,KAAK;AAClB,cAAM,gBACJ,KAAK,UAAU,IACV,sBAAsB,KAAK,CAAC,GAAG,eAAe,OAAO,cAAc,KACpE,mBACA;AAEN,YAAI,WAAW,YAAY,KAAK,UAAU,GAAG;AAC3C,kBAAQ,KAAK;AAAA,YACX;AAAA,YACA,WAAW;AAAA,YACX,QAAQ;AAAA,YACR,UAAU;AAAA,YACV,SAAS,KAAK,CAAC;AAAA,YACf,SAAS,KAAK,CAAC;AAAA,YACf,aAAa,KAAK,CAAC;AAAA,UACrB,CAAC;AAAA,QACH,WAAW,WAAW,YAAY,KAAK,UAAU,GAAG;AAClD,kBAAQ,KAAK;AAAA,YACX;AAAA,YACA,WAAW;AAAA,YACX;AAAA,YACA,UAAU;AAAA,YACV,SAAS,KAAK,CAAC;AAAA,YACf,aAAa,KAAK,CAAC;AAAA,YACnB,GAAI,cAAc,EAAE,QAAQ,KAAK,IAAI,CAAC;AAAA,UACxC,CAAC;AAAA,QACH;AAAA,MACF;AAAA,IACF;AAIA,eAAW,OAAO,OAAO,KAAK,IAAI,GAAG;AACnC,UAAI,QAAQ,UAAU,QAAQ,WAAW,QAAQ,SAAS,QAAQ,MAAO;AACzE,YAAM,QAAS,KAAa,GAAG;AAC/B,UAAI,MAAM,QAAQ,KAAK,GAAG;AACxB,mBAAW,QAAQ,OAAO;AACxB,cAAI,QAAQ,OAAO,SAAS,YAAY,KAAK,KAAM,OAAM,MAAM,aAAa;AAAA,QAC9E;AAAA,MACF,WAAW,SAAS,OAAO,UAAU,YAAa,MAAc,MAAM;AACpE,cAAM,OAAO,aAAa;AAAA,MAC5B;AAAA,IACF;AAAA,EACF;AAEA,QAAM,KAAK,CAAC,CAAC;AACb,SAAO;AACT;AAIA,IAAM,oBAAoB;AAE1B,SAAS,qBAAqB,WAAgB,OAA0C;AACtF,QAAM,MAAM,YAAY,WAAW,KAAK;AACxC,SAAO,OAAO,QAAQ,WAAW,MAAM;AACzC;AAGA,SAAS,yBACP,MACA,OACA,QACmB;AACnB,QAAM,YAAsC,CAAC;AAC7C,MAAI;AACJ,MAAI;AAEJ,aAAW,QAAQ,KAAK,cAAc,CAAC,GAAG;AACxC,QAAI,KAAK,SAAS,oBAAoB,KAAK,SAAS,WAAY;AAChE,UAAM,MAAM,KAAK,KAAK,SAAS,KAAK,KAAK;AACzC,QAAI,OAAO,QAAQ,SAAU;AAE7B,UAAM,WAAW,kBAAkB,KAAK,GAAG;AAC3C,QAAI,UAAU;AACZ,YAAM,aAAa,OAAO,WAAW,SAAS,CAAC,KAAK,GAAG;AACvD,YAAM,SAAS,yBAAyB,KAAK,OAAO,OAAO,MAAM;AACjE,YAAM,aAA8C,CAAC;AACrD,UAAI;AACJ,iBAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,MAAM,GAAG;AAC3C,YAAI,MAAM,UAAU,OAAO,MAAM,UAAU;AACzC,mBAAS;AAAA,QACX,WAAW,OAAO,MAAM,YAAY,OAAO,MAAM,UAAU;AACzD,qBAAW,CAAC,IAAI;AAAA,QAClB;AAAA,MACF;AACA,gBAAU,KAAK,EAAE,YAAY,YAAY,GAAI,SAAS,EAAE,MAAM,OAAO,IAAI,CAAC,EAAG,CAAC;AAAA,IAChF,WAAW,QAAQ,QAAQ;AACzB,aAAO,qBAAqB,KAAK,OAAO,KAAK,KAAK;AAAA,IACpD,WAAW,QAAQ,YAAY;AAC7B,iBAAW,qBAAqB,KAAK,OAAO,KAAK,KAAK;AAAA,IACxD;AAAA,EACF;AAEA,YAAU,KAAK,CAAC,GAAG,MAAM,EAAE,aAAa,EAAE,UAAU;AAEpD,SAAO;AAAA,IACL,QAAQ;AAAA,IACR;AAAA,IACA,GAAI,OAAO,EAAE,KAAK,IAAI,CAAC;AAAA,IACvB,GAAI,WAAW,EAAE,SAAS,IAAI,CAAC;AAAA,EACjC;AACF;AAGA,SAAS,8BACP,UACA,OACA,QACoB;AACpB,QAAM,UAAU,SAAS,cAAc,CAAC,GAAG;AAAA,IACzC,CAAC,OAAY,EAAE,KAAK,QAAQ,EAAE,KAAK,WAAW;AAAA,EAChD,GAAG;AACH,MAAI,CAAC,UAAU,OAAO,SAAS,kBAAmB,QAAO;AACzD,MAAI,QAAQ;AACZ,aAAW,MAAM,OAAO,YAAY,CAAC,GAAG;AACtC,QAAI,CAAC,MAAM,GAAG,SAAS,mBAAoB;AAC3C,UAAM,IAAI,yBAAyB,IAAI,OAAO,MAAM;AACpD,QAAI,OAAO,EAAE,aAAa,SAAU,UAAS,EAAE;AAAA,EACjD;AACA,SAAO,QAAQ,IAAI,QAAQ;AAC7B;AAGA,SAAS,0BACP,MACA,OACA,QACmB;AACnB,QAAM,WAAW,KAAK,YAAY,CAAC;AACnC,QAAM,MAID,CAAC;AAEN,aAAW,MAAM,UAAU;AACzB,QAAI,CAAC,MAAM,GAAG,SAAS,mBAAoB;AAC3C,UAAM,SAAS,yBAAyB,IAAI,OAAO,MAAM;AACzD,UAAM,aAA8C,CAAC;AACrD,QAAI;AACJ,QAAI;AACJ,eAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,MAAM,GAAG;AAC3C,UAAI,MAAM,cAAc,OAAO,MAAM,UAAU;AAC7C,mBAAW;AAAA,MACb,WAAW,MAAM,UAAU,OAAO,MAAM,UAAU;AAChD,eAAO;AAAA,MACT,WAAW,OAAO,MAAM,YAAY,OAAO,MAAM,UAAU;AACzD,mBAAW,CAAC,IAAI;AAAA,MAClB;AAAA,IACF;AACA,QAAI,KAAK,EAAE,YAAY,UAAU,KAAK,CAAC;AAAA,EACzC;AAEA,QAAM,gBAAgB,IAAI,OAAO,CAAC,KAAK,MAAM,OAAO,EAAE,YAAY,IAAI,CAAC;AACvE,QAAM,YAAsC,CAAC;AAE7C,MAAI,gBAAgB,GAAG;AACrB,QAAI,aAAa;AACjB,eAAW,SAAS,KAAK;AACvB,oBAAc,MAAM,YAAY;AAChC,YAAM,aAAa,KAAK,MAAO,aAAa,gBAAiB,GAAG;AAChE,gBAAU,KAAK;AAAA,QACb;AAAA,QACA,YAAY,MAAM;AAAA,QAClB,GAAI,MAAM,OAAO,EAAE,MAAM,MAAM,KAAK,IAAI,CAAC;AAAA,MAC3C,CAAC;AAAA,IACH;AAAA,EACF,OAAO;AACL,aAAS,IAAI,GAAG,IAAI,IAAI,QAAQ,KAAK;AACnC,YAAM,QAAQ,IAAI,CAAC;AACnB,UAAI,CAAC,MAAO;AACZ,YAAM,aAAa,IAAI,SAAS,IAAI,KAAK,MAAO,KAAK,IAAI,SAAS,KAAM,GAAG,IAAI;AAC/E,gBAAU,KAAK;AAAA,QACb;AAAA,QACA,YAAY,MAAM;AAAA,QAClB,GAAI,MAAM,OAAO,EAAE,MAAM,MAAM,KAAK,IAAI,CAAC;AAAA,MAC3C,CAAC;AAAA,IACH;AAAA,EACF;AAEA,SAAO,EAAE,QAAQ,gBAAgB,UAAU;AAC7C;AAGA,SAAS,0BAA0B,MAAW,OAAyC;AACrF,QAAM,aAA+C,oBAAI,IAAI;AAC7D,MAAI;AACJ,MAAI;AAEJ,aAAW,QAAQ,KAAK,cAAc,CAAC,GAAG;AACxC,QAAI,KAAK,SAAS,oBAAoB,KAAK,SAAS,WAAY;AAChE,UAAM,MAAM,KAAK,KAAK,QAAQ,KAAK,KAAK;AACxC,QAAI,OAAO,QAAQ,SAAU;AAE7B,QAAI,KAAK,OAAO,SAAS,mBAAmB;AAC1C,YAAM,SAA8B,CAAC;AACrC,iBAAW,MAAM,KAAK,MAAM,YAAY,CAAC,GAAG;AAC1C,cAAM,MAAM,YAAY,IAAI,KAAK;AACjC,YAAI,OAAO,QAAQ,YAAY,OAAO,QAAQ,UAAU;AACtD,iBAAO,KAAK,GAAG;AAAA,QACjB;AAAA,MACF;AACA,UAAI,OAAO,SAAS,EAAG,YAAW,IAAI,KAAK,MAAM;AAAA,IACnD,WAAW,QAAQ,QAAQ;AACzB,aAAO,qBAAqB,KAAK,OAAO,KAAK,KAAK;AAAA,IACpD,WAAW,QAAQ,YAAY;AAC7B,iBAAW,qBAAqB,KAAK,OAAO,KAAK,KAAK;AAAA,IACxD;AAAA,EACF;AAEA,QAAM,SAAS,KAAK,IAAI,GAAG,CAAC,GAAG,WAAW,OAAO,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,CAAC;AAC3E,QAAM,YAAsC,CAAC;AAE7C,WAAS,IAAI,GAAG,IAAI,QAAQ,KAAK;AAC/B,UAAM,aAAa,SAAS,IAAI,KAAK,MAAO,KAAK,SAAS,KAAM,GAAG,IAAI;AACvE,UAAM,aAA8C,CAAC;AACrD,eAAW,CAAC,KAAK,MAAM,KAAK,YAAY;AACtC,UAAI,IAAI,OAAO,OAAQ,YAAW,GAAG,IAAI,OAAO,CAAC;AAAA,IACnD;AACA,cAAU,KAAK,EAAE,YAAY,WAAW,CAAC;AAAA,EAC3C;AAEA,SAAO;AAAA,IACL,QAAQ;AAAA,IACR;AAAA,IACA,GAAI,OAAO,EAAE,KAAK,IAAI,CAAC;AAAA,IACvB,GAAI,WAAW,EAAE,SAAS,IAAI,CAAC;AAAA,EACjC;AACF;AAGA,SAAS,mBACP,MACA,OACA,QAC+B;AAC/B,MAAI,CAAC,KAAM,QAAO;AAElB,MAAI,KAAK,SAAS,mBAAmB;AACnC,WAAO,0BAA0B,MAAM,OAAO,MAAM;AAAA,EACtD;AAEA,MAAI,KAAK,SAAS,mBAAoB,QAAO;AAE7C,QAAM,QAAQ,KAAK,cAAc,CAAC;AAClC,MAAI,mBAAmB;AACvB,MAAI,gBAAgB;AAEpB,aAAW,QAAQ,OAAO;AACxB,QAAI,KAAK,SAAS,oBAAoB,KAAK,SAAS,WAAY;AAChE,UAAM,MAAM,KAAK,KAAK,SAAS,KAAK,KAAK;AACzC,QAAI,OAAO,QAAQ,YAAY,kBAAkB,KAAK,GAAG,GAAG;AAC1D,yBAAmB;AACnB;AAAA,IACF;AACA,QAAI,KAAK,OAAO,SAAS,mBAAmB;AAC1C,sBAAgB;AAAA,IAClB;AAAA,EACF;AAEA,MAAI,iBAAkB,QAAO,yBAAyB,MAAM,OAAO,MAAM;AACzE,MAAI,cAAe,QAAO,0BAA0B,MAAM,KAAK;AAE/D,SAAO;AACT;AAUA,SAAS,oBACP,MACA,OACA,QACmC;AACnC,MAAI,CAAC,KAAM,QAAO;AAElB,MAAI;AACJ,MAAI,aAA+B;AACnC,MAAI,YAAY;AAChB,MAAI,UAAU;AAEd,MAAI,KAAK,SAAS,oBAAoB;AACpC,eAAW,QAAQ,KAAK,cAAc,CAAC,GAAG;AACxC,UAAI,CAAC,iBAAiB,IAAI,EAAG;AAC7B,YAAM,MAAM,YAAY,IAAI;AAC5B,UAAI,QAAQ,OAAQ,YAAW,KAAK;AAAA,eAC3B,QAAQ,cAAc;AAC7B,cAAM,MAAM,YAAY,KAAK,OAAO,KAAK;AACzC,qBAAa,OAAO,QAAQ,WAAW,MAAM,QAAQ;AAAA,MACvD,WAAW,QAAQ,aAAa;AAC9B,cAAM,MAAM,YAAY,KAAK,OAAO,KAAK;AACzC,YAAI,OAAO,QAAQ,SAAU,aAAY;AAAA,MAC3C,WAAW,QAAQ,QAAQ;AACzB,cAAM,MAAM,YAAY,KAAK,OAAO,KAAK;AACzC,YAAI,QAAQ,QAAS,WAAU;AAAA,MACjC;AAAA,IACF;AAAA,EACF,WAAW,KAAK,SAAS,mBAAmB;AAC1C,eAAW;AAAA,EACb;AAEA,MAAI,CAAC,YAAY,SAAS,SAAS,kBAAmB,QAAO;AAE7D,QAAM,WAAW,SAAS,YAAY,CAAC;AACvC,QAAM,SAA0C,CAAC;AACjD,aAAW,QAAQ,UAAU;AAC3B,QAAI,CAAC,QAAQ,KAAK,SAAS,mBAAoB;AAC/C,UAAM,MAAM,yBAAyB,MAAM,OAAO,MAAM;AACxD,UAAM,IAAI,OAAO,IAAI,MAAM,WAAW,IAAI,IAAI;AAC9C,UAAM,IAAI,OAAO,IAAI,MAAM,WAAW,IAAI,IAAI;AAC9C,QAAI,MAAM,UAAa,MAAM,OAAW,QAAO,KAAK,EAAE,GAAG,EAAE,CAAC;AAAA,EAC9D;AAEA,SAAO,aAAa,QAAQ,WAAW,YAAY,OAAO;AAC5D;AAKA,SAAS,qBACP,MACA,OACA,QAC2B;AAC3B,QAAM,OAAO,yBAAyB,KAAK,SAAS,OAAO,MAAM;AACjE,QAAM,aAA8C,CAAC;AACrD,QAAM,SAAkC,CAAC;AACzC,MAAI;AACJ,MAAI,yBAAyB;AAC7B,MAAI;AAEJ,aAAW,CAAC,KAAK,GAAG,KAAK,OAAO,QAAQ,IAAI,GAAG;AAC7C,QAAI,iBAAiB,IAAI,GAAG,EAAG;AAC/B,QAAI,iBAAiB,IAAI,GAAG,EAAG;AAE/B,QAAI,QAAQ,aAAa;AACvB,YAAM,SAAS,iBAAiB,KAAK,SAAS,WAAW;AACzD,sBAAgB,mBAAmB,QAAQ,OAAO,MAAM;AACxD,UAAI,CAAC,iBAAiB,OAAQ,0BAAyB;AACvD;AAAA,IACF;AAEA,QAAI,QAAQ,cAAc;AACxB,YAAM,SAAS,iBAAiB,KAAK,SAAS,YAAY;AAC1D,yBAAmB,oBAAoB,QAAQ,OAAO,MAAM;AAC5D;AAAA,IACF;AAEA,QAAI,QAAQ,WAAY;AAExB,QAAI,YAAY,IAAI,GAAG,GAAG;AACxB,YAAM,YAAY,yBAAyB,KAAK,SAAS,KAAK,MAAM;AACpE,UAAI,cAAc,QAAW;AAC3B,eAAO,GAAG,IAAI,SAAS,SAAS;AAAA,MAClC,WAAW,QAAQ,QAAW;AAC5B,eAAO,GAAG,IAAI;AAAA,MAChB;AACA;AAAA,IACF;AAEA,QAAI,OAAO,QAAQ,YAAY,OAAO,QAAQ,UAAU;AACtD,iBAAW,GAAG,IAAI;AAAA,IACpB;AAAA,EACF;AAEA,MAAI,iBAAiB,OAAO,KAAK,aAAa,UAAU;AACtD,kBAAc,WAAW,KAAK;AAAA,EAChC;AAEA,MAAI,kBAAkB;AACpB,UAAM,EAAE,UAAU,IAAI;AACtB,QAAI,CAAC,eAAe;AAClB,YAAM,KAA+B,UAAU,IAAI,CAAC,IAAI,OAAO;AAAA,QAC7D,YAAY,UAAU,SAAS,IAAI,KAAK,MAAO,KAAK,UAAU,SAAS,KAAM,GAAG,IAAI;AAAA,QACpF,YAAY,EAAE,GAAG,GAAG,GAAG,GAAG,GAAG,EAAE;AAAA,MACjC,EAAE;AACF,sBAAgB,EAAE,QAAQ,cAAc,WAAW,GAAG;AAAA,IACxD,OAAO;AACL,YAAM,MAAM,cAAc;AAC1B,UAAI,IAAI,WAAW,UAAU,QAAQ;AACnC,iBAAS,IAAI,GAAG,IAAI,IAAI,QAAQ,KAAK;AACnC,gBAAM,KAAK,IAAI,CAAC;AAChB,gBAAM,KAAK,UAAU,CAAC;AACtB,cAAI,MAAM,IAAI;AACZ,eAAG,WAAW,IAAI,GAAG;AACrB,eAAG,WAAW,IAAI,GAAG;AAAA,UACvB;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,MAAI;AACJ,MAAI,KAAK,WAAW,YAAY,KAAK,SAAS;AAC5C,qBAAiB,CAAC;AAClB,UAAM,WAAW,yBAAyB,KAAK,SAAS,OAAO,MAAM;AACrE,eAAW,CAAC,KAAK,GAAG,KAAK,OAAO,QAAQ,QAAQ,GAAG;AACjD,UAAI,OAAO,QAAQ,YAAY,OAAO,QAAQ,UAAU;AACtD,uBAAe,GAAG,IAAI;AAAA,MACxB;AAAA,IACF;AAAA,EACF;AAEA,QAAM,iBAAiB,CAAC,CAAC,KAAK;AAC9B,QAAM,SAAS,iBAAiB,oBAAoB,KAAK,aAAa,KAAK,IAAI;AAC/E,QAAM,WACJ,OAAO,WAAW,WAAW,SAAS,OAAO,WAAW,WAAW,SAAS;AAC9E,MAAI,WAAW,OAAO,KAAK,aAAa,WAAW,KAAK,WAAW;AACnE,QAAM,OAAO,OAAO,KAAK,SAAS,WAAW,KAAK,OAAO;AAEzD,MAAI,aAAa,UAAa,eAAe;AAC3C,eAAW,8BAA8B,KAAK,SAAS,OAAO,MAAM;AAAA,EACtE;AAEA,QAAM,OAAkC;AAAA,IACtC,gBAAgB,KAAK;AAAA,IACrB,QAAQ,KAAK;AAAA,IACb;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACA,MAAI,CAAC,eAAgB,MAAK,mBAAmB;AAC7C,MAAI,QAAQ,2BAA2B,UAAU;AACjD,MAAI,CAAC,SAAS,eAAe;AAC3B,UAAM,UAAmC,CAAC;AAC1C,eAAW,MAAM,cAAc,WAAW;AACxC,iBAAW,KAAK,OAAO,KAAK,GAAG,UAAU,EAAG,SAAQ,CAAC,IAAI;AAAA,IAC3D;AACA,YAAQ,2BAA2B,OAAO;AAAA,EAC5C;AACA,MAAI,MAAO,MAAK,gBAAgB;AAChC,MAAI,KAAK,OAAQ,MAAK,SAAS;AAC/B,MAAI,OAAO,KAAK,MAAM,EAAE,SAAS,EAAG,MAAK,SAAS;AAClD,MAAI,cAAe,MAAK,YAAY;AACpC,MAAI,iBAAkB,MAAK,UAAU,iBAAiB;AACtD,MAAI,uBAAwB,MAAK,yBAAyB;AAC1D,MAAI,KAAK,aAAa,iBAAkB,MAAK,wBAAwB;AACrE,QAAM,aAAa,eAAe,KAAK,IAAI;AAC3C,MAAI,WAAY,MAAK,aAAa;AAClC,SAAO;AACT;AAIA,IAAM,wBAAwB;AAG9B,SAAS,sBAAsB,KAAa,QAAgB,WAAkC;AAC5F,QAAM,UAAU,IAAI,KAAK;AACzB,MAAI,YAAY,GAAI,QAAO;AAC3B,MAAI,QAAQ,WAAW,IAAI,GAAG;AAC5B,UAAME,KAAI,OAAO,WAAW,QAAQ,MAAM,CAAC,CAAC;AAC5C,WAAO,OAAO,SAASA,EAAC,IAAI,SAASA,KAAI;AAAA,EAC3C;AACA,MAAI,QAAQ,WAAW,IAAI,GAAG;AAC5B,UAAMA,KAAI,OAAO,WAAW,QAAQ,MAAM,CAAC,CAAC;AAC5C,WAAO,OAAO,SAASA,EAAC,IAAI,SAASA,KAAI;AAAA,EAC3C;AACA,MAAI,YAAY,IAAK,QAAO;AAC5B,MAAI,YAAY,IAAK,QAAO;AAC5B,MAAI,QAAQ,WAAW,GAAG,GAAG;AAC3B,UAAMA,KAAI,OAAO,WAAW,QAAQ,MAAM,CAAC,CAAC;AAC5C,WAAO,OAAO,SAASA,EAAC,IAAI,YAAYA,KAAI;AAAA,EAC9C;AACA,MAAI,QAAQ,WAAW,GAAG,GAAG;AAC3B,UAAMA,KAAI,OAAO,WAAW,QAAQ,MAAM,CAAC,CAAC;AAC5C,WAAO,OAAO,SAASA,EAAC,IAAI,SAASA,KAAI;AAAA,EAC3C;AACA,QAAM,IAAI,OAAO,WAAW,OAAO;AACnC,SAAO,OAAO,SAAS,CAAC,IAAI,IAAI;AAClC;AAEA,SAAS,sBACP,OACA,UACM;AACN,MAAI,CAAC,SAAU;AACf,aAAW,QAAQ,OAAO;AACxB,QAAI,KAAK,WAAW,MAAO;AAC3B,QAAI,KAAK,aAAa,UAAa,SAAS,aAAa,QAAW;AAClE,WAAK,WAAW,SAAS;AAAA,IAC3B;AACA,QAAI,KAAK,SAAS,UAAa,SAAS,SAAS,QAAW;AAC1D,WAAK,OAAO,SAAS;AAAA,IACvB;AAAA,EACF;AACF;AAGA,SAAS,yBAAyB,OAA0C;AAC1E,MAAI,SAAS;AACb,MAAI,YAAY;AAChB,aAAW,QAAQ,OAAO;AAKxB,QAAI,KAAK,WAAW,SAAS,KAAK,QAAQ;AACxC,WAAK,gBAAgB;AACrB;AAAA,IACF;AACA,UAAM,WAAW,KAAK,WAAW,QAAQ,IAAK,KAAK,YAAY;AAC/D,QAAI;AAEJ,QAAI,KAAK,kBAAkB;AACzB,cAAQ;AAAA,IACV,WAAW,OAAO,KAAK,aAAa,UAAU;AAC5C,cAAQ,KAAK;AAAA,IACf,WAAW,OAAO,KAAK,aAAa,UAAU;AAC5C,cAAQ,sBAAsB,KAAK,UAAU,QAAQ,SAAS;AAAA,IAChE,OAAO;AACL,cAAQ;AAAA,IACV;AAEA,QAAI,SAAS,MAAM;AACjB,WAAK,gBAAgB,KAAK,IAAI,GAAG,KAAK;AACtC,kBAAY,KAAK;AACjB,eAAS,KAAK,IAAI,QAAQ,KAAK,gBAAgB,QAAQ;AAAA,IACzD;AAAA,EACF;AACF;AAEA,SAAS,aAAa,GAAkB,GAA0B;AAChE,QAAM,OAAO,EAAE,KAAK,QAAQ,UAAU,KAAK;AAC3C,QAAM,OAAO,EAAE,KAAK,QAAQ,UAAU,KAAK;AAC3C,MAAI,CAAC,QAAQ,CAAC,KAAM,QAAO;AAC3B,SAAO,KAAK,OAAO,KAAK,QAAQ,KAAK,SAAS,KAAK;AACrD;AAIA,SAAS,iBAAiB,GAAkB,GAA0B;AACpE,QAAM,KAAK,EAAE,KAAK;AAClB,QAAM,KAAK,EAAE,KAAK;AAClB,MAAI,OAAO,UAAa,OAAO,OAAW,QAAO,aAAa,GAAG,CAAC;AAClE,MAAI,OAAO,OAAW,QAAO;AAC7B,MAAI,OAAO,OAAW,QAAO;AAC7B,SAAO,KAAK;AACd;AAEA,SAAS,qBAAqB,OAA8B;AAC1D,QAAM,KAAK,gBAAgB;AAC7B;AAIA,SAAS,gBAAgB,OAAqD;AAC5E,QAAM,SAAS,oBAAI,IAAoB;AACvC,SAAO,MAAM,IAAI,CAAC,SAAS;AACzB,UAAM,SACJ,OAAO,KAAK,aAAa,WACrB,OAAO,KAAK,MAAM,KAAK,WAAW,GAAI,CAAC,IACvC,OAAO,KAAK,QAAQ;AAC1B,UAAM,cAAc,KAAK,gBAAgB,IAAI,KAAK,aAAa,KAAK;AACpE,UAAM,OAAO,GAAG,KAAK,cAAc,IAAI,KAAK,MAAM,IAAI,MAAM,GAAG,WAAW;AAC1E,UAAM,SAAS,OAAO,IAAI,IAAI,KAAK,KAAK;AACxC,WAAO,IAAI,MAAM,KAAK;AACtB,UAAM,KAAK,UAAU,IAAI,OAAO,GAAG,IAAI,IAAI,KAAK;AAChD,WAAO,EAAE,GAAG,MAAM,GAAG;AAAA,EACvB,CAAC;AACH;AAeO,SAAS,6BAA6B,QAAgD;AAC3F,MAAI;AACF,UAAM,MAAY,YAAM,QAAQ;AAAA,MAC9B,aAAa;AAAA,MACb,YAAY;AAAA,MACZ,WAAW;AAAA,IACb,CAAC;AACD,UAAM,QAAQ,qBAAqB,GAAG;AACtC,UAAM,iBAAiB,sBAAsB,KAAK,KAAK;AACvD,UAAM,YAAY,gBAAgB,KAAK,KAAK;AAC5C,UAAM,MAAmB,UAAU,OAAO,EAAE,MAAM,cAAc,MAAM,KAAK;AAC3E,UAAM,cAAc,mBAAmB,KAAK,MAAM;AAClD,UAAM,QAAQ,kBAAkB,KAAK,KAAK,OAAO,cAAc;AAC/D,yBAAqB,KAAK;AAC1B,UAAM,WAAW,MAAM,IAAI,CAAC,SAAS,qBAAqB,MAAM,OAAO,MAAM,CAAC;AAC9E,0BAAsB,UAAU,UAAU,QAAQ;AAClD,6BAAyB,QAAQ;AACjC,UAAM,aAAa,gBAAgB,QAAQ;AAC3C,UAAM,UAAU,MAAM,IAAI,CAAC,MAAM,OAAO;AAAA,MACtC,IAAI,WAAW,CAAC,EAAG;AAAA,MACnB;AAAA,MACA,WAAW,WAAW,CAAC;AAAA,IACzB,EAAE;AACF,WAAO,EAAE,KAAK,aAAa,aAAa,UAAU,QAAQ,MAAM,QAAQ;AAAA,EAC1E,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAQO,SAAS,qBAAqB,QAA4B;AAC/D,MAAI;AACF,UAAM,MAAY,YAAM,QAAQ;AAAA,MAC9B,aAAa;AAAA,MACb,YAAY;AAAA,MACZ,WAAW;AAAA,IACb,CAAC;AACD,UAAM,QAAQ,qBAAqB,GAAG;AACtC,UAAM,YAAY,gBAAgB,KAAK,KAAK;AAC5C,UAAM,MAAmB,UAAU,OAAO,EAAE,MAAM,cAAc,MAAM,KAAK;AAC3E,UAAM,cAAc,mBAAmB,KAAK,MAAM;AAMlD,QAAI,IAAI,SAAS,cAAc;AAC7B,UAAI;AACF,gCAAwB,KAAK,aAAa,CAAC,SAAS,YAAY,MAAM,KAAK,CAAC;AAAA,MAC9E,QAAQ;AAAA,MAER;AAAA,IACF;AACA,UAAM,iBAAiB,sBAAsB,KAAK,KAAK;AACvD,UAAM,QAAQ,kBAAkB,KAAK,KAAK,OAAO,cAAc;AAC/D,yBAAqB,KAAK;AAC1B,UAAM,WAAW,MAAM,IAAI,CAAC,SAAS,qBAAqB,MAAM,OAAO,MAAM,CAAC;AAC9E,0BAAsB,UAAU,UAAU,QAAQ;AAClD,6BAAyB,QAAQ;AACjC,UAAM,aAAa,gBAAgB,QAAQ;AAK3C,UAAM,cACJ,IAAI,SAAS,eACT,wBAAwB,WAAW,kDACnC,GAAG,aAAa,WAAW,CAAC;AAClC,UAAM,gBAAgB,OAAO,MAAM,IAAI,OAAO,cAAc,WAAW,EAAE,CAAC;AAC1E,UAAM,mBACJ,IAAI,SAAS,eACT,SAAS,WAAW,wCACpB,GAAG,WAAW;AACpB,UAAM,WAAW,gBAAgB,CAAC,KAAK;AAEvC,UAAM,cAAc,OAAO,YAAY,GAAG,WAAW,GAAG;AACxD,QAAI,YAAY;AAChB,QAAI,gBAAgB,IAAI;AACtB,YAAM,YAAY,OAAO,MAAM,WAAW;AAC1C,YAAM,YAAY,UAAU,QAAQ,GAAG;AACvC,UAAI,cAAc,IAAI;AACpB,oBAAY,OAAO,MAAM,cAAc,YAAY,CAAC,EAAE,KAAK;AAAA,MAC7D;AAAA,IACF;AAEA,UAAM,SAAqB,EAAE,YAAY,aAAa,UAAU,UAAU;AAC1E,QAAI,UAAU,gBAAgB,EAAG,QAAO,oBAAoB;AAC5D,QAAI,UAAU,gBAAgB,KAAK,UAAU,QAAQ;AACnD,aAAO,6BAA6B;AACtC,WAAO;AAAA,EACT,QAAQ;AACN,WAAO,EAAE,YAAY,CAAC,GAAG,aAAa,MAAM,UAAU,IAAI,WAAW,GAAG;AAAA,EAC1E;AACF;;;AE3sCA,SAAS,iBAAiB;AAGnB,IAAM,gBAAgB,oBAAI,IAAI;AAAA,EACnC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAGD,SAAS,MAAM,KAAqB;AAClC,MAAI,IAAI;AACR,WAAS,IAAI,GAAG,IAAI,IAAI,QAAQ,KAAK;AACnC,SAAK,IAAI,WAAW,CAAC;AACrB,QAAI,KAAK,KAAK,GAAG,QAAU;AAAA,EAC7B;AACA,SAAO,MAAM;AACf;AAMA,SAAS,OAAO,MAAsB;AACpC,QAAM,KAAK,SAAS,GAAG,SAAS,EAAE;AAElC,QAAM,OAAO,EAAE,UAAU,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,GAAG,GAAG;AAC5D,SAAO,MAAM,IAAI;AACnB;AAGA,SAAS,QAAQ,IAAqB;AACpC,MAAI,OAAO;AACX,KAAG,WAAW,QAAQ,CAAC,MAAM;AAC3B,QAAI,EAAE,aAAa,EAAG,SAAS,EAAW,aAAa;AAAA,EACzD,CAAC;AACD,SAAO,KAAK,KAAK;AACnB;AAEA,SAAS,WAAW,IAAqB;AAGvC,QAAM,QAAQ,MAAM,KAAK,GAAG,UAAU,EACnC,OAAO,CAAC,MAAM,CAAC,EAAE,KAAK,WAAW,UAAU,CAAC,EAC5C,IAAI,CAAC,MAAM,GAAG,EAAE,IAAI,KAAO,EAAE,KAAK,EAAE,EACpC,KAAK,EACL,KAAK,GAAM;AACd,SAAO,GAAG,GAAG,QAAQ,YAAY,CAAC,IAAI,KAAK,IAAI,QAAQ,EAAE,CAAC;AAC5D;AAsBO,SAAS,SAAS,IAAa,UAA+B;AACnE,QAAM,MAAM,WAAW,EAAE;AACzB,MAAI,KAAK,OAAO,MAAM,GAAG,CAAC;AAC1B,MAAI,MAAM;AACV,SAAO,SAAS,IAAI,EAAE,GAAG;AACvB,WAAO;AAKP,QAAI,MAAM,KAAO;AACf,WAAK,OAAO,MAAM,GAAG,MAAM,GAAG,SAAS,EAAE,CAAC,IAAI,GAAG;AACjD;AAAA,IACF;AACA,SAAK,OAAO,MAAM,GAAG,GAAG,IAAI,GAAG,EAAE,CAAC;AAAA,EACpC;AACA,WAAS,IAAI,EAAE;AACf,SAAO;AACT;AAEO,SAAS,YAAY,MAAsB;AAGhD,QAAM,mBAAmB,wBAAwB,KAAK,IAAI;AAC1D,QAAM,UAAU,CAAC;AACjB,QAAM,EAAE,SAAS,IAAI,UACjB,UAAU,2CAA2C,IAAI,gBAAgB,IACzE,UAAU,IAAI;AAClB,QAAM,OAAO,SAAS;AACtB,MAAI,CAAC,KAAM,QAAO;AAElB,QAAM,WAAW,oBAAI,IAAY;AAIjC,aAAW,MAAM,MAAM,KAAK,KAAK,iBAAiB,cAAc,CAAC,GAAG;AAClE,UAAM,WAAW,GAAG,aAAa,YAAY;AAC7C,QAAI,SAAU,UAAS,IAAI,QAAQ;AAAA,EACrC;AAEA,aAAW,MAAM,MAAM,KAAK,KAAK,iBAAiB,GAAG,CAAC,GAAG;AACvD,QAAI,cAAc,IAAI,GAAG,QAAQ,YAAY,CAAC,EAAG;AACjD,QAAI,GAAG,aAAa,YAAY,EAAG;AACnC,OAAG,aAAa,cAAc,SAAS,IAAI,QAAQ,CAAC;AAAA,EACtD;AAEA,SAAO,UAAU,SAAS,KAAK,aAAa,KAAK,SAAS,SAAS;AACrE;;;ACjIO,SAAS,YACd,MACA,MACA,OACA,KACgB;AAChB,QAAM,WAAW,MAAM,GAAG,GAAG,IAAI,IAAI,MAAM,IAAI,IAAI;AACnD,aAAW,MAAM,KAAK,iBAAiB,QAAQ,GAAG;AAChD,QAAI,GAAG,aAAa,IAAI,MAAM,MAAO,QAAO;AAAA,EAC9C;AACA,SAAO;AACT;;;ACLA,OAAO,iBAAiB;AAoBxB,YAAYC,gBAAe;AA8E3B,SAAS,iCAAiC,WAAgC;AACxE,WAAS,IAAI,UAAU,SAAS,GAAG,KAAK,GAAG,KAAK;AAC9C,QAAI,UAAU,CAAC,GAAG,SAAS,sBAAuB,QAAO,UAAU,CAAC;AAAA,EACtE;AACA,SAAO;AACT;AAuWO,SAAS,0BAA0B,QAAgB,aAA6B;AACrF,QAAM,SAAS,6BAA6B,MAAM;AAClD,MAAI,CAAC,OAAQ,QAAO;AACpB,QAAM,SAAS,OAAO,QAAQ,KAAK,CAAC,MAAM,EAAE,OAAO,WAAW;AAC9D,MAAI,CAAC,OAAQ,QAAO;AAEpB,QAAM,KAAK,IAAI,YAAY,MAAM;AACjC,QAAM,IAAI,OAAO,KAAK;AACtB,QAAM,WAAW,iCAAiC,OAAO,KAAK,SAAS;AAEvE,MAAI,EAAE,QAAQ,QAAQ,SAAS,oBAAoB,UAAU,eAAe,GAAG;AAE7E,UAAM,MACJ,SAAS,MAAM,OAAO,UAAU,OAAO,SAAS,GAAG,MAAM,OACrD,SAAS,MAAM,IACf,SAAS;AACf,OAAG,OAAO,SAAS,OAAO,GAAG;AAAA,EAC/B,OAAO;AAEL,OAAG,OAAO,EAAE,OAAO,OAAO,KAAK,EAAE,GAAG;AAAA,EACtC;AAEA,SAAO,GAAG,SAAS;AACrB;;;AC1dA,IAAM,cAAc,oBAAI,IAAY,CAAC,SAAS,SAAS,OAAO,CAAC;AAW/D,SAAS,eAAe,IAAyC;AAC/D,QAAM,MAAM,GAAG,QAAQ,YAAY;AACnC,MAAI,QAAQ,QAAS,QAAO;AAC5B,MAAI,QAAQ,MAAO,QAAO;AAC1B,MAAI,QAAQ,QAAS,QAAO;AAE5B,QAAM,WAAW,GAAG,aAAa,WAAW;AAC5C,MAAI,aAAa,cAAe,QAAO;AACvC,MAAI,aAAa,OAAQ,QAAO;AAEhC,MACE,QAAQ,SACR,QAAQ,OACR,QAAQ,QACR,QAAQ,QACR,QAAQ,QACR,QAAQ,QACR;AACA,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAEA,SAAS,eAAe,IAAqB;AAC3C,QAAM,WAAW,GAAG,aAAa,WAAW;AAC5C,MAAI,SAAU,QAAO;AAErB,QAAM,OAAO,eAAe,EAAE;AAC9B,MAAI,SAAS,QAAQ;AACnB,UAAM,OAAO,GAAG,aAAa,KAAK,EAAE,MAAM,GAAG,EAAE,KAAK;AACpD,WAAO,KAAK,WAAW,KAAK,OAAO,QAAQ;AAAA,EAC7C;AAEA,QAAM,MAAM,GAAG,aAAa,KAAK;AACjC,MAAI,KAAK;AACP,UAAM,WAAW,IAAI,MAAM,GAAG,EAAE,IAAI,KAAK;AACzC,WAAO,SAAS,MAAM,GAAG,EAAE,CAAC,KAAK;AAAA,EACnC;AAEA,SAAO,GAAG,MAAM,GAAG,WAAW,SAAS,EAAE,MAAM,GAAG,EAAE,CAAC,KAAK;AAC5D;AAEA,SAAS,UAAU,IAAqB;AACtC,QAAM,YAAY,GAAG,aAAa,YAAY;AAC9C,MAAI,UAAW,QAAO,SAAS,WAAW,EAAE,KAAK;AAEjD,QAAM,QAAS,GAAmB,OAAO;AACzC,MAAI,MAAO,QAAO,SAAS,OAAO,EAAE,KAAK;AAEzC,SAAO;AACT;AAEA,SAAS,uBAAuB,KAAe,SAA0C;AACvF,QAAM,QAAQ,IAAI,eAAe,OAAO,KAAK,IAAI,cAAc,QAAQ;AACvE,MAAI,OAAO;AACT,UAAM,cAAe,MAAsB;AAC3C,QAAI,aAAa,SAAS,aAAa,QAAQ;AAC7C,YAAM,IAAI,SAAS,YAAY,OAAO,EAAE;AACxC,YAAM,IAAI,SAAS,YAAY,QAAQ,EAAE;AACzC,UAAI,KAAK,GAAG;AACV,eAAO,gCAAgC,GAAG,CAAC;AAAA,MAC7C;AAAA,IACF;AAAA,EACF;AAEA,MAAI,SAAS;AACX,UAAM,aAAa,QAAQ;AAAA,MACzB;AAAA,IACF;AACA,QAAI,YAAY;AACd,YAAM,IAAI,SAAS,WAAW,CAAC,KAAK,IAAI,EAAE;AAC1C,YAAM,IAAI,SAAS,WAAW,CAAC,KAAK,IAAI,EAAE;AAC1C,aAAO,gCAAgC,GAAG,CAAC;AAAA,IAC7C;AACA,UAAM,oBAAoB,QAAQ;AAAA,MAChC;AAAA,IACF;AACA,QAAI,mBAAmB;AACrB,YAAM,IAAI,SAAS,kBAAkB,CAAC,KAAK,IAAI,EAAE;AACjD,YAAM,IAAI,SAAS,kBAAkB,CAAC,KAAK,IAAI,EAAE;AACjD,aAAO,gCAAgC,GAAG,CAAC;AAAA,IAC7C;AAAA,EACF;AAEA,SAAO;AACT;AAEA,SAAS,wBAAwB,KAAwC;AACvE,QAAM,SAAS,IAAI;AACnB,QAAM,iBAAiB,OAAO,aAAa,iBAAiB;AAC5D,MACE,mBAAmB,eACnB,mBAAmB,cACnB,mBAAmB,kBACnB,mBAAmB,iBACnB,mBAAmB,YACnB,mBAAmB,aACnB;AACA,WAAO;AAAA,EACT;AAEA,QAAM,YAAY,OAAO,aAAa,wBAAwB;AAC9D,QAAM,aAAa,OAAO,aAAa,yBAAyB;AAChE,MAAI,aAAa,YAAY;AAC3B,UAAM,QAAQ,SAAS,WAAW,EAAE;AACpC,UAAM,SAAS,SAAS,YAAY,EAAE;AACtC,QAAI,SAAS,QAAQ;AACnB,aAAO,gCAAgC,OAAO,MAAM;AAAA,IACtD;AAAA,EACF;AAEA,SAAO;AACT;AAEA,IAAM,iBAAiB;AACvB,IAAM,eAAe;AAErB,SAAS,gCAAgC,OAAe,QAAkC;AACxF,QAAM,WAAW,KAAK,IAAI,OAAO,MAAM;AACvC,MAAI,UAAU,QAAQ;AACpB,WAAO,YAAY,iBAAiB,cAAc;AAAA,EACpD;AACA,QAAM,cAAc,QAAQ;AAC5B,QAAM,QAAQ,YAAY;AAC1B,MAAI,YAAa,QAAO,QAAQ,iBAAiB;AACjD,SAAO,QAAQ,gBAAgB;AACjC;AAEO,SAAS,UAAU,MAA0B;AAClD,QAAM,UAAU,YAAY,IAAI;AAChC,QAAM,SAAS,IAAI,UAAU;AAC7B,QAAM,MAAM,OAAO,gBAAgB,SAAS,WAAW;AAEvD,QAAM,WAA8B,CAAC;AACrC,QAAM,YAAwC,CAAC;AAC/C,MAAI,YAAY;AAEhB,QAAM,SAAS,IAAI;AACnB,QAAM,mBAAmB,OAAO,aAAa,oBAAoB;AACjE,MAAI,eAA8B;AAClC,MAAI,kBAAkB;AACpB,QAAI;AACF,qBAAe,KAAK,MAAM,gBAAgB;AAAA,IAC5C,QAAQ;AACN,qBAAe;AAAA,IACjB;AAAA,EACF;AAEA,QAAM,gBAAgB,IAAI,iBAAiB,cAAc;AAEzD,gBAAc,QAAQ,CAAC,OAAO;AAC5B,UAAM,OAAO,eAAe,EAAE;AAC9B,QAAI,CAAC,KAAM;AAEX,UAAM,QAAQ,WAAW,GAAG,aAAa,YAAY,KAAK,GAAG;AAC7D,UAAM,UAAU,GAAG,aAAa,UAAU;AAE1C,QAAI;AACJ,QAAI,SAAS;AACX,iBAAW,KAAK,IAAI,GAAG,WAAW,OAAO,IAAI,KAAK;AAAA,IACpD,OAAO;AACL,iBAAW;AAAA,IACb;AAWA,UAAM,KAAK,GAAG,aAAa,YAAY,KAAK,GAAG,MAAM,WAAW,EAAE,SAAS;AAC3E,UAAM,OAAO,eAAe,EAAE;AAC9B,UAAM,SAAS,UAAU,EAAE;AAG3B,UAAM,gBAAgB,GAAG,aAAa,gBAAgB;AACtD,QAAI,eAAe;AACjB,UAAI;AACF,cAAM,kBAAkB,KAAK,MAAM,aAAa;AAChD,YAAI,MAAM,QAAQ,eAAe,KAAK,gBAAgB,SAAS,GAAG;AAChE,oBAAU,EAAE,IAAI;AAAA,QAClB;AAAA,MACF,QAAQ;AAAA,MAER;AAAA,IACF;AAGA,UAAM,QAAQ,GAAG,aAAa,QAAQ;AACtC,UAAM,QAAQ,GAAG,aAAa,QAAQ;AACtC,UAAM,YAAY,GAAG,aAAa,YAAY;AAC9C,UAAM,cAAc,GAAG,aAAa,cAAc;AAClD,UAAM,IAAI,QAAQ,WAAW,KAAK,IAAI;AACtC,UAAM,IAAI,QAAQ,WAAW,KAAK,IAAI;AACtC,UAAM,QAAQ,YAAY,WAAW,SAAS,IAAI;AAClD,UAAM,UAAU,cAAc,WAAW,WAAW,IAAI;AAExD,QAAI,SAAS,QAAQ;AACnB,YAAM,SAAS,GAAG;AAClB,YAAM,UAAU,QAAQ,eAAe;AACvC,YAAM,QAAQ,GAAG,aAAa,YAAY,KAAK;AAC/C,YAAM,eAAe,GAAG,aAAa,gBAAgB;AACrD,YAAM,WAAW,eAAe,SAAS,cAAc,EAAE,IAAI;AAC7D,YAAM,iBAAiB,GAAG,aAAa,kBAAkB;AACzD,YAAM,aAAa,iBAAiB,SAAS,gBAAgB,EAAE,IAAI;AACnE,YAAM,aAAa,GAAG,aAAa,kBAAkB,KAAK;AAC1D,YAAM,iBAAiB,GAAG,aAAa,kBAAkB;AACzD,YAAM,aAAa,mBAAmB,UAAU,QAAQ;AAGxD,YAAM,kBAAkB,GAAG,aAAa,mBAAmB;AAC3D,YAAM,cAAc,oBAAoB,SAAS,OAAO;AACxD,YAAM,mBAAmB,GAAG,aAAa,yBAAyB,KAAK;AACvE,YAAM,uBAAuB,GAAG,aAAa,yBAAyB;AACtE,YAAM,mBAAmB,uBACrB,SAAS,sBAAsB,EAAE,IACjC;AAGJ,YAAM,oBAAoB,GAAG,aAAa,qBAAqB;AAC/D,YAAM,gBAAgB,sBAAsB,SAAS,OAAO;AAC5D,YAAM,qBAAqB,GAAG,aAAa,2BAA2B,KAAK;AAC3E,YAAM,2BAA2B,GAAG,aAAa,6BAA6B;AAC9E,YAAM,uBAAuB,2BACzB,SAAS,0BAA0B,EAAE,IACrC;AACJ,YAAM,0BAA0B,GAAG,aAAa,4BAA4B;AAC5E,YAAM,sBAAsB,0BACxB,SAAS,yBAAyB,EAAE,IACpC;AAEJ,YAAM,cAAmC;AAAA,QACvC;AAAA,QACA,MAAM;AAAA,QACN;AAAA,QACA;AAAA,QACA,WAAW;AAAA,QACX;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF;AACA,eAAS,KAAK,WAAW;AAAA,IAC3B,WAAW,SAAS,eAAe;AAEjC,YAAM,SAAS,GAAG,cAAc,QAAQ;AACxC,YAAM,MAAM,QAAQ,aAAa,KAAK,KAAK,GAAG,aAAa,KAAK,KAAK;AACrE,YAAM,gBAAgB,GAAG,aAAa,qBAAqB,KAAK;AAChE,YAAM,qBAAqB,GAAG,aAAa,sBAAsB;AACjE,YAAM,iBAAiB,qBAAqB,WAAW,kBAAkB,IAAI;AAC7E,YAAM,kBAAkB,GAAG,aAAa,mBAAmB;AAC3D,YAAM,cAAc,kBAAkB,SAAS,iBAAiB,EAAE,IAAI;AACtE,YAAM,mBAAmB,GAAG,aAAa,oBAAoB;AAC7D,YAAM,eAAe,mBAAmB,SAAS,kBAAkB,EAAE,IAAI;AAGzE,YAAM,qBAAqB,GAAG,aAAa,sBAAsB;AACjE,UAAI;AACJ,UAAI,oBAAoB;AACtB,YAAI;AACF,2BAAiB,KAAK,MAAM,kBAAkB;AAAA,QAChD,QAAQ;AAAA,QAER;AAAA,MACF;AAEA,YAAM,qBAAiD;AAAA,QACrD;AAAA,QACA,MAAM;AAAA,QACN;AAAA,QACA;AAAA,QACA;AAAA,QACA,WAAW;AAAA,QACX;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF;AACA,eAAS,KAAK,kBAAkB;AAAA,IAClC,OAAO;AACL,UAAI,CAAC,YAAY,IAAI,IAAI,EAAG;AAE5B,YAAM,MAAM,GAAG,aAAa,KAAK,KAAK;AACtC,YAAM,qBAAqB,GAAG,aAAa,kBAAkB;AAC7D,YAAM,iBAAiB,qBAAqB,WAAW,kBAAkB,IAAI;AAC7E,YAAM,qBAAqB,GAAG,aAAa,sBAAsB;AACjE,YAAM,iBAAiB,qBAAqB,WAAW,kBAAkB,IAAI;AAC7E,YAAM,cAAc,GAAG,aAAa,YAAY;AAChD,YAAM,UAAU,gBAAgB,SAAS,OAAO;AAChD,YAAM,aAAa,GAAG,aAAa,aAAa;AAChD,YAAM,SAAS,aAAa,WAAW,UAAU,IAAI;AACrD,YAAM,eAAe,GAAG,aAAa,gBAAgB;AACrD,YAAM,WAAW,iBAAiB,SAAS,OAAO;AAElD,YAAM,eAAqC;AAAA,QACzC;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA,WAAW;AAAA,QACX;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF;AACA,eAAS,KAAK,YAAY;AAAA,IAC5B;AAAA,EACF,CAAC;AAED,QAAM,aAAa,IAAI,iBAAiB,QAAQ;AAChD,MAAI,aAA4B;AAEhC,aAAW,UAAU,YAAY;AAC/B,UAAM,MAAM,OAAO,aAAa,KAAK;AACrC,QAAI,OAAO,IAAI,SAAS,MAAM,EAAG;AAEjC,UAAM,UAAU,OAAO,aAAa,KAAK;AACzC,QAAI,YAAY,QAAQ,SAAS,MAAM,KAAK,QAAQ,SAAS,UAAU,IAAI;AACzE,mBAAa;AACb;AAAA,IACF;AAAA,EACF;AAGA,aAAW,WAAW,UAAU;AAC9B,UAAM,mBAAmB,UAAU,QAAQ,EAAE;AAC7C,QAAI,CAAC,oBAAoB,iBAAiB,WAAW,EAAG;AAExD,UAAM,QAAQ,QAAQ,KAAK;AAC3B,UAAM,QAAQ,QAAQ,KAAK;AAC3B,UAAM,YACJ,QAAQ,SAAS,WAAW,QAAQ,SAAS,WAAW,QAAQ,SAAS,gBACnE,QAA8D,SAAS,IACzE;AAEN,cAAU,QAAQ,EAAE,IAAI,mBAAmB,kBAAkB,OAAO,OAAO,SAAS;AAAA,EACtF;AAEA,QAAM,YAAY,IAAI,iBAAiB,OAAO;AAC9C,QAAM,YACJ,MAAM,KAAK,SAAS,EACjB,IAAI,CAAC,MAAM,EAAE,aAAa,KAAK,CAAC,EAChC,OAAO,OAAO,EACd,KAAK,MAAM,KAAK;AAErB,QAAM,kBAAkB,MAAM,KAAK,SAAS,EAAE;AAAA,IAC5C,CAAC,MAAM,EAAE,aAAa,gBAAgB,MAAM;AAAA,EAC9C;AACA,QAAM,uBACJ,gBACG,IAAI,CAAC,MAAM,EAAE,aAAa,KAAK,CAAC,EAChC,OAAO,OAAO,EACd,KAAK,MAAM,KAAK;AAErB,QAAM,SAAS,gBAAgB,wBAAwB;AAEvD,QAAM,aAAa,wBAAwB,GAAG,KAAK,uBAAuB,KAAK,SAAS;AAGxF,QAAM,qBAAqB,wBAAwB,GAAG;AAEtD,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AAEA,SAAS,wBAAwB,KAAoC;AACnE,QAAM,gBAAgB,IAAI,eAAe,sBAAsB;AAC/D,MAAI,CAAC,eAAe;AAClB,WAAO,CAAC;AAAA,EACV;AAEA,QAAM,oBAAoB,cAAc,aAAa,qBAAqB;AAC1E,MAAI,CAAC,mBAAmB;AACtB,WAAO,CAAC;AAAA,EACV;AAEA,MAAI;AACF,UAAM,SAAS,KAAK,MAAM,iBAAiB;AAC3C,QAAI,MAAM,QAAQ,MAAM,GAAG;AACzB,aAAO,OAAO;AAAA,QACZ,CAAC,OACC,OAAO,OAAO,YACd,OAAO,QACP,OAAO,GAAG,OAAO,YACjB,OAAO,GAAG,SAAS,YACnB,OAAO,GAAG,SAAS,YACnB,GAAG,SAAS,QACZ,OAAO,GAAG,KAAK,UAAU,YACzB,OAAO,GAAG,KAAK,WAAW,YAC1B,OAAO,GAAG,KAAK,WAAW;AAAA,MAC9B;AAAA,IACF;AAAA,EACF,QAAQ;AAAA,EAER;AAEA,SAAO,CAAC;AACV;AAEA,SAAS,mBACP,WACA,OACA,OACA,WACY;AACZ,QAAM,cAAc;AACpB,QAAM,eAAe;AAErB,QAAM,eAAe,CAAC,OAA2B,SAC/C,UAAU,UAAa,KAAK,IAAI,QAAQ,IAAI,KAAK,gBAAgB,KAAK,IAAI,IAAI,IAAI;AAEpF,QAAM,oBAAoB,UAAU,OAAO,CAAC,OAAO,KAAK,IAAI,GAAG,IAAI,KAAK,WAAW;AAEnF,QAAM,kBAAkB,kBAAkB,KAAK,CAAC,OAAO;AACrD,UAAM,QAAQ,GAAG,cAAc,CAAC;AAChC,QACE,aAAa,MAAM,GAAG,KAAK,KAC3B,aAAa,MAAM,GAAG,KAAK,KAC1B,cAAc,KAAK,aAAa,MAAM,OAAO,SAAS,GACvD;AACA,aAAO;AAAA,IACT;AACA,WAAO;AAAA,EACT,CAAC;AAED,SAAO,UAAU,IAAI,CAAC,OAAO;AAC3B,UAAM,kBAA+C,CAAC;AACtD,eAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,GAAG,cAAc,CAAC,CAAC,GAAG;AAC9D,UAAI,OAAO,UAAU,SAAU;AAC/B,UAAI,mBAAmB,QAAQ,KAAK;AAClC,wBAAgB,IAAI,QAAQ;AAAA,MAC9B,WAAW,mBAAmB,QAAQ,KAAK;AACzC,wBAAgB,IAAI,QAAQ;AAAA,MAC9B,WAAW,mBAAmB,QAAQ,SAAS;AAC7C,wBAAgB,QAAQ,cAAc,IAAI,QAAQ,YAAY;AAAA,MAChE,OAAO;AACL,QAAC,gBAA2C,GAAG,IAAI;AAAA,MACrD;AAAA,IACF;AAEA,WAAO;AAAA,MACL,GAAG;AAAA,MACH,MAAM,KAAK,IAAI,GAAG,GAAG,IAAI;AAAA,MACzB,YAAY;AAAA,IACd;AAAA,EACF,CAAC;AACH;AAEO,SAAS,oBACd,MACA,WACA,SACQ;AACR,QAAM,SAAS,IAAI,UAAU;AAC7B,QAAM,MAAM,OAAO,gBAAgB,MAAM,WAAW;AAEpD,QAAM,KAAK,IAAI,eAAe,SAAS,KAAK,YAAY,KAAK,aAAa,SAAS;AACnF,MAAI,CAAC,GAAI,QAAO;AAEhB,MAAI,QAAQ,cAAc,QAAW;AACnC,OAAG,aAAa,cAAc,OAAO,QAAQ,SAAS,CAAC;AACvD,QAAI,GAAG,aAAa,UAAU,KAAK,QAAQ,aAAa,QAAW;AACjE,SAAG,aAAa,YAAY,OAAO,QAAQ,YAAY,QAAQ,QAAQ,CAAC;AAAA,IAC1E;AAAA,EACF;AAEA,MAAI,QAAQ,aAAa,QAAW;AAClC,UAAM,QAAQ,WAAW,GAAG,aAAa,YAAY,KAAK,GAAG;AAC7D,OAAG,aAAa,YAAY,OAAO,QAAQ,QAAQ,QAAQ,CAAC;AAC5D,OAAG,gBAAgB,eAAe;AAAA,EACpC;AAEA,MAAI,QAAQ,SAAS,QAAW;AAC9B,OAAG,aAAa,aAAa,QAAQ,IAAI;AAAA,EAC3C;AAEA,MAAI,QAAQ,WAAW,QAAW;AAChC,OAAG,aAAa,cAAc,OAAO,QAAQ,MAAM,CAAC;AAAA,EACtD;AAGA,MAAI,SAAS,WAAW,QAAQ,QAAQ,QAAW;AACjD,OAAG,aAAa,OAAO,QAAQ,GAAG;AAAA,EACpC;AAGA,MAAI,aAAa,WAAW,QAAQ,YAAY,QAAW;AACzD,UAAM,SAAS,GAAG;AAClB,QAAI,QAAQ;AACV,aAAO,cAAc,QAAQ;AAAA,IAC/B;AAAA,EACF;AAEA,MAAI,WAAW,WAAW,QAAQ,UAAU,QAAW;AACrD,OAAG,aAAa,cAAc,QAAQ,KAAK;AAAA,EAC7C;AAEA,MAAI,cAAc,WAAW,QAAQ,aAAa,QAAW;AAC3D,OAAG,aAAa,kBAAkB,OAAO,QAAQ,QAAQ,CAAC;AAAA,EAC5D;AAEA,MAAI,gBAAgB,SAAS;AAC3B,QAAI,QAAQ,eAAe,OAAO;AAChC,SAAG,aAAa,oBAAoB,OAAO;AAAA,IAC7C,OAAO;AACL,SAAG,gBAAgB,kBAAkB;AAAA,IACvC;AAAA,EACF;AAGA,MAAI,YAAY,SAAS;AACvB,QAAI,QAAQ,WAAW,UAAa,QAAQ,WAAW,GAAG;AACxD,SAAG,aAAa,eAAe,OAAO,QAAQ,MAAM,CAAC;AAAA,IACvD,OAAO;AACL,SAAG,gBAAgB,aAAa;AAAA,IAClC;AAAA,EACF;AAGA,MAAI,cAAc,SAAS;AACzB,QAAI,QAAQ,aAAa,MAAM;AAC7B,SAAG,aAAa,kBAAkB,MAAM;AAAA,IAC1C,OAAO;AACL,SAAG,gBAAgB,gBAAgB;AAAA,IACrC;AAAA,EACF;AAEA,SAAO,sBAAsB,IAAI,gBAAgB;AACnD;AAEO,SAAS,iBACd,MACA,SAC8B;AAC9B,QAAM,SAAS,IAAI,UAAU;AAC7B,QAAM,MAAM,OAAO,gBAAgB,MAAM,WAAW;AAGpD,QAAM,YACJ,IAAI,cAAc,uBAAuB,KACzC,IAAI,cAAc,YAAY,KAC9B,IAAI,cAAc,QAAQ,KAC1B,IAAI;AAEN,QAAM,KAAK,QAAQ,MAAM,WAAW,KAAK,IAAI,CAAC;AAE9C,MAAI;AAEJ,WAAS,gBAAgB,IAAa,SAAqC;AACzE,QAAI,QAAQ,IAAK,IAAG,aAAa,OAAO,QAAQ,GAAG;AACnD,QAAI,QAAQ,WAAW,UAAa,QAAQ,WAAW,GAAG;AACxD,SAAG,aAAa,eAAe,OAAO,QAAQ,MAAM,CAAC;AAAA,IACvD;AAAA,EACF;AAEA,UAAQ,QAAQ,MAAM;AAAA,IACpB,KAAK,SAAS;AACZ,YAAM,UAAU;AAChB,cAAQ,IAAI,cAAc,OAAO;AACjC,YAAM,aAAa,SAAS,EAAE;AAC9B,YAAM,aAAa,eAAe,EAAE;AACpC,sBAAgB,OAAO,OAAO;AAC9B,UAAI,QAAQ,UAAU;AACpB,cAAM,aAAa,kBAAkB,MAAM;AAAA,MAC7C;AACA;AAAA,IACF;AAAA,IACA,KAAK,SAAS;AACZ,YAAM,UAAU;AAChB,cAAQ,IAAI,cAAc,KAAK;AAC/B,UAAI,QAAQ,IAAK,OAAM,aAAa,OAAO,QAAQ,GAAG;AACtD,YAAM,aAAa,OAAO,QAAQ,IAAI;AACtC;AAAA,IACF;AAAA,IACA,KAAK,SAAS;AACZ,YAAM,UAAU;AAChB,cAAQ,IAAI,cAAc,OAAO;AACjC,sBAAgB,OAAO,OAAO;AAC9B;AAAA,IACF;AAAA,IACA,KAAK;AAAA,IACL,SAAS;AACP,YAAM,SAAS;AACf,cAAQ,IAAI,cAAc,KAAK;AAC/B,YAAM,cAAc,IAAI,cAAc,KAAK;AAC3C,kBAAY,cAAc,OAAO,WAAW,QAAQ;AACpD,YAAM,YAAY,WAAW;AAC7B,UAAI,OAAO,OAAO;AAChB,cAAM,aAAa,cAAc,OAAO,KAAK;AAAA,MAC/C;AACA,UAAI,OAAO,UAAU;AACnB,cAAM,aAAa,kBAAkB,OAAO,OAAO,QAAQ,CAAC;AAAA,MAC9D;AACA;AAAA,IACF;AAAA,EACF;AAEA,QAAM,KAAK;AACX,QAAM,aAAa,cAAc,OAAO,QAAQ,SAAS,CAAC;AAC1D,QAAM,aAAa,YAAY,OAAO,QAAQ,YAAY,QAAQ,QAAQ,CAAC;AAC3E,QAAM,aAAa,cAAc,OAAO,QAAQ,MAAM,CAAC;AACvD,QAAM,aAAa,aAAa,QAAQ,IAAI;AAE5C,YAAU,YAAY,KAAK;AAE3B,SAAO;AAAA,IACL,MAAM,sBAAsB,IAAI,gBAAgB;AAAA,IAChD;AAAA,EACF;AACF;AAEA,SAAS,kBAAkB,UAAkB,IAAqB;AAChE,SACE,aAAa,IAAI,EAAE,MACnB,aAAa,gBAAgB,EAAE,QAC/B,aAAa,gBAAgB,EAAE;AAEnC;AAEA,SAAS,eAAe,QAAgB,WAA2B;AAMjE,MAAI,UAAU;AACd,aAAS;AACP,UAAM,SAAS,6BAA6B,OAAO;AACnD,QAAI,CAAC,OAAQ,QAAO;AACpB,UAAM,QAAQ,OAAO,QAAQ;AAAA,MAAK,CAAC,MACjC,kBAAkB,EAAE,UAAU,gBAAgB,SAAS;AAAA,IACzD;AACA,QAAI,CAAC,MAAO,QAAO;AACnB,UAAM,UAAU,0BAA0B,SAAS,MAAM,EAAE;AAE3D,QAAI,YAAY,QAAS,QAAO;AAChC,cAAU;AAAA,EACZ;AACF;AAEA,SAAS,sBAAsB,KAAe,WAAyB;AACrE,aAAW,UAAU,MAAM,KAAK,IAAI,iBAAiB,QAAQ,CAAC,GAAG;AAC/D,UAAM,OAAO,OAAO,eAAe;AACnC,QAAI,CAAC,KAAK,SAAS,MAAM,KAAK,CAAC,KAAK,SAAS,eAAe,EAAG;AAC/D,UAAM,UAAU,eAAe,MAAM,SAAS;AAC9C,QAAI,YAAY,KAAM,QAAO,cAAc;AAAA,EAC7C;AACF;AAEO,SAAS,sBAAsB,MAAc,WAA2B;AAC7E,QAAM,SAAS,IAAI,UAAU;AAC7B,QAAM,MAAM,OAAO,gBAAgB,MAAM,WAAW;AACpD,MAAI,eAAe,SAAS,GAAG,OAAO;AACtC,wBAAsB,KAAK,SAAS;AACpC,SAAO,sBAAsB,IAAI,gBAAgB;AACnD;AAQO,SAAS,2BAA2B,MAAmC;AAC5E,QAAM,SAAS,IAAI,UAAU;AAC7B,QAAM,MAAM,OAAO,gBAAgB,MAAM,WAAW;AACpD,QAAM,SAAS,IAAI;AAEnB,QAAM,gBAAgB,OAAO,aAAa,qBAAqB;AAC/D,QAAM,cAAc,OAAO,aAAa,2BAA2B;AACnE,QAAM,sBAAsB,cAAc,WAAW,WAAW,IAAI;AAEpE,QAAM,YAAY,0BAA0B,MAAM;AAElD,SAAO;AAAA,IACL;AAAA,IACA,qBACE,uBAAuB,SAAS,mBAAmB,IAAI,sBAAsB;AAAA,IAC/E;AAAA,EACF;AACF;AAEA,SAAS,0BAA0B,QAAwC;AACzE,QAAM,gBAAgB,OAAO,aAAa,4BAA4B;AACtE,MAAI,CAAC,eAAe;AAClB,WAAO,CAAC;AAAA,EACV;AAEA,MAAI;AACF,UAAM,SAAS,KAAK,MAAM,aAAa;AACvC,QAAI,CAAC,MAAM,QAAQ,MAAM,GAAG;AAC1B,aAAO,CAAC;AAAA,IACV;AAEA,WAAO,OAAO,OAAO,CAAC,MAAgC;AACpD,UAAI,OAAO,MAAM,YAAY,MAAM,KAAM,QAAO;AAChD,UAAI,OAAO,EAAE,OAAO,YAAY,OAAO,EAAE,UAAU,SAAU,QAAO;AACpE,UAAI,CAAC,CAAC,UAAU,UAAU,SAAS,WAAW,QAAQ,QAAQ,OAAO,EAAE,SAAS,EAAE,IAAI;AACpF,eAAO;AAET,cAAQ,EAAE,MAAM;AAAA,QACd,KAAK;AACH,iBAAO,OAAO,EAAE,YAAY;AAAA,QAC9B,KAAK;AACH,iBAAO,OAAO,EAAE,YAAY;AAAA,QAC9B,KAAK;AACH,iBAAO,OAAO,EAAE,YAAY;AAAA,QAC9B,KAAK;AACH,iBAAO,OAAO,EAAE,YAAY;AAAA,QAC9B,KAAK;AACH,iBAAO,OAAO,EAAE,YAAY,YAAY,MAAM,QAAQ,EAAE,OAAO;AAAA,QACjE,KAAK;AAEH,iBAAO,OAAO,EAAE,YAAY;AAAA,QAC9B,KAAK;AAEH,iBAAO,OAAO,EAAE,YAAY;AAAA,QAC9B;AACE,iBAAO;AAAA,MACX;AAAA,IACF,CAAC;AAAA,EACH,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;AAEO,SAAS,wBAAwB,MAAgC;AACtE,QAAM,SAAmB,CAAC;AAC1B,QAAM,WAAqB,CAAC;AAE5B,QAAM,SAAS,IAAI,UAAU;AAC7B,QAAM,MAAM,OAAO,gBAAgB,MAAM,WAAW;AACpD,QAAM,SAAS,IAAI;AAEnB,QAAM,gBAAgB,OAAO,aAAa,qBAAqB;AAC/D,MAAI,CAAC,eAAe;AAClB,WAAO,KAAK,yDAAyD;AAAA,EACvE;AAEA,QAAM,cAAc,OAAO,aAAa,2BAA2B;AACnE,MAAI,CAAC,aAAa;AAChB,WAAO,KAAK,+DAA+D;AAAA,EAC7E,OAAO;AACL,UAAM,WAAW,WAAW,WAAW;AACvC,QAAI,CAAC,SAAS,QAAQ,KAAK,YAAY,GAAG;AACxC,aAAO,KAAK,4DAA4D;AAAA,IAC1E;AAAA,EACF;AAEA,QAAM,QAAQ,IAAI,eAAe,OAAO;AACxC,MAAI,CAAC,OAAO;AACV,WAAO,KAAK,wBAAwB;AAAA,EACtC;AAEA,MAAI,eAAe,KAAK,IAAI,GAAG;AAC7B,WAAO,KAAK,2DAA2D;AAAA,EACzE;AAEA,MAAI,kBAAkB,KAAK,IAAI,GAAG;AAChC,WAAO,KAAK,8BAA8B;AAAA,EAC5C;AAEA,QAAM,UAAU,IAAI,iBAAiB,QAAQ;AAC7C,MAAI,QAAQ,SAAS,GAAG;AACtB,aAAS,KAAK,wEAAwE;AAAA,EACxF;AAEA,QAAM,aAAa,kBAAkB,GAAG;AACxC,MAAI,YAAY;AACd,UAAM,iBAAiB,wBAAwB,UAAU;AACzD,WAAO,KAAK,GAAG,eAAe,MAAM;AACpC,aAAS,KAAK,GAAG,eAAe,QAAQ;AAAA,EAC1C;AAEA,SAAO;AAAA,IACL,OAAO,OAAO,WAAW;AAAA,IACzB;AAAA,IACA;AAAA,EACF;AACF;AAEA,SAAS,kBAAkB,KAA8B;AACvD,QAAM,UAAU,IAAI,iBAAiB,QAAQ;AAC7C,aAAW,UAAU,SAAS;AAC5B,UAAM,UAAU,OAAO,eAAe;AACtC,QACE,QAAQ,SAAS,eAAe,KAChC,QAAQ,SAAS,OAAO,KACxB,QAAQ,SAAS,MAAM,GACvB;AACA,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;;;AC50BA,YAAYC,YAAW;AACvB,OAAOC,kBAAiB;AAQxB,SAAS,YAAY,OAAkD;AACrE,SAAO,OAAO,QAAQ,KAAK,EAAE,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM,GAAG,UAAQ,CAAC,CAAC,KAAK,eAAY,CAAC,CAAC,EAAE;AACjF;AAEA,SAAS,gBAAgB,MAA6B;AACpD,QAAM,aAAa,KAAK,WAAW,aAAa,CAAC,GAC9C,OAAO,CAAC,MAAM,OAAO,EAAE,WAAW,MAAM,YAAY,OAAO,EAAE,WAAW,MAAM,QAAQ,EACtF,IAAI,CAAC,MAAM,QAAQ,eAAY,EAAE,WAAW,CAAE,CAAC,QAAQ,eAAY,EAAE,WAAW,CAAE,CAAC,IAAI;AAC1F,QAAM,YAAY,KAAK,SAAS,SAAS,CAAC,GAAG,aAAa;AAC1D,QAAM,aAAa,KAAK,SAAS;AACjC,QAAM,QAAQ,aAAa,iBAAiB,eAAY,UAA6B,CAAC,KAAK;AAC3F,SAAO,wBAAwB,UAAU,KAAK,IAAI,CAAC,iBAAiB,SAAS,GAAG,KAAK;AACvF;AAEA,SAAS,eAAe,MAA6B;AACnD,QAAM,OAAO,KAAK,WAAW,aAAa,CAAC,GAAG,IAAI,CAAC,MAAM;AACvD,UAAM,OAAO,YAAY,EAAE,UAAU;AACrC,QAAI,EAAE,KAAM,MAAK,KAAK,SAAS,eAAY,EAAE,IAAI,CAAC,EAAE;AACpD,WAAO,IAAI,EAAE,UAAU,SAAS,KAAK,KAAK,IAAI,CAAC;AAAA,EACjD,CAAC;AACD,MAAI,KAAK,WAAW,SAAU,KAAI,KAAK,aAAa,eAAY,KAAK,UAAU,QAAQ,CAAC,EAAE;AAC1F,SAAO,gBAAgB,IAAI,KAAK,IAAI,CAAC;AACvC;AAGA,SAAS,eAAe,MAA+B;AACrD,QAAM,QAAkB,CAAC;AACzB,MAAI,KAAK,SAAS,QAAS,OAAM,KAAK,gBAAgB,IAAI,CAAC;AAAA,WAClD,KAAK,UAAW,OAAM,KAAK,eAAe,IAAI,CAAC;AACxD,QAAM,KAAK,GAAG,YAAY,KAAK,UAAU,CAAC;AAC1C,MAAI,KAAK,WAAW,SAAS,KAAK,aAAa,QAAW;AACxD,UAAM,KAAK,aAAa,eAAY,KAAK,QAAQ,CAAC,EAAE;AAAA,EACtD;AACA,MAAI,KAAK,KAAM,OAAM,KAAK,SAAS,eAAY,KAAK,IAAI,CAAC,EAAE;AAC3D,aAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,KAAK,UAAU,CAAC,CAAC,GAAG;AACtD,UAAM,KAAK,GAAG,UAAQ,CAAC,CAAC,KAAK,eAAY,CAAoB,CAAC,EAAE;AAAA,EAClE;AACA,SAAO;AACT;AAGA,SAAS,wBAAwB,aAAqB,MAA6B;AACjF,QAAM,MAAM,KAAK,eAAe,IAAI,EAAE,KAAK,IAAI,CAAC;AAChD,QAAM,MAAM;AAAA,IACV,KAAK,kBAAkB,OAAO,KAAK,aAAa,WAAW,KAAK,WAAW;AAAA,EAC7E;AACA,QAAM,MAAM,eAAY,KAAK,cAAc;AAC3C,MAAI,KAAK,WAAW,UAAU;AAC5B,UAAM,OAAO,KAAK,YAAY,KAAK,kBAAkB,CAAC,CAAC,EAAE,KAAK,IAAI,CAAC;AACnE,WAAO,GAAG,WAAW,WAAW,GAAG,KAAK,IAAI,KAAK,GAAG,KAAK,GAAG;AAAA,EAC9D;AACA,SAAO,GAAG,WAAW,IAAI,KAAK,MAAM,IAAI,GAAG,KAAK,GAAG,KAAK,GAAG;AAC7D;AAGA,SAAS,WAAW,MAA8B;AAChD,SAAO,KAAK,YAAY,SAAS,YAAY,KAAK,YAAY,SAAS;AACzE;AAGA,SAAS,mBAAmB,QAAwB;AAClD,SAAa,aAAM,QAAQ,EAAE,aAAa,UAAU,YAAY,SAAS,CAAC,EAAE,QAAQ,CAAC;AACvF;AAGA,SAAS,kBAAkB,YAAoB,OAAe,KAA0B;AACtF,aAAW,QAAQ,YAAY;AAC7B,QAAI,KAAK,SAAS,SAAS,KAAK,OAAO,IAAK,QAAO;AAAA,EACrD;AACA,SAAO;AACT;AAEA,SAAS,kBAAkB,MAAY,OAA6B;AAClE,MAAI,KAAK,SAAS,sBAAuB,QAAO,MAAM,IAAI,KAAK,IAAI,IAAI;AACvE,MAAI,KAAK,SAAS,uBAAuB;AACvC,YAAQ,KAAK,gBAAgB,CAAC,GAAG,KAAK,CAAC,MAAY,MAAM,IAAI,EAAE,IAAI,IAAI,CAAC;AAAA,EAC1E;AACA,SAAO;AACT;AAOO,SAAS,uBAAuB,QAAwB;AAC7D,QAAM,SAAS,qBAAqB,MAAM;AAC1C,QAAM,WAAW,OAAO,WAAW,OAAO,CAAC,MAAM,WAAW,CAAC,KAAK,EAAE,YAAY,WAAW;AAC3F,MAAI,SAAS,WAAW,EAAG,QAAO;AAElC,QAAM,aAAa,mBAAmB,MAAM;AAI5C,QAAM,cAAc,oBAAI,IAA2B;AACnD,QAAM,cAAc,oBAAI,IAAY;AACpC,aAAW,QAAQ,UAAU;AAC3B,QAAI,KAAK,YAAY,GAAI,aAAY,IAAI,KAAK,WAAW,EAAE;AAC3D,UAAM,CAAC,GAAG,CAAC,IAAI,KAAK,WAAY;AAChC,UAAM,OAAO,kBAAkB,YAAY,GAAG,CAAC;AAC/C,QAAI,CAAC,KAAM;AACX,UAAM,OAAO,YAAY,IAAI,IAAI,KAAK,CAAC;AACvC,SAAK,KAAK,IAAI;AACd,gBAAY,IAAI,MAAM,IAAI;AAAA,EAC5B;AACA,MAAI,YAAY,SAAS,EAAG,QAAO;AAEnC,QAAM,KAAK,IAAIC,aAAY,MAAM;AACjC,aAAW,CAAC,MAAM,KAAK,KAAK,aAAa;AACvC,UAAM,WAAW,MAAM,IAAI,CAAC,MAAM,wBAAwB,OAAO,aAAa,CAAC,CAAC,EAAE,KAAK,IAAI;AAC3F,OAAG,UAAU,KAAK,OAAO,KAAK,KAAK,QAAQ;AAAA,EAC7C;AAEA,aAAW,QAAQ,YAAY;AAC7B,QAAI,kBAAkB,MAAM,WAAW,EAAG,IAAG,OAAO,KAAK,OAAO,KAAK,GAAG;AAAA,EAC1E;AACA,SAAO,GAAG,SAAS;AACrB;","names":["GSAP_METHODS","isFunctionNode","n","acornWalk","acorn","MagicString","MagicString"]}
|