@idealyst/animate 1.2.29

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.
@@ -0,0 +1,151 @@
1
+ /**
2
+ * usePresence - Web implementation
3
+ *
4
+ * Manages mount/unmount animations by keeping elements in the DOM
5
+ * during exit animations.
6
+ */
7
+
8
+ import { useState, useEffect, useRef, useMemo, useCallback } from 'react';
9
+ import { resolveDuration, resolveEasing } from '@idealyst/theme/animation';
10
+ import type { UsePresenceOptions, UsePresenceResult, AnimatableStyle } from './types';
11
+
12
+ /**
13
+ * Hook that manages presence animations for mount/unmount.
14
+ * The element stays rendered during exit animation.
15
+ *
16
+ * @param isVisible - Whether the element should be visible
17
+ * @param options - Animation configuration with enter/exit styles
18
+ * @returns Object with isPresent, style, and exit function
19
+ *
20
+ * @example
21
+ * ```tsx
22
+ * const { isPresent, style } = usePresence(isOpen, {
23
+ * enter: { opacity: 1, transform: [{ translateY: 0 }] },
24
+ * exit: { opacity: 0, transform: [{ translateY: -20 }] },
25
+ * duration: 'normal',
26
+ * });
27
+ *
28
+ * return isPresent && <div style={style}>Modal content</div>;
29
+ * ```
30
+ */
31
+ export function usePresence(isVisible: boolean, options: UsePresenceOptions): UsePresenceResult {
32
+ const { enter, exit, initial, duration = 'normal', easing = 'easeOut', delay = 0 } = options;
33
+
34
+ // Track whether the element should be in the DOM
35
+ const [isPresent, setIsPresent] = useState(isVisible);
36
+ // Track whether we're animating in or out
37
+ const [isEntering, setIsEntering] = useState(false);
38
+ // Track initial mount
39
+ const isInitialMount = useRef(true);
40
+ // Timeout ref for cleanup
41
+ const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
42
+
43
+ const durationMs = resolveDuration(duration);
44
+
45
+ // Handle visibility changes
46
+ useEffect(() => {
47
+ // Clear any pending timeout
48
+ if (timeoutRef.current) {
49
+ clearTimeout(timeoutRef.current);
50
+ timeoutRef.current = null;
51
+ }
52
+
53
+ if (isVisible) {
54
+ // Entering: mount immediately, then animate in
55
+ setIsPresent(true);
56
+ // Use double RAF to ensure DOM is ready before animating
57
+ requestAnimationFrame(() => {
58
+ requestAnimationFrame(() => {
59
+ setIsEntering(true);
60
+ });
61
+ });
62
+ } else if (!isInitialMount.current) {
63
+ // Exiting: animate out, then unmount
64
+ setIsEntering(false);
65
+ timeoutRef.current = setTimeout(() => {
66
+ setIsPresent(false);
67
+ }, durationMs + delay);
68
+ }
69
+
70
+ isInitialMount.current = false;
71
+
72
+ return () => {
73
+ if (timeoutRef.current) {
74
+ clearTimeout(timeoutRef.current);
75
+ }
76
+ };
77
+ }, [isVisible, durationMs, delay]);
78
+
79
+ // Manual exit trigger
80
+ const triggerExit = useCallback(() => {
81
+ if (timeoutRef.current) {
82
+ clearTimeout(timeoutRef.current);
83
+ }
84
+ setIsEntering(false);
85
+ timeoutRef.current = setTimeout(() => {
86
+ setIsPresent(false);
87
+ }, durationMs + delay);
88
+ }, [durationMs, delay]);
89
+
90
+ // Convert transform array to CSS transform string
91
+ const transformToString = (transform: any[] | undefined): string | undefined => {
92
+ if (!transform) return undefined;
93
+
94
+ return transform
95
+ .map((t) => {
96
+ const [key, value] = Object.entries(t)[0];
97
+ switch (key) {
98
+ case 'translateX':
99
+ case 'translateY':
100
+ return `${key}(${typeof value === 'number' ? `${value}px` : value})`;
101
+ default:
102
+ return `${key}(${value})`;
103
+ }
104
+ })
105
+ .join(' ');
106
+ };
107
+
108
+ // Build the style object
109
+ const style = useMemo(() => {
110
+ const currentStyle = isEntering ? enter : (initial ?? exit);
111
+ const easingCss = resolveEasing(easing);
112
+
113
+ const result: Record<string, any> = {};
114
+
115
+ // Copy style properties
116
+ Object.entries(currentStyle).forEach(([key, value]) => {
117
+ if (key !== 'transform' && value !== undefined) {
118
+ result[key] = value;
119
+ }
120
+ });
121
+
122
+ // Handle transform
123
+ const transformStr = transformToString(currentStyle.transform);
124
+ if (transformStr) {
125
+ result.transform = transformStr;
126
+ }
127
+
128
+ // Add transition (always, since we want to animate both enter and exit)
129
+ const properties = Object.keys(currentStyle)
130
+ .filter((k) => k !== 'transform')
131
+ .map((k) => k.replace(/([A-Z])/g, '-$1').toLowerCase());
132
+ if (currentStyle.transform) {
133
+ properties.push('transform');
134
+ }
135
+
136
+ if (properties.length > 0) {
137
+ const delayStr = delay > 0 ? ` ${delay}ms` : '';
138
+ result.transition = properties
139
+ .map((prop) => `${prop} ${durationMs}ms ${easingCss}${delayStr}`)
140
+ .join(', ');
141
+ }
142
+
143
+ return result as AnimatableStyle;
144
+ }, [isEntering, enter, exit, initial, durationMs, easing, delay]);
145
+
146
+ return {
147
+ isPresent,
148
+ style,
149
+ exit: triggerExit,
150
+ };
151
+ }