react-native-glitter 1.0.4 → 1.0.6

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/README.md CHANGED
@@ -170,6 +170,7 @@ function ControlledGlitter() {
170
170
  | `testID` | `string` | - | Test ID for e2e testing |
171
171
  | `accessibilityLabel` | `string` | - | Accessibility label for screen readers |
172
172
  | `accessible` | `boolean` | `true` | Whether the component is accessible |
173
+ | `respectReduceMotion` | `boolean` | `true` | Whether to respect the system's "Reduce Motion" setting |
173
174
 
174
175
  ## Examples
175
176
 
@@ -264,6 +265,44 @@ function ControlledGlitter() {
264
265
  </Glitter>
265
266
  ```
266
267
 
268
+ ## Ref API
269
+
270
+ You can control the animation programmatically using a ref:
271
+
272
+ ```tsx
273
+ import { useRef } from 'react';
274
+ import { Glitter, type GlitterRef } from 'react-native-glitter';
275
+
276
+ function MyComponent() {
277
+ const glitterRef = useRef<GlitterRef>(null);
278
+
279
+ const handleStart = () => glitterRef.current?.start();
280
+ const handleStop = () => glitterRef.current?.stop();
281
+ const handleRestart = () => glitterRef.current?.restart();
282
+ const checkStatus = () => console.log(glitterRef.current?.isAnimating());
283
+
284
+ return (
285
+ <>
286
+ <Glitter ref={glitterRef} active={false}>
287
+ <View style={styles.box} />
288
+ </Glitter>
289
+ <Button title="Start" onPress={handleStart} />
290
+ <Button title="Stop" onPress={handleStop} />
291
+ <Button title="Restart" onPress={handleRestart} />
292
+ </>
293
+ );
294
+ }
295
+ ```
296
+
297
+ ### Ref Methods
298
+
299
+ | Method | Return | Description |
300
+ |--------|--------|-------------|
301
+ | `start()` | `void` | Start the shimmer animation |
302
+ | `stop()` | `void` | Stop the shimmer animation |
303
+ | `restart()` | `void` | Restart the animation from the beginning |
304
+ | `isAnimating()` | `boolean` | Check if animation is currently running |
305
+
267
306
  ## TypeScript
268
307
 
269
308
  This library is written in TypeScript and includes type definitions:
@@ -272,6 +311,7 @@ This library is written in TypeScript and includes type definitions:
272
311
  import {
273
312
  Glitter,
274
313
  type GlitterProps,
314
+ type GlitterRef,
275
315
  type GlitterMode,
276
316
  type GlitterPosition,
277
317
  type GlitterDirection,
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect, useRef, useState, useCallback, useMemo, memo, } from 'react';
3
- import { View, Animated, StyleSheet, Easing, } from 'react-native';
2
+ import { useEffect, useRef, useState, useCallback, useMemo, memo, useImperativeHandle, forwardRef, } from 'react';
3
+ import { View, Animated, StyleSheet, Easing, AccessibilityInfo, } from 'react-native';
4
4
  function generateGlitterOpacities(count, peak = 1) {
5
5
  const opacities = [];
6
6
  const center = (count - 1) / 2;
@@ -52,10 +52,37 @@ const HEIGHT_MULTIPLIER = 1.5;
52
52
  const NORMAL_FADE_RATIO = (HEIGHT_MULTIPLIER - 1) / HEIGHT_MULTIPLIER / 2;
53
53
  const ANIMATED_SEGMENTS = generateVerticalSegments(0.25);
54
54
  const NORMAL_SEGMENTS = generateVerticalSegments(NORMAL_FADE_RATIO);
55
- function GlitterComponent({ children, duration = 1500, delay = 400, color = 'rgba(255, 255, 255, 0.8)', angle = 20, shimmerWidth = 60, active = true, style, easing, mode = 'normal', position = 'center', direction = 'left-to-right', iterations = -1, onAnimationStart, onAnimationComplete, testID, accessibilityLabel, accessible = true, }) {
55
+ function GlitterComponent({ children, duration = 1500, delay = 400, color = 'rgba(255, 255, 255, 0.8)', angle = 20, shimmerWidth = 60, active = true, style, easing, mode = 'normal', position = 'center', direction = 'left-to-right', iterations = -1, onAnimationStart, onAnimationComplete, testID, accessibilityLabel, accessible = true, respectReduceMotion = true, }, ref) {
56
56
  const animatedValue = useRef(new Animated.Value(0)).current;
57
57
  const [containerWidth, setContainerWidth] = useState(0);
58
58
  const [containerHeight, setContainerHeight] = useState(0);
59
+ const [reduceMotionEnabled, setReduceMotionEnabled] = useState(false);
60
+ // Detect system reduce motion preference
61
+ useEffect(() => {
62
+ if (!respectReduceMotion) {
63
+ setReduceMotionEnabled(false);
64
+ return;
65
+ }
66
+ let isMounted = true;
67
+ AccessibilityInfo.isReduceMotionEnabled()
68
+ .then((enabled) => {
69
+ if (isMounted) {
70
+ setReduceMotionEnabled(enabled);
71
+ }
72
+ })
73
+ .catch(() => {
74
+ // Ignore errors (e.g., on web where this might not be supported)
75
+ });
76
+ const subscription = AccessibilityInfo.addEventListener('reduceMotionChanged', (enabled) => {
77
+ if (isMounted) {
78
+ setReduceMotionEnabled(enabled);
79
+ }
80
+ });
81
+ return () => {
82
+ isMounted = false;
83
+ subscription.remove();
84
+ };
85
+ }, [respectReduceMotion]);
59
86
  const animationRef = useRef(null);
60
87
  const currentIterationRef = useRef(null);
61
88
  const iterationCount = useRef(0);
@@ -68,8 +95,25 @@ function GlitterComponent({ children, duration = 1500, delay = 400, color = 'rgb
68
95
  currentIterationRef.current?.stop();
69
96
  currentIterationRef.current = null;
70
97
  }, []);
98
+ const restartAnimation = useCallback(() => {
99
+ stopAnimation();
100
+ animatedValue.setValue(0);
101
+ // Trigger re-render to start animation
102
+ setContainerWidth((prev) => prev);
103
+ }, [stopAnimation, animatedValue]);
104
+ useImperativeHandle(ref, () => ({
105
+ start: () => {
106
+ if (!isAnimatingRef.current && containerWidth > 0) {
107
+ // Force start by triggering the effect
108
+ setContainerWidth((prev) => prev);
109
+ }
110
+ },
111
+ stop: stopAnimation,
112
+ restart: restartAnimation,
113
+ isAnimating: () => isAnimatingRef.current,
114
+ }), [stopAnimation, restartAnimation, containerWidth]);
71
115
  const startAnimation = useCallback(() => {
72
- if (!active || containerWidth === 0)
116
+ if (!active || containerWidth === 0 || reduceMotionEnabled)
73
117
  return;
74
118
  stopAnimation();
75
119
  animatedValue.setValue(0);
@@ -122,6 +166,7 @@ function GlitterComponent({ children, duration = 1500, delay = 400, color = 'rgb
122
166
  onAnimationStart,
123
167
  onAnimationComplete,
124
168
  stopAnimation,
169
+ reduceMotionEnabled,
125
170
  ]);
126
171
  useEffect(() => {
127
172
  startAnimation();
@@ -198,7 +243,10 @@ function GlitterComponent({ children, duration = 1500, delay = 400, color = 'rgb
198
243
  : [],
199
244
  },
200
245
  ], [layerWidth, lineHeight, isAnimated, transformOriginOffset, scaleY]);
201
- return (_jsxs(View, { style: [styles.container, style], onLayout: onLayout, testID: testID, accessibilityLabel: accessibilityLabel, accessible: accessible, children: [children, active && containerWidth > 0 && containerHeight > 0 && (_jsx(Animated.View, { style: shimmerContainerStyle, pointerEvents: "none", children: _jsx(View, { style: rotationWrapperStyle, children: shimmerLayers.map((layer, layerIndex) => (_jsx(Animated.View, { style: getShimmerLineStyle(layer.position), children: segments.map((segment, vIndex) => (_jsx(View, { style: [
246
+ return (_jsxs(View, { style: [styles.container, style], onLayout: onLayout, testID: testID, accessibilityLabel: accessibilityLabel, accessible: accessible, children: [children, active &&
247
+ !reduceMotionEnabled &&
248
+ containerWidth > 0 &&
249
+ containerHeight > 0 && (_jsx(Animated.View, { style: shimmerContainerStyle, pointerEvents: "none", children: _jsx(View, { style: rotationWrapperStyle, children: shimmerLayers.map((layer, layerIndex) => (_jsx(Animated.View, { style: getShimmerLineStyle(layer.position), children: segments.map((segment, vIndex) => (_jsx(View, { style: [
202
250
  styles.segment,
203
251
  {
204
252
  height: lineHeight * segment.heightRatio,
@@ -231,5 +279,42 @@ const styles = StyleSheet.create({
231
279
  width: '100%',
232
280
  },
233
281
  });
234
- export const Glitter = memo(GlitterComponent);
282
+ const ForwardedGlitter = forwardRef(GlitterComponent);
283
+ /**
284
+ * A beautiful shimmer/glitter effect component for React Native.
285
+ * Wrap any component to add a sparkling diagonal shine animation.
286
+ *
287
+ * @example
288
+ * ```tsx
289
+ * // Basic usage
290
+ * <Glitter>
291
+ * <View style={styles.card}>
292
+ * <Text>This content will shimmer!</Text>
293
+ * </View>
294
+ * </Glitter>
295
+ *
296
+ * // With custom options
297
+ * <Glitter
298
+ * duration={2000}
299
+ * color="rgba(255, 215, 0, 0.5)"
300
+ * mode="expand"
301
+ * direction="right-to-left"
302
+ * >
303
+ * <View style={styles.premiumButton} />
304
+ * </Glitter>
305
+ *
306
+ * // With ref for programmatic control
307
+ * const glitterRef = useRef<GlitterRef>(null);
308
+ * <Glitter ref={glitterRef} active={false}>
309
+ * <View style={styles.box} />
310
+ * </Glitter>
311
+ * // Later: glitterRef.current?.start();
312
+ * ```
313
+ *
314
+ * @see {@link GlitterProps} for available props
315
+ * @see {@link GlitterRef} for ref methods
316
+ */
317
+ export const Glitter = memo(ForwardedGlitter);
318
+ // Set display name for React DevTools
319
+ Glitter.displayName = 'Glitter';
235
320
  export default Glitter;
@@ -1,31 +1,218 @@
1
- import { type ReactNode, type ReactElement } from 'react';
1
+ import { type ReactNode } from 'react';
2
2
  import { type StyleProp, type ViewStyle } from 'react-native';
3
+ /**
4
+ * Animation mode for the shimmer effect.
5
+ * - `normal`: Constant size shimmer line
6
+ * - `expand`: Shimmer line starts small and grows
7
+ * - `shrink`: Shimmer line starts full size and shrinks
8
+ */
3
9
  export type GlitterMode = 'normal' | 'expand' | 'shrink';
10
+ /**
11
+ * Position where the shimmer line shrinks to or expands from.
12
+ * Only applies when mode is 'expand' or 'shrink'.
13
+ * - `top`: Shrinks to/expands from the top
14
+ * - `center`: Shrinks to/expands from the center
15
+ * - `bottom`: Shrinks to/expands from the bottom
16
+ */
4
17
  export type GlitterPosition = 'top' | 'center' | 'bottom';
18
+ /**
19
+ * Direction of the shimmer animation movement.
20
+ * - `left-to-right`: Shimmer moves from left to right
21
+ * - `right-to-left`: Shimmer moves from right to left
22
+ */
5
23
  export type GlitterDirection = 'left-to-right' | 'right-to-left';
24
+ /**
25
+ * Props for the Glitter component.
26
+ *
27
+ * @example
28
+ * ```tsx
29
+ * <Glitter
30
+ * duration={1500}
31
+ * color="rgba(255, 255, 255, 0.8)"
32
+ * mode="expand"
33
+ * >
34
+ * <View style={styles.card} />
35
+ * </Glitter>
36
+ * ```
37
+ */
6
38
  export interface GlitterProps {
39
+ /**
40
+ * The content to apply the shimmer effect to.
41
+ * Can be any valid React node.
42
+ */
7
43
  children: ReactNode;
44
+ /**
45
+ * Duration of one shimmer animation cycle in milliseconds.
46
+ * @default 1500
47
+ */
8
48
  duration?: number;
49
+ /**
50
+ * Delay between animation cycles in milliseconds.
51
+ * @default 400
52
+ */
9
53
  delay?: number;
54
+ /**
55
+ * Color of the shimmer effect.
56
+ * Supports any valid React Native color format (rgba, hex, rgb, named colors).
57
+ * @default 'rgba(255, 255, 255, 0.8)'
58
+ * @example 'rgba(255, 215, 0, 0.5)' // Gold shimmer
59
+ */
10
60
  color?: string;
61
+ /**
62
+ * Angle of the shimmer in degrees.
63
+ * 0 = horizontal, 45 = diagonal.
64
+ * @default 20
65
+ */
11
66
  angle?: number;
67
+ /**
68
+ * Width of the shimmer band in pixels.
69
+ * @default 60
70
+ */
12
71
  shimmerWidth?: number;
72
+ /**
73
+ * Whether the animation is active.
74
+ * Set to false to pause the animation.
75
+ * @default true
76
+ */
13
77
  active?: boolean;
78
+ /**
79
+ * Additional styles for the container View.
80
+ */
14
81
  style?: StyleProp<ViewStyle>;
82
+ /**
83
+ * Custom easing function for the animation.
84
+ * If not provided, uses a smooth bezier curve (0.4, 0, 0.2, 1).
85
+ * @param value - Input value between 0 and 1
86
+ * @returns Output value between 0 and 1
87
+ * @example (value) => value * value // Ease in quad
88
+ */
15
89
  easing?: (value: number) => number;
90
+ /**
91
+ * Animation mode for the shimmer line.
92
+ * @default 'normal'
93
+ */
16
94
  mode?: GlitterMode;
95
+ /**
96
+ * Position where the line shrinks/expands.
97
+ * Only applies when mode is 'expand' or 'shrink'.
98
+ * @default 'center'
99
+ */
17
100
  position?: GlitterPosition;
101
+ /**
102
+ * Direction of the shimmer animation.
103
+ * @default 'left-to-right'
104
+ */
18
105
  direction?: GlitterDirection;
106
+ /**
107
+ * Number of animation cycles.
108
+ * Set to -1 for infinite loop.
109
+ * @default -1
110
+ */
19
111
  iterations?: number;
112
+ /**
113
+ * Callback fired when the animation starts.
114
+ * Called once at the beginning of the animation sequence.
115
+ */
20
116
  onAnimationStart?: () => void;
117
+ /**
118
+ * Callback fired when all iterations complete.
119
+ * Only called when iterations is a positive number.
120
+ */
21
121
  onAnimationComplete?: () => void;
22
- /** Test ID for e2e testing */
122
+ /**
123
+ * Test ID for e2e testing frameworks like Detox.
124
+ */
23
125
  testID?: string;
24
- /** Accessibility label for screen readers */
126
+ /**
127
+ * Accessibility label for screen readers.
128
+ * Describes the shimmer effect to visually impaired users.
129
+ */
25
130
  accessibilityLabel?: string;
26
- /** Whether the component is accessible (default: true) */
131
+ /**
132
+ * Whether the component is accessible.
133
+ * @default true
134
+ */
27
135
  accessible?: boolean;
136
+ /**
137
+ * Whether to respect the system's "Reduce Motion" accessibility setting.
138
+ * When enabled and the user has reduced motion enabled, the shimmer animation will be disabled.
139
+ * @default true
140
+ */
141
+ respectReduceMotion?: boolean;
28
142
  }
29
- declare function GlitterComponent({ children, duration, delay, color, angle, shimmerWidth, active, style, easing, mode, position, direction, iterations, onAnimationStart, onAnimationComplete, testID, accessibilityLabel, accessible, }: GlitterProps): ReactElement;
30
- export declare const Glitter: import("react").MemoExoticComponent<typeof GlitterComponent>;
143
+ /**
144
+ * Ref methods exposed by the Glitter component for programmatic control.
145
+ *
146
+ * @example
147
+ * ```tsx
148
+ * const glitterRef = useRef<GlitterRef>(null);
149
+ *
150
+ * // Control animation programmatically
151
+ * glitterRef.current?.start();
152
+ * glitterRef.current?.stop();
153
+ * glitterRef.current?.restart();
154
+ *
155
+ * // Check animation status
156
+ * if (glitterRef.current?.isAnimating()) {
157
+ * console.log('Animation is running');
158
+ * }
159
+ * ```
160
+ */
161
+ export interface GlitterRef {
162
+ /**
163
+ * Start the shimmer animation.
164
+ * Has no effect if already animating or container has no size.
165
+ */
166
+ start: () => void;
167
+ /**
168
+ * Stop the shimmer animation immediately.
169
+ * Cleans up all animation references.
170
+ */
171
+ stop: () => void;
172
+ /**
173
+ * Restart the shimmer animation from the beginning.
174
+ * Stops current animation and starts fresh.
175
+ */
176
+ restart: () => void;
177
+ /**
178
+ * Check if animation is currently running.
179
+ * @returns true if animation is active, false otherwise
180
+ */
181
+ isAnimating: () => boolean;
182
+ }
183
+ /**
184
+ * A beautiful shimmer/glitter effect component for React Native.
185
+ * Wrap any component to add a sparkling diagonal shine animation.
186
+ *
187
+ * @example
188
+ * ```tsx
189
+ * // Basic usage
190
+ * <Glitter>
191
+ * <View style={styles.card}>
192
+ * <Text>This content will shimmer!</Text>
193
+ * </View>
194
+ * </Glitter>
195
+ *
196
+ * // With custom options
197
+ * <Glitter
198
+ * duration={2000}
199
+ * color="rgba(255, 215, 0, 0.5)"
200
+ * mode="expand"
201
+ * direction="right-to-left"
202
+ * >
203
+ * <View style={styles.premiumButton} />
204
+ * </Glitter>
205
+ *
206
+ * // With ref for programmatic control
207
+ * const glitterRef = useRef<GlitterRef>(null);
208
+ * <Glitter ref={glitterRef} active={false}>
209
+ * <View style={styles.box} />
210
+ * </Glitter>
211
+ * // Later: glitterRef.current?.start();
212
+ * ```
213
+ *
214
+ * @see {@link GlitterProps} for available props
215
+ * @see {@link GlitterRef} for ref methods
216
+ */
217
+ export declare const Glitter: import("react").NamedExoticComponent<GlitterProps & import("react").RefAttributes<GlitterRef>>;
31
218
  export default Glitter;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-glitter",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "A beautiful shimmer/glitter effect component for React Native. Add a sparkling diagonal shine animation to any component!",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
@@ -15,17 +15,6 @@
15
15
  "files": [
16
16
  "src",
17
17
  "lib",
18
- "android",
19
- "ios",
20
- "cpp",
21
- "*.podspec",
22
- "react-native.config.js",
23
- "!ios/build",
24
- "!android/build",
25
- "!android/gradle",
26
- "!android/gradlew",
27
- "!android/gradlew.bat",
28
- "!android/local.properties",
29
18
  "!**/__tests__",
30
19
  "!**/__fixtures__",
31
20
  "!**/__mocks__",
package/src/index.tsx CHANGED
@@ -5,47 +5,227 @@ import {
5
5
  useCallback,
6
6
  useMemo,
7
7
  memo,
8
+ useImperativeHandle,
9
+ forwardRef,
8
10
  type ReactNode,
9
11
  type ReactElement,
12
+ type ForwardedRef,
10
13
  } from 'react';
11
14
  import {
12
15
  View,
13
16
  Animated,
14
17
  StyleSheet,
15
18
  Easing,
19
+ AccessibilityInfo,
16
20
  type LayoutChangeEvent,
17
21
  type StyleProp,
18
22
  type ViewStyle,
19
23
  } from 'react-native';
20
24
 
25
+ /**
26
+ * Animation mode for the shimmer effect.
27
+ * - `normal`: Constant size shimmer line
28
+ * - `expand`: Shimmer line starts small and grows
29
+ * - `shrink`: Shimmer line starts full size and shrinks
30
+ */
21
31
  export type GlitterMode = 'normal' | 'expand' | 'shrink';
22
32
 
33
+ /**
34
+ * Position where the shimmer line shrinks to or expands from.
35
+ * Only applies when mode is 'expand' or 'shrink'.
36
+ * - `top`: Shrinks to/expands from the top
37
+ * - `center`: Shrinks to/expands from the center
38
+ * - `bottom`: Shrinks to/expands from the bottom
39
+ */
23
40
  export type GlitterPosition = 'top' | 'center' | 'bottom';
24
41
 
42
+ /**
43
+ * Direction of the shimmer animation movement.
44
+ * - `left-to-right`: Shimmer moves from left to right
45
+ * - `right-to-left`: Shimmer moves from right to left
46
+ */
25
47
  export type GlitterDirection = 'left-to-right' | 'right-to-left';
26
48
 
49
+ /**
50
+ * Props for the Glitter component.
51
+ *
52
+ * @example
53
+ * ```tsx
54
+ * <Glitter
55
+ * duration={1500}
56
+ * color="rgba(255, 255, 255, 0.8)"
57
+ * mode="expand"
58
+ * >
59
+ * <View style={styles.card} />
60
+ * </Glitter>
61
+ * ```
62
+ */
27
63
  export interface GlitterProps {
64
+ /**
65
+ * The content to apply the shimmer effect to.
66
+ * Can be any valid React node.
67
+ */
28
68
  children: ReactNode;
69
+
70
+ /**
71
+ * Duration of one shimmer animation cycle in milliseconds.
72
+ * @default 1500
73
+ */
29
74
  duration?: number;
75
+
76
+ /**
77
+ * Delay between animation cycles in milliseconds.
78
+ * @default 400
79
+ */
30
80
  delay?: number;
81
+
82
+ /**
83
+ * Color of the shimmer effect.
84
+ * Supports any valid React Native color format (rgba, hex, rgb, named colors).
85
+ * @default 'rgba(255, 255, 255, 0.8)'
86
+ * @example 'rgba(255, 215, 0, 0.5)' // Gold shimmer
87
+ */
31
88
  color?: string;
89
+
90
+ /**
91
+ * Angle of the shimmer in degrees.
92
+ * 0 = horizontal, 45 = diagonal.
93
+ * @default 20
94
+ */
32
95
  angle?: number;
96
+
97
+ /**
98
+ * Width of the shimmer band in pixels.
99
+ * @default 60
100
+ */
33
101
  shimmerWidth?: number;
102
+
103
+ /**
104
+ * Whether the animation is active.
105
+ * Set to false to pause the animation.
106
+ * @default true
107
+ */
34
108
  active?: boolean;
109
+
110
+ /**
111
+ * Additional styles for the container View.
112
+ */
35
113
  style?: StyleProp<ViewStyle>;
114
+
115
+ /**
116
+ * Custom easing function for the animation.
117
+ * If not provided, uses a smooth bezier curve (0.4, 0, 0.2, 1).
118
+ * @param value - Input value between 0 and 1
119
+ * @returns Output value between 0 and 1
120
+ * @example (value) => value * value // Ease in quad
121
+ */
36
122
  easing?: (value: number) => number;
123
+
124
+ /**
125
+ * Animation mode for the shimmer line.
126
+ * @default 'normal'
127
+ */
37
128
  mode?: GlitterMode;
129
+
130
+ /**
131
+ * Position where the line shrinks/expands.
132
+ * Only applies when mode is 'expand' or 'shrink'.
133
+ * @default 'center'
134
+ */
38
135
  position?: GlitterPosition;
136
+
137
+ /**
138
+ * Direction of the shimmer animation.
139
+ * @default 'left-to-right'
140
+ */
39
141
  direction?: GlitterDirection;
142
+
143
+ /**
144
+ * Number of animation cycles.
145
+ * Set to -1 for infinite loop.
146
+ * @default -1
147
+ */
40
148
  iterations?: number;
149
+
150
+ /**
151
+ * Callback fired when the animation starts.
152
+ * Called once at the beginning of the animation sequence.
153
+ */
41
154
  onAnimationStart?: () => void;
155
+
156
+ /**
157
+ * Callback fired when all iterations complete.
158
+ * Only called when iterations is a positive number.
159
+ */
42
160
  onAnimationComplete?: () => void;
43
- /** Test ID for e2e testing */
161
+
162
+ /**
163
+ * Test ID for e2e testing frameworks like Detox.
164
+ */
44
165
  testID?: string;
45
- /** Accessibility label for screen readers */
166
+
167
+ /**
168
+ * Accessibility label for screen readers.
169
+ * Describes the shimmer effect to visually impaired users.
170
+ */
46
171
  accessibilityLabel?: string;
47
- /** Whether the component is accessible (default: true) */
172
+
173
+ /**
174
+ * Whether the component is accessible.
175
+ * @default true
176
+ */
48
177
  accessible?: boolean;
178
+
179
+ /**
180
+ * Whether to respect the system's "Reduce Motion" accessibility setting.
181
+ * When enabled and the user has reduced motion enabled, the shimmer animation will be disabled.
182
+ * @default true
183
+ */
184
+ respectReduceMotion?: boolean;
185
+ }
186
+
187
+ /**
188
+ * Ref methods exposed by the Glitter component for programmatic control.
189
+ *
190
+ * @example
191
+ * ```tsx
192
+ * const glitterRef = useRef<GlitterRef>(null);
193
+ *
194
+ * // Control animation programmatically
195
+ * glitterRef.current?.start();
196
+ * glitterRef.current?.stop();
197
+ * glitterRef.current?.restart();
198
+ *
199
+ * // Check animation status
200
+ * if (glitterRef.current?.isAnimating()) {
201
+ * console.log('Animation is running');
202
+ * }
203
+ * ```
204
+ */
205
+ export interface GlitterRef {
206
+ /**
207
+ * Start the shimmer animation.
208
+ * Has no effect if already animating or container has no size.
209
+ */
210
+ start: () => void;
211
+
212
+ /**
213
+ * Stop the shimmer animation immediately.
214
+ * Cleans up all animation references.
215
+ */
216
+ stop: () => void;
217
+
218
+ /**
219
+ * Restart the shimmer animation from the beginning.
220
+ * Stops current animation and starts fresh.
221
+ */
222
+ restart: () => void;
223
+
224
+ /**
225
+ * Check if animation is currently running.
226
+ * @returns true if animation is active, false otherwise
227
+ */
228
+ isAnimating: () => boolean;
49
229
  }
50
230
 
51
231
  function generateGlitterOpacities(count: number, peak: number = 1): number[] {
@@ -114,29 +294,68 @@ const NORMAL_FADE_RATIO = (HEIGHT_MULTIPLIER - 1) / HEIGHT_MULTIPLIER / 2;
114
294
  const ANIMATED_SEGMENTS = generateVerticalSegments(0.25);
115
295
  const NORMAL_SEGMENTS = generateVerticalSegments(NORMAL_FADE_RATIO);
116
296
 
117
- function GlitterComponent({
118
- children,
119
- duration = 1500,
120
- delay = 400,
121
- color = 'rgba(255, 255, 255, 0.8)',
122
- angle = 20,
123
- shimmerWidth = 60,
124
- active = true,
125
- style,
126
- easing,
127
- mode = 'normal',
128
- position = 'center',
129
- direction = 'left-to-right',
130
- iterations = -1,
131
- onAnimationStart,
132
- onAnimationComplete,
133
- testID,
134
- accessibilityLabel,
135
- accessible = true,
136
- }: GlitterProps): ReactElement {
297
+ function GlitterComponent(
298
+ {
299
+ children,
300
+ duration = 1500,
301
+ delay = 400,
302
+ color = 'rgba(255, 255, 255, 0.8)',
303
+ angle = 20,
304
+ shimmerWidth = 60,
305
+ active = true,
306
+ style,
307
+ easing,
308
+ mode = 'normal',
309
+ position = 'center',
310
+ direction = 'left-to-right',
311
+ iterations = -1,
312
+ onAnimationStart,
313
+ onAnimationComplete,
314
+ testID,
315
+ accessibilityLabel,
316
+ accessible = true,
317
+ respectReduceMotion = true,
318
+ }: GlitterProps,
319
+ ref: ForwardedRef<GlitterRef>
320
+ ): ReactElement {
137
321
  const animatedValue = useRef(new Animated.Value(0)).current;
138
322
  const [containerWidth, setContainerWidth] = useState(0);
139
323
  const [containerHeight, setContainerHeight] = useState(0);
324
+ const [reduceMotionEnabled, setReduceMotionEnabled] = useState(false);
325
+
326
+ // Detect system reduce motion preference
327
+ useEffect(() => {
328
+ if (!respectReduceMotion) {
329
+ setReduceMotionEnabled(false);
330
+ return;
331
+ }
332
+
333
+ let isMounted = true;
334
+
335
+ AccessibilityInfo.isReduceMotionEnabled()
336
+ .then((enabled) => {
337
+ if (isMounted) {
338
+ setReduceMotionEnabled(enabled);
339
+ }
340
+ })
341
+ .catch(() => {
342
+ // Ignore errors (e.g., on web where this might not be supported)
343
+ });
344
+
345
+ const subscription = AccessibilityInfo.addEventListener(
346
+ 'reduceMotionChanged',
347
+ (enabled) => {
348
+ if (isMounted) {
349
+ setReduceMotionEnabled(enabled);
350
+ }
351
+ }
352
+ );
353
+
354
+ return () => {
355
+ isMounted = false;
356
+ subscription.remove();
357
+ };
358
+ }, [respectReduceMotion]);
140
359
  const animationRef = useRef<ReturnType<typeof Animated.loop> | null>(null);
141
360
  const currentIterationRef = useRef<ReturnType<
142
361
  typeof Animated.sequence
@@ -154,8 +373,31 @@ function GlitterComponent({
154
373
  currentIterationRef.current = null;
155
374
  }, []);
156
375
 
376
+ const restartAnimation = useCallback(() => {
377
+ stopAnimation();
378
+ animatedValue.setValue(0);
379
+ // Trigger re-render to start animation
380
+ setContainerWidth((prev) => prev);
381
+ }, [stopAnimation, animatedValue]);
382
+
383
+ useImperativeHandle(
384
+ ref,
385
+ () => ({
386
+ start: () => {
387
+ if (!isAnimatingRef.current && containerWidth > 0) {
388
+ // Force start by triggering the effect
389
+ setContainerWidth((prev) => prev);
390
+ }
391
+ },
392
+ stop: stopAnimation,
393
+ restart: restartAnimation,
394
+ isAnimating: () => isAnimatingRef.current,
395
+ }),
396
+ [stopAnimation, restartAnimation, containerWidth]
397
+ );
398
+
157
399
  const startAnimation = useCallback(() => {
158
- if (!active || containerWidth === 0) return;
400
+ if (!active || containerWidth === 0 || reduceMotionEnabled) return;
159
401
 
160
402
  stopAnimation();
161
403
  animatedValue.setValue(0);
@@ -213,6 +455,7 @@ function GlitterComponent({
213
455
  onAnimationStart,
214
456
  onAnimationComplete,
215
457
  stopAnimation,
458
+ reduceMotionEnabled,
216
459
  ]);
217
460
 
218
461
  useEffect(() => {
@@ -349,32 +592,35 @@ function GlitterComponent({
349
592
  accessible={accessible}
350
593
  >
351
594
  {children}
352
- {active && containerWidth > 0 && containerHeight > 0 && (
353
- <Animated.View style={shimmerContainerStyle} pointerEvents="none">
354
- <View style={rotationWrapperStyle}>
355
- {shimmerLayers.map((layer, layerIndex) => (
356
- <Animated.View
357
- key={layerIndex}
358
- style={getShimmerLineStyle(layer.position)}
359
- >
360
- {segments.map((segment, vIndex) => (
361
- <View
362
- key={vIndex}
363
- style={[
364
- styles.segment,
365
- {
366
- height: lineHeight * segment.heightRatio,
367
- backgroundColor: color,
368
- opacity: layer.opacity * segment.opacity,
369
- },
370
- ]}
371
- />
372
- ))}
373
- </Animated.View>
374
- ))}
375
- </View>
376
- </Animated.View>
377
- )}
595
+ {active &&
596
+ !reduceMotionEnabled &&
597
+ containerWidth > 0 &&
598
+ containerHeight > 0 && (
599
+ <Animated.View style={shimmerContainerStyle} pointerEvents="none">
600
+ <View style={rotationWrapperStyle}>
601
+ {shimmerLayers.map((layer, layerIndex) => (
602
+ <Animated.View
603
+ key={layerIndex}
604
+ style={getShimmerLineStyle(layer.position)}
605
+ >
606
+ {segments.map((segment, vIndex) => (
607
+ <View
608
+ key={vIndex}
609
+ style={[
610
+ styles.segment,
611
+ {
612
+ height: lineHeight * segment.heightRatio,
613
+ backgroundColor: color,
614
+ opacity: layer.opacity * segment.opacity,
615
+ },
616
+ ]}
617
+ />
618
+ ))}
619
+ </Animated.View>
620
+ ))}
621
+ </View>
622
+ </Animated.View>
623
+ )}
378
624
  </View>
379
625
  );
380
626
  }
@@ -404,6 +650,45 @@ const styles = StyleSheet.create({
404
650
  },
405
651
  });
406
652
 
407
- export const Glitter = memo(GlitterComponent);
653
+ const ForwardedGlitter = forwardRef(GlitterComponent);
654
+
655
+ /**
656
+ * A beautiful shimmer/glitter effect component for React Native.
657
+ * Wrap any component to add a sparkling diagonal shine animation.
658
+ *
659
+ * @example
660
+ * ```tsx
661
+ * // Basic usage
662
+ * <Glitter>
663
+ * <View style={styles.card}>
664
+ * <Text>This content will shimmer!</Text>
665
+ * </View>
666
+ * </Glitter>
667
+ *
668
+ * // With custom options
669
+ * <Glitter
670
+ * duration={2000}
671
+ * color="rgba(255, 215, 0, 0.5)"
672
+ * mode="expand"
673
+ * direction="right-to-left"
674
+ * >
675
+ * <View style={styles.premiumButton} />
676
+ * </Glitter>
677
+ *
678
+ * // With ref for programmatic control
679
+ * const glitterRef = useRef<GlitterRef>(null);
680
+ * <Glitter ref={glitterRef} active={false}>
681
+ * <View style={styles.box} />
682
+ * </Glitter>
683
+ * // Later: glitterRef.current?.start();
684
+ * ```
685
+ *
686
+ * @see {@link GlitterProps} for available props
687
+ * @see {@link GlitterRef} for ref methods
688
+ */
689
+ export const Glitter = memo(ForwardedGlitter);
690
+
691
+ // Set display name for React DevTools
692
+ Glitter.displayName = 'Glitter';
408
693
 
409
694
  export default Glitter;