Please add an advanced interactive cursor trail and "wind flow" effect to my React project.
1. Create a new file called CursorTrail.tsx (or .jsx) and paste this exact code:
import { useEffect, useRef } from 'react';
import { useLocation } from 'react-router-dom';
export default function CursorTrail() {
const canvasRef = useRef<HTMLCanvasElement>(null);
const location = useLocation();
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
let width = window.innerWidth;
let height = window.innerHeight;
canvas.width = width;
canvas.height = height;
let particles: any[] = [];
let ripples: any[] = [];
let mouse = { x: width / 2, y: height / 2, vx: 0, vy: 0 };
let lastMouse = { x: width / 2, y: height / 2 };
let isMobile = width < 768;
const handleResize = () => {
width = window.innerWidth;
height = window.innerHeight;
canvas.width = width;
canvas.height = height;
isMobile = width < 768;
};
window.addEventListener('resize', handleResize);
const handleMouseMove = (e: MouseEvent) => {
lastMouse.x = mouse.x;
lastMouse.y = mouse.y;
mouse.x = e.clientX;
mouse.y = e.clientY;
mouse.vx = mouse.x - lastMouse.x;
mouse.vy = mouse.y - lastMouse.y;
};
window.addEventListener('mousemove', handleMouseMove);
const handleClick = (e: MouseEvent) => {
if (isMobile) return;
ripples.push(new Ripple(e.clientX, e.clientY, location.pathname === '/' && window.scrollY < window.innerHeight));
};
window.addEventListener('click', handleClick);
class Ripple {
x: number; y: number; radius: number; maxRadius: number; life: number; maxLife: number; isHero: boolean;
constructor(x: number, y: number, isHero: boolean) {
this.x = x; this.y = y; this.radius = 0; this.maxRadius = 60; this.life = 1; this.maxLife = 1; this.isHero = isHero;
}
update() {
this.radius += (this.maxRadius - this.radius) * 0.1;
this.life -= 0.03;
}
draw(ctx: CanvasRenderingContext2D) {
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
const color = this.isHero ? '255, 255, 255' : '14, 165, 233';
ctx.strokeStyle = `rgba(${color}, ${Math.max(0, this.life * 0.5)})`;
ctx.lineWidth = 2;
ctx.stroke();
}
}
class Particle {
x: number; y: number; vx: number; vy: number; life: number; maxLife: number; size: number; color: string; isWind: boolean;
constructor(x: number, y: number, vx: number, vy: number, isHero: boolean) {
this.x = x; this.y = y;
const speed = Math.sqrt(vx * vx + vy * vy);
const angle = Math.atan2(vy, vx) + (Math.random() - 0.5) * (isHero ? 0.3 : 0.8);
const speedMultiplier = isHero ? (Math.random() * 0.4 + 0.2) : (Math.random() * 0.2 + 0.1);
this.vx = Math.cos(angle) * speed * speedMultiplier;
this.vy = Math.sin(angle) * speed * speedMultiplier;
this.maxLife = isHero ? Math.random() * 60 + 30 : Math.random() * 30 + 15;
this.life = this.maxLife;
this.isWind = isHero;
this.size = isHero ? Math.random() * 3 + 1.5 : Math.random() * 2 + 0.5;
const colors = isHero
? ['255, 255, 255', '186, 230, 253', '125, 211, 252']
: ['14, 165, 233', '2, 132, 199', '148, 163, 184'];
this.color = colors[Math.floor(Math.random() * colors.length)];
}
update() {
this.x += this.vx; this.y += this.vy;
if (this.isWind) {
this.vx += Math.sin(this.life * 0.05) * 0.2;
this.vy -= 0.15;
this.vx *= 0.98; this.vy *= 0.98;
} else {
this.vx *= 0.92; this.vy *= 0.92;
}
this.life--;
}
draw(ctx: CanvasRenderingContext2D) {
if (this.life <= 0) return;
const alpha = (this.life / this.maxLife) * (this.isWind ? 0.8 : 0.5);
ctx.beginPath();
if (this.isWind) {
ctx.moveTo(this.x, this.y);
ctx.lineTo(this.x - this.vx * 3, this.y - this.vy * 3);
ctx.strokeStyle = `rgba(${this.color}, ${alpha})`;
ctx.lineWidth = this.size;
ctx.lineCap = 'round';
ctx.stroke();
} else {
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.fillStyle = `rgba(${this.color}, ${alpha})`;
ctx.fill();
ctx.shadowBlur = 10;
ctx.shadowColor = `rgba(${this.color}, ${alpha * 0.5})`;
}
ctx.shadowBlur = 0;
}
}
let animationFrame: number;
const render = () => {
ctx.clearRect(0, 0, width, height);
if (!isMobile) {
const speed = Math.sqrt(mouse.vx * mouse.vx + mouse.vy * mouse.vy);
const isHero = location.pathname === '/' && window.scrollY < window.innerHeight - 100;
if (speed > 1) {
const numParticles = isHero ? Math.min(Math.floor(speed / 3), 8) : Math.min(Math.floor(speed / 8), 3);
for (let i = 0; i < numParticles; i++) {
particles.push(new Particle(mouse.x, mouse.y, mouse.vx, mouse.vy, isHero));
}
}
mouse.vx *= 0.5; mouse.vy *= 0.5;
for (let i = particles.length - 1; i >= 0; i--) {
particles[i].update(); particles[i].draw(ctx);
if (particles[i].life <= 0) particles.splice(i, 1);
}
for (let i = ripples.length - 1; i >= 0; i--) {
ripples[i].update(); ripples[i].draw(ctx);
if (ripples[i].life <= 0) ripples.splice(i, 1);
}
}
animationFrame = requestAnimationFrame(render);
};
render();
return () => {
window.removeEventListener('resize', handleResize);
window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('click', handleClick);
cancelAnimationFrame(animationFrame);
};
}, [location.pathname]);
return <canvas ref={canvasRef} className="fixed inset-0 pointer-events-none z-[100]" style={{ opacity: 1 }} />;
}
2. Add these CSS classes to your global CSS file (e.g., index.css or globals.css):
/* Interactive Hover Effects */
.hover-lift {
transition: all 0.3s ease-out;
}
.hover-lift:hover {
transform: translateY(-4px);
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
.button-glow {
position: relative;
overflow: hidden;
transition: all 0.3s ease;
}
.button-glow::after {
content: '';
position: absolute;
inset: 0;
background-color: rgba(255, 255, 255, 0.2);
opacity: 0;
transition: opacity 0.3s ease;
pointer-events: none;
}
.button-glow:hover::after {
opacity: 1;
}
3. Integration Instructions:
Import <CursorTrail /> into your main App.tsx or Layout.tsx file and place it near the top of your layout (it is fixed and pointer-events-none, so it won't block clicks).
Add the hover-lift class to any cards or containers you want to float up smoothly.
Add the button-glow class to your primary Call-to-Action buttons.