import React, { useState, useEffect, useRef, useMemo, useCallback } from 'react';
import { motion, useAnimation } from 'framer-motion';
// --- Helper Functions ---
const random = (min, max) => Math.random() * (max - min) + min;
const uuid = () =>
typeof crypto !== 'undefined' && crypto.randomUUID
? crypto.randomUUID()
: `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
// Interpolate between two colors
const interpolateColor = (color1, color2, factor) => {
if (factor < 0) factor = 0;
if (factor > 1) factor = 1;
const c1 = parseInt(color1.substring(1), 16);
const c2 = parseInt(color2.substring(1), 16);
const r1 = (c1 >> 16) & 0xff;
const g1 = (c1 >> 8) & 0xff;
const b1 = c1 & 0xff;
const r2 = (c2 >> 16) & 0xff;
const g2 = (c2 >> 8) & 0xff;
const b2 = c2 & 0xff;
const r = Math.round(r1 + (r2 - r1) * factor);
const g = Math.round(g1 + (g2 - g1) * factor);
const b = Math.round(b1 + (b2 - b1) * factor);
return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase()}`;
};
const START_COLOR = '#4ade80'; // Dull Green (Tailwind green-400)
const END_COLOR = '#00FF00'; // Bright Neon Green
// --- Particle Component ---
const Particle = ({ id, initialX, initialY, targetPointX, targetPointY, color, status, screenHeight, screenWidth, onComplete }) => {
const controls = useAnimation();
const [currentState, setCurrentState] = useState(status);
const particleRef = useRef({ isComplete: false }); // Ref to track completion status
useEffect(() => {
setCurrentState(status); // Update state if prop changes
}, [status]);
useEffect(() => {
if (particleRef.current.isComplete) return; // Don't restart animation if already completing
let animationSequence;
const durationBase = random(6, 12); // Base duration for normal movement
const fadeDuration = 1.5; // Normal fade duration
// Slow fade durations for lingering particles
const lingerFadeDuration = random(8, 15); // Longer fade for failed/revoked
const lingerDelay = random(3, 7); // Delay before slow fade starts
switch (currentState) {
case 'FAILED':
// Drop to bottom, linger as smaller "corpse", then fade slowly
animationSequence = async () => {
// 1. Drop to near the bottom
await controls.start({
x: random(0, screenWidth), // Scatter slightly at the bottom
y: screenHeight - random(10, 15), // Settle very close to the bottom edge
opacity: 1,
scale: 0.8, // Slightly smaller "corpse"
transition: { duration: random(1, 2.5), ease: "easeIn" }
});
// 2. Linger, then fade slowly
await controls.start({
opacity: 0,
transition: { duration: lingerFadeDuration, delay: lingerDelay }
});
if (!particleRef.current.isComplete) {
particleRef.current.isComplete = true;
onComplete(id);
}
};
break;
case 'REVOKED':
// Flee to edges, linger smaller/huddled, then fade slowly
{
const edgeX = initialX < screenWidth / 2 ? random(-15, 0) : random(screenWidth, screenWidth + 15);
const edgeY = random(0, screenHeight);
animationSequence = async () => {
// 1. Move quickly to the edge
await controls.start({
x: edgeX,
y: edgeY,
opacity: 1,
scale: 0.6, // Smaller, huddled look
transition: { duration: random(0.5, 1.5), ease: "easeOut" }
});
// 2. Linger, then fade slowly
await controls.start({
opacity: 0,
transition: { duration: lingerFadeDuration, delay: lingerDelay }
});
if (!particleRef.current.isComplete) {
particleRef.current.isComplete = true;
onComplete(id);
}
};
}
break;
case 'RUNNING':
case 'PENDING':
case 'COMPLETED':
default:
// Move towards the target point on the bar and fade (original behavior)
animationSequence = async () => {
// 1. Quick fade-in
await controls.start({
opacity: 1,
scale: 1, // Start at full size immediately
transition: { duration: 0.3, ease: "easeOut" } // Fast fade-in
});
// 2. Move towards the target point
await controls.start({
x: targetPointX,
y: targetPointY,
// Opacity and scale are already 1 from the previous step
transition: { duration: durationBase, ease: "circOut" }
});
// 3. Fade out near the target
await controls.start({
opacity: 0,
scale: 0.3, // Shrink slightly as it fades
transition: { duration: fadeDuration }
});
if (!particleRef.current.isComplete) {
particleRef.current.isComplete = true;
onComplete(id);
}
};
break;
}
animationSequence().catch(err => {
// Handle potential animation errors (e.g., component unmounted)
if (err.name !== 'AbortError' && !particleRef.current.isComplete) {
console.error("Particle animation error:", err);
// Ensure cleanup happens even on error if not already completed
particleRef.current.isComplete = true;
onComplete(id);
}
});
// Cleanup function for when component unmounts or dependencies change
// This helps prevent calling onComplete multiple times if status changes rapidly
return () => {
controls.stop(); // Stop any ongoing animation
// If the component unmounts before the animation naturally completes,
// ensure we call onComplete if it hasn't been called yet.
// This is important for states like RUNNING->FAILED transition.
if (!particleRef.current.isComplete) {
// We might not call onComplete here directly,
// as the state change might trigger a new animation
// that WILL eventually call onComplete. Let the new effect handle it.
// If we *do* call it here, it might remove the particle before the
// FAILED/REVOKED animation can even start.
// Let's rely on the isComplete flag within the async sequences.
}
};
// Ensure dependencies cover all inputs for the animation logic
}, [controls, id, initialX, initialY, targetPointX, targetPointY, color, currentState, screenHeight, screenWidth, onComplete]);
return (