@@ -50,6 +50,81 @@ type InternalContext = AnimationContext & {
5050}
5151
5252let nextOwnerId = 1
53+ const ANIMATION_TRACKER_KEY = "__frameScript_AnimationTracker"
54+
55+ type AnimationTracker = {
56+ pending : number
57+ start : ( ) => ( ) => void
58+ wait : ( ) => Promise < void >
59+ }
60+
61+ const getAnimationTracker = ( ) : AnimationTracker => {
62+ const g = globalThis as unknown as Record < string , unknown >
63+ const existing = g [ ANIMATION_TRACKER_KEY ] as AnimationTracker | undefined
64+ if ( existing ) return existing
65+
66+ let pending = 0
67+ const waiters = new Set < ( ) => void > ( )
68+
69+ const notifyIfReady = ( ) => {
70+ if ( pending !== 0 ) return
71+ for ( const resolve of Array . from ( waiters ) ) {
72+ resolve ( )
73+ }
74+ waiters . clear ( )
75+ }
76+
77+ const tracker : AnimationTracker = {
78+ get pending ( ) {
79+ return pending
80+ } ,
81+ start : ( ) => {
82+ pending += 1
83+ let done = false
84+ return ( ) => {
85+ if ( done ) return
86+ done = true
87+ pending = Math . max ( 0 , pending - 1 )
88+ notifyIfReady ( )
89+ }
90+ } ,
91+ wait : ( ) => {
92+ if ( pending === 0 ) {
93+ return Promise . resolve ( )
94+ }
95+ return new Promise < void > ( ( resolve ) => {
96+ waiters . add ( resolve )
97+ } )
98+ } ,
99+ }
100+
101+ g [ ANIMATION_TRACKER_KEY ] = tracker
102+ return tracker
103+ }
104+
105+ const installAnimationApi = ( ) => {
106+ if ( typeof window === "undefined" ) return
107+ const tracker = getAnimationTracker ( )
108+ const waitAnimationsReady = async ( ) => {
109+ // Wait until pending is zero and stays zero through a tick (handles StrictMode double-effects).
110+ while ( true ) {
111+ if ( tracker . pending === 0 ) {
112+ if ( typeof window . requestAnimationFrame !== "function" ) {
113+ return
114+ }
115+ await new Promise < void > ( ( resolve ) => window . requestAnimationFrame ( ( ) => resolve ( ) ) )
116+ if ( tracker . pending === 0 ) return
117+ }
118+ await tracker . wait ( )
119+ }
120+ }
121+
122+ ; ( window as any ) . __frameScript = {
123+ ...( window as any ) . __frameScript ,
124+ waitAnimationsReady,
125+ getAnimationsPending : ( ) => tracker . pending ,
126+ }
127+ }
53128
54129const toFrames = ( value : number ) => Math . max ( 0 , Math . round ( value ) )
55130const isDev = typeof import . meta !== "undefined" && Boolean ( ( import . meta as any ) . env ?. DEV )
@@ -198,6 +273,16 @@ export const useAnimation = (
198273 useProvideClipDuration ( durationFrames )
199274
200275 useLayoutEffect ( ( ) => {
276+ installAnimationApi ( )
277+ const tracker = getAnimationTracker ( )
278+ const finishPending = tracker . start ( )
279+ let finished = false
280+ const finalize = ( ) => {
281+ if ( finished ) return
282+ finished = true
283+ finishPending ( )
284+ }
285+
201286 if ( ! ownerIdRef . current ) {
202287 ownerIdRef . current = nextOwnerId
203288 nextOwnerId += 1
@@ -272,11 +357,13 @@ export const useAnimation = (
272357 }
273358
274359 if ( runIdRef . current !== runId ) {
360+ finalize ( )
275361 return
276362 }
277363 const nextDuration = Math . max ( 1 , Math . round ( internal . maxFrame ) )
278364 setDurationFrames ( nextDuration )
279365 setReady ( true )
366+ finalize ( )
280367 }
281368
282369 void execute ( )
@@ -289,6 +376,7 @@ export const useAnimation = (
289376 variable . _state . segments . length = 0
290377 }
291378 }
379+ finalize ( )
292380 }
293381 } , deps )
294382
0 commit comments