import { useEffect, useRef, useCallback } from 'react';
import { gsap } from 'gsap';
interface AnimationOptions {
duration?: number;
delay?: number;
ease?: string;
}
export const useGSAPAnimations = () => {
const timelineRef = useRef<gsap.core.Timeline | null>(null);
useEffect(() => {
// Create a master timeline
timelineRef.current = gsap.timeline();
// Set up accessibility preferences
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (prefersReducedMotion) {
gsap.globalTimeline.timeScale(0.5); // Reduce animation speed
}
return () => {
if (timelineRef.current) {
timelineRef.current.kill();
}
};
}, []);
// Pokemon card entrance animations with stagger
const animateCardsEntrance = useCallback((cardRefs: HTMLElement[], options: AnimationOptions = {}) => {
const { duration = 0.6, delay = 0, ease = "power2.out" } = options;
gsap.fromTo(cardRefs,
{
opacity: 0,
y: 50,
scale: 0.9,
rotationX: -15
},
{
opacity: 1,
y: 0,
scale: 1,
rotationX: 0,
duration,
delay,
ease,
stagger: 0.1,
clearProps: "transform"
}
);
}, []);
// Enhanced hover animations for Pokemon cards
const createCardHoverAnimation = useCallback((cardRef: HTMLElement) => {
const imageRef = cardRef.querySelector('img');
const contentRef = cardRef.querySelector('.card-content');
const hoverTl = gsap.timeline({ paused: true });
hoverTl
.to(cardRef, {
scale: 1.05,
y: -8,
boxShadow: '0 20px 40px rgba(0,0,0,0.3)',
duration: 0.3,
ease: "power2.out"
})
.to(imageRef, {
scale: 1.1,
rotation: 5,
duration: 0.3,
ease: "power2.out"
}, 0)
.to(contentRef, {
y: -2,
duration: 0.3,
ease: "power2.out"
}, 0);
const handleMouseEnter = () => hoverTl.play();
const handleMouseLeave = () => hoverTl.reverse();
cardRef.addEventListener('mouseenter', handleMouseEnter);
cardRef.addEventListener('mouseleave', handleMouseLeave);
return () => {
cardRef.removeEventListener('mouseenter', handleMouseEnter);
cardRef.removeEventListener('mouseleave', handleMouseLeave);
hoverTl.kill();
};
}, []);
// Pokemon image zoom/reveal effects for selection
const animatePokemonSelection = useCallback((
fromImageRef: HTMLElement,
toContainerRef: HTMLElement,
options: AnimationOptions & { onComplete?: () => void } = {}
) => {
const { duration = 0.8, ease = "power2.inOut", onComplete } = options;
// Get positions for the morph animation
const fromRect = fromImageRef.getBoundingClientRect();
const toRect = toContainerRef.getBoundingClientRect();
// Clone the image for the transition
const clonedImage = fromImageRef.cloneNode(true) as HTMLElement;
clonedImage.style.position = 'fixed';
clonedImage.style.top = `${fromRect.top}px`;
clonedImage.style.left = `${fromRect.left}px`;
clonedImage.style.width = `${fromRect.width}px`;
clonedImage.style.height = `${fromRect.height}px`;
clonedImage.style.zIndex = '9999';
clonedImage.style.pointerEvents = 'none';
document.body.appendChild(clonedImage);
const tl = gsap.timeline();
// Animate the cloned image to the new position
tl.to(clonedImage, {
x: toRect.left - fromRect.left,
y: toRect.top - fromRect.top,
scale: toRect.width / fromRect.width,
duration,
ease,
onComplete: () => {
document.body.removeChild(clonedImage);
// Fade in the actual target container
gsap.fromTo(toContainerRef,
{ opacity: 0, scale: 0.9 },
{ opacity: 1, scale: 1, duration: 0.4, ease: "power2.out",
onComplete
}
);
}
});
return tl;
}, []);
// View transition animations
const animateViewTransition = useCallback((
outgoingRef: HTMLElement | null,
incomingRef: HTMLElement | null,
direction: 'left' | 'right' | 'up' | 'down' = 'left',
options: AnimationOptions & { onComplete?: () => void } = {}
) => {
const { duration = 0.6, ease = "power2.inOut", onComplete } = options;
const getTransformValues = (dir: typeof direction) => {
switch (dir) {
case 'left': return { x: -100, y: 0 };
case 'right': return { x: 100, y: 0 };
case 'up': return { x: 0, y: -100 };
case 'down': return { x: 0, y: 100 };
}
};
const transform = getTransformValues(direction);
const tl = gsap.timeline();
if (outgoingRef) {
tl.to(outgoingRef, {
opacity: 0,
x: -transform.x,
y: -transform.y,
duration: duration / 2,
ease
});
}
if (incomingRef) {
tl.fromTo(incomingRef,
{
opacity: 0,
x: transform.x,
y: transform.y
},
{
opacity: 1,
x: 0,
y: 0,
duration: duration / 2,
ease,
onComplete
},
outgoingRef ? duration / 2 : 0
);
}
return tl;
}, []);
// Animated stat bars
const animateStatBars = useCallback((statRefs: { ref: HTMLElement; value: number; max: number }[]) => {
const tl = gsap.timeline();
statRefs.forEach((stat, index) => {
const percentage = (stat.value / stat.max) * 100;
tl.fromTo(stat.ref,
{ width: '0%' },
{
width: `${percentage}%`,
duration: 0.8,
ease: "power2.out",
delay: index * 0.1
},
0.2
);
});
return tl;
}, []);
// Loading spinner animation
const createLoadingAnimation = useCallback((spinnerRef: HTMLElement) => {
return gsap.to(spinnerRef, {
rotation: 360,
duration: 1,
ease: "none",
repeat: -1
});
}, []);
// Type badge pulse animation for effectiveness
const animateTypeEffectiveness = useCallback((
badgeRefs: HTMLElement[],
effectiveness: ('weak' | 'resist' | 'immune')[]
) => {
const tl = gsap.timeline();
badgeRefs.forEach((badge, index) => {
const effect = effectiveness[index];
let color = '#ffffff';
let scale = 1;
switch (effect) {
case 'weak':
color = '#ef4444'; // red-500
scale = 1.1;
break;
case 'resist':
color = '#22c55e'; // green-500
scale = 0.95;
break;
case 'immune':
color = '#3b82f6'; // blue-500
scale = 1.05;
break;
}
tl.to(badge, {
backgroundColor: color,
scale,
duration: 0.3,
ease: "power2.out",
yoyo: true,
repeat: 1
}, index * 0.1);
});
return tl;
}, []);
return {
animateCardsEntrance,
createCardHoverAnimation,
animatePokemonSelection,
animateViewTransition,
animateStatBars,
createLoadingAnimation,
animateTypeEffectiveness,
timeline: timelineRef.current
};
};
export default useGSAPAnimations;