reanimated-pause-state 0.1.0

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/src/index.ts ADDED
@@ -0,0 +1,553 @@
1
+ /**
2
+ * reanimated-pause-plugin
3
+ *
4
+ * Pause and resume Reanimated animations with structured state snapshots.
5
+ */
6
+
7
+ import type {
8
+ AnimationSnapshot,
9
+ AnimationRegistration,
10
+ PauseSnapshot,
11
+ AnimationType,
12
+ SharedValueRef,
13
+ SnapshotOptions,
14
+ } from './types';
15
+
16
+ export type { AnimationSnapshot, PauseSnapshot, AnimationType, SnapshotOptions } from './types';
17
+
18
+ // Type declarations for globals
19
+ declare global {
20
+ var __RPP__: {
21
+ paused: boolean;
22
+ pausedAt: number | null;
23
+ snapshot: PauseSnapshot | null;
24
+ } | undefined;
25
+ var __RPP_PAUSED__: boolean | undefined;
26
+ var __RPP_INSTALLED__: { installed: boolean; version: string } | undefined;
27
+ var __RPP_ORIGINAL_RAF__: ((callback: (timestamp: number) => void) => number) | undefined;
28
+ var __RPP_PENDING_CALLBACKS__: Array<(timestamp: number) => void> | undefined;
29
+ var __RPP_PAUSED_AT__: number | undefined;
30
+ var __RPP_ACCUMULATED_PAUSE__: number | undefined;
31
+ var __RPP_REGISTRY__: {
32
+ animations: Map<string, AnimationRegistration>;
33
+ register: (meta: any) => void;
34
+ unregister: (id: string) => void;
35
+ getAll: () => AnimationRegistration[];
36
+ } | undefined;
37
+ }
38
+
39
+ const VERSION = '0.1.0';
40
+
41
+ /**
42
+ * Initialize JS-side state and registry
43
+ */
44
+ function ensureInitialized(): void {
45
+ if (typeof globalThis.__RPP__ === 'undefined') {
46
+ globalThis.__RPP__ = {
47
+ paused: false,
48
+ pausedAt: null,
49
+ snapshot: null,
50
+ };
51
+ }
52
+
53
+ if (typeof globalThis.__RPP_REGISTRY__ === 'undefined') {
54
+ const animations = new Map<string, AnimationRegistration>();
55
+
56
+ globalThis.__RPP_REGISTRY__ = {
57
+ animations,
58
+ register: (meta: any) => {
59
+ // Skip registration in production - zero overhead
60
+ if (typeof __DEV__ !== 'undefined' && !__DEV__) {
61
+ return;
62
+ }
63
+
64
+ const config = meta.__rpp_config || {};
65
+
66
+ // Extract toValue and duration from config
67
+ const toValue = config.toValue ?? 0;
68
+ const duration = config.duration;
69
+
70
+ // Capture SharedValue reference if available
71
+ const sharedValue: SharedValueRef | undefined = meta.__rpp_sharedValue;
72
+
73
+ // Read the current value from SharedValue as the fromValue
74
+ // This works because Reanimated syncs SharedValue.value to JS thread
75
+ const fromValue = sharedValue?.value ?? 0;
76
+
77
+ const registration: AnimationRegistration = {
78
+ id: String(meta.__rpp_id),
79
+ type: meta.__rpp_type || 'unknown',
80
+ callsite: {
81
+ file: meta.__rpp_file || 'unknown',
82
+ line: meta.__rpp_line || 0,
83
+ column: meta.__rpp_column,
84
+ },
85
+ config,
86
+ startTime: Date.now(),
87
+ fromValue,
88
+ toValue,
89
+ duration,
90
+ sharedValue, // Store reference for reading current value at pause time
91
+ sharedValueName: meta.__rpp_sharedValueName,
92
+ };
93
+ animations.set(registration.id, registration);
94
+
95
+ // Don't auto-cleanup - animations may repeat or user may pause at any time
96
+ // Cleanup will happen on app reload or explicit clear
97
+ },
98
+ unregister: (id: string) => {
99
+ animations.delete(id);
100
+ },
101
+ getAll: () => Array.from(animations.values()),
102
+ };
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Check if the Babel plugin is installed
108
+ */
109
+ export function isInstalled(): boolean {
110
+ return globalThis.__RPP_INSTALLED__?.installed === true;
111
+ }
112
+
113
+ /**
114
+ * Check if animations are paused
115
+ */
116
+ export function isPaused(): boolean {
117
+ ensureInitialized();
118
+ return globalThis.__RPP__!.paused;
119
+ }
120
+
121
+ /**
122
+ * Get the last pause snapshot (or null if not paused/no snapshot)
123
+ */
124
+ export function getSnapshot(): PauseSnapshot | null {
125
+ ensureInitialized();
126
+ return globalThis.__RPP__!.snapshot;
127
+ }
128
+
129
+ /**
130
+ * Take a snapshot now with optional filtering
131
+ * This is the main API for getting animation state
132
+ */
133
+ export function snapshotNow(opts?: SnapshotOptions): PauseSnapshot {
134
+ // No-op in production
135
+ if (typeof __DEV__ !== 'undefined' && !__DEV__) {
136
+ return { timestamp: 0, timestampISO: '', animations: [], totalCount: 0, summary: 'disabled in production' };
137
+ }
138
+
139
+ ensureInitialized();
140
+ return captureSnapshot(opts);
141
+ }
142
+
143
+ /**
144
+ * Get snapshot as JSON string (for clipboard/export)
145
+ */
146
+ export function getSnapshotJSON(opts?: SnapshotOptions): string {
147
+ const snapshot = snapshotNow(opts);
148
+ return JSON.stringify(snapshot, null, 2);
149
+ }
150
+
151
+ /**
152
+ * Get snapshot as prompt-ready markdown
153
+ * Optimized for pasting into Claude/LLM context
154
+ */
155
+ export function getSnapshotMarkdown(opts?: SnapshotOptions): string {
156
+ const snapshot = snapshotNow(opts);
157
+ if (!snapshot || snapshot.animations.length === 0) {
158
+ return '**Animation State:** No active animations captured.';
159
+ }
160
+
161
+ let md = `## Animation State at Pause\n\n`;
162
+ md += `> Captured: ${snapshot.timestampISO}\n\n`;
163
+
164
+ for (const anim of snapshot.animations) {
165
+ // Use SharedValue name as heading if available
166
+ const heading = anim.sharedValueName
167
+ ? `\`${anim.sharedValueName}\` (${anim.type})`
168
+ : `${anim.type} animation`;
169
+ md += `### ${heading}\n\n`;
170
+
171
+ // Current value is the most important for debugging
172
+ md += `| Property | Value |\n`;
173
+ md += `|----------|-------|\n`;
174
+ md += `| **Current** | \`${anim.values.current.toFixed(2)}\` |\n`;
175
+ md += `| From → To | ${anim.values.from} → ${anim.values.to} |\n`;
176
+
177
+ if (anim.timing.duration) {
178
+ const progress = anim.timing.progress?.toFixed(1) || '0';
179
+ md += `| Progress | ${progress}% (${anim.timing.elapsed}ms / ${anim.timing.duration}ms) |\n`;
180
+ } else {
181
+ md += `| Elapsed | ${anim.timing.elapsed}ms |\n`;
182
+ }
183
+
184
+ md += `| Location | \`${anim.callsite.file}:${anim.callsite.line}\` |\n`;
185
+
186
+ // Show relevant config
187
+ const cfg = anim.config.config as Record<string, unknown>;
188
+ if (anim.type === 'spring' && cfg.damping) {
189
+ md += `| Spring | damping=${cfg.damping}`;
190
+ if (cfg.mass) md += `, mass=${cfg.mass}`;
191
+ if (cfg.stiffness) md += `, stiffness=${cfg.stiffness}`;
192
+ md += ` |\n`;
193
+ }
194
+ if (anim.type === 'repeat') {
195
+ const reps = cfg.numberOfReps as number | undefined;
196
+ md += `| Repeat | ${reps === -1 ? 'infinite' : reps || 'unknown'}`;
197
+ if (cfg.reverse) md += ' (reversing)';
198
+ md += ` |\n`;
199
+ }
200
+
201
+ md += `\n`;
202
+ }
203
+
204
+ return md;
205
+ }
206
+
207
+ /**
208
+ * Install the frame interceptor on UI thread
209
+ * Only installs in __DEV__ mode - zero cost in production
210
+ */
211
+ function installFrameInterceptor(): void {
212
+ // Skip in production - this is a dev/design review tool
213
+ if (typeof __DEV__ !== 'undefined' && !__DEV__) {
214
+ return;
215
+ }
216
+
217
+ try {
218
+ const { runOnUI } = require('react-native-reanimated');
219
+
220
+ runOnUI(() => {
221
+ 'worklet';
222
+
223
+ if (globalThis.__RPP_ORIGINAL_RAF__) {
224
+ return;
225
+ }
226
+
227
+ const originalRAF = globalThis.requestAnimationFrame;
228
+ if (!originalRAF) {
229
+ console.warn('[rpp] requestAnimationFrame not found on UI thread');
230
+ return;
231
+ }
232
+
233
+ globalThis.__RPP_ORIGINAL_RAF__ = originalRAF;
234
+ globalThis.__RPP_PENDING_CALLBACKS__ = [];
235
+ globalThis.__RPP_PAUSED__ = false;
236
+ globalThis.__RPP_PAUSED_AT__ = 0;
237
+ globalThis.__RPP_ACCUMULATED_PAUSE__ = 0;
238
+
239
+ (globalThis as any).requestAnimationFrame = function(
240
+ callback: (timestamp: number) => void
241
+ ): number {
242
+ if (globalThis.__RPP_PAUSED__) {
243
+ globalThis.__RPP_PENDING_CALLBACKS__!.push(callback);
244
+ return -1;
245
+ }
246
+
247
+ return globalThis.__RPP_ORIGINAL_RAF__!((timestamp: number) => {
248
+ const adjustedTimestamp =
249
+ timestamp - (globalThis.__RPP_ACCUMULATED_PAUSE__ || 0);
250
+ callback(adjustedTimestamp);
251
+ });
252
+ };
253
+
254
+ })();
255
+ } catch (e) {
256
+ console.warn('[rpp] Failed to install frame interceptor:', e);
257
+ }
258
+ }
259
+
260
+ /**
261
+ * Flush pending callbacks after resume
262
+ */
263
+ function flushPendingCallbacks(): void {
264
+ try {
265
+ const { runOnUI } = require('react-native-reanimated');
266
+
267
+ runOnUI(() => {
268
+ 'worklet';
269
+
270
+ const pending = globalThis.__RPP_PENDING_CALLBACKS__ || [];
271
+ globalThis.__RPP_PENDING_CALLBACKS__ = [];
272
+
273
+ if (pending.length === 0) return;
274
+
275
+ const originalRAF = globalThis.__RPP_ORIGINAL_RAF__;
276
+ if (originalRAF) {
277
+ originalRAF((timestamp: number) => {
278
+ const adjusted =
279
+ timestamp - (globalThis.__RPP_ACCUMULATED_PAUSE__ || 0);
280
+ for (const cb of pending) {
281
+ try {
282
+ cb(adjusted);
283
+ } catch (e) {
284
+ // Ignore errors in callbacks
285
+ }
286
+ }
287
+ });
288
+ }
289
+ })();
290
+ } catch (e) {
291
+ console.warn('[rpp] Failed to flush callbacks:', e);
292
+ }
293
+ }
294
+
295
+ /**
296
+ * Create a snapshot of current animation state with optional filtering
297
+ */
298
+ function captureSnapshot(opts?: SnapshotOptions): PauseSnapshot {
299
+ ensureInitialized();
300
+
301
+ const now = Date.now();
302
+ let registrations = globalThis.__RPP_REGISTRY__?.getAll() || [];
303
+
304
+ // Tier 1: Filter by file
305
+ if (opts?.filterFile) {
306
+ registrations = registrations.filter((reg) =>
307
+ reg.callsite.file.includes(opts.filterFile!)
308
+ );
309
+
310
+ // Tier 2: Filter by line proximity (requires filterFile)
311
+ if (opts.filterLine !== undefined) {
312
+ const threshold = opts.proximityThreshold ?? 200;
313
+ registrations = registrations.filter(
314
+ (reg) => Math.abs(reg.callsite.line - opts.filterLine!) < threshold
315
+ );
316
+ }
317
+ }
318
+
319
+ // Apply max limit
320
+ const maxAnimations = opts?.maxAnimations ?? 20;
321
+ if (registrations.length > maxAnimations) {
322
+ registrations = registrations.slice(0, maxAnimations);
323
+ }
324
+
325
+ const animations: AnimationSnapshot[] = registrations.map((reg) => {
326
+ const elapsed = now - reg.startTime;
327
+ const progress = reg.duration ? (elapsed / reg.duration) * 100 : undefined;
328
+
329
+ // Read the current value from the SharedValue if available
330
+ // Reanimated syncs SharedValue.value to JS thread automatically
331
+ let currentValue = reg.fromValue;
332
+ if (reg.sharedValue && typeof reg.sharedValue.value === 'number') {
333
+ currentValue = reg.sharedValue.value;
334
+ }
335
+
336
+ const snapshot: AnimationSnapshot = {
337
+ id: reg.id,
338
+ type: reg.type as AnimationType,
339
+ sharedValueName: reg.sharedValueName,
340
+ callsite: reg.callsite,
341
+ config: {
342
+ type: reg.type as any,
343
+ config: reg.config as any,
344
+ },
345
+ timing: {
346
+ startTime: reg.startTime,
347
+ elapsed,
348
+ duration: reg.duration,
349
+ progress: progress ? Math.min(progress, 100) : undefined,
350
+ },
351
+ values: {
352
+ from: reg.fromValue,
353
+ to: reg.toValue,
354
+ current: currentValue,
355
+ },
356
+ description: formatDescription(reg, elapsed, currentValue),
357
+ };
358
+
359
+ return snapshot;
360
+ });
361
+
362
+ const snapshot: PauseSnapshot = {
363
+ timestamp: now,
364
+ timestampISO: new Date(now).toISOString(),
365
+ animations,
366
+ totalCount: animations.length,
367
+ summary: `${animations.length} animation(s) captured`,
368
+ };
369
+
370
+ return snapshot;
371
+ }
372
+
373
+ /**
374
+ * Format a human-readable description optimized for prompt context
375
+ */
376
+ function formatDescription(
377
+ reg: AnimationRegistration,
378
+ elapsed: number,
379
+ currentValue?: number
380
+ ): string {
381
+ const file = reg.callsite.file.split('/').pop() || reg.callsite.file;
382
+ const location = `${file}:${reg.callsite.line}`;
383
+ const config = reg.config as Record<string, unknown>;
384
+
385
+ // Start with SharedValue name if available (most useful for prompts)
386
+ let desc = reg.sharedValueName ? `${reg.sharedValueName}: ` : '';
387
+
388
+ desc += `${reg.type} `;
389
+
390
+ // Add config-specific details
391
+ switch (reg.type) {
392
+ case 'timing': {
393
+ if (reg.toValue !== 0) {
394
+ desc += `to ${reg.toValue} `;
395
+ }
396
+ if (reg.duration) {
397
+ desc += `(${reg.duration}ms) `;
398
+ }
399
+ break;
400
+ }
401
+ case 'spring': {
402
+ if (reg.toValue !== 0) {
403
+ desc += `to ${reg.toValue} `;
404
+ }
405
+ if (config.damping) {
406
+ desc += `damping=${config.damping} `;
407
+ }
408
+ break;
409
+ }
410
+ case 'repeat': {
411
+ const reps = config.numberOfReps as number | undefined;
412
+ if (reps === -1) {
413
+ desc += '(infinite) ';
414
+ } else if (reps) {
415
+ desc += `(${reps}x) `;
416
+ }
417
+ if (config.reverse) {
418
+ desc += 'reverse ';
419
+ }
420
+ break;
421
+ }
422
+ case 'delay': {
423
+ if (config.delayMs) {
424
+ desc += `${config.delayMs}ms `;
425
+ }
426
+ break;
427
+ }
428
+ case 'decay': {
429
+ if (config.velocity) {
430
+ desc += `velocity=${config.velocity} `;
431
+ }
432
+ break;
433
+ }
434
+ case 'sequence': {
435
+ if (config.count) {
436
+ desc += `(${config.count} animations) `;
437
+ }
438
+ break;
439
+ }
440
+ }
441
+
442
+ // Current value is the most important info for debugging
443
+ if (currentValue !== undefined) {
444
+ desc += `| current=${currentValue.toFixed(2)} `;
445
+ }
446
+
447
+ desc += `| ${elapsed}ms elapsed`;
448
+ if (reg.duration) {
449
+ const progress = Math.min((elapsed / reg.duration) * 100, 100).toFixed(0);
450
+ desc += ` (${progress}%)`;
451
+ }
452
+
453
+ desc += ` | ${location}`;
454
+
455
+ return desc;
456
+ }
457
+
458
+ /**
459
+ * Pause all animations and capture snapshot
460
+ * No-op in production builds
461
+ */
462
+ export function pause(): PauseSnapshot {
463
+ // No-op in production
464
+ if (typeof __DEV__ !== 'undefined' && !__DEV__) {
465
+ return { timestamp: 0, timestampISO: '', animations: [], totalCount: 0, summary: 'disabled in production' };
466
+ }
467
+
468
+ ensureInitialized();
469
+
470
+ if (globalThis.__RPP__!.paused) {
471
+ return globalThis.__RPP__!.snapshot!;
472
+ }
473
+
474
+ installFrameInterceptor();
475
+
476
+ // Capture snapshot before pausing
477
+ const snapshot = captureSnapshot();
478
+ globalThis.__RPP__!.snapshot = snapshot;
479
+ globalThis.__RPP__!.paused = true;
480
+ globalThis.__RPP__!.pausedAt = Date.now();
481
+
482
+ try {
483
+ const { runOnUI } = require('react-native-reanimated');
484
+
485
+ runOnUI(() => {
486
+ 'worklet';
487
+ globalThis.__RPP_PAUSED__ = true;
488
+ globalThis.__RPP_PAUSED_AT__ =
489
+ (globalThis as any)._getAnimationTimestamp?.() || Date.now();
490
+ })();
491
+ } catch (e) {
492
+ console.warn('[rpp] Failed to pause on UI thread:', e);
493
+ }
494
+
495
+ return snapshot;
496
+ }
497
+
498
+ /**
499
+ * Resume all animations
500
+ */
501
+ export function resume(): void {
502
+ ensureInitialized();
503
+
504
+ if (!globalThis.__RPP__!.paused) {
505
+ return;
506
+ }
507
+
508
+ globalThis.__RPP__!.paused = false;
509
+
510
+ try {
511
+ const { runOnUI } = require('react-native-reanimated');
512
+
513
+ runOnUI(() => {
514
+ 'worklet';
515
+
516
+ const pausedAt = globalThis.__RPP_PAUSED_AT__ || 0;
517
+ const now = (globalThis as any)._getAnimationTimestamp?.() || Date.now();
518
+ const pauseDuration = now - pausedAt;
519
+
520
+ globalThis.__RPP_ACCUMULATED_PAUSE__ =
521
+ (globalThis.__RPP_ACCUMULATED_PAUSE__ || 0) + pauseDuration;
522
+
523
+ globalThis.__RPP_PAUSED__ = false;
524
+ })();
525
+
526
+ flushPendingCallbacks();
527
+ } catch (e) {
528
+ console.warn('[rpp] Failed to resume:', e);
529
+ }
530
+
531
+ }
532
+
533
+ /**
534
+ * Toggle pause state
535
+ */
536
+ export function toggle(): PauseSnapshot | null {
537
+ if (isPaused()) {
538
+ resume();
539
+ return null;
540
+ } else {
541
+ return pause();
542
+ }
543
+ }
544
+
545
+ // Auto-initialize registry immediately so Babel-injected trackers work
546
+ ensureInitialized();
547
+
548
+ // Auto-install interceptor in dev mode (delayed to ensure Reanimated is ready)
549
+ if (typeof __DEV__ !== 'undefined' && __DEV__) {
550
+ setTimeout(() => {
551
+ installFrameInterceptor();
552
+ }, 100);
553
+ }
package/src/types.ts ADDED
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Animation snapshot captured at pause time
3
+ */
4
+ export interface AnimationSnapshot {
5
+ /** Unique ID for this animation instance */
6
+ id: string;
7
+
8
+ /** Animation type: 'timing' | 'spring' | 'decay' | 'repeat' | 'sequence' | 'delay' */
9
+ type: AnimationType;
10
+
11
+ /** Variable name of the animated SharedValue (e.g., "translateX") */
12
+ sharedValueName?: string;
13
+
14
+ /** Source location where the animation was created */
15
+ callsite: {
16
+ file: string;
17
+ line: number;
18
+ column?: number;
19
+ };
20
+
21
+ /** Animation configuration */
22
+ config: AnimationConfig;
23
+
24
+ /** Timing information */
25
+ timing: {
26
+ /** When the animation started (ms since epoch) */
27
+ startTime: number;
28
+ /** How long the animation has been running (ms) */
29
+ elapsed: number;
30
+ /** Expected duration (for timing animations) */
31
+ duration?: number;
32
+ /** Progress as percentage 0-100 (for timing animations) */
33
+ progress?: number;
34
+ };
35
+
36
+ /** Value information */
37
+ values: {
38
+ /** Starting value */
39
+ from: number;
40
+ /** Target value */
41
+ to: number;
42
+ /** Current value at pause */
43
+ current: number;
44
+ };
45
+
46
+ /** Human-readable description */
47
+ description: string;
48
+ }
49
+
50
+ export type AnimationType =
51
+ | 'timing'
52
+ | 'spring'
53
+ | 'decay'
54
+ | 'repeat'
55
+ | 'sequence'
56
+ | 'delay'
57
+ | 'unknown';
58
+
59
+ export interface TimingConfig {
60
+ duration: number;
61
+ easing?: string;
62
+ }
63
+
64
+ export interface SpringConfig {
65
+ damping?: number;
66
+ mass?: number;
67
+ stiffness?: number;
68
+ overshootClamping?: boolean;
69
+ restDisplacementThreshold?: number;
70
+ restSpeedThreshold?: number;
71
+ }
72
+
73
+ export interface DecayConfig {
74
+ velocity?: number;
75
+ deceleration?: number;
76
+ }
77
+
78
+ export interface RepeatConfig {
79
+ numberOfReps?: number;
80
+ reverse?: boolean;
81
+ }
82
+
83
+ export type AnimationConfig =
84
+ | { type: 'timing'; config: TimingConfig }
85
+ | { type: 'spring'; config: SpringConfig }
86
+ | { type: 'decay'; config: DecayConfig }
87
+ | { type: 'repeat'; config: RepeatConfig }
88
+ | { type: 'sequence'; config: { count: number } }
89
+ | { type: 'delay'; config: { delayMs: number } }
90
+ | { type: 'unknown'; config: Record<string, unknown> };
91
+
92
+ /**
93
+ * Full pause state snapshot
94
+ */
95
+ export interface PauseSnapshot {
96
+ /** When the snapshot was taken */
97
+ timestamp: number;
98
+
99
+ /** ISO timestamp string */
100
+ timestampISO: string;
101
+
102
+ /** All active animations at pause time */
103
+ animations: AnimationSnapshot[];
104
+
105
+ /** Total count of tracked animations */
106
+ totalCount: number;
107
+
108
+ /** Summary for quick reference */
109
+ summary: string;
110
+ }
111
+
112
+ /**
113
+ * Options for filtering snapshots
114
+ */
115
+ export interface SnapshotOptions {
116
+ /** Filter animations by file path (partial match) */
117
+ filterFile?: string;
118
+
119
+ /** Filter animations by line proximity (requires filterFile) */
120
+ filterLine?: number;
121
+
122
+ /** Max distance in lines from filterLine (default: 200) */
123
+ proximityThreshold?: number;
124
+
125
+ /** Max animations to return (default: 20) */
126
+ maxAnimations?: number;
127
+ }
128
+
129
+ /**
130
+ * SharedValue type from Reanimated (minimal interface for our needs)
131
+ */
132
+ export interface SharedValueRef {
133
+ value: number;
134
+ }
135
+
136
+ /**
137
+ * Registration data for tracking an animation
138
+ */
139
+ export interface AnimationRegistration {
140
+ id: string;
141
+ type: AnimationType;
142
+ callsite: {
143
+ file: string;
144
+ line: number;
145
+ column?: number;
146
+ };
147
+ config: Record<string, unknown>;
148
+ startTime: number;
149
+ fromValue: number;
150
+ toValue: number;
151
+ duration?: number;
152
+ /** Reference to the SharedValue being animated */
153
+ sharedValue?: SharedValueRef;
154
+ /** Variable name of the SharedValue (e.g., "translateX", "scale") */
155
+ sharedValueName?: string;
156
+ }