"use client"; import React, { useEffect, useRef, useState } from "react"; export interface InteractiveGridBackgroundProps extends React.HTMLProps { gridSize?: number; gridColor?: string; darkGridColor?: string; effectColor?: string; darkEffectColor?: string; trailLength?: number; width?: number; height?: number; idleSpeed?: number; glow?: boolean; glowRadius?: number; children?: React.ReactNode; showFade?: boolean; fadeIntensity?: number; idleRandomCount?: number; // ✅ how many random cells move during idle } const InteractiveGridBackground: React.FC = ({ gridSize = 50, gridColor = "#e5e7eb", darkGridColor = "#27272a", effectColor = "rgba(0, 0, 0, 0.5)", darkEffectColor = "rgba(255, 255, 255, 0.5)", trailLength = 3, width, height, idleSpeed = 0.2, glow = true, glowRadius = 20, children, showFade = true, fadeIntensity = 20, idleRandomCount = 5, className, ...props }) => { const canvasRef = useRef(null); const containerRef = useRef(null); const [isDarkMode, setIsDarkMode] = useState(false); const trailRef = useRef<{ x: number; y: number }[]>([]); const idleTargetsRef = useRef<{ x: number; y: number }[]>([]); const idlePositionsRef = useRef<{ x: number; y: number }[]>([]); const mouseActiveRef = useRef(false); const lastMouseTimeRef = useRef(Date.now()); // Detect dark mode useEffect(() => { const updateDarkMode = () => { const prefersDark = window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches; setIsDarkMode( document.documentElement.classList.contains("dark") || prefersDark ); }; updateDarkMode(); const observer = new MutationObserver(() => updateDarkMode()); observer.observe(document.documentElement, { attributes: true }); return () => observer.disconnect(); }, []); // Mouse tracking useEffect(() => { const handleMouseMove = (e: MouseEvent) => { const container = containerRef.current; if (!container) return; const rect = container.getBoundingClientRect(); const rawX = e.clientX - rect.left; const rawY = e.clientY - rect.top; if (rawX < 0 || rawY < 0 || rawX > rect.width || rawY > rect.height) return; mouseActiveRef.current = true; lastMouseTimeRef.current = Date.now(); const snappedX = Math.floor(rawX / gridSize); const snappedY = Math.floor(rawY / gridSize); const last = trailRef.current[0]; if (!last || last.x !== snappedX || last.y !== snappedY) { trailRef.current.unshift({ x: snappedX, y: snappedY }); if (trailRef.current.length > trailLength) trailRef.current.pop(); } }; window.addEventListener("mousemove", handleMouseMove); return () => window.removeEventListener("mousemove", handleMouseMove); }, [gridSize, trailLength]); // Drawing logic useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext("2d"); if (!ctx) return; const canvasWidth = width || window.innerWidth; const canvasHeight = height || window.innerHeight; canvas.width = canvasWidth; canvas.height = canvasHeight; const cols = Math.floor(canvasWidth / gridSize); const rows = Math.floor(canvasHeight / gridSize); const lineColor = isDarkMode ? darkGridColor : gridColor; const glowColor = isDarkMode ? darkEffectColor : effectColor; // Initialize idle positions idleTargetsRef.current = Array.from({ length: idleRandomCount }, () => ({ x: Math.floor(Math.random() * cols), y: Math.floor(Math.random() * rows), })); idlePositionsRef.current = idleTargetsRef.current.map((p) => ({ ...p })); const draw = () => { ctx.clearRect(0, 0, canvasWidth, canvasHeight); // Draw grid lines ctx.strokeStyle = lineColor; ctx.lineWidth = 1; for (let x = 0; x <= canvasWidth; x += gridSize) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, canvasHeight); ctx.stroke(); } for (let y = 0; y <= canvasHeight; y += gridSize) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(canvasWidth, y); ctx.stroke(); } // Idle animation logic const idleThreshold = 2000; if (Date.now() - lastMouseTimeRef.current > idleThreshold) { mouseActiveRef.current = false; idlePositionsRef.current.forEach((pos, i) => { const target = idleTargetsRef.current[i]; const dx = target.x - pos.x; const dy = target.y - pos.y; if (Math.abs(dx) < 0.01 && Math.abs(dy) < 0.01) { // new random target when reached idleTargetsRef.current[i] = { x: Math.floor(Math.random() * cols), y: Math.floor(Math.random() * rows), }; } else { pos.x += dx * idleSpeed; pos.y += dy * idleSpeed; } const roundedX = Math.round(pos.x); const roundedY = Math.round(pos.y); const last = trailRef.current[0]; if (!last || last.x !== roundedX || last.y !== roundedY) { trailRef.current.unshift({ x: roundedX, y: roundedY }); if (trailRef.current.length > trailLength * idleRandomCount) trailRef.current.pop(); } }); } // Draw trail glow trailRef.current.forEach((cell, idx) => { const alpha = 1 - idx * (1 / (trailLength + 1)); const rgbaColor = glowColor.replace(/[\d.]+\)$/g, `${alpha})`); ctx.fillStyle = rgbaColor; if (glow) { ctx.shadowColor = rgbaColor; ctx.shadowBlur = glowRadius; } else { ctx.shadowBlur = 0; } ctx.fillRect(cell.x * gridSize, cell.y * gridSize, gridSize, gridSize); }); requestAnimationFrame(draw); }; draw(); }, [ gridSize, width, height, gridColor, darkGridColor, effectColor, darkEffectColor, isDarkMode, trailLength, idleSpeed, glow, glowRadius, idleRandomCount, ]); return (
{showFade && (
)}
{children}
); }; export default InteractiveGridBackground;