react-native-smooth-clip-view 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/README.md +171 -43
  2. package/SmoothClipView.podspec +8 -2
  3. package/android/CMakeLists.txt +39 -0
  4. package/android/build.gradle +69 -2
  5. package/android/proguard-rules.pro +6 -0
  6. package/android/src/main/cpp/SmoothClipAndroid.h +59 -0
  7. package/android/src/main/cpp/SmoothClipBindings.cpp +428 -0
  8. package/android/src/main/cpp/SmoothClipRegistry.cpp +945 -0
  9. package/android/src/main/java/com/smoothclipview/ClipGeometryNormalizer.kt +25 -0
  10. package/android/src/main/java/com/smoothclipview/SmoothClipBindings.kt +55 -0
  11. package/android/src/main/java/com/smoothclipview/SmoothClipModule.kt +128 -0
  12. package/android/src/main/java/com/smoothclipview/SmoothClipView.kt +164 -30
  13. package/android/src/main/java/com/smoothclipview/SmoothClipViewManager.kt +107 -10
  14. package/android/src/main/java/com/smoothclipview/SmoothClipViewPackage.kt +18 -2
  15. package/cpp/SmoothClipRegistry.h +93 -0
  16. package/cpp/SmoothClipSharedGeometry.h +50 -0
  17. package/ios/SmoothClipModuleProvider.h +9 -0
  18. package/ios/SmoothClipModuleProvider.mm +16 -0
  19. package/ios/SmoothClipRegistry.mm +754 -0
  20. package/ios/SmoothClipTurboModule.cpp +290 -0
  21. package/ios/SmoothClipTurboModule.h +132 -0
  22. package/ios/SmoothClipView.mm +976 -85
  23. package/ios/SmoothClipViewRegistry.h +27 -0
  24. package/lib/module/NativeSmoothClipModule.js +5 -0
  25. package/lib/module/NativeSmoothClipModule.js.map +1 -0
  26. package/lib/module/SmoothClipView.android.js +4 -0
  27. package/lib/module/SmoothClipView.android.js.map +1 -0
  28. package/lib/module/SmoothClipView.ios.js +34 -0
  29. package/lib/module/SmoothClipView.ios.js.map +1 -0
  30. package/lib/module/SmoothClipView.js +26 -12
  31. package/lib/module/SmoothClipView.js.map +1 -1
  32. package/lib/module/SmoothClipView.web.js +28 -12
  33. package/lib/module/SmoothClipView.web.js.map +1 -1
  34. package/lib/module/SmoothClipViewNativeComponent.ts +14 -1
  35. package/lib/module/driverState.js +102 -0
  36. package/lib/module/driverState.js.map +1 -0
  37. package/lib/module/driverTypes.js +4 -0
  38. package/lib/module/driverTypes.js.map +1 -0
  39. package/lib/module/drivers.android.js +4 -0
  40. package/lib/module/drivers.android.js.map +1 -0
  41. package/lib/module/drivers.ios.js +380 -0
  42. package/lib/module/drivers.ios.js.map +1 -0
  43. package/lib/module/drivers.js +276 -0
  44. package/lib/module/drivers.js.map +1 -0
  45. package/lib/module/geometry.js +19 -0
  46. package/lib/module/geometry.js.map +1 -1
  47. package/lib/module/index.js +2 -0
  48. package/lib/module/index.js.map +1 -1
  49. package/lib/module/index.web.js +2 -0
  50. package/lib/module/index.web.js.map +1 -1
  51. package/lib/module/nativeCompletion.js +21 -0
  52. package/lib/module/nativeCompletion.js.map +1 -0
  53. package/lib/module/reactRequests.js +50 -0
  54. package/lib/module/reactRequests.js.map +1 -0
  55. package/lib/module/smoothClipNative.android.js +15 -0
  56. package/lib/module/smoothClipNative.android.js.map +1 -0
  57. package/lib/module/smoothClipNative.js +8 -0
  58. package/lib/module/smoothClipNative.js.map +1 -0
  59. package/lib/typescript/src/NativeSmoothClipModule.d.ts +19 -0
  60. package/lib/typescript/src/NativeSmoothClipModule.d.ts.map +1 -0
  61. package/lib/typescript/src/SmoothClipView.android.d.ts +3 -0
  62. package/lib/typescript/src/SmoothClipView.android.d.ts.map +1 -0
  63. package/lib/typescript/src/SmoothClipView.d.ts +5 -6
  64. package/lib/typescript/src/SmoothClipView.d.ts.map +1 -1
  65. package/lib/typescript/src/SmoothClipView.ios.d.ts +9 -0
  66. package/lib/typescript/src/SmoothClipView.ios.d.ts.map +1 -0
  67. package/lib/typescript/src/SmoothClipView.web.d.ts +5 -7
  68. package/lib/typescript/src/SmoothClipView.web.d.ts.map +1 -1
  69. package/lib/typescript/src/SmoothClipViewNativeComponent.d.ts +4 -0
  70. package/lib/typescript/src/SmoothClipViewNativeComponent.d.ts.map +1 -1
  71. package/lib/typescript/src/driverState.d.ts +31 -0
  72. package/lib/typescript/src/driverState.d.ts.map +1 -0
  73. package/lib/typescript/src/driverTypes.d.ts +61 -0
  74. package/lib/typescript/src/driverTypes.d.ts.map +1 -0
  75. package/lib/typescript/src/drivers.android.d.ts +3 -0
  76. package/lib/typescript/src/drivers.android.d.ts.map +1 -0
  77. package/lib/typescript/src/drivers.d.ts +5 -0
  78. package/lib/typescript/src/drivers.d.ts.map +1 -0
  79. package/lib/typescript/src/drivers.ios.d.ts +5 -0
  80. package/lib/typescript/src/drivers.ios.d.ts.map +1 -0
  81. package/lib/typescript/src/geometry.d.ts +8 -0
  82. package/lib/typescript/src/geometry.d.ts.map +1 -1
  83. package/lib/typescript/src/index.d.ts +4 -1
  84. package/lib/typescript/src/index.d.ts.map +1 -1
  85. package/lib/typescript/src/index.web.d.ts +4 -1
  86. package/lib/typescript/src/index.web.d.ts.map +1 -1
  87. package/lib/typescript/src/nativeCompletion.d.ts +7 -0
  88. package/lib/typescript/src/nativeCompletion.d.ts.map +1 -0
  89. package/lib/typescript/src/reactRequests.d.ts +7 -0
  90. package/lib/typescript/src/reactRequests.d.ts.map +1 -0
  91. package/lib/typescript/src/smoothClipNative.android.d.ts +4 -0
  92. package/lib/typescript/src/smoothClipNative.android.d.ts.map +1 -0
  93. package/lib/typescript/src/smoothClipNative.d.ts +3 -0
  94. package/lib/typescript/src/smoothClipNative.d.ts.map +1 -0
  95. package/package.json +12 -7
  96. package/src/NativeSmoothClipModule.ts +104 -0
  97. package/src/SmoothClipView.android.tsx +2 -0
  98. package/src/SmoothClipView.ios.tsx +36 -0
  99. package/src/SmoothClipView.tsx +31 -27
  100. package/src/SmoothClipView.web.tsx +26 -22
  101. package/src/SmoothClipViewNativeComponent.ts +14 -1
  102. package/src/driverState.ts +150 -0
  103. package/src/driverTypes.ts +91 -0
  104. package/src/drivers.android.ts +6 -0
  105. package/src/drivers.ios.ts +665 -0
  106. package/src/drivers.ts +416 -0
  107. package/src/geometry.ts +39 -0
  108. package/src/index.ts +19 -1
  109. package/src/index.web.ts +19 -1
  110. package/src/nativeCompletion.ts +37 -0
  111. package/src/reactRequests.ts +64 -0
  112. package/src/smoothClipNative.android.ts +20 -0
  113. package/src/smoothClipNative.ts +5 -0
@@ -0,0 +1,665 @@
1
+ import { useEffect, useRef } from 'react';
2
+ import { useSharedValue, type SharedValue } from 'react-native-reanimated';
3
+ import { isRNRuntime, scheduleOnRN, scheduleOnUI } from 'react-native-worklets';
4
+ import NativeSmoothClipModule from './smoothClipNative';
5
+ import type {
6
+ ClipReduceMotion,
7
+ KeyframedClipAnimation,
8
+ SmoothClipAnimation,
9
+ SmoothClipDriver,
10
+ SmoothClipDriverOptions,
11
+ SpringClipAnimation,
12
+ TimingClipAnimation,
13
+ } from './driverTypes';
14
+ import {
15
+ allocateDriverId,
16
+ attachDriverState,
17
+ createDriverState,
18
+ detachDriverState,
19
+ getDriverState,
20
+ setDriverState,
21
+ } from './driverState';
22
+ import {
23
+ completeNativeAnimation,
24
+ synchronizeNativeCompletion,
25
+ } from './nativeCompletion';
26
+ import {
27
+ clipPresentationEquals,
28
+ createClipPresentation,
29
+ isFiniteClipPresentation,
30
+ type ClipGeometry,
31
+ type SmoothClipPresentation,
32
+ } from './geometry';
33
+ import {
34
+ createReactRequest,
35
+ rejectDriverRequests,
36
+ resolveReactRequest,
37
+ } from './reactRequests';
38
+
39
+ const INTERACTIVE = 0;
40
+ const NATIVE = 1;
41
+
42
+ const setPresentationHostFunction = NativeSmoothClipModule.setClipPresentation;
43
+ const beginInteractionHostFunction = NativeSmoothClipModule.beginInteraction;
44
+ const animateTimingHostFunction = NativeSmoothClipModule.animateTiming;
45
+ const animateSpringHostFunction = NativeSmoothClipModule.animateSpring;
46
+ const animateKeyframesHostFunction = NativeSmoothClipModule.animateKeyframes;
47
+ const rejectAnimationHostFunction = NativeSmoothClipModule.rejectAnimation;
48
+ const cancelAnimationHostFunction = NativeSmoothClipModule.cancelAnimation;
49
+ const destroyDriverHostFunction = NativeSmoothClipModule.destroyDriver;
50
+
51
+ // Workaround for react-native-worklets 0.10: the RN-side serialization cache
52
+ // hands out one remote-function registry id per function forever, but the
53
+ // registry entry is deleted as soon as any transient UI-side proxy of that
54
+ // function is garbage collected. A later scheduleOnRN for the same function
55
+ // then reads `undefined` from the registry and aborts in Value::getObject.
56
+ // Pinning one UI-side reference for the app lifetime keeps the proxy — and
57
+ // with it the registry entry — alive. Remove once worklets decouples registry
58
+ // cleanup from proxy lifetime.
59
+ scheduleOnUI((resolver: typeof resolveReactRequest) => {
60
+ 'worklet';
61
+ (globalThis as Record<string, unknown>).__smoothClipPinnedResolver = resolver;
62
+ }, resolveReactRequest);
63
+
64
+ function reduceMotionCode(value: ClipReduceMotion): number {
65
+ switch (value) {
66
+ case 'always':
67
+ return 1;
68
+ case 'never':
69
+ return 2;
70
+ default:
71
+ return 0;
72
+ }
73
+ }
74
+
75
+ function animationIsFinite(
76
+ target: SmoothClipPresentation,
77
+ animation: SmoothClipAnimation
78
+ ): boolean {
79
+ 'worklet';
80
+ if (animation.type === 'timing') {
81
+ return (
82
+ Number.isFinite(animation.duration) &&
83
+ animation.controlPoints.every(Number.isFinite)
84
+ );
85
+ }
86
+ if (animation.type === 'spring') {
87
+ return [
88
+ animation.mass ?? 1,
89
+ animation.stiffness ?? 100,
90
+ animation.damping ?? 10,
91
+ animation.initialVelocity === 'inherit'
92
+ ? 0
93
+ : (animation.initialVelocity ?? 0),
94
+ ].every(Number.isFinite);
95
+ }
96
+ if (!Number.isFinite(animation.duration) || animation.frames.length < 2) {
97
+ return false;
98
+ }
99
+ let previousOffset = -1;
100
+ for (const frame of animation.frames) {
101
+ if (
102
+ !Number.isFinite(frame.offset) ||
103
+ frame.offset < 0 ||
104
+ frame.offset > 1 ||
105
+ frame.offset <= previousOffset ||
106
+ !isFiniteClipPresentation(frame.presentation)
107
+ ) {
108
+ return false;
109
+ }
110
+ previousOffset = frame.offset;
111
+ }
112
+ return (
113
+ animation.frames[0]?.offset === 0 &&
114
+ previousOffset === 1 &&
115
+ clipPresentationEquals(
116
+ animation.frames[animation.frames.length - 1]?.presentation ?? null,
117
+ target
118
+ )
119
+ );
120
+ }
121
+
122
+ function presentationFromNative(
123
+ values: ReadonlyArray<number>,
124
+ fallback: SmoothClipPresentation,
125
+ offset = 0
126
+ ): SmoothClipPresentation {
127
+ 'worklet';
128
+ if (values.length < offset + 7) return fallback;
129
+ const presentation = {
130
+ clip: {
131
+ x: values[offset] as number,
132
+ y: values[offset + 1] as number,
133
+ width: values[offset + 2] as number,
134
+ height: values[offset + 3] as number,
135
+ radius: values[offset + 4] as number,
136
+ },
137
+ contentTranslateX: values[offset + 5] as number,
138
+ contentTranslateY: values[offset + 6] as number,
139
+ };
140
+ return isFiniteClipPresentation(presentation) ? presentation : fallback;
141
+ }
142
+
143
+ function flattenedKeyframes(animation: KeyframedClipAnimation): number[] {
144
+ 'worklet';
145
+ const values: number[] = [];
146
+ for (const frame of animation.frames) {
147
+ const { clip, contentTranslateX, contentTranslateY } = frame.presentation;
148
+ values.push(
149
+ frame.offset,
150
+ clip.x,
151
+ clip.y,
152
+ clip.width,
153
+ clip.height,
154
+ clip.radius,
155
+ contentTranslateX,
156
+ contentTranslateY
157
+ );
158
+ }
159
+ return values;
160
+ }
161
+
162
+ function uiOnly(): never {
163
+ 'worklet';
164
+ throw new Error(
165
+ '[SmoothClipView] driver.ui methods must run on the UI runtime. Use driver.react from React code.'
166
+ );
167
+ }
168
+
169
+ export function useSmoothClipDriver(
170
+ initialValue: ClipGeometry | SmoothClipPresentation,
171
+ options: SmoothClipDriverOptions = {}
172
+ ): SmoothClipDriver {
173
+ const initialPresentation =
174
+ 'clip' in initialValue
175
+ ? initialValue
176
+ : createClipPresentation(initialValue);
177
+ const presentation = useSharedValue(initialPresentation);
178
+ const ownership = useSharedValue(INTERACTIVE);
179
+ const suppressDeliveries = useSharedValue(0);
180
+ const activeAnimationId = useSharedValue(0);
181
+ // Non-zero after a setScalars hot write: driver.presentation is then stale,
182
+ // so animateTo must start from the native registry's latest value instead
183
+ // of snapping back to the stale SharedValue.
184
+ const scalarsStale = useSharedValue(0);
185
+ const callbackRef = useRef(options.onAnimationComplete);
186
+ const driverRef = useRef<SmoothClipDriver | null>(null);
187
+ callbackRef.current = options.onAnimationComplete;
188
+
189
+ if (driverRef.current === null) {
190
+ const driverId = allocateDriverId();
191
+ const reduceMotion = reduceMotionCode(options.reduceMotion ?? 'system');
192
+
193
+ const seedPresentation = (next: SmoothClipPresentation) => {
194
+ 'worklet';
195
+ if (!clipPresentationEquals(presentation.value, next)) {
196
+ suppressDeliveries.value += 1;
197
+ presentation.value = next;
198
+ }
199
+ };
200
+
201
+ const beginOnUI = (): SmoothClipPresentation => {
202
+ 'worklet';
203
+ const current = presentationFromNative(
204
+ beginInteractionHostFunction(driverId),
205
+ presentation.value
206
+ );
207
+ activeAnimationId.value = 0;
208
+ ownership.value = INTERACTIVE;
209
+ scalarsStale.value = 0;
210
+ seedPresentation(current);
211
+ return current;
212
+ };
213
+
214
+ const setOnUI = (next: SmoothClipPresentation): void => {
215
+ 'worklet';
216
+ if (!isFiniteClipPresentation(next)) return;
217
+ activeAnimationId.value = 0;
218
+ ownership.value = INTERACTIVE;
219
+ scalarsStale.value = 0;
220
+ const { clip, contentTranslateX, contentTranslateY } = next;
221
+ setPresentationHostFunction(
222
+ driverId,
223
+ clip.x,
224
+ clip.y,
225
+ clip.width,
226
+ clip.height,
227
+ clip.radius,
228
+ contentTranslateX,
229
+ contentTranslateY,
230
+ true
231
+ );
232
+ seedPresentation(next);
233
+ };
234
+
235
+ const setScalarsOnUI = (
236
+ x: number,
237
+ y: number,
238
+ width: number,
239
+ height: number,
240
+ radius: number,
241
+ contentTranslateX: number,
242
+ contentTranslateY: number
243
+ ): void => {
244
+ 'worklet';
245
+ if (
246
+ !Number.isFinite(x) ||
247
+ !Number.isFinite(y) ||
248
+ !Number.isFinite(width) ||
249
+ !Number.isFinite(height) ||
250
+ !Number.isFinite(radius) ||
251
+ !Number.isFinite(contentTranslateX) ||
252
+ !Number.isFinite(contentTranslateY)
253
+ ) {
254
+ return;
255
+ }
256
+ if (activeAnimationId.value !== 0) activeAnimationId.value = 0;
257
+ if (ownership.value !== INTERACTIVE) ownership.value = INTERACTIVE;
258
+ if (scalarsStale.value === 0) scalarsStale.value = 1;
259
+ setPresentationHostFunction(
260
+ driverId,
261
+ x,
262
+ y,
263
+ width,
264
+ height,
265
+ radius,
266
+ contentTranslateX,
267
+ contentTranslateY,
268
+ true
269
+ );
270
+ };
271
+
272
+ const animateOnUI = (
273
+ target: SmoothClipPresentation,
274
+ animation: SmoothClipAnimation
275
+ ): number => {
276
+ 'worklet';
277
+ if (
278
+ !isFiniteClipPresentation(target) ||
279
+ !animationIsFinite(target, animation)
280
+ ) {
281
+ return rejectAnimationHostFunction(driverId);
282
+ }
283
+
284
+ // After a setScalars hot write the SharedValue no longer matches what
285
+ // is on screen; passing it as the start would snap the animation back.
286
+ // Without an interactive start, native resolves the start from its own
287
+ // latest value instead.
288
+ const scalarsWereStale = scalarsStale.value !== 0;
289
+ const hasInteractiveStart =
290
+ ownership.value === INTERACTIVE && !scalarsWereStale;
291
+ const start = presentation.value;
292
+ ownership.value = NATIVE;
293
+ presentation.value = target;
294
+ scalarsStale.value = 0;
295
+ const startClip = start.clip;
296
+ const targetClip = target.clip;
297
+ let animationId: number;
298
+
299
+ if (animation.type === 'timing') {
300
+ const [x1, y1, x2, y2] = animation.controlPoints;
301
+ animationId = animateTimingHostFunction(
302
+ driverId,
303
+ hasInteractiveStart,
304
+ startClip.x,
305
+ startClip.y,
306
+ startClip.width,
307
+ startClip.height,
308
+ startClip.radius,
309
+ start.contentTranslateX,
310
+ start.contentTranslateY,
311
+ targetClip.x,
312
+ targetClip.y,
313
+ targetClip.width,
314
+ targetClip.height,
315
+ targetClip.radius,
316
+ target.contentTranslateX,
317
+ target.contentTranslateY,
318
+ Math.max(0, animation.duration),
319
+ x1,
320
+ y1,
321
+ x2,
322
+ y2,
323
+ reduceMotion
324
+ );
325
+ } else if (animation.type === 'spring') {
326
+ const inheritVelocity =
327
+ animation.initialVelocity === undefined ||
328
+ animation.initialVelocity === 'inherit';
329
+ animationId = animateSpringHostFunction(
330
+ driverId,
331
+ hasInteractiveStart,
332
+ startClip.x,
333
+ startClip.y,
334
+ startClip.width,
335
+ startClip.height,
336
+ startClip.radius,
337
+ start.contentTranslateX,
338
+ start.contentTranslateY,
339
+ targetClip.x,
340
+ targetClip.y,
341
+ targetClip.width,
342
+ targetClip.height,
343
+ targetClip.radius,
344
+ target.contentTranslateX,
345
+ target.contentTranslateY,
346
+ Math.max(0.0001, animation.mass ?? 1),
347
+ Math.max(0.0001, animation.stiffness ?? 100),
348
+ Math.max(0, animation.damping ?? 10),
349
+ inheritVelocity ? 0 : (animation.initialVelocity as number),
350
+ inheritVelocity,
351
+ reduceMotion
352
+ );
353
+ } else {
354
+ animationId = animateKeyframesHostFunction(
355
+ driverId,
356
+ hasInteractiveStart,
357
+ startClip.x,
358
+ startClip.y,
359
+ startClip.width,
360
+ startClip.height,
361
+ startClip.radius,
362
+ start.contentTranslateX,
363
+ start.contentTranslateY,
364
+ targetClip.x,
365
+ targetClip.y,
366
+ targetClip.width,
367
+ targetClip.height,
368
+ targetClip.radius,
369
+ target.contentTranslateX,
370
+ target.contentTranslateY,
371
+ Math.max(0, animation.duration),
372
+ flattenedKeyframes(animation),
373
+ reduceMotion
374
+ );
375
+ }
376
+ if (animationId === 0) {
377
+ // Native rejected the transition after ownership was transferred.
378
+ // Restore interactive ownership and the pre-animation value, then
379
+ // emit a standalone finished:false completion so completion-driven
380
+ // state machines observe exactly one result per animateTo call.
381
+ ownership.value = INTERACTIVE;
382
+ presentation.value = start;
383
+ if (scalarsWereStale) scalarsStale.value = 1;
384
+ activeAnimationId.value = 0;
385
+ return rejectAnimationHostFunction(driverId);
386
+ }
387
+ activeAnimationId.value = animationId;
388
+ return animationId;
389
+ };
390
+
391
+ const cancelOnUI = (
392
+ animationId = 0,
393
+ behavior: 'current' | 'target' = 'current'
394
+ ): SmoothClipPresentation => {
395
+ 'worklet';
396
+ const values = cancelAnimationHostFunction(
397
+ driverId,
398
+ animationId,
399
+ behavior === 'target' ? 1 : 0
400
+ );
401
+ if (values.length < 8 || values[0] !== 1) return presentation.value;
402
+ const current = presentationFromNative(values, presentation.value, 1);
403
+ activeAnimationId.value = 0;
404
+ ownership.value = INTERACTIVE;
405
+ scalarsStale.value = 0;
406
+ seedPresentation(current);
407
+ return current;
408
+ };
409
+
410
+ const driver: SmoothClipDriver = {
411
+ kind: 'hybrid',
412
+ presentation,
413
+ ui: {
414
+ beginInteraction() {
415
+ 'worklet';
416
+ if (isRNRuntime()) return uiOnly();
417
+ return beginOnUI();
418
+ },
419
+ set(next) {
420
+ 'worklet';
421
+ if (isRNRuntime()) return uiOnly();
422
+ setOnUI(next);
423
+ },
424
+ setScalars(
425
+ x,
426
+ y,
427
+ width,
428
+ height,
429
+ radius,
430
+ contentTranslateX,
431
+ contentTranslateY
432
+ ) {
433
+ 'worklet';
434
+ if (isRNRuntime()) return uiOnly();
435
+ setScalarsOnUI(
436
+ x,
437
+ y,
438
+ width,
439
+ height,
440
+ radius,
441
+ contentTranslateX,
442
+ contentTranslateY
443
+ );
444
+ },
445
+ animateTo(target, animation) {
446
+ 'worklet';
447
+ if (isRNRuntime()) return uiOnly();
448
+ return animateOnUI(target, animation);
449
+ },
450
+ cancel(animationId = 0, behavior = 'current') {
451
+ 'worklet';
452
+ if (isRNRuntime()) return uiOnly();
453
+ return cancelOnUI(animationId, behavior);
454
+ },
455
+ },
456
+ react: {
457
+ beginInteraction() {
458
+ const { requestId, promise } =
459
+ createReactRequest<SmoothClipPresentation>(driverId);
460
+ scheduleOnUI(() => {
461
+ 'worklet';
462
+ const result = beginOnUI();
463
+ scheduleOnRN(resolveReactRequest, driverId, requestId, result);
464
+ });
465
+ return promise;
466
+ },
467
+ set(next) {
468
+ const { requestId, promise } = createReactRequest<void>(driverId);
469
+ scheduleOnUI(() => {
470
+ 'worklet';
471
+ setOnUI(next);
472
+ scheduleOnRN(resolveReactRequest, driverId, requestId, undefined);
473
+ });
474
+ return promise;
475
+ },
476
+ animateTo(target, animation) {
477
+ const { requestId, promise } = createReactRequest<number>(
478
+ driverId,
479
+ true
480
+ );
481
+ scheduleOnUI(() => {
482
+ 'worklet';
483
+ const result = animateOnUI(target, animation);
484
+ scheduleOnRN(
485
+ resolveReactRequest,
486
+ driverId,
487
+ requestId,
488
+ result,
489
+ true
490
+ );
491
+ });
492
+ return promise;
493
+ },
494
+ cancel(animationId = 0, behavior = 'current') {
495
+ const { requestId, promise } =
496
+ createReactRequest<SmoothClipPresentation>(driverId, true);
497
+ scheduleOnUI(() => {
498
+ 'worklet';
499
+ const result = cancelOnUI(animationId, behavior);
500
+ scheduleOnRN(
501
+ resolveReactRequest,
502
+ driverId,
503
+ requestId,
504
+ result,
505
+ true
506
+ );
507
+ });
508
+ return promise;
509
+ },
510
+ },
511
+ };
512
+
513
+ setDriverState(
514
+ driver,
515
+ createDriverState(
516
+ driverId,
517
+ initialPresentation,
518
+ presentation,
519
+ callbackRef,
520
+ activeAnimationId,
521
+ ownership
522
+ )
523
+ );
524
+ driverRef.current = driver;
525
+ }
526
+
527
+ const driver = driverRef.current;
528
+
529
+ useEffect(() => {
530
+ const currentDriver = driverRef.current;
531
+ if (!currentDriver) return undefined;
532
+ const state = getDriverState(currentDriver);
533
+ const { driverId } = state;
534
+ // Re-attach on every effect run: StrictMode/<Activity> replay the effect
535
+ // after its cleanup detached this state and destroyed the native driver.
536
+ // The authoritative seed below recreates the native entry in that case.
537
+ attachDriverState(state);
538
+
539
+ const subscription = NativeSmoothClipModule.onClipAnimationComplete(
540
+ (result) => {
541
+ if (result.driverId !== driverId) return;
542
+ synchronizeNativeCompletion(
543
+ activeAnimationId,
544
+ ownership,
545
+ result.animationId,
546
+ result.finished
547
+ );
548
+ completeNativeAnimation(driverId, result.animationId, result.finished);
549
+ }
550
+ );
551
+
552
+ scheduleOnUI(
553
+ (
554
+ source: SharedValue<SmoothClipPresentation>,
555
+ listenerId: number,
556
+ nativeDriverId: number,
557
+ owner: SharedValue<number>,
558
+ suppressed: SharedValue<number>,
559
+ active: SharedValue<number>,
560
+ stale: SharedValue<number>,
561
+ setter: typeof setPresentationHostFunction
562
+ ) => {
563
+ 'worklet';
564
+ let last: SmoothClipPresentation | null = null;
565
+ const deliver = (next: SmoothClipPresentation) => {
566
+ if (
567
+ !isFiniteClipPresentation(next) ||
568
+ clipPresentationEquals(last, next)
569
+ ) {
570
+ return;
571
+ }
572
+ // Record `last` only for values native has actually observed:
573
+ // deliveries dropped during native ownership must stay eligible
574
+ // for re-delivery once interactive ownership returns.
575
+ if (owner.value !== INTERACTIVE) return;
576
+ if (suppressed.value > 0) {
577
+ suppressed.value -= 1;
578
+ last = next;
579
+ return;
580
+ }
581
+ last = next;
582
+ if (stale.value !== 0) stale.value = 0;
583
+ const { clip, contentTranslateX, contentTranslateY } = next;
584
+ setter(
585
+ nativeDriverId,
586
+ clip.x,
587
+ clip.y,
588
+ clip.width,
589
+ clip.height,
590
+ clip.radius,
591
+ contentTranslateX,
592
+ contentTranslateY,
593
+ false
594
+ );
595
+ };
596
+ source.addListener(listenerId, deliver);
597
+ // Authoritative take-ownership seed. Creates the native entry before
598
+ // any view mounts on the first run, and revives a driver destroyed by
599
+ // an effect-replay cleanup (StrictMode/<Activity>) on later runs.
600
+ const current = source.value;
601
+ if (isFiniteClipPresentation(current)) {
602
+ active.value = 0;
603
+ owner.value = INTERACTIVE;
604
+ stale.value = 0;
605
+ last = current;
606
+ setter(
607
+ nativeDriverId,
608
+ current.clip.x,
609
+ current.clip.y,
610
+ current.clip.width,
611
+ current.clip.height,
612
+ current.clip.radius,
613
+ current.contentTranslateX,
614
+ current.contentTranslateY,
615
+ true
616
+ );
617
+ }
618
+ },
619
+ presentation,
620
+ driverId,
621
+ driverId,
622
+ ownership,
623
+ suppressDeliveries,
624
+ activeAnimationId,
625
+ scalarsStale,
626
+ setPresentationHostFunction
627
+ );
628
+
629
+ return () => {
630
+ subscription.remove();
631
+ rejectDriverRequests(driverId);
632
+ detachDriverState(state);
633
+ scheduleOnUI(
634
+ (
635
+ source: SharedValue<SmoothClipPresentation>,
636
+ listenerId: number,
637
+ nativeDriverId: number,
638
+ destroy: typeof destroyDriverHostFunction
639
+ ) => {
640
+ 'worklet';
641
+ source.removeListener(listenerId);
642
+ destroy(nativeDriverId);
643
+ },
644
+ presentation,
645
+ driverId,
646
+ driverId,
647
+ destroyDriverHostFunction
648
+ );
649
+ };
650
+ }, [
651
+ activeAnimationId,
652
+ ownership,
653
+ presentation,
654
+ scalarsStale,
655
+ suppressDeliveries,
656
+ ]);
657
+
658
+ return driver;
659
+ }
660
+
661
+ export type {
662
+ KeyframedClipAnimation,
663
+ SpringClipAnimation,
664
+ TimingClipAnimation,
665
+ };