diff --git a/packages/core/src/runtime/timeline.test.ts b/packages/core/src/runtime/timeline.test.ts index 803a684d9..1996907c0 100644 --- a/packages/core/src/runtime/timeline.test.ts +++ b/packages/core/src/runtime/timeline.test.ts @@ -402,6 +402,92 @@ describe("collectRuntimeTimelinePayload", () => { expect(s2?.duration).toBe(3); }); + it("does not offset GSAP scene clips by the master timeline start time", () => { + const root = document.createElement("div"); + root.setAttribute("data-composition-id", "main"); + root.setAttribute("data-duration", "20"); + document.body.appendChild(root); + + const scene = document.createElement("div"); + scene.id = "scene-1"; + root.appendChild(scene); + + const masterTimeline = { + duration: () => 20, + time: () => 1.25, + play: () => {}, + pause: () => {}, + seek: () => {}, + add: () => {}, + paused: () => {}, + set: () => {}, + startTime: () => 1.25, + getChildren: () => [ + { + targets: () => [scene], + startTime: () => 4, + duration: () => 3, + parent: masterTimeline, + }, + ], + }; + + (window as any).__timelines = { + main: masterTimeline, + }; + + const result = collectRuntimeTimelinePayload(defaultParams); + const clip = result.clips.find((c) => c.id === "scene-1"); + expect(clip).toBeDefined(); + expect(clip?.start).toBe(4); + expect(clip?.duration).toBe(3); + }); + + it("keeps nested GSAP timeline offsets below the master timeline", () => { + const root = document.createElement("div"); + root.setAttribute("data-composition-id", "main"); + root.setAttribute("data-duration", "20"); + document.body.appendChild(root); + + const scene = document.createElement("div"); + scene.id = "scene-1"; + root.appendChild(scene); + + const masterTimeline = { + duration: () => 20, + time: () => 1.25, + play: () => {}, + pause: () => {}, + seek: () => {}, + add: () => {}, + paused: () => {}, + set: () => {}, + startTime: () => 1.25, + getChildren: () => [] as unknown[], + }; + const nestedTimeline = { + startTime: () => 6, + parent: masterTimeline, + }; + const nestedTween = { + targets: () => [scene], + startTime: () => 2, + duration: () => 3, + parent: nestedTimeline, + }; + masterTimeline.getChildren = () => [nestedTween]; + + (window as any).__timelines = { + main: masterTimeline, + }; + + const result = collectRuntimeTimelinePayload(defaultParams); + const clip = result.clips.find((c) => c.id === "scene-1"); + expect(clip).toBeDefined(); + expect(clip?.start).toBe(8); + expect(clip?.duration).toBe(3); + }); + it("bubbles child tween ranges up to scene-level ancestors", () => { const root = document.createElement("div"); root.setAttribute("data-composition-id", "main"); diff --git a/packages/core/src/runtime/timeline.ts b/packages/core/src/runtime/timeline.ts index d56d55ab9..91856ca51 100644 --- a/packages/core/src/runtime/timeline.ts +++ b/packages/core/src/runtime/timeline.ts @@ -416,7 +416,7 @@ export function collectRuntimeTimelinePayload(params: { targets?: () => Element[]; startTime?: () => number; duration?: () => number; - parent?: { startTime?: () => number }; + parent?: GsapTween; }; const tlWithChildren = masterTimeline as typeof masterTimeline & { getChildren?: (nested: boolean, tweens: boolean, timelines: boolean) => GsapTween[]; @@ -452,9 +452,9 @@ export function collectRuntimeTimelinePayload(params: { continue; let tweenStart = tween.startTime(); let parent = tween.parent; - while (parent && typeof parent.startTime === "function") { + while (parent && parent !== masterTimeline && typeof parent.startTime === "function") { tweenStart += parent.startTime(); - parent = (parent as GsapTween).parent; + parent = parent.parent; } const tweenEnd = tweenStart + tween.duration(); if (!Number.isFinite(tweenStart) || !Number.isFinite(tweenEnd)) continue;