Docs

Click Cursor Effect

An interactive React component that adds a dynamic bubble effect, visually tracking cursor movement in real time.

Installation

Install dependencies

npm install framer-motion lucide-react

Run the following command

It will create a new file click-cursor-effect.tsx inside the components/mage-ui/cursor-effects directory.

mkdir -p components/mage-ui/cursor-effects && touch components/mage-ui/cursor-effects/click-cursor-effect.tsx

Paste the code

Open the newly created file and paste the following code:

 
'use client';
 
import React, { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
 
const ClickEffectCursor = () => {
  const [mousePosition, setMousePosition] = useState({ x: null, y: null });
  const [clicks, setClicks] = useState([]);
  const [isHovering, setIsHovering] = useState(false);
  const [trail, setTrail] = useState([]);
 
  // Handle mouse movement
  useEffect(() => {
    const updateMousePosition = (e) => {
      setMousePosition({ x: e.clientX, y: e.clientY });
 
      // Add point to trail
      setTrail((prev) => [
        ...prev.slice(-20),
        { x: e.clientX, y: e.clientY, id: Date.now() },
      ]);
    };
 
    window.addEventListener('mousemove', updateMousePosition);
    return () => window.removeEventListener('mousemove', updateMousePosition);
  }, []);
 
  const handleClick = (e) => {
    const newClick = {
      x: e.clientX,
      y: e.clientY,
      id: Date.now(),
    };
    setClicks((prev) => [...prev, newClick]);
 
    // Remove click effect after animation
    setTimeout(() => {
      setClicks((prev) => prev.filter((click) => click.id !== newClick.id));
    }, 1000);
  };
 
  return (
    <div
      className='w-full h-screen bg-black'
      onClick={handleClick}
      onMouseEnter={() => setIsHovering(true)}
      onMouseLeave={() => setIsHovering(false)}
    >
      {/* Mouse trail */}
      <AnimatePresence>
        {trail.map((point, index) => (
          <motion.div
            key={point.id}
            className='fixed pointer-events-none'
            initial={{ scale: 1, opacity: 0.5 }}
            animate={{ scale: 0, opacity: 0 }}
            exit={{ opacity: 0 }}
            style={{
              left: point.x,
              top: point.y,
              transform: 'translate(-50%, -50%)',
            }}
            transition={{ duration: 0.5 }}
          >
            <div
              className='w-2 h-2 bg-pink-400 rounded-full'
              style={{
                opacity: (index / trail.length) * 0.5,
              }}
            />
          </motion.div>
        ))}
      </AnimatePresence>
 
      {/* Main cursor */}
      {mousePosition.x !== null && mousePosition.y !== null && (
        <motion.div
          className='fixed pointer-events-none z-50'
          animate={{
            x: mousePosition.x,
            y: mousePosition.y,
            scale: isHovering ? 1.5 : 1,
          }}
          transition={{
            type: 'spring',
            stiffness: 500,
            damping: 28,
            mass: 0.5,
          }}
          style={{
            transform: 'translate(-50%, -50%)',
          }}
        >
          <div className='relative'>
            <div className='w-6 h-6 bg-pink-500 rounded-full mix-blend-screen' />
            <div className='absolute inset-0 w-6 h-6 border-2 border-pink-300 rounded-full animate-ping' />
          </div>
        </motion.div>
      )}
 
      {/* Click effects */}
      <AnimatePresence>
        {clicks.map((click) => (
          <React.Fragment key={click.id}>
            {/* Ripple effect */}
            <motion.div
              className='fixed pointer-events-none'
              style={{
                left: click.x,
                top: click.y,
                transform: 'translate(-50%, -50%)',
              }}
              initial={{ scale: 0, opacity: 1 }}
              animate={{ scale: 2.5, opacity: 0 }}
              exit={{ opacity: 0 }}
              transition={{ duration: 0.8, ease: 'easeOut' }}
            >
              <div className='w-12 h-12 border-2 border-pink-400 rounded-full' />
            </motion.div>
 
            {/* Particle explosion */}
            {[...Array(12)].map((_, i) => (
              <motion.div
                key={i}
                className='fixed pointer-events-none w-2 h-2 bg-gradient-to-r from-pink-400 to-purple-500 rounded-full'
                style={{
                  left: click.x,
                  top: click.y,
                }}
                initial={{ scale: 0 }}
                animate={{
                  scale: [0, 1, 0],
                  x: Math.cos((i * Math.PI) / 6) * 80,
                  y: Math.sin((i * Math.PI) / 6) * 80,
                  opacity: [1, 0],
                }}
                transition={{
                  duration: 0.6,
                  ease: 'easeOut',
                  times: [0, 0.2, 1],
                }}
              />
            ))}
          </React.Fragment>
        ))}
      </AnimatePresence>
    </div>
  );
};
 
export default ClickEffectCursor;