import React, { useEffect, useState } from 'react';
/**
* Props for the EmojiBurst component
*/
export interface EmojiBurstProps {
/**
* Whether the burst animation is currently active.
* When set to true, starts a new burst animation from the specified position.
* Setting to false will not stop an ongoing animation - use onComplete for cleanup.
*/
isActive: boolean;
/**
* The position where the burst should originate from, in viewport coordinates.
* Typically obtained from element.getBoundingClientRect() or mouse event coordinates.
*
* @example
* ```tsx
* // From button center
* const rect = buttonRef.current.getBoundingClientRect();
* const position = {
* x: rect.left + rect.width / 2,
* y: rect.top + rect.height / 2
* };
*
* // From mouse click
* const position = { x: event.clientX, y: event.clientY };
* ```
*/
position: { x: number; y: number };
/**
* The emoji to display in the burst animation.
* Special behavior: When set to '👍', automatically uses diverse skin tone variants.
*
* @default "👍"
*
* @example
* ```tsx
* emoji="🎉" // Party celebration
* emoji="❤️" // Love reaction
* emoji="😂" // Laughter
* emoji="👍" // Uses skin tone variants: 👍, 👍🏻, 👍🏽, 👍🏿
* ```
*/
emoji?: string;
/**
* Duration of the burst animation in milliseconds.
* Controls how long emojis remain visible before fading out completely.
* Longer durations allow emojis to travel further and fade more gradually.
*
*
* - Animation uses requestAnimationFrame for smooth 60fps performance
* - Emojis begin fading after 2/3 of the duration
*/
duration: number;
/**
* Callback function called when the animation completes and all emojis have faded out.
* Use this to clean up state, typically by setting isActive to false.
*
* @example
* ```tsx
* onComplete={() => {
* setBurstActive(false);
* // Optional: trigger other effects
* playSound('burst-complete');
* }}
* ```
*/
onComplete: () => void;
}
/**
* Internal interface representing a single emoji in the burst animation
*/
interface FlyingEmoji {
/** Unique identifier for the emoji */
id: number;
/** The emoji character to display */
emoji: string;
/** Current x-coordinate position */
x: number;
/** Current y-coordinate position */
y: number;
/** Velocity in the x direction */
vx: number;
/** Velocity in the y direction */
vy: number;
/** Current rotation angle in degrees */
rotation: number;
/** Speed of rotation */
rotationSpeed: number;
/** Current opacity value (0-1) */
opacity: number;
/** Current scale factor */
scale: number;
}
/**
* EmojiBurst component creates an animated burst of emoji characters
*
* Features:
* - Creates a circular burst of emojis with different skin tones (for thumbs-up) or the specified emoji
* - Animates emojis with physics-based motion (velocity, gravity, rotation)
* - Automatically fades out and cleans up after animation completes
* - Non-interactive visual effect (pointer-events-none)
* - Randomized trajectories and rotation for natural movement
* - Adaptive emoji variants for inclusive representation
*
* @example
* // Basic reaction burst on button click
* const [burstActive, setBurstActive] = useState(false);
* const [burstPosition, setBurstPosition] = useState({ x: 0, y: 0 });
* const [burstDuration] = useState(2000); // 2 seconds
*
* const handleReaction = (event: React.MouseEvent) => {
* const rect = event.currentTarget.getBoundingClientRect();
* setBurstPosition({
* x: rect.left + rect.width / 2,
* y: rect.top + rect.height / 2
* });
* setBurstActive(true);
* };
*
* return (
* <>
*
* setBurstActive(false)}
* />
* >
* );
*
* @example
* // Custom emoji with longer duration
* setShowCelebration(false)}
* />
*
* @example
* // Triggered from mouse coordinates
* const handleEmojiReaction = (event: MouseEvent, selectedEmoji: string) => {
* setBurstPosition({ x: event.clientX, y: event.clientY });
* setBurstEmoji(selectedEmoji);
* setBurstActive(true);
* };
*
* setBurstActive(false)}
* />
*/
export const EmojiBurst = ({
isActive,
position,
emoji = '👍',
duration,
onComplete,
}: EmojiBurstProps) => {
const [emojis, setEmojis] = useState([]);
useEffect(() => {
if (!isActive) return;
// Create burst of emojis
const newEmojis: FlyingEmoji[] = [];
// Use skin tone variations for thumbs-up, otherwise use the provided emoji
const emojiVariants = emoji === '👍' ? ['👍', '👍🏻', '👍🏽', '👍🏿'] : [emoji];
for (let i = 0; i < 12; i++) {
const angle = (i / 12) * Math.PI * 2;
const speed = 3 + Math.random() * 4;
newEmojis.push({
id: i,
emoji: emojiVariants[Math.floor(Math.random() * emojiVariants.length)] || emoji,
x: position.x,
y: position.y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed - 2, // Slight upward bias
rotation: 0,
rotationSpeed: (Math.random() - 0.5) * 20,
opacity: 1,
scale: 0.8 + Math.random() * 0.4,
});
}
setEmojis(newEmojis);
// Animation loop
let animationFrame: number;
const startTime = Date.now();
const animate = () => {
const elapsed = Date.now() - startTime;
const progress = elapsed / duration;
if (progress >= 1) {
setEmojis([]);
onComplete();
return;
}
setEmojis((currentEmojis) =>
currentEmojis.map((emoji) => ({
...emoji,
x: emoji.x + emoji.vx,
y: emoji.y + emoji.vy,
vy: emoji.vy + 0.3, // Gravity
rotation: emoji.rotation + emoji.rotationSpeed,
opacity: Math.max(0, 1 - progress * 1.5), // Fade out
scale: emoji.scale * (1 - progress * 0.3), // Shrink slightly
}))
);
animationFrame = requestAnimationFrame(animate);
};
animate();
return () => {
if (animationFrame) {
cancelAnimationFrame(animationFrame);
}
};
}, [isActive, position, emoji, duration, onComplete]);
if (!isActive || emojis.length === 0) return;
return (
{emojis.map((emoji) => (
{emoji.emoji}
))}
);
};