()
| 1136 | let childrenBound = false; |
| 1137 | // fallow-ignore-next-line complexity |
| 1138 | const bindRootTimelineIfAvailable = (): boolean => { |
| 1139 | if (!externalCompositionsReady) return false; |
| 1140 | const currentTimeline = state.capturedTimeline; |
| 1141 | const currentDuration = getTimelineDurationSeconds(currentTimeline); |
| 1142 | const currentTimelineUsable = isUsableTimelineDuration(currentDuration); |
| 1143 | // Skip rebind ONLY if we already have a usable timeline AND children have been bound. |
| 1144 | // Without childrenBound check, the TARGET_DURATION spacer makes the timeline "usable" |
| 1145 | // before child composition timelines are added, causing them to never be discovered. |
| 1146 | if (currentTimeline && currentTimelineUsable && childrenBound) return false; |
| 1147 | const resolution = resolveRootTimelineFromDocument(); |
| 1148 | if (!resolution.timeline) return false; |
| 1149 | if (currentTimeline && currentTimeline === resolution.timeline) { |
| 1150 | if (typeof currentTimeline.timeScale === "function") { |
| 1151 | currentTimeline.timeScale(state.playbackRate); |
| 1152 | } |
| 1153 | return false; |
| 1154 | } |
| 1155 | state.capturedTimeline = resolution.timeline; |
| 1156 | if (typeof state.capturedTimeline.timeScale === "function") { |
| 1157 | state.capturedTimeline.timeScale(state.playbackRate); |
| 1158 | } |
| 1159 | const boundDuration = getSafeTimelineDurationSeconds(state.capturedTimeline, 0); |
| 1160 | if (boundDuration <= 0) { |
| 1161 | // No resolvable duration (e.g. a set()-only timeline, or one whose |
| 1162 | // duration isn't known yet). Kick GSAP off the creation position so the |
| 1163 | // set() renders. For a finite-but-zero timeline progress(1) === progress(0); |
| 1164 | // for an infinite-repeat timeline this lands on the first iteration's end |
| 1165 | // frame, which is the best we can do without a known cycle length. |
| 1166 | if (typeof state.capturedTimeline.progress === "function") { |
| 1167 | state.capturedTimeline.progress(1, true); |
| 1168 | state.capturedTimeline.progress(0, false); |
| 1169 | state.capturedTimeline.pause(); |
| 1170 | } |
| 1171 | } |
| 1172 | if (boundDuration > 0) { |
| 1173 | try { |
| 1174 | clock.setDuration(boundDuration); |
| 1175 | } catch { |
| 1176 | // clock not yet initialized — duration will be set during TransportClock setup |
| 1177 | } |
| 1178 | |
| 1179 | if (typeof state.capturedTimeline.totalTime === "function") { |
| 1180 | // GSAP won't render tl.set() at position 0 when the paused timeline |
| 1181 | // starts there — play/pause/seek/totalTime are all no-ops at the |
| 1182 | // creation position. Force the set to render by cycling progress past |
| 1183 | // 0 (when the timeline implements it), then seek to the prior playhead |
| 1184 | // (state.currentTime) so a rebind after a user scrub or soft-reload |
| 1185 | // restore doesn't snap back to 0. |
| 1186 | if (typeof state.capturedTimeline.progress === "function") { |
| 1187 | state.capturedTimeline.progress(0.0001, true); |
| 1188 | } |
| 1189 | const seekTime = Math.max(0, state.currentTime || 0); |
| 1190 | state.capturedTimeline.totalTime(seekTime, false); |
| 1191 | state.capturedTimeline.pause(); |
| 1192 | } |
| 1193 | |
| 1194 | // GSAP bakes the CSS `translate` into style.transform on seek. |
| 1195 | // The Studio seek wrapper (installStudioManualEditSeekReapply) calls |
no test coverage detected