"use client"; import { useEffect, useRef, type ReactNode } from "react"; import { useReducedMotion } from "motion/react"; type CursorTrailProps = { /** The area the trail draws over. */ children: ReactNode; /** Trail colour. */ color?: string; /** Width of the trail at the pointer, in pixels. */ width?: number; /** Seconds a point of the trail lives. */ life?: number; className?: string; }; /* CursorTrail: a ribbon follows the pointer, thinning and fading behind it. Draws only while there is a trail to show. */ export function CursorTrail({ children, color = "#a78bfa", width = 10, life = 0.5, className = "" }: CursorTrailProps) { const reduce = useReducedMotion(); const zone = useRef(null); const canvas = useRef(null); useEffect(() => { const box = zone.current; const c = canvas.current; const ctx = c?.getContext("2d"); if (reduce || !box || !c || !ctx) return; const points: { x: number; y: number; t: number }[] = []; let frame = 0; let scale = 1; const resize = () => { scale = Math.min(window.devicePixelRatio, 2); c.width = box.clientWidth * scale; c.height = box.clientHeight * scale; }; const observer = new ResizeObserver(resize); observer.observe(box); const draw = () => { const now = performance.now(); const span = life * 1000; while (points.length && now - points[0].t > span) points.shift(); ctx.setTransform(scale, 0, 0, scale, 0, 0); ctx.clearRect(0, 0, box.clientWidth, box.clientHeight); ctx.strokeStyle = color; ctx.lineCap = "butt"; ctx.lineJoin = "round"; // Each piece runs mid-point to mid-point, so the corners stay smooth. for (let i = 1; i < points.length - 1; i++) { const [a, b, d] = [points[i - 1], points[i], points[i + 1]]; const fade = 1 - (now - b.t) / span; ctx.globalAlpha = fade; ctx.lineWidth = width * fade; ctx.beginPath(); ctx.moveTo((a.x + b.x) / 2, (a.y + b.y) / 2); ctx.quadraticCurveTo(b.x, b.y, (b.x + d.x) / 2, (b.y + d.y) / 2); ctx.stroke(); } frame = points.length ? requestAnimationFrame(draw) : 0; }; const onMove = (e: PointerEvent) => { const r = box.getBoundingClientRect(); points.push({ x: e.clientX - r.left, y: e.clientY - r.top, t: performance.now() }); if (!frame) frame = requestAnimationFrame(draw); }; box.addEventListener("pointermove", onMove); return () => { box.removeEventListener("pointermove", onMove); observer.disconnect(); cancelAnimationFrame(frame); }; }, [reduce, color, width, life]); return (
{children}
); }