rn-confetti-love 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,232 @@
1
+ # 🎊 rn-confetti-love
2
+
3
+ Beautiful animated confetti effects for React Native. While designed with love-themed apps in mind, it is fully customizable for any celebration!
4
+
5
+ ![npm](https://img.shields.io/npm/v/rn-confetti-love)
6
+ ![license](https://img.shields.io/npm/l/rn-confetti-love)
7
+
8
+ ## ✨ Features
9
+
10
+ - 🎨 **Full Customization**: Control colors, emojis, texts, images, and shapes
11
+ - 💖 **Love Presets**: Built-in specialized presets for romantic effects
12
+ - ⬆️ **Direction Control**: Top-to-bottom (fall) or bottom-to-top (rise) animations
13
+ - 🎭 **Custom Content**: Mix emojis, text, avatars, and custom views
14
+ - ⚡ **Performant**: Built with React Native Reanimated for 60fps animations
15
+ - 📱 **Responsive**: Auto-adjusts to screen dimensions
16
+
17
+ ## 📦 Installation
18
+
19
+ ```bash
20
+ npm install rn-confetti-love
21
+ # or
22
+ yarn add rn-confetti-love
23
+ ```
24
+
25
+ ### Peer Dependencies
26
+
27
+ Make sure you have the following packages installed:
28
+
29
+ ```bash
30
+ npm install react-native-reanimated expo-image
31
+ ```
32
+
33
+ > **Note**: `expo-image` is optional and only required if you want to display avatar images/custom images in the confetti.
34
+
35
+ ## 🚀 Usage
36
+
37
+ ### `LoveConfetti` Component
38
+
39
+ A simplified wrapper component that uses the default love-themed configuration.
40
+
41
+ ```tsx
42
+ import { LoveConfetti } from 'rn-confetti-love';
43
+
44
+ <LoveConfetti
45
+ isVisible={showConfetti}
46
+ onComplete={() => setShowConfetti(false)}
47
+ />
48
+ ```
49
+
50
+ > **Note**: `LoveConfetti` is primarily for backward compatibility. For new implementations, we recommend using the main `Confetti` component.
51
+
52
+ ### `Confetti` Component (Customizable)
53
+
54
+ For full control, use the `Confetti` component with the `config` prop.
55
+
56
+ ```tsx
57
+ import { Confetti } from 'rn-confetti-love';
58
+
59
+ <Confetti
60
+ isVisible={true}
61
+ config={{
62
+ emojis: ['🎉', '🥳'],
63
+ colors: ['#FFD700', '#FF0000']
64
+ }}
65
+ onComplete={onDone}
66
+ />
67
+ ```
68
+
69
+ ## 🛠️ Advanced Customization
70
+
71
+ Pass a `config` object to customize every aspect of the confetti.
72
+
73
+ ### Custom Emojis & Colors
74
+
75
+ Create a birthday or celebration theme:
76
+
77
+ ```tsx
78
+ <Confetti
79
+ isVisible={true}
80
+ pieceCount={100}
81
+ config={{
82
+ emojis: ['🎉', '🎂', '🥳', '🎈'],
83
+ colors: ['#FFD700', '#FF6347', '#4169E1', '#32CD32'],
84
+ distribution: { emojis: 1 } // 100% emojis
85
+ }}
86
+ onComplete={onDone}
87
+ />
88
+ ```
89
+
90
+ ### Custom Text/Names
91
+
92
+ Display custom messages or names:
93
+
94
+ ```tsx
95
+ <Confetti
96
+ isVisible={true}
97
+ config={{
98
+ texts: ['Winner!', 'Level Up', '+100 XP'],
99
+ colors: ['#FFD700'], // Gold text
100
+ distribution: { texts: 1 }
101
+ }}
102
+ onComplete={onDone}
103
+ />
104
+ ```
105
+
106
+ ### Custom Images & Avatars
107
+
108
+ Mix images with other confetti pieces:
109
+
110
+ ```tsx
111
+ <Confetti
112
+ isVisible={true}
113
+ config={{
114
+ images: ['https://example.com/avatar1.png', 'https://example.com/avatar2.png'],
115
+ emojis: ['🌟', '✨'],
116
+ distribution: { images: 0.3, emojis: 0.7 } // 30% images, 70% sparkles
117
+ }}
118
+ onComplete={onDone}
119
+ />
120
+ ```
121
+
122
+ ### Fine-Tuning Animations
123
+
124
+ Control speed, sway, and rotation:
125
+
126
+ ```tsx
127
+ <Confetti
128
+ isVisible={true}
129
+ config={{
130
+ animation: {
131
+ minDuration: 4000, // Slower animation
132
+ maxDuration: 6000,
133
+ minSway: 50, // Wider sway
134
+ maxSway: 100,
135
+ maxRotation: 720, // More spinning
136
+ }
137
+ }}
138
+ onComplete={onDone}
139
+ />
140
+ ```
141
+
142
+ ## 🔄 Direction Options
143
+
144
+ ### Top to Bottom (`direction="top-to-bottom"`)
145
+ Classic confetti falling from the sky.
146
+
147
+ ### Bottom to Top (`direction="bottom-to-top"`)
148
+ Hearts rising up like floating balloons - perfect for "send love" interactions!
149
+
150
+ ## 🎯 Use Cases
151
+
152
+ - 💑 Anniversary celebrations
153
+ - 💝 Valentine's Day features
154
+ - 💌 Sending love messages
155
+ - 🎉 Relationship milestones
156
+ - 💒 Wedding apps
157
+ - 💕 Romantic date apps
158
+
159
+ ## 📖 API Reference
160
+
161
+ ### `Confetti` Props
162
+
163
+ | Prop | Type | Default | Description |
164
+ |------|------|---------|-------------|
165
+ | `isVisible` | `boolean` | **required** | Controls visibility of the confetti |
166
+ | `config` | `ConfettiConfig` | `undefined` | Configuration object for full customization |
167
+ | `direction` | `'top-to-bottom' \| 'bottom-to-top'` | `'top-to-bottom'` | Animation direction |
168
+ | `pieceCount` | `number` | `55` | Number of confetti pieces |
169
+ | `onComplete` | `() => void` | `undefined` | Callback when all pieces finish animating |
170
+
171
+ ### `ConfettiConfig` Object
172
+
173
+ The `config` prop accepts an object with the following optional keys:
174
+
175
+ | Key | Type | Description |
176
+ |-----|------|-------------|
177
+ | `emojis` | `string[]` | Array of emoji characters to use |
178
+ | `texts` | `string[]` | Array of text strings to display |
179
+ | `initials` | `string[]` | Array of characters for initial circles |
180
+ | `images` | `string[]` | Array of image URIs |
181
+ | `colors` | `string[]` | Array of colors for text and shapes |
182
+ | `distribution` | `ContentDistribution` | Object defining % of each content type |
183
+ | `animation` | `AnimationConfig` | Timing and movement settings |
184
+ | `sizes` | `SizeConfig` | Size boundaries for different pieces |
185
+ | `styles` | `ConfettiStyles` | Custom RN styles for piece components |
186
+ | `customContent` | `ConfettiContentItem[]` | Array for fully custom weighted content |
187
+
188
+ #### `AnimationConfig`
189
+
190
+ | Key | Default | Description |
191
+ |-----|---------|-------------|
192
+ | `minDuration` | `3000` | Minimum animation duration (ms) |
193
+ | `maxDuration` | `5000` | Maximum animation duration (ms) |
194
+ | `maxDelay` | `1500` | Max start delay for separate pieces (ms) |
195
+ | `minSway` | `30` | Minimum horizontal sway (px) |
196
+ | `maxSway` | `90` | Maximum horizontal sway (px) |
197
+ | `maxRotation` | `360` | Max rotation in degrees |
198
+
199
+ ### Legacy Props (Deprecated)
200
+
201
+ These props are still supported but mapped internally to the new `config` system.
202
+
203
+ - `variant`: Use `config` presets instead
204
+ - `partner1Name`/`partner2Name`: Use `config.texts`
205
+ - `partner1Avatar`/`partner2Avatar`: Use `config.images`
206
+
207
+ ## 🧩 Types
208
+
209
+ ```typescript
210
+ export interface ConfettiConfig {
211
+ emojis?: string[];
212
+ texts?: string[];
213
+ initials?: string[];
214
+ images?: string[];
215
+ colors?: string[];
216
+ distribution?: {
217
+ emojis?: number; // 0-1
218
+ texts?: number; // 0-1
219
+ initials?: number; // 0-1
220
+ images?: number; // 0-1
221
+ };
222
+ // ... and more
223
+ }
224
+ ```
225
+
226
+ ## 📄 License
227
+
228
+ MIT
229
+
230
+ ---
231
+
232
+ Made with 💖
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "rn-confetti-love",
3
+ "version": "1.0.0",
4
+ "description": "Beautiful animated confetti effects for React Native with love-themed variants featuring hearts, names, initials, and avatars",
5
+ "main": "src/index.ts",
6
+ "types": "src/index.ts",
7
+ "react-native": "src/index.ts",
8
+ "source": "src/index.ts",
9
+ "files": [
10
+ "src",
11
+ "README.md",
12
+ "LICENSE"
13
+ ],
14
+ "scripts": {
15
+ "typescript": "tsc --noEmit"
16
+ },
17
+ "keywords": [
18
+ "react-native",
19
+ "confetti",
20
+ "animation",
21
+ "love",
22
+ "hearts",
23
+ "celebration",
24
+ "effects",
25
+ "reanimated",
26
+ "expo"
27
+ ],
28
+ "author": "ShanuChandi",
29
+ "license": "MIT",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "https://github.com/ShanuChandi/rn-confetti-love.git"
33
+ },
34
+ "bugs": {
35
+ "url": " https://github.com/ShanuChandi/rn-confetti-love/issues"
36
+ },
37
+ "homepage": "https://github.com/ShanuChandi/rn-confetti-love#readme",
38
+ "peerDependencies": {
39
+ "expo-image": ">=1.0.0",
40
+ "react": ">=17.0.0",
41
+ "react-native": ">=0.64.0",
42
+ "react-native-reanimated": ">=2.0.0"
43
+ },
44
+ "peerDependenciesMeta": {
45
+ "expo-image": {
46
+ "optional": true
47
+ }
48
+ },
49
+ "devDependencies": {
50
+ "@types/react": "^18.3.27",
51
+ "@types/react-native": "^0.72.8",
52
+ "typescript": "^5.0.0"
53
+ }
54
+ }
@@ -0,0 +1,667 @@
1
+ import { Image } from 'expo-image';
2
+ import React, { useCallback, useEffect, useRef, useState } from 'react';
3
+ import { Dimensions, StyleSheet, Text, View, ImageStyle, TextStyle, ViewStyle } from 'react-native';
4
+ import Animated, {
5
+ Easing,
6
+ useAnimatedStyle,
7
+ useSharedValue,
8
+ withDelay,
9
+ withTiming,
10
+ } from 'react-native-reanimated';
11
+
12
+ const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window');
13
+
14
+ // ============================================================================
15
+ // TYPES & INTERFACES
16
+ // ============================================================================
17
+
18
+ /** Types of confetti pieces that can be rendered */
19
+ export type ConfettiPieceType = 'emoji' | 'text' | 'image';
20
+
21
+ /** Direction for confetti animation */
22
+ export type ConfettiDirection = 'top-to-bottom' | 'bottom-to-top';
23
+
24
+ /** Animation timing configuration */
25
+ export interface AnimationConfig {
26
+ /** Minimum animation duration in ms (default: 3000) */
27
+ minDuration?: number;
28
+ /** Maximum animation duration in ms (default: 5000) */
29
+ maxDuration?: number;
30
+ /** Maximum delay before piece starts animating in ms (default: 1500) */
31
+ maxDelay?: number;
32
+ /** Minimum horizontal sway amount in pixels (default: 30) */
33
+ minSway?: number;
34
+ /** Maximum horizontal sway amount in pixels (default: 90) */
35
+ maxSway?: number;
36
+ /** Sway cycle duration in ms (default: 800) */
37
+ swayCycleDuration?: number;
38
+ /** Maximum rotation in degrees (default: 360) */
39
+ maxRotation?: number;
40
+ /** Fade out starts at this percentage of duration (default: 0.7 = 70%) */
41
+ fadeOutStart?: number;
42
+ }
43
+
44
+ /** Size configuration for confetti pieces */
45
+ export interface SizeConfig {
46
+ /** Minimum scale factor (default: 0.5) */
47
+ minScale?: number;
48
+ /** Maximum scale factor (default: 1.5) */
49
+ maxScale?: number;
50
+ /** Emoji font size (default: 28) */
51
+ emojiFontSize?: number;
52
+ /** Text font size (default: 14) */
53
+ textFontSize?: number;
54
+ /** Initial circle size (default: 36) */
55
+ initialCircleSize?: number;
56
+ /** Initial text font size (default: 18) */
57
+ initialFontSize?: number;
58
+ /** Avatar image size (default: 40) */
59
+ avatarSize?: number;
60
+ }
61
+
62
+ /** Custom content item for confetti */
63
+ export interface ConfettiContentItem {
64
+ /** Type of content */
65
+ type: ConfettiPieceType;
66
+ /** Content value - emoji/text string or image URI */
67
+ value: string;
68
+ /** Optional weight for random selection (default: 1) */
69
+ weight?: number;
70
+ /** Optional custom color for this item */
71
+ color?: string;
72
+ }
73
+
74
+ /** Distribution configuration for built-in content types */
75
+ export interface ContentDistribution {
76
+ /** Percentage of emoji pieces (0-1, default: 0.5) */
77
+ emojis?: number;
78
+ /** Percentage of text pieces like names (0-1, default: 0.3) */
79
+ texts?: number;
80
+ /** Percentage of initial/letter pieces (0-1, default: 0.1) */
81
+ initials?: number;
82
+ /** Percentage of image pieces (0-1, default: 0.1) */
83
+ images?: number;
84
+ }
85
+
86
+ /** Style customization for confetti pieces */
87
+ export interface ConfettiStyles {
88
+ /** Custom emoji text style */
89
+ emoji?: TextStyle;
90
+ /** Custom text style */
91
+ text?: TextStyle;
92
+ /** Custom initial circle container style */
93
+ initialCircle?: ViewStyle;
94
+ /** Custom initial text style */
95
+ initialText?: TextStyle;
96
+ /** Custom image container style */
97
+ imageContainer?: ViewStyle;
98
+ /** Custom image style */
99
+ image?: ImageStyle;
100
+ }
101
+
102
+ /** Main configuration object for full customization */
103
+ export interface ConfettiConfig {
104
+ // === Content Configuration ===
105
+ /** Array of emoji strings to use (default: heart emojis) */
106
+ emojis?: string[];
107
+ /** Array of text strings to display (e.g., names) */
108
+ texts?: string[];
109
+ /** Array of single characters/initials to display */
110
+ initials?: string[];
111
+ /** Array of image URIs to display */
112
+ images?: string[];
113
+ /** Custom content items with full control */
114
+ customContent?: ConfettiContentItem[];
115
+ /** Distribution weights for content types (ignored if customContent is provided) */
116
+ distribution?: ContentDistribution;
117
+
118
+ // === Color Configuration ===
119
+ /** Array of colors to use for text/initials (default: pink/love colors) */
120
+ colors?: string[];
121
+
122
+ // === Animation Configuration ===
123
+ /** Animation timing settings */
124
+ animation?: AnimationConfig;
125
+
126
+ // === Size Configuration ===
127
+ /** Size settings for pieces */
128
+ sizes?: SizeConfig;
129
+
130
+ // === Style Overrides ===
131
+ /** Custom styles for different piece types */
132
+ styles?: ConfettiStyles;
133
+ }
134
+
135
+ /** Internal confetti piece data structure */
136
+ interface ConfettiPiece {
137
+ id: number;
138
+ type: ConfettiPieceType;
139
+ content: string;
140
+ imageUri?: string;
141
+ isInitial?: boolean;
142
+ startX: number;
143
+ startY: number;
144
+ endY: number;
145
+ rotation: number;
146
+ scale: number;
147
+ delay: number;
148
+ duration: number;
149
+ swayAmount: number;
150
+ swayCycleDuration: number;
151
+ color: string;
152
+ direction: ConfettiDirection;
153
+ }
154
+
155
+ /** Props for the main Confetti component */
156
+ export interface ConfettiProps {
157
+ /** Controls visibility of the confetti */
158
+ isVisible: boolean;
159
+ /** Animation direction (default: 'top-to-bottom') */
160
+ direction?: ConfettiDirection;
161
+ /** Number of confetti pieces (default: 55) */
162
+ pieceCount?: number;
163
+ /** Full configuration object for customization */
164
+ config?: ConfettiConfig;
165
+ /** Callback when all pieces finish animating */
166
+ onComplete?: () => void;
167
+
168
+ // === Legacy props for backward compatibility ===
169
+ /** @deprecated Use config.texts instead */
170
+ partner1Name?: string;
171
+ /** @deprecated Use config.texts instead */
172
+ partner2Name?: string;
173
+ /** @deprecated Use config.images instead */
174
+ partner1Avatar?: string;
175
+ /** @deprecated Use config.images instead */
176
+ partner2Avatar?: string;
177
+ /** @deprecated Use config instead */
178
+ variant?: 'love' | 'hearts-only';
179
+ }
180
+
181
+ // ============================================================================
182
+ // DEFAULT VALUES
183
+ // ============================================================================
184
+
185
+ /** Default heart/love emojis */
186
+ export const DEFAULT_EMOJIS = ['❤️', '💕', '💖', '💗', '💝', '💘', '💓', '💞', '🩷', '♥️'];
187
+
188
+ /** Default love-themed colors */
189
+ export const DEFAULT_COLORS = [
190
+ '#E29588', // Primary pink
191
+ '#FF6B8A', // Bright pink
192
+ '#FF85A2', // Light pink
193
+ '#FFB6C1', // Light pink 2
194
+ '#FFC0CB', // Pink
195
+ '#FF69B4', // Hot pink
196
+ '#FF1493', // Deep pink
197
+ '#DB7093', // Pale violet red
198
+ '#E67E92', // Rose
199
+ '#F8A5B8', // Sakura pink
200
+ ];
201
+
202
+ /** Default animation configuration */
203
+ const DEFAULT_ANIMATION: Required<AnimationConfig> = {
204
+ minDuration: 3000,
205
+ maxDuration: 5000,
206
+ maxDelay: 1500,
207
+ minSway: 30,
208
+ maxSway: 90,
209
+ swayCycleDuration: 800,
210
+ maxRotation: 360,
211
+ fadeOutStart: 0.7,
212
+ };
213
+
214
+ /** Default size configuration */
215
+ const DEFAULT_SIZES: Required<SizeConfig> = {
216
+ minScale: 0.5,
217
+ maxScale: 1.5,
218
+ emojiFontSize: 28,
219
+ textFontSize: 14,
220
+ initialCircleSize: 36,
221
+ initialFontSize: 18,
222
+ avatarSize: 40,
223
+ };
224
+
225
+ /** Default content distribution */
226
+ const DEFAULT_DISTRIBUTION: Required<ContentDistribution> = {
227
+ emojis: 0.5,
228
+ texts: 0.2,
229
+ initials: 0.2,
230
+ images: 0.1,
231
+ };
232
+
233
+ // ============================================================================
234
+ // HELPER FUNCTIONS
235
+ // ============================================================================
236
+
237
+ /** Merge user config with defaults */
238
+ const mergeConfig = (config?: ConfettiConfig): Required<Omit<ConfettiConfig, 'customContent' | 'texts' | 'initials' | 'images'>> & ConfettiConfig => {
239
+ return {
240
+ emojis: config?.emojis ?? DEFAULT_EMOJIS,
241
+ texts: config?.texts,
242
+ initials: config?.initials,
243
+ images: config?.images,
244
+ customContent: config?.customContent,
245
+ distribution: { ...DEFAULT_DISTRIBUTION, ...config?.distribution },
246
+ colors: config?.colors ?? DEFAULT_COLORS,
247
+ animation: { ...DEFAULT_ANIMATION, ...config?.animation },
248
+ sizes: { ...DEFAULT_SIZES, ...config?.sizes },
249
+ styles: config?.styles,
250
+ };
251
+ };
252
+
253
+ /** Generate confetti pieces based on configuration */
254
+ const generateConfettiPieces = (
255
+ direction: ConfettiDirection,
256
+ count: number,
257
+ config: ReturnType<typeof mergeConfig>,
258
+ // Legacy props
259
+ legacyTexts?: string[],
260
+ legacyImages?: string[],
261
+ ): ConfettiPiece[] => {
262
+ const pieces: ConfettiPiece[] = [];
263
+ const { emojis, texts, initials, images, customContent, distribution, colors, animation, sizes } = config;
264
+
265
+ // Merge legacy props with config
266
+ const allTexts = texts ?? legacyTexts ?? [];
267
+ const allImages = images ?? legacyImages ?? [];
268
+ const allInitials = initials ?? allTexts.map(t => t.charAt(0).toUpperCase());
269
+
270
+ // Calculate start and end positions based on direction
271
+ const isBottomToTop = direction === 'bottom-to-top';
272
+ const getStartY = () => isBottomToTop
273
+ ? SCREEN_HEIGHT + 100 + Math.random() * 200
274
+ : -100 - Math.random() * 200;
275
+ const getEndY = () => isBottomToTop
276
+ ? -150
277
+ : SCREEN_HEIGHT + 150;
278
+
279
+ for (let i = 0; i < count; i++) {
280
+ let type: ConfettiPieceType;
281
+ let content = '';
282
+ let imageUri: string | undefined;
283
+ let isInitial = false;
284
+ let pieceColor = colors[Math.floor(Math.random() * colors.length)];
285
+
286
+ // If custom content is provided, use weighted random selection
287
+ if (customContent && customContent.length > 0) {
288
+ const totalWeight = customContent.reduce((sum, item) => sum + (item.weight ?? 1), 0);
289
+ let random = Math.random() * totalWeight;
290
+
291
+ for (const item of customContent) {
292
+ random -= item.weight ?? 1;
293
+ if (random <= 0) {
294
+ type = item.type;
295
+ if (item.type === 'image') {
296
+ imageUri = item.value;
297
+ } else {
298
+ content = item.value;
299
+ }
300
+ if (item.color) {
301
+ pieceColor = item.color;
302
+ }
303
+ break;
304
+ }
305
+ }
306
+ type = type! ?? 'emoji';
307
+ } else {
308
+ // Use distribution-based selection
309
+ const rand = Math.random();
310
+ const dist = distribution!;
311
+
312
+ if (rand < dist.emojis! && emojis.length > 0) {
313
+ type = 'emoji';
314
+ content = emojis[Math.floor(Math.random() * emojis.length)];
315
+ } else if (rand < dist.emojis! + dist.initials! && allInitials.length > 0) {
316
+ type = 'text';
317
+ content = allInitials[Math.floor(Math.random() * allInitials.length)];
318
+ isInitial = true;
319
+ } else if (rand < dist.emojis! + dist.initials! + dist.texts! && allTexts.length > 0) {
320
+ type = 'text';
321
+ content = allTexts[Math.floor(Math.random() * allTexts.length)];
322
+ } else if (allImages.length > 0) {
323
+ type = 'image';
324
+ imageUri = allImages[Math.floor(Math.random() * allImages.length)];
325
+ } else {
326
+ // Fallback to emoji
327
+ type = 'emoji';
328
+ content = emojis[Math.floor(Math.random() * emojis.length)];
329
+ }
330
+ }
331
+
332
+ const duration = animation!.minDuration! + Math.random() * (animation!.maxDuration! - animation!.minDuration!);
333
+ const swayAmount = animation!.minSway! + Math.random() * (animation!.maxSway! - animation!.minSway!);
334
+ const scale = sizes!.minScale! + Math.random() * (sizes!.maxScale! - sizes!.minScale!);
335
+
336
+ pieces.push({
337
+ id: i,
338
+ type,
339
+ content,
340
+ imageUri,
341
+ isInitial,
342
+ startX: Math.random() * SCREEN_WIDTH,
343
+ startY: getStartY(),
344
+ endY: getEndY(),
345
+ rotation: Math.random() * animation!.maxRotation! * 2 - animation!.maxRotation!,
346
+ scale,
347
+ delay: Math.random() * animation!.maxDelay!,
348
+ duration,
349
+ swayAmount,
350
+ swayCycleDuration: animation!.swayCycleDuration!,
351
+ color: pieceColor,
352
+ direction,
353
+ });
354
+ }
355
+
356
+ return pieces;
357
+ };
358
+
359
+ // ============================================================================
360
+ // COMPONENTS
361
+ // ============================================================================
362
+
363
+ interface ConfettiPieceComponentProps {
364
+ piece: ConfettiPiece;
365
+ config: ReturnType<typeof mergeConfig>;
366
+ onFinish: () => void;
367
+ }
368
+
369
+ /** Individual animated confetti piece */
370
+ const ConfettiPieceComponent: React.FC<ConfettiPieceComponentProps> = ({ piece, config, onFinish }) => {
371
+ const translateY = useSharedValue(piece.startY);
372
+ const translateX = useSharedValue(piece.startX);
373
+ const rotate = useSharedValue(0);
374
+ const opacity = useSharedValue(1);
375
+
376
+ const { animation, sizes, styles: customStyles } = config;
377
+
378
+ useEffect(() => {
379
+ // Animate movement
380
+ translateY.value = withDelay(
381
+ piece.delay,
382
+ withTiming(piece.endY, {
383
+ duration: piece.duration,
384
+ easing: Easing.out(Easing.quad),
385
+ })
386
+ );
387
+
388
+ // Handle completion
389
+ const completionTimeout = setTimeout(() => {
390
+ onFinish();
391
+ }, piece.delay + piece.duration);
392
+
393
+ // Animate rotation
394
+ rotate.value = withDelay(
395
+ piece.delay,
396
+ withTiming(piece.rotation, {
397
+ duration: piece.duration,
398
+ easing: Easing.linear,
399
+ })
400
+ );
401
+
402
+ // Animate horizontal sway
403
+ const swayCycle = () => {
404
+ translateX.value = withTiming(
405
+ piece.startX + piece.swayAmount,
406
+ { duration: piece.swayCycleDuration, easing: Easing.inOut(Easing.sin) },
407
+ () => {
408
+ translateX.value = withTiming(
409
+ piece.startX - piece.swayAmount,
410
+ { duration: piece.swayCycleDuration, easing: Easing.inOut(Easing.sin) },
411
+ () => {
412
+ translateX.value = withTiming(
413
+ piece.startX,
414
+ { duration: piece.swayCycleDuration, easing: Easing.inOut(Easing.sin) }
415
+ );
416
+ }
417
+ );
418
+ }
419
+ );
420
+ };
421
+
422
+ const timeout = setTimeout(swayCycle, piece.delay);
423
+
424
+ // Fade out
425
+ opacity.value = withDelay(
426
+ piece.delay + piece.duration * animation!.fadeOutStart!,
427
+ withTiming(0, { duration: piece.duration * (1 - animation!.fadeOutStart!) })
428
+ );
429
+
430
+ return () => {
431
+ clearTimeout(timeout);
432
+ clearTimeout(completionTimeout);
433
+ };
434
+ }, []);
435
+
436
+ const animatedStyle = useAnimatedStyle(() => {
437
+ return {
438
+ transform: [
439
+ { translateX: translateX.value },
440
+ { translateY: translateY.value },
441
+ { rotate: `${rotate.value}deg` },
442
+ { scale: piece.scale },
443
+ ],
444
+ opacity: opacity.value,
445
+ };
446
+ });
447
+
448
+ const renderContent = () => {
449
+ switch (piece.type) {
450
+ case 'emoji':
451
+ return (
452
+ <Text style={[
453
+ styles.emoji,
454
+ { fontSize: sizes!.emojiFontSize },
455
+ customStyles?.emoji
456
+ ]}>
457
+ {piece.content}
458
+ </Text>
459
+ );
460
+ case 'text':
461
+ if (piece.isInitial) {
462
+ return (
463
+ <View style={[
464
+ styles.initialCircle,
465
+ {
466
+ width: sizes!.initialCircleSize,
467
+ height: sizes!.initialCircleSize,
468
+ borderRadius: sizes!.initialCircleSize! / 2,
469
+ backgroundColor: `${piece.color}30`
470
+ },
471
+ customStyles?.initialCircle
472
+ ]}>
473
+ <Text style={[
474
+ styles.initialText,
475
+ { fontSize: sizes!.initialFontSize, color: piece.color },
476
+ customStyles?.initialText
477
+ ]}>
478
+ {piece.content}
479
+ </Text>
480
+ </View>
481
+ );
482
+ }
483
+ return (
484
+ <Text style={[
485
+ styles.text,
486
+ { fontSize: sizes!.textFontSize, color: piece.color },
487
+ customStyles?.text
488
+ ]}>
489
+ {piece.content}
490
+ </Text>
491
+ );
492
+ case 'image':
493
+ return (
494
+ <View style={[
495
+ styles.imageContainer,
496
+ {
497
+ width: sizes!.avatarSize,
498
+ height: sizes!.avatarSize,
499
+ borderRadius: sizes!.avatarSize! / 2
500
+ },
501
+ customStyles?.imageContainer
502
+ ]}>
503
+ <Image
504
+ source={{ uri: piece.imageUri }}
505
+ style={[styles.image, customStyles?.image]}
506
+ contentFit="cover"
507
+ />
508
+ </View>
509
+ );
510
+ default:
511
+ return null;
512
+ }
513
+ };
514
+
515
+ return (
516
+ <Animated.View style={[styles.confettiPiece, animatedStyle]}>
517
+ {renderContent()}
518
+ </Animated.View>
519
+ );
520
+ };
521
+
522
+ /** Main Confetti Component */
523
+ const Confetti: React.FC<ConfettiProps> = ({
524
+ isVisible,
525
+ direction = 'top-to-bottom',
526
+ pieceCount = 55,
527
+ config,
528
+ onComplete,
529
+ // Legacy props
530
+ partner1Name,
531
+ partner2Name,
532
+ partner1Avatar,
533
+ partner2Avatar,
534
+ variant,
535
+ }) => {
536
+ const [pieces, setPieces] = useState<ConfettiPiece[]>([]);
537
+ const finishedCount = useRef(0);
538
+ const totalPieces = useRef(0);
539
+
540
+ // Merge configuration with defaults
541
+ const mergedConfig = React.useMemo(() => {
542
+ // Handle legacy variant prop
543
+ if (variant === 'hearts-only') {
544
+ return mergeConfig({
545
+ ...config,
546
+ distribution: { emojis: 1, texts: 0, initials: 0, images: 0 },
547
+ });
548
+ }
549
+ return mergeConfig(config);
550
+ }, [config, variant]);
551
+
552
+ // Build legacy arrays for backward compatibility
553
+ const legacyTexts = React.useMemo(() => {
554
+ const texts: string[] = [];
555
+ if (partner1Name) texts.push(partner1Name);
556
+ if (partner2Name) texts.push(partner2Name);
557
+ return texts.length > 0 ? texts : undefined;
558
+ }, [partner1Name, partner2Name]);
559
+
560
+ const legacyImages = React.useMemo(() => {
561
+ const images: string[] = [];
562
+ if (partner1Avatar) images.push(partner1Avatar);
563
+ if (partner2Avatar) images.push(partner2Avatar);
564
+ return images.length > 0 ? images : undefined;
565
+ }, [partner1Avatar, partner2Avatar]);
566
+
567
+ useEffect(() => {
568
+ if (isVisible) {
569
+ const newPieces = generateConfettiPieces(
570
+ direction,
571
+ pieceCount,
572
+ mergedConfig,
573
+ legacyTexts,
574
+ legacyImages
575
+ );
576
+ setPieces(newPieces);
577
+ totalPieces.current = newPieces.length;
578
+ finishedCount.current = 0;
579
+ } else {
580
+ setPieces([]);
581
+ }
582
+ }, [isVisible, direction, pieceCount, mergedConfig, legacyTexts, legacyImages]);
583
+
584
+ const handlePieceFinish = useCallback(() => {
585
+ finishedCount.current += 1;
586
+ if (finishedCount.current >= totalPieces.current && onComplete) {
587
+ onComplete();
588
+ }
589
+ }, [onComplete]);
590
+
591
+ if (!isVisible || pieces.length === 0) {
592
+ return null;
593
+ }
594
+
595
+ return (
596
+ <View style={styles.container} pointerEvents="none">
597
+ {pieces.map((piece) => (
598
+ <ConfettiPieceComponent
599
+ key={piece.id}
600
+ piece={piece}
601
+ config={mergedConfig}
602
+ onFinish={handlePieceFinish}
603
+ />
604
+ ))}
605
+ </View>
606
+ );
607
+ };
608
+
609
+ // ============================================================================
610
+ // STYLES
611
+ // ============================================================================
612
+
613
+ const styles = StyleSheet.create({
614
+ container: {
615
+ ...StyleSheet.absoluteFillObject,
616
+ zIndex: 9999,
617
+ elevation: 9999,
618
+ },
619
+ confettiPiece: {
620
+ position: 'absolute',
621
+ },
622
+ emoji: {
623
+ fontSize: 28,
624
+ textShadowColor: 'rgba(0, 0, 0, 0.1)',
625
+ textShadowOffset: { width: 1, height: 1 },
626
+ textShadowRadius: 2,
627
+ },
628
+ text: {
629
+ fontSize: 14,
630
+ fontWeight: '600',
631
+ textShadowColor: 'rgba(0, 0, 0, 0.15)',
632
+ textShadowOffset: { width: 1, height: 1 },
633
+ textShadowRadius: 2,
634
+ },
635
+ initialCircle: {
636
+ width: 36,
637
+ height: 36,
638
+ borderRadius: 18,
639
+ alignItems: 'center',
640
+ justifyContent: 'center',
641
+ borderWidth: 2,
642
+ borderColor: 'rgba(255, 255, 255, 0.5)',
643
+ },
644
+ initialText: {
645
+ fontSize: 18,
646
+ fontWeight: '700',
647
+ },
648
+ imageContainer: {
649
+ width: 40,
650
+ height: 40,
651
+ borderRadius: 20,
652
+ overflow: 'hidden',
653
+ borderWidth: 2,
654
+ borderColor: 'rgba(255, 255, 255, 0.6)',
655
+ shadowColor: '#000',
656
+ shadowOffset: { width: 0, height: 2 },
657
+ shadowOpacity: 0.2,
658
+ shadowRadius: 4,
659
+ elevation: 4,
660
+ },
661
+ image: {
662
+ width: '100%',
663
+ height: '100%',
664
+ },
665
+ });
666
+
667
+ export default Confetti;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * LoveConfetti - A wrapper around the base Confetti component
3
+ * for backward compatibility with existing usage.
4
+ *
5
+ * For new implementations, consider using the Confetti component directly
6
+ * with the appropriate variant and direction props.
7
+ */
8
+
9
+ import React from 'react';
10
+ import Confetti, { ConfettiConfig } from './Confetti';
11
+
12
+ interface LoveConfettiProps {
13
+ isVisible: boolean;
14
+ config?: ConfettiConfig;
15
+ onComplete: () => void;
16
+ }
17
+
18
+ const LoveConfetti: React.FC<LoveConfettiProps> = ({
19
+ isVisible,
20
+ config,
21
+ onComplete,
22
+ }) => {
23
+ return (
24
+ <Confetti
25
+ isVisible={isVisible}
26
+ direction="top-to-bottom"
27
+ pieceCount={55}
28
+ config={config}
29
+ onComplete={onComplete}
30
+ />
31
+ );
32
+ };
33
+
34
+ export default LoveConfetti;
package/src/index.ts ADDED
@@ -0,0 +1,21 @@
1
+ /**
2
+ * rn-confetti-love
3
+ * Beautiful animated confetti effects for React Native
4
+ */
5
+
6
+ // Main components
7
+ export { default as Confetti } from './Confetti';
8
+ export { default as LoveConfetti } from './LoveConfetti';
9
+
10
+ // Types
11
+ export type {
12
+ ConfettiProps,
13
+ ConfettiDirection,
14
+ ConfettiConfig,
15
+ ConfettiPieceType,
16
+ AnimationConfig,
17
+ SizeConfig,
18
+ ConfettiContentItem,
19
+ ContentDistribution,
20
+ ConfettiStyles,
21
+ } from './Confetti';