react-native-glitter 1.0.7 → 1.0.8

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
@@ -17,10 +17,11 @@ Works with both **React Native CLI** and **Expo** projects - no native dependenc
17
17
 
18
18
  - 🚀 **Zero native dependencies** - Pure JavaScript/TypeScript implementation
19
19
  - 📱 **Cross-platform** - Works on iOS, Android, and Web
20
- - 🎨 **Customizable** - Control color, speed, angle, and more
20
+ - 🎨 **Customizable** - Control color, speed, angle, opacity, and more
21
21
  - ⚡ **Performant** - Uses native driver for smooth 60fps animations
22
22
  - 🔧 **TypeScript** - Full TypeScript support with type definitions
23
23
  - ✨ **Animation Modes** - Normal, expand, and shrink effects
24
+ - ♿ **Accessible** - Respects system "Reduce Motion" setting
24
25
 
25
26
  ## Installation
26
27
 
@@ -155,9 +156,11 @@ function ControlledGlitter() {
155
156
  | `children` | `ReactNode` | **required** | The content to apply the shimmer effect to |
156
157
  | `duration` | `number` | `1500` | Duration of one shimmer animation cycle in milliseconds |
157
158
  | `delay` | `number` | `400` | Delay between animation cycles in milliseconds |
159
+ | `initialDelay` | `number` | `0` | Initial delay before the first animation starts (useful for staggering) |
158
160
  | `color` | `string` | `'rgba(255, 255, 255, 0.8)'` | Color of the shimmer effect |
159
161
  | `angle` | `number` | `20` | Angle of the shimmer in degrees |
160
162
  | `shimmerWidth` | `number` | `60` | Width of the shimmer band in pixels |
163
+ | `opacity` | `number` | `1` | Overall opacity of the shimmer effect (0-1) |
161
164
  | `active` | `boolean` | `true` | Whether the animation is active |
162
165
  | `style` | `ViewStyle` | - | Additional styles for the container |
163
166
  | `easing` | `(value: number) => number` | - | Custom easing function for the animation |
@@ -167,10 +170,12 @@ function ControlledGlitter() {
167
170
  | `iterations` | `number` | `-1` | Number of animation cycles (-1 for infinite) |
168
171
  | `onAnimationStart` | `() => void` | - | Callback when animation starts |
169
172
  | `onAnimationComplete` | `() => void` | - | Callback when all iterations complete |
173
+ | `onIterationComplete` | `(iteration: number) => void` | - | Callback when each iteration completes |
170
174
  | `testID` | `string` | - | Test ID for e2e testing |
171
175
  | `accessibilityLabel` | `string` | - | Accessibility label for screen readers |
172
176
  | `accessible` | `boolean` | `true` | Whether the component is accessible |
173
177
  | `respectReduceMotion` | `boolean` | `true` | Whether to respect the system's "Reduce Motion" setting |
178
+ | `pauseOnBackground` | `boolean` | `true` | Whether to pause animation when app goes to background |
174
179
 
175
180
  ## Examples
176
181
 
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useRef, useState, useCallback, useMemo, memo, useImperativeHandle, forwardRef, } from 'react';
3
- import { View, Animated, StyleSheet, Easing, AccessibilityInfo, } from 'react-native';
3
+ import { View, Animated, StyleSheet, Easing, AccessibilityInfo, AppState, } from 'react-native';
4
4
  function generateGlitterOpacities(count, peak = 1) {
5
5
  const opacities = [];
6
6
  const center = (count - 1) / 2;
@@ -52,11 +52,26 @@ 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, respectReduceMotion = true, }, ref) {
55
+ function GlitterComponent({ children, duration = 1500, delay = 400, color = 'rgba(255, 255, 255, 0.8)', angle = 20, shimmerWidth = 60, opacity: shimmerOpacity = 1, active = true, style, easing, mode = 'normal', position = 'center', direction = 'left-to-right', iterations = -1, initialDelay = 0, onAnimationStart, onAnimationComplete, onIterationComplete, testID, accessibilityLabel, accessible = true, respectReduceMotion = true, pauseOnBackground = 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
59
  const [reduceMotionEnabled, setReduceMotionEnabled] = useState(false);
60
+ const [isAppActive, setIsAppActive] = useState(true);
61
+ // Detect app state changes (background/foreground)
62
+ useEffect(() => {
63
+ if (!pauseOnBackground) {
64
+ setIsAppActive(true);
65
+ return;
66
+ }
67
+ const handleAppStateChange = (nextAppState) => {
68
+ setIsAppActive(nextAppState === 'active');
69
+ };
70
+ const subscription = AppState.addEventListener('change', handleAppStateChange);
71
+ return () => {
72
+ subscription.remove();
73
+ };
74
+ }, [pauseOnBackground]);
60
75
  // Detect system reduce motion preference
61
76
  useEffect(() => {
62
77
  if (!respectReduceMotion) {
@@ -85,11 +100,16 @@ function GlitterComponent({ children, duration = 1500, delay = 400, color = 'rgb
85
100
  }, [respectReduceMotion]);
86
101
  const animationRef = useRef(null);
87
102
  const currentIterationRef = useRef(null);
103
+ const initialDelayRef = useRef(null);
88
104
  const iterationCount = useRef(0);
89
105
  const isAnimatingRef = useRef(false);
90
106
  const defaultEasing = useMemo(() => Easing.bezier(0.4, 0, 0.2, 1), []);
91
107
  const stopAnimation = useCallback(() => {
92
108
  isAnimatingRef.current = false;
109
+ if (initialDelayRef.current) {
110
+ clearTimeout(initialDelayRef.current);
111
+ initialDelayRef.current = null;
112
+ }
93
113
  animationRef.current?.stop();
94
114
  animationRef.current = null;
95
115
  currentIterationRef.current?.stop();
@@ -113,13 +133,12 @@ function GlitterComponent({ children, duration = 1500, delay = 400, color = 'rgb
113
133
  isAnimating: () => isAnimatingRef.current,
114
134
  }), [stopAnimation, restartAnimation, containerWidth]);
115
135
  const startAnimation = useCallback(() => {
116
- if (!active || containerWidth === 0 || reduceMotionEnabled)
136
+ if (!active || containerWidth === 0 || reduceMotionEnabled || !isAppActive)
117
137
  return;
118
138
  stopAnimation();
119
139
  animatedValue.setValue(0);
120
140
  iterationCount.current = 0;
121
141
  isAnimatingRef.current = true;
122
- onAnimationStart?.();
123
142
  const createSingleIteration = () => Animated.sequence([
124
143
  Animated.timing(animatedValue, {
125
144
  toValue: 1,
@@ -137,6 +156,7 @@ function GlitterComponent({ children, duration = 1500, delay = 400, color = 'rgb
137
156
  currentIterationRef.current.start(({ finished }) => {
138
157
  if (finished && isAnimatingRef.current) {
139
158
  iterationCount.current += 1;
159
+ onIterationComplete?.(iterationCount.current);
140
160
  if (iterations === -1 || iterationCount.current < iterations) {
141
161
  runIteration();
142
162
  }
@@ -147,26 +167,35 @@ function GlitterComponent({ children, duration = 1500, delay = 400, color = 'rgb
147
167
  }
148
168
  });
149
169
  };
150
- if (iterations === -1) {
151
- animationRef.current = Animated.loop(createSingleIteration());
152
- animationRef.current.start();
170
+ const beginAnimation = () => {
171
+ if (!isAnimatingRef.current)
172
+ return;
173
+ onAnimationStart?.();
174
+ runIteration();
175
+ };
176
+ // Apply initial delay before first animation
177
+ if (initialDelay > 0) {
178
+ initialDelayRef.current = setTimeout(beginAnimation, initialDelay);
153
179
  }
154
180
  else {
155
- runIteration();
181
+ beginAnimation();
156
182
  }
157
183
  }, [
158
184
  active,
159
185
  containerWidth,
160
186
  duration,
161
187
  delay,
188
+ initialDelay,
162
189
  animatedValue,
163
190
  easing,
164
191
  defaultEasing,
165
192
  iterations,
166
193
  onAnimationStart,
167
194
  onAnimationComplete,
195
+ onIterationComplete,
168
196
  stopAnimation,
169
197
  reduceMotionEnabled,
198
+ isAppActive,
170
199
  ]);
171
200
  useEffect(() => {
172
201
  startAnimation();
@@ -225,7 +254,10 @@ function GlitterComponent({ children, duration = 1500, delay = 400, color = 'rgb
225
254
  backgroundColor: color,
226
255
  baseOpacity: segment.opacity,
227
256
  })), [segments, lineHeight, color]);
228
- const shimmerContainerStyle = useMemo(() => [styles.shimmerContainer, { transform: [{ translateX }] }], [translateX]);
257
+ const shimmerContainerStyle = useMemo(() => [
258
+ styles.shimmerContainer,
259
+ { transform: [{ translateX }], opacity: shimmerOpacity },
260
+ ], [translateX, shimmerOpacity]);
229
261
  const rotationWrapperStyle = useMemo(() => [
230
262
  styles.rotationWrapper,
231
263
  {
@@ -253,6 +285,7 @@ function GlitterComponent({ children, duration = 1500, delay = 400, color = 'rgb
253
285
  ], [layerWidth, lineHeight, isAnimated, transformOriginOffset, scaleY]);
254
286
  return (_jsxs(View, { style: [styles.container, style], onLayout: onLayout, testID: testID, accessibilityLabel: accessibilityLabel, accessible: accessible, children: [children, active &&
255
287
  !reduceMotionEnabled &&
288
+ isAppActive &&
256
289
  containerWidth > 0 &&
257
290
  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: segmentStyles.map((segmentStyle, vIndex) => (_jsx(View, { style: [
258
291
  styles.segment,
@@ -51,6 +51,13 @@ export interface GlitterProps {
51
51
  * @default 400
52
52
  */
53
53
  delay?: number;
54
+ /**
55
+ * Initial delay before the first animation cycle starts in milliseconds.
56
+ * Useful for staggering multiple shimmer effects.
57
+ * @default 0
58
+ * @example 500 // Wait 500ms before starting the first animation
59
+ */
60
+ initialDelay?: number;
54
61
  /**
55
62
  * Color of the shimmer effect.
56
63
  * Supports any valid React Native color format (rgba, hex, rgb, named colors).
@@ -69,6 +76,13 @@ export interface GlitterProps {
69
76
  * @default 60
70
77
  */
71
78
  shimmerWidth?: number;
79
+ /**
80
+ * Overall opacity of the shimmer effect.
81
+ * Value between 0 and 1.
82
+ * @default 1
83
+ * @example 0.5 // 50% opacity shimmer
84
+ */
85
+ opacity?: number;
72
86
  /**
73
87
  * Whether the animation is active.
74
88
  * Set to false to pause the animation.
@@ -119,6 +133,13 @@ export interface GlitterProps {
119
133
  * Only called when iterations is a positive number.
120
134
  */
121
135
  onAnimationComplete?: () => void;
136
+ /**
137
+ * Callback fired when each iteration completes.
138
+ * Receives the current iteration number (1-indexed).
139
+ * @param iteration - The iteration number that just completed (starts at 1)
140
+ * @example (iteration) => console.log(`Iteration ${iteration} completed`)
141
+ */
142
+ onIterationComplete?: (iteration: number) => void;
122
143
  /**
123
144
  * Test ID for e2e testing frameworks like Detox.
124
145
  */
@@ -139,6 +160,12 @@ export interface GlitterProps {
139
160
  * @default true
140
161
  */
141
162
  respectReduceMotion?: boolean;
163
+ /**
164
+ * Whether to pause the animation when the app goes to background.
165
+ * Helps save battery by stopping unnecessary animations.
166
+ * @default true
167
+ */
168
+ pauseOnBackground?: boolean;
142
169
  }
143
170
  /**
144
171
  * Ref methods exposed by the Glitter component for programmatic control.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-glitter",
3
- "version": "1.0.7",
3
+ "version": "1.0.8",
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",
package/src/index.tsx CHANGED
@@ -17,9 +17,11 @@ import {
17
17
  StyleSheet,
18
18
  Easing,
19
19
  AccessibilityInfo,
20
+ AppState,
20
21
  type LayoutChangeEvent,
21
22
  type StyleProp,
22
23
  type ViewStyle,
24
+ type AppStateStatus,
23
25
  } from 'react-native';
24
26
 
25
27
  /**
@@ -79,6 +81,14 @@ export interface GlitterProps {
79
81
  */
80
82
  delay?: number;
81
83
 
84
+ /**
85
+ * Initial delay before the first animation cycle starts in milliseconds.
86
+ * Useful for staggering multiple shimmer effects.
87
+ * @default 0
88
+ * @example 500 // Wait 500ms before starting the first animation
89
+ */
90
+ initialDelay?: number;
91
+
82
92
  /**
83
93
  * Color of the shimmer effect.
84
94
  * Supports any valid React Native color format (rgba, hex, rgb, named colors).
@@ -100,6 +110,14 @@ export interface GlitterProps {
100
110
  */
101
111
  shimmerWidth?: number;
102
112
 
113
+ /**
114
+ * Overall opacity of the shimmer effect.
115
+ * Value between 0 and 1.
116
+ * @default 1
117
+ * @example 0.5 // 50% opacity shimmer
118
+ */
119
+ opacity?: number;
120
+
103
121
  /**
104
122
  * Whether the animation is active.
105
123
  * Set to false to pause the animation.
@@ -159,6 +177,14 @@ export interface GlitterProps {
159
177
  */
160
178
  onAnimationComplete?: () => void;
161
179
 
180
+ /**
181
+ * Callback fired when each iteration completes.
182
+ * Receives the current iteration number (1-indexed).
183
+ * @param iteration - The iteration number that just completed (starts at 1)
184
+ * @example (iteration) => console.log(`Iteration ${iteration} completed`)
185
+ */
186
+ onIterationComplete?: (iteration: number) => void;
187
+
162
188
  /**
163
189
  * Test ID for e2e testing frameworks like Detox.
164
190
  */
@@ -182,6 +208,13 @@ export interface GlitterProps {
182
208
  * @default true
183
209
  */
184
210
  respectReduceMotion?: boolean;
211
+
212
+ /**
213
+ * Whether to pause the animation when the app goes to background.
214
+ * Helps save battery by stopping unnecessary animations.
215
+ * @default true
216
+ */
217
+ pauseOnBackground?: boolean;
185
218
  }
186
219
 
187
220
  /**
@@ -302,6 +335,7 @@ function GlitterComponent(
302
335
  color = 'rgba(255, 255, 255, 0.8)',
303
336
  angle = 20,
304
337
  shimmerWidth = 60,
338
+ opacity: shimmerOpacity = 1,
305
339
  active = true,
306
340
  style,
307
341
  easing,
@@ -309,12 +343,15 @@ function GlitterComponent(
309
343
  position = 'center',
310
344
  direction = 'left-to-right',
311
345
  iterations = -1,
346
+ initialDelay = 0,
312
347
  onAnimationStart,
313
348
  onAnimationComplete,
349
+ onIterationComplete,
314
350
  testID,
315
351
  accessibilityLabel,
316
352
  accessible = true,
317
353
  respectReduceMotion = true,
354
+ pauseOnBackground = true,
318
355
  }: GlitterProps,
319
356
  ref: ForwardedRef<GlitterRef>
320
357
  ): ReactElement {
@@ -322,6 +359,28 @@ function GlitterComponent(
322
359
  const [containerWidth, setContainerWidth] = useState(0);
323
360
  const [containerHeight, setContainerHeight] = useState(0);
324
361
  const [reduceMotionEnabled, setReduceMotionEnabled] = useState(false);
362
+ const [isAppActive, setIsAppActive] = useState(true);
363
+
364
+ // Detect app state changes (background/foreground)
365
+ useEffect(() => {
366
+ if (!pauseOnBackground) {
367
+ setIsAppActive(true);
368
+ return;
369
+ }
370
+
371
+ const handleAppStateChange = (nextAppState: AppStateStatus) => {
372
+ setIsAppActive(nextAppState === 'active');
373
+ };
374
+
375
+ const subscription = AppState.addEventListener(
376
+ 'change',
377
+ handleAppStateChange
378
+ );
379
+
380
+ return () => {
381
+ subscription.remove();
382
+ };
383
+ }, [pauseOnBackground]);
325
384
 
326
385
  // Detect system reduce motion preference
327
386
  useEffect(() => {
@@ -360,6 +419,7 @@ function GlitterComponent(
360
419
  const currentIterationRef = useRef<ReturnType<
361
420
  typeof Animated.sequence
362
421
  > | null>(null);
422
+ const initialDelayRef = useRef<ReturnType<typeof setTimeout> | null>(null);
363
423
  const iterationCount = useRef(0);
364
424
  const isAnimatingRef = useRef(false);
365
425
 
@@ -367,6 +427,10 @@ function GlitterComponent(
367
427
 
368
428
  const stopAnimation = useCallback(() => {
369
429
  isAnimatingRef.current = false;
430
+ if (initialDelayRef.current) {
431
+ clearTimeout(initialDelayRef.current);
432
+ initialDelayRef.current = null;
433
+ }
370
434
  animationRef.current?.stop();
371
435
  animationRef.current = null;
372
436
  currentIterationRef.current?.stop();
@@ -397,15 +461,14 @@ function GlitterComponent(
397
461
  );
398
462
 
399
463
  const startAnimation = useCallback(() => {
400
- if (!active || containerWidth === 0 || reduceMotionEnabled) return;
464
+ if (!active || containerWidth === 0 || reduceMotionEnabled || !isAppActive)
465
+ return;
401
466
 
402
467
  stopAnimation();
403
468
  animatedValue.setValue(0);
404
469
  iterationCount.current = 0;
405
470
  isAnimatingRef.current = true;
406
471
 
407
- onAnimationStart?.();
408
-
409
472
  const createSingleIteration = () =>
410
473
  Animated.sequence([
411
474
  Animated.timing(animatedValue, {
@@ -426,6 +489,8 @@ function GlitterComponent(
426
489
  ({ finished }: { finished: boolean }) => {
427
490
  if (finished && isAnimatingRef.current) {
428
491
  iterationCount.current += 1;
492
+ onIterationComplete?.(iterationCount.current);
493
+
429
494
  if (iterations === -1 || iterationCount.current < iterations) {
430
495
  runIteration();
431
496
  } else {
@@ -437,25 +502,35 @@ function GlitterComponent(
437
502
  );
438
503
  };
439
504
 
440
- if (iterations === -1) {
441
- animationRef.current = Animated.loop(createSingleIteration());
442
- animationRef.current.start();
443
- } else {
505
+ const beginAnimation = () => {
506
+ if (!isAnimatingRef.current) return;
507
+
508
+ onAnimationStart?.();
444
509
  runIteration();
510
+ };
511
+
512
+ // Apply initial delay before first animation
513
+ if (initialDelay > 0) {
514
+ initialDelayRef.current = setTimeout(beginAnimation, initialDelay);
515
+ } else {
516
+ beginAnimation();
445
517
  }
446
518
  }, [
447
519
  active,
448
520
  containerWidth,
449
521
  duration,
450
522
  delay,
523
+ initialDelay,
451
524
  animatedValue,
452
525
  easing,
453
526
  defaultEasing,
454
527
  iterations,
455
528
  onAnimationStart,
456
529
  onAnimationComplete,
530
+ onIterationComplete,
457
531
  stopAnimation,
458
532
  reduceMotionEnabled,
533
+ isAppActive,
459
534
  ]);
460
535
 
461
536
  useEffect(() => {
@@ -559,8 +634,11 @@ function GlitterComponent(
559
634
  );
560
635
 
561
636
  const shimmerContainerStyle = useMemo(
562
- () => [styles.shimmerContainer, { transform: [{ translateX }] }],
563
- [translateX]
637
+ () => [
638
+ styles.shimmerContainer,
639
+ { transform: [{ translateX }], opacity: shimmerOpacity },
640
+ ],
641
+ [translateX, shimmerOpacity]
564
642
  );
565
643
 
566
644
  const rotationWrapperStyle = useMemo(
@@ -607,6 +685,7 @@ function GlitterComponent(
607
685
  {children}
608
686
  {active &&
609
687
  !reduceMotionEnabled &&
688
+ isAppActive &&
610
689
  containerWidth > 0 &&
611
690
  containerHeight > 0 && (
612
691
  <Animated.View style={shimmerContainerStyle} pointerEvents="none">