@idealyst/animate 1.2.58 → 1.2.59

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@idealyst/animate",
3
- "version": "1.2.58",
3
+ "version": "1.2.59",
4
4
  "description": "Cross-platform animation hooks for React and React Native",
5
5
  "readme": "README.md",
6
6
  "main": "src/index.ts",
@@ -30,7 +30,7 @@
30
30
  "publish:npm": "npm publish"
31
31
  },
32
32
  "peerDependencies": {
33
- "@idealyst/theme": "^1.2.58",
33
+ "@idealyst/theme": "^1.2.59",
34
34
  "react": ">=16.8.0",
35
35
  "react-native": ">=0.60.0",
36
36
  "react-native-reanimated": ">=3.0.0",
@@ -48,7 +48,7 @@
48
48
  }
49
49
  },
50
50
  "devDependencies": {
51
- "@idealyst/theme": "^1.2.58",
51
+ "@idealyst/theme": "^1.2.59",
52
52
  "@types/react": "^19.1.0",
53
53
  "react": "^19.1.0",
54
54
  "react-native": "^0.80.1",
@@ -41,6 +41,7 @@ export type {
41
41
  AnimatableStyle,
42
42
  AnimatableProperties,
43
43
  TransformProperty,
44
+ TransformObject,
44
45
  AnimationOptions,
45
46
  PlatformOverrides,
46
47
  UseAnimatedStyleOptions,
package/src/index.ts CHANGED
@@ -36,6 +36,7 @@ export type {
36
36
  AnimatableStyle,
37
37
  AnimatableProperties,
38
38
  TransformProperty,
39
+ TransformObject,
39
40
  AnimationOptions,
40
41
  PlatformOverrides,
41
42
  UseAnimatedStyleOptions,
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Transform normalization utilities.
3
+ *
4
+ * Converts the simplified TransformObject syntax to the React Native
5
+ * array format used internally by both web and native implementations.
6
+ */
7
+
8
+ import type { TransformObject, TransformProperty } from './types';
9
+
10
+ /**
11
+ * Type guard to detect if a transform is using the new object syntax
12
+ * vs the legacy array format.
13
+ */
14
+ export function isTransformObject(
15
+ transform: TransformObject | TransformProperty[] | undefined
16
+ ): transform is TransformObject {
17
+ return transform !== undefined && !Array.isArray(transform);
18
+ }
19
+
20
+ /**
21
+ * Normalizes a TransformObject to the React Native array format.
22
+ *
23
+ * @example
24
+ * normalizeTransform({ x: 10, y: 20, scale: 1.2, rotate: 45 })
25
+ * // Returns: [{ translateX: 10 }, { translateY: 20 }, { scale: 1.2 }, { rotate: '45deg' }]
26
+ */
27
+ export function normalizeTransform(transform: TransformObject): TransformProperty[] {
28
+ const result: TransformProperty[] = [];
29
+
30
+ // Perspective should come first for proper 3D transforms
31
+ if (transform.perspective !== undefined) {
32
+ result.push({ perspective: transform.perspective });
33
+ }
34
+
35
+ // Translation
36
+ if (transform.x !== undefined) {
37
+ result.push({ translateX: transform.x });
38
+ }
39
+ if (transform.y !== undefined) {
40
+ result.push({ translateY: transform.y });
41
+ }
42
+
43
+ // Scale
44
+ if (transform.scale !== undefined) {
45
+ result.push({ scale: transform.scale });
46
+ }
47
+ if (transform.scaleX !== undefined) {
48
+ result.push({ scaleX: transform.scaleX });
49
+ }
50
+ if (transform.scaleY !== undefined) {
51
+ result.push({ scaleY: transform.scaleY });
52
+ }
53
+
54
+ // Rotation - convert number to degrees string
55
+ if (transform.rotate !== undefined) {
56
+ const rotation =
57
+ typeof transform.rotate === 'number' ? `${transform.rotate}deg` : transform.rotate;
58
+ result.push({ rotate: rotation });
59
+ }
60
+ if (transform.rotateX !== undefined) {
61
+ result.push({ rotateX: transform.rotateX });
62
+ }
63
+ if (transform.rotateY !== undefined) {
64
+ result.push({ rotateY: transform.rotateY });
65
+ }
66
+ if (transform.rotateZ !== undefined) {
67
+ result.push({ rotateZ: transform.rotateZ });
68
+ }
69
+
70
+ // Skew
71
+ if (transform.skewX !== undefined) {
72
+ result.push({ skewX: transform.skewX });
73
+ }
74
+ if (transform.skewY !== undefined) {
75
+ result.push({ skewY: transform.skewY });
76
+ }
77
+
78
+ return result;
79
+ }
package/src/types.ts CHANGED
@@ -9,7 +9,7 @@ import type { Duration, EasingKey, SpringType } from '@idealyst/theme/animation'
9
9
  export type AnimatableStyle = ViewStyle | TextStyle | ImageStyle;
10
10
  export type StyleValue = string | number | undefined;
11
11
 
12
- // Transform types (React Native style)
12
+ // Transform types (React Native array style - legacy)
13
13
  export type TransformProperty =
14
14
  | { perspective: number }
15
15
  | { rotate: string }
@@ -24,6 +24,41 @@ export type TransformProperty =
24
24
  | { skewX: string }
25
25
  | { skewY: string };
26
26
 
27
+ /**
28
+ * Simplified transform object syntax.
29
+ * Cleaner alternative to the React Native array format.
30
+ *
31
+ * @example
32
+ * // Instead of: transform: [{ translateX: 10 }, { translateY: 20 }, { scale: 1.2 }]
33
+ * // Use: transform: { x: 10, y: 20, scale: 1.2 }
34
+ */
35
+ export interface TransformObject {
36
+ /** translateX - horizontal movement in pixels */
37
+ x?: number;
38
+ /** translateY - vertical movement in pixels */
39
+ y?: number;
40
+ /** Uniform scale (1 = normal, 2 = double, 0.5 = half) */
41
+ scale?: number;
42
+ /** Horizontal scale */
43
+ scaleX?: number;
44
+ /** Vertical scale */
45
+ scaleY?: number;
46
+ /** Rotation in degrees (number) or string ("45deg") */
47
+ rotate?: number | string;
48
+ /** X-axis rotation (string, e.g., "45deg") */
49
+ rotateX?: string;
50
+ /** Y-axis rotation (string, e.g., "45deg") */
51
+ rotateY?: string;
52
+ /** Z-axis rotation (string, e.g., "45deg") */
53
+ rotateZ?: string;
54
+ /** X-axis skew (string, e.g., "10deg") */
55
+ skewX?: string;
56
+ /** Y-axis skew (string, e.g., "10deg") */
57
+ skewY?: string;
58
+ /** Perspective for 3D transforms */
59
+ perspective?: number;
60
+ }
61
+
27
62
  // Animatable properties (subset that can be animated smoothly)
28
63
  export interface AnimatableProperties {
29
64
  // Opacity
@@ -55,8 +90,18 @@ export interface AnimatableProperties {
55
90
  bottom?: number | string;
56
91
  left?: number | string;
57
92
 
58
- // Transform (preferred for performance)
59
- transform?: TransformProperty[];
93
+ /**
94
+ * Transform - preferred for performance (GPU accelerated).
95
+ * Accepts either the new object syntax or legacy array format.
96
+ *
97
+ * @example
98
+ * // New object syntax (recommended)
99
+ * transform: { x: 10, y: 20, scale: 1.2, rotate: 45 }
100
+ *
101
+ * // Legacy array syntax (still supported)
102
+ * transform: [{ translateX: 10 }, { translateY: 20 }, { scale: 1.2 }]
103
+ */
104
+ transform?: TransformObject | TransformProperty[];
60
105
  }
61
106
 
62
107
  // Animation configuration
@@ -15,7 +15,13 @@ import {
15
15
  Easing,
16
16
  } from 'react-native-reanimated';
17
17
  import { timingConfig, springConfig, isSpringEasing } from '@idealyst/theme/animation';
18
- import type { AnimatableProperties, UseAnimatedStyleOptions, AnimatableStyle } from './types';
18
+ import type {
19
+ AnimatableProperties,
20
+ UseAnimatedStyleOptions,
21
+ AnimatableStyle,
22
+ TransformProperty,
23
+ } from './types';
24
+ import { isTransformObject, normalizeTransform } from './normalizeTransform';
19
25
 
20
26
  /**
21
27
  * Hook that returns an animated style object.
@@ -29,14 +35,21 @@ import type { AnimatableProperties, UseAnimatedStyleOptions, AnimatableStyle } f
29
35
  * ```tsx
30
36
  * import Animated from 'react-native-reanimated';
31
37
  *
38
+ * // New object syntax (recommended)
32
39
  * const style = useAnimatedStyle({
33
40
  * opacity: isVisible ? 1 : 0,
34
- * transform: [{ translateY: isVisible ? 0 : 20 }],
41
+ * transform: { y: isVisible ? 0 : 20 },
35
42
  * }, {
36
43
  * duration: 'normal',
37
44
  * easing: 'easeOut',
38
45
  * });
39
46
  *
47
+ * // Legacy array syntax (still supported)
48
+ * const legacyStyle = useAnimatedStyle({
49
+ * opacity: isVisible ? 1 : 0,
50
+ * transform: [{ translateY: isVisible ? 0 : 20 }],
51
+ * });
52
+ *
40
53
  * return <Animated.View style={style}>Content</Animated.View>;
41
54
  * ```
42
55
  */
@@ -53,6 +66,15 @@ export function useAnimatedStyle(
53
66
  const useSpring = native?.useSpring ?? isSpringEasing(finalEasing);
54
67
  const springType = native?.springType ?? (isSpringEasing(finalEasing) ? finalEasing : 'spring');
55
68
 
69
+ // Normalize transform to array format if using object syntax
70
+ const normalizedTransform = useMemo((): TransformProperty[] | undefined => {
71
+ if (!style.transform) return undefined;
72
+ if (isTransformObject(style.transform)) {
73
+ return normalizeTransform(style.transform);
74
+ }
75
+ return style.transform;
76
+ }, [style.transform]);
77
+
56
78
  // Create shared values for each animatable property
57
79
  const opacity = useSharedValue(style.opacity ?? 1);
58
80
  const backgroundColor = useSharedValue(style.backgroundColor ?? 'transparent');
@@ -60,6 +82,14 @@ export function useAnimatedStyle(
60
82
  const borderWidth = useSharedValue(style.borderWidth ?? 0);
61
83
  const borderRadius = useSharedValue(style.borderRadius ?? 0);
62
84
 
85
+ // Layout properties
86
+ const top = useSharedValue(style.top ?? 0);
87
+ const right = useSharedValue(style.right ?? 0);
88
+ const bottom = useSharedValue(style.bottom ?? 0);
89
+ const left = useSharedValue(style.left ?? 0);
90
+ const width = useSharedValue(style.width ?? 'auto');
91
+ const height = useSharedValue(style.height ?? 'auto');
92
+
63
93
  // Transform values
64
94
  const translateX = useSharedValue(0);
65
95
  const translateY = useSharedValue(0);
@@ -67,8 +97,14 @@ export function useAnimatedStyle(
67
97
  const scaleX = useSharedValue(1);
68
98
  const scaleY = useSharedValue(1);
69
99
  const rotate = useSharedValue('0deg');
100
+ const rotateX = useSharedValue('0deg');
101
+ const rotateY = useSharedValue('0deg');
102
+ const rotateZ = useSharedValue('0deg');
103
+ const skewX = useSharedValue('0deg');
104
+ const skewY = useSharedValue('0deg');
105
+ const perspective = useSharedValue(1000);
70
106
 
71
- // Extract transform values from style
107
+ // Extract transform values from normalized array
72
108
  const transforms = useMemo(() => {
73
109
  const result = {
74
110
  translateX: 0,
@@ -77,10 +113,16 @@ export function useAnimatedStyle(
77
113
  scaleX: 1,
78
114
  scaleY: 1,
79
115
  rotate: '0deg',
116
+ rotateX: '0deg',
117
+ rotateY: '0deg',
118
+ rotateZ: '0deg',
119
+ skewX: '0deg',
120
+ skewY: '0deg',
121
+ perspective: 1000,
80
122
  };
81
123
 
82
- if (style.transform) {
83
- style.transform.forEach((t) => {
124
+ if (normalizedTransform) {
125
+ normalizedTransform.forEach((t) => {
84
126
  const [key, value] = Object.entries(t)[0];
85
127
  if (key in result) {
86
128
  (result as any)[key] = value;
@@ -89,7 +131,7 @@ export function useAnimatedStyle(
89
131
  }
90
132
 
91
133
  return result;
92
- }, [style.transform]);
134
+ }, [normalizedTransform]);
93
135
 
94
136
  // Animate function
95
137
  const animate = (sharedValue: any, targetValue: any, isString = false) => {
@@ -113,6 +155,7 @@ export function useAnimatedStyle(
113
155
 
114
156
  // Update shared values when style changes
115
157
  useEffect(() => {
158
+ // Visual properties
116
159
  if (style.opacity !== undefined) {
117
160
  opacity.value = animate(opacity, style.opacity);
118
161
  }
@@ -129,6 +172,26 @@ export function useAnimatedStyle(
129
172
  borderRadius.value = animate(borderRadius, style.borderRadius);
130
173
  }
131
174
 
175
+ // Layout properties
176
+ if (style.top !== undefined) {
177
+ top.value = animate(top, style.top);
178
+ }
179
+ if (style.right !== undefined) {
180
+ right.value = animate(right, style.right);
181
+ }
182
+ if (style.bottom !== undefined) {
183
+ bottom.value = animate(bottom, style.bottom);
184
+ }
185
+ if (style.left !== undefined) {
186
+ left.value = animate(left, style.left);
187
+ }
188
+ if (style.width !== undefined) {
189
+ width.value = animate(width, style.width);
190
+ }
191
+ if (style.height !== undefined) {
192
+ height.value = animate(height, style.height);
193
+ }
194
+
132
195
  // Update transform values
133
196
  translateX.value = animate(translateX, transforms.translateX);
134
197
  translateY.value = animate(translateY, transforms.translateY);
@@ -136,32 +199,60 @@ export function useAnimatedStyle(
136
199
  scaleX.value = animate(scaleX, transforms.scaleX);
137
200
  scaleY.value = animate(scaleY, transforms.scaleY);
138
201
  rotate.value = animate(rotate, transforms.rotate, true);
202
+ rotateX.value = animate(rotateX, transforms.rotateX, true);
203
+ rotateY.value = animate(rotateY, transforms.rotateY, true);
204
+ rotateZ.value = animate(rotateZ, transforms.rotateZ, true);
205
+ skewX.value = animate(skewX, transforms.skewX, true);
206
+ skewY.value = animate(skewY, transforms.skewY, true);
207
+ perspective.value = animate(perspective, transforms.perspective);
139
208
  }, [
140
209
  style.opacity,
141
210
  style.backgroundColor,
142
211
  style.borderColor,
143
212
  style.borderWidth,
144
213
  style.borderRadius,
214
+ style.top,
215
+ style.right,
216
+ style.bottom,
217
+ style.left,
218
+ style.width,
219
+ style.height,
145
220
  transforms,
146
221
  ]);
147
222
 
148
- // Create animated style
223
+ // Create animated style - only include properties that were specified
149
224
  const animatedStyle = useReanimatedStyle(() => {
150
- return {
225
+ const result: Record<string, any> = {
151
226
  opacity: opacity.value,
152
227
  backgroundColor: backgroundColor.value,
153
228
  borderColor: borderColor.value,
154
229
  borderWidth: borderWidth.value,
155
230
  borderRadius: borderRadius.value,
156
231
  transform: [
232
+ { perspective: perspective.value },
157
233
  { translateX: translateX.value },
158
234
  { translateY: translateY.value },
159
235
  { scale: scale.value },
160
236
  { scaleX: scaleX.value },
161
237
  { scaleY: scaleY.value },
162
238
  { rotate: rotate.value },
239
+ { rotateX: rotateX.value },
240
+ { rotateY: rotateY.value },
241
+ { rotateZ: rotateZ.value },
242
+ { skewX: skewX.value },
243
+ { skewY: skewY.value },
163
244
  ],
164
245
  };
246
+
247
+ // Only include layout properties if they were specified (avoid overriding with defaults)
248
+ if (style.top !== undefined) result.top = top.value;
249
+ if (style.right !== undefined) result.right = right.value;
250
+ if (style.bottom !== undefined) result.bottom = bottom.value;
251
+ if (style.left !== undefined) result.left = left.value;
252
+ if (style.width !== undefined) result.width = width.value;
253
+ if (style.height !== undefined) result.height = height.value;
254
+
255
+ return result;
165
256
  });
166
257
 
167
258
  return animatedStyle as AnimatableStyle;
@@ -5,9 +5,15 @@
5
5
  * Uses CSS transitions for smooth, performant animations.
6
6
  */
7
7
 
8
- import { useMemo, useRef, useEffect } from 'react';
9
- import { cssTransition, resolveDuration, resolveEasing } from '@idealyst/theme/animation';
10
- import type { AnimatableProperties, UseAnimatedStyleOptions, AnimatableStyle } from './types';
8
+ import { useMemo } from 'react';
9
+ import { resolveDuration, resolveEasing } from '@idealyst/theme/animation';
10
+ import type {
11
+ AnimatableProperties,
12
+ UseAnimatedStyleOptions,
13
+ AnimatableStyle,
14
+ TransformProperty,
15
+ } from './types';
16
+ import { isTransformObject, normalizeTransform } from './normalizeTransform';
11
17
 
12
18
  /**
13
19
  * Hook that returns an animated style object.
@@ -19,14 +25,21 @@ import type { AnimatableProperties, UseAnimatedStyleOptions, AnimatableStyle } f
19
25
  *
20
26
  * @example
21
27
  * ```tsx
28
+ * // New object syntax (recommended)
22
29
  * const style = useAnimatedStyle({
23
30
  * opacity: isVisible ? 1 : 0,
24
- * transform: [{ translateY: isVisible ? 0 : 20 }],
31
+ * transform: { y: isVisible ? 0 : 20 },
25
32
  * }, {
26
33
  * duration: 'normal',
27
34
  * easing: 'easeOut',
28
35
  * });
29
36
  *
37
+ * // Legacy array syntax (still supported)
38
+ * const legacyStyle = useAnimatedStyle({
39
+ * opacity: isVisible ? 1 : 0,
40
+ * transform: [{ translateY: isVisible ? 0 : 20 }],
41
+ * });
42
+ *
30
43
  * return <div style={style}>Content</div>;
31
44
  * ```
32
45
  */
@@ -41,20 +54,22 @@ export function useAnimatedStyle(
41
54
  const finalEasing = web?.easing ?? easing;
42
55
  const finalDelay = web?.delay ?? delay;
43
56
 
44
- // Track if this is the initial render
45
- const isInitialRender = useRef(true);
46
-
47
- useEffect(() => {
48
- isInitialRender.current = false;
49
- }, []);
57
+ // Normalize transform to array format if using object syntax
58
+ const normalizedTransform = useMemo((): TransformProperty[] | undefined => {
59
+ if (!style.transform) return undefined;
60
+ if (isTransformObject(style.transform)) {
61
+ return normalizeTransform(style.transform);
62
+ }
63
+ return style.transform;
64
+ }, [style.transform]);
50
65
 
51
66
  // Convert transform array to CSS transform string
52
67
  const transformString = useMemo(() => {
53
- if (!style.transform || !Array.isArray(style.transform)) {
68
+ if (!normalizedTransform) {
54
69
  return undefined;
55
70
  }
56
71
 
57
- return style.transform
72
+ return normalizedTransform
58
73
  .map((t) => {
59
74
  const [key, value] = Object.entries(t)[0];
60
75
  // Handle different transform types
@@ -79,63 +94,23 @@ export function useAnimatedStyle(
79
94
  }
80
95
  })
81
96
  .join(' ');
82
- }, [style.transform]);
97
+ }, [normalizedTransform]);
83
98
 
84
- // Build the transition string
99
+ // Build the transition string - use 'all' for simplicity and to catch any property
85
100
  const transition = useMemo(() => {
86
101
  // Use custom transition if provided
87
102
  if (web?.transition) {
88
103
  return web.transition;
89
104
  }
90
105
 
91
- // Get all animatable property names from the style
92
- const properties = Object.keys(style).filter((key) => key !== 'transform');
93
- if (style.transform) {
94
- properties.push('transform');
95
- }
96
-
97
- if (properties.length === 0) {
98
- return undefined;
99
- }
100
-
101
- // Map RN property names to CSS property names
102
- const cssProperties = properties.map((prop) => {
103
- switch (prop) {
104
- case 'backgroundColor':
105
- return 'background-color';
106
- case 'borderColor':
107
- return 'border-color';
108
- case 'borderWidth':
109
- return 'border-width';
110
- case 'borderRadius':
111
- return 'border-radius';
112
- case 'borderTopLeftRadius':
113
- return 'border-top-left-radius';
114
- case 'borderTopRightRadius':
115
- return 'border-top-right-radius';
116
- case 'borderBottomLeftRadius':
117
- return 'border-bottom-left-radius';
118
- case 'borderBottomRightRadius':
119
- return 'border-bottom-right-radius';
120
- case 'maxHeight':
121
- return 'max-height';
122
- case 'maxWidth':
123
- return 'max-width';
124
- case 'minHeight':
125
- return 'min-height';
126
- case 'minWidth':
127
- return 'min-width';
128
- default:
129
- return prop;
130
- }
131
- });
132
-
133
106
  const durationMs = resolveDuration(finalDuration);
134
107
  const easingCss = resolveEasing(finalEasing);
135
108
  const delayStr = finalDelay > 0 ? ` ${finalDelay}ms` : '';
136
109
 
137
- return cssProperties.map((prop) => `${prop} ${durationMs}ms ${easingCss}${delayStr}`).join(', ');
138
- }, [style, finalDuration, finalEasing, finalDelay, web?.transition]);
110
+ // Use 'all' to animate any property that changes
111
+ // This is simpler and catches properties we might not explicitly list
112
+ return `all ${durationMs}ms ${easingCss}${delayStr}`;
113
+ }, [finalDuration, finalEasing, finalDelay, web?.transition]);
139
114
 
140
115
  // Build the final style object
141
116
  const animatedStyle = useMemo(() => {
@@ -153,8 +128,9 @@ export function useAnimatedStyle(
153
128
  result.transform = transformString;
154
129
  }
155
130
 
156
- // Add transition (skip on initial render to avoid animation on mount)
157
- if (transition && !isInitialRender.current) {
131
+ // Always include transition - CSS handles the timing correctly
132
+ // The transition property must be present BEFORE values change for animation to work
133
+ if (transition) {
158
134
  result.transition = transition;
159
135
  }
160
136
 
@@ -4,7 +4,7 @@
4
4
  * Manages mount/unmount animations using Reanimated.
5
5
  */
6
6
 
7
- import { useState, useEffect, useRef, useCallback } from 'react';
7
+ import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
8
8
  import {
9
9
  useSharedValue,
10
10
  useAnimatedStyle,
@@ -15,7 +15,14 @@ import {
15
15
  runOnJS,
16
16
  } from 'react-native-reanimated';
17
17
  import { timingConfig, springConfig, isSpringEasing, resolveDuration } from '@idealyst/theme/animation';
18
- import type { UsePresenceOptions, UsePresenceResult, AnimatableStyle } from './types';
18
+ import type {
19
+ UsePresenceOptions,
20
+ UsePresenceResult,
21
+ AnimatableStyle,
22
+ AnimatableProperties,
23
+ TransformProperty,
24
+ } from './types';
25
+ import { isTransformObject, normalizeTransform } from './normalizeTransform';
19
26
 
20
27
  /**
21
28
  * Hook that manages presence animations for mount/unmount.
@@ -57,23 +64,37 @@ export function usePresence(isVisible: boolean, options: UsePresenceOptions): Us
57
64
  const exitOpacity = exit.opacity ?? 0;
58
65
  const initialOpacity = initial?.opacity ?? exitOpacity;
59
66
 
60
- // Extract transform values
67
+ // Normalize transforms to array format
68
+ const normalizedEnterTransform = useMemo((): TransformProperty[] => {
69
+ if (!enter.transform) return [];
70
+ return isTransformObject(enter.transform)
71
+ ? normalizeTransform(enter.transform)
72
+ : enter.transform;
73
+ }, [enter.transform]);
74
+
75
+ const normalizedExitTransform = useMemo((): TransformProperty[] => {
76
+ if (!exit.transform) return [];
77
+ return isTransformObject(exit.transform)
78
+ ? normalizeTransform(exit.transform)
79
+ : exit.transform;
80
+ }, [exit.transform]);
81
+
82
+ // Extract transform values from normalized arrays
61
83
  const getTransformValue = (
62
- style: typeof enter | typeof exit,
84
+ transforms: TransformProperty[],
63
85
  key: string,
64
86
  defaultValue: number | string
65
87
  ) => {
66
- if (!style.transform) return defaultValue;
67
- const transform = style.transform.find((t) => key in t);
88
+ const transform = transforms.find((t) => key in t);
68
89
  return transform ? (transform as any)[key] : defaultValue;
69
90
  };
70
91
 
71
- const enterTranslateY = getTransformValue(enter, 'translateY', 0) as number;
72
- const exitTranslateY = getTransformValue(exit, 'translateY', 0) as number;
73
- const enterTranslateX = getTransformValue(enter, 'translateX', 0) as number;
74
- const exitTranslateX = getTransformValue(exit, 'translateX', 0) as number;
75
- const enterScale = getTransformValue(enter, 'scale', 1) as number;
76
- const exitScale = getTransformValue(exit, 'scale', 1) as number;
92
+ const enterTranslateY = getTransformValue(normalizedEnterTransform, 'translateY', 0) as number;
93
+ const exitTranslateY = getTransformValue(normalizedExitTransform, 'translateY', 0) as number;
94
+ const enterTranslateX = getTransformValue(normalizedEnterTransform, 'translateX', 0) as number;
95
+ const exitTranslateX = getTransformValue(normalizedExitTransform, 'translateX', 0) as number;
96
+ const enterScale = getTransformValue(normalizedEnterTransform, 'scale', 1) as number;
97
+ const exitScale = getTransformValue(normalizedExitTransform, 'scale', 1) as number;
77
98
 
78
99
  // Animation helper
79
100
  const animateTo = useCallback(
@@ -7,7 +7,14 @@
7
7
 
8
8
  import { useState, useEffect, useRef, useMemo, useCallback } from 'react';
9
9
  import { resolveDuration, resolveEasing } from '@idealyst/theme/animation';
10
- import type { UsePresenceOptions, UsePresenceResult, AnimatableStyle } from './types';
10
+ import type {
11
+ UsePresenceOptions,
12
+ UsePresenceResult,
13
+ AnimatableStyle,
14
+ AnimatableProperties,
15
+ TransformProperty,
16
+ } from './types';
17
+ import { isTransformObject, normalizeTransform } from './normalizeTransform';
11
18
 
12
19
  /**
13
20
  * Hook that manages presence animations for mount/unmount.
@@ -87,11 +94,18 @@ export function usePresence(isVisible: boolean, options: UsePresenceOptions): Us
87
94
  }, durationMs + delay);
88
95
  }, [durationMs, delay]);
89
96
 
90
- // Convert transform array to CSS transform string
91
- const transformToString = (transform: any[] | undefined): string | undefined => {
97
+ // Convert transform to CSS transform string (handles both object and array formats)
98
+ const transformToString = (
99
+ transform: AnimatableProperties['transform']
100
+ ): string | undefined => {
92
101
  if (!transform) return undefined;
93
102
 
94
- return transform
103
+ // Normalize if it's an object
104
+ const normalizedTransform: TransformProperty[] = isTransformObject(transform)
105
+ ? normalizeTransform(transform)
106
+ : transform;
107
+
108
+ return normalizedTransform
95
109
  .map((t) => {
96
110
  const [key, value] = Object.entries(t)[0];
97
111
  switch (key) {