Docs
Click Spark
Spark Effect On Click
Run the following command
It will create a new file click-spark.tsx
inside the components/mage-ui/cursor-effects
directory.
mkdir -p components/mage-ui/cursor-effects && touch components/mage-ui/cursor-effects/click-spark.tsx
Paste the code
Open the newly created file and paste the following code:
"use client";
import { useRef, useEffect, useCallback, ReactNode } from "react";
interface ClickSparkProps {
sparkColor?: string;
sparkSize?: number;
sparkRadius?: number;
sparkCount?: number;
duration?: number;
easing?: "linear" | "ease-in" | "ease-out" | "ease-in-out";
extraScale?: number;
children?: ReactNode;
}
const ClickSpark: React.FC<ClickSparkProps> = ({
sparkColor = "#fff",
sparkSize = 10,
sparkRadius = 15,
sparkCount = 8,
duration = 400,
easing = "ease-out",
extraScale = 1.0,
children
}) => {
const canvasRef = useRef<HTMLCanvasElement>(null);
const sparksRef = useRef<Array<{ x: number, y: number, angle: number, startTime: number }>>([]);
const startTimeRef = useRef<number | null>(null);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const parent = canvas.parentElement;
if (!parent) return;
let resizeTimeout: NodeJS.Timeout;
const resizeCanvas = () => {
const { width, height } = parent.getBoundingClientRect();
if (canvas.width !== width || canvas.height !== height) {
canvas.width = width;
canvas.height = height;
}
};
const handleResize = () => {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(resizeCanvas, 100);
};
const ro = new ResizeObserver(handleResize);
ro.observe(parent);
resizeCanvas();
return () => {
ro.disconnect();
clearTimeout(resizeTimeout);
};
}, []);
const easeFunc = useCallback(
(t: number) => {
switch (easing) {
case "linear":
return t;
case "ease-in":
return t * t;
case "ease-in-out":
return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
default:
return t * (2 - t);
}
},
[easing]
);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
let animationId: number;
const draw = (timestamp: number) => {
if (!startTimeRef.current) {
startTimeRef.current = timestamp;
}
ctx.clearRect(0, 0, canvas.width, canvas.height);
sparksRef.current = sparksRef.current.filter((spark) => {
const elapsed = timestamp - spark.startTime;
if (elapsed >= duration) {
return false;
}
const progress = elapsed / duration;
const eased = easeFunc(progress);
const distance = eased * sparkRadius * extraScale;
const lineLength = sparkSize * (1 - eased);
const x1 = spark.x + distance * Math.cos(spark.angle);
const y1 = spark.y + distance * Math.sin(spark.angle);
const x2 = spark.x + (distance + lineLength) * Math.cos(spark.angle);
const y2 = spark.y + (distance + lineLength) * Math.sin(spark.angle);
ctx.strokeStyle = sparkColor;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
return true;
});
animationId = requestAnimationFrame(draw);
};
animationId = requestAnimationFrame(draw);
return () => {
cancelAnimationFrame(animationId);
};
}, [
sparkColor,
sparkSize,
sparkRadius,
sparkCount,
duration,
easeFunc,
extraScale,
]);
const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
const canvas = canvasRef.current;
if (!canvas) return;
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const now = performance.now();
const newSparks = Array.from({ length: sparkCount }, (_, i) => ({
x,
y,
angle: (2 * Math.PI * i) / sparkCount,
startTime: now,
}));
sparksRef.current.push(...newSparks);
};
return (
<div
className="relative h-screen w-[90vw]"
onClick={handleClick}
>
<canvas
ref={canvasRef}
className="absolute top-0 left-0 w-full h-full block pointer-events-none select-none"
/>
{children}
</div>
);
};
export default ClickSpark;