Kinetic Matrix
Kinetic Matrix é um componente para React e Tailwind CSS, escrito para a NedDev e teu para usares. Copia e cola no teu projeto.
NedDev
other
O que é
Kinetic Matrix é um componente para React e Tailwind CSS, escrito para a NedDev e teu para usares. Copia e cola no teu projeto.
Como usar
Abre o componente no catálogo, carrega em copiar, e cola no teu projeto. O prompt copiado leva o código e as regras para o teu agente o reproduzir sem alterar nada.
Quatro cópias grátis por semana. Sem cartão.
Precisa de
npm i lucide-react
Licença
Este componente foi escrito para este catálogo. É teu para usares em trabalho de cliente e em produtos que vendas, nos termos de uso.
Kinetic Matrix
"use client";
import React, { useEffect, useRef, useState, useCallback } from 'react';
import { Sparkles, Play, Pause } from 'lucide-react';
import { cn } from '@/lib/utils';
interface MatrixNode {
x: number;
y: number;
vx: number;
vy: number;
baseX: number;
baseY: number;
col: number;
row: number;
radius: number;
label: string;
tension: number;
pulsePhase: number;
}
interface SynapticPulse {
fromNode: number;
toNode: number;
progress: number;
speed: number;
}
interface GravitationalShockwave {
x: number;
y: number;
radius: number;
maxRadius: number;
power: number;
}
export interface KineticMatrixProps {
title?: string;
className?: string;
}
export function KineticMatrix({
title = "TOPOLOGY",
className = "",
}: KineticMatrixProps) {
const containerRef = useRef<HTMLDivElement | null>(null);
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const [isDarkMode, setIsDarkMode] = useState(true);
const [isRunning, setIsRunning] = useState(true);
// Sync color scheme preference
useEffect(() => {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
setIsDarkMode(mediaQuery.matches);
const handler = (e: MediaQueryListEvent) => setIsDarkMode(e.matches);
mediaQuery.addEventListener('change', handler);
return () => mediaQuery.removeEventListener('change', handler);
}, []);
// Pointer state with smooth inertia
const pointerRef = useRef({
x: -2000,
y: -2000,
prevX: -2000,
prevY: -2000,
vx: 0,
vy: 0,
radius: 220,
isDown: false,
});
const nodesRef = useRef<MatrixNode[]>([]);
const pulsesRef = useRef<SynapticPulse[]>([]);
const shockwavesRef = useRef<GravitationalShockwave[]>([]);
const dimensionsRef = useRef({ width: 0, height: 0, cols: 0, rows: 0, spacing: 52 });
// Grid lattice initializer
const buildLattice = useCallback((width: number, height: number) => {
const spacing = 52;
const cols = Math.ceil(width / spacing) + 1;
const rows = Math.ceil(height / spacing) + 1;
const nodes: MatrixNode[] = [];
for (let c = 0; c < cols; c++) {
for (let r = 0; r < rows; r++) {
const x = c * spacing;
const y = r * spacing;
nodes.push({
x,
y,
vx: 0,
vy: 0,
baseX: x,
baseY: y,
col: c,
row: r,
radius: 1.4,
label: `0x${((c * 17 + r * 31) % 256).toString(16).padStart(2, '0').toUpperCase()}`,
tension: 0,
pulsePhase: Math.random() * Math.PI * 2,
});
}
}
dimensionsRef.current = { width, height, cols, rows, spacing };
nodesRef.current = nodes;
pulsesRef.current = [];
}, []);
// Canvas Resize Observer with subpixel rounding correction
useEffect(() => {
const container = containerRef.current;
const canvas = canvasRef.current;
if (!container || !canvas) return;
const ctx = canvas.getContext('2d', { alpha: false });
if (!ctx) return;
const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
const rect = entry.contentRect;
const dpr = Math.min(window.devicePixelRatio || 1, 2);
canvas.width = Math.floor(rect.width * dpr);
canvas.height = Math.floor(rect.height * dpr);
canvas.style.width = `${rect.width}px`;
canvas.style.height = `${rect.height}px`;
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.scale(dpr, dpr);
buildLattice(rect.width, rect.height);
}
});
resizeObserver.observe(container);
return () => resizeObserver.disconnect();
}, [buildLattice]);
// Main High-Performance Simulation & Rendering Loop
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d', { alpha: false });
if (!ctx) return;
let animId = 0;
let lastTime = performance.now();
const render = (now: number) => {
const dt = Math.min((now - lastTime) / 1000, 0.033);
lastTime = now;
if (!isRunning) {
animId = requestAnimationFrame(render);
return;
}
const { width, height, cols, rows, spacing } = dimensionsRef.current;
const nodes = nodesRef.current;
const pulses = pulsesRef.current;
const shockwaves = shockwavesRef.current;
const pointer = pointerRef.current;
// Pointer velocity interpolation
pointer.vx = (pointer.x - pointer.prevX) / (dt * 1000 || 1);
pointer.vy = (pointer.y - pointer.prevY) / (dt * 1000 || 1);
pointer.prevX = pointer.x;
pointer.prevY = pointer.y;
const mouseSpeed = Math.sqrt(pointer.vx * pointer.vx + pointer.vy * pointer.vy);
const isDark = document.documentElement.classList.contains('dark') || isDarkMode;
const bgColor = isDark ? '#06070a' : '#f9fafb';
const nodeColor = isDark ? '255, 255, 255' : '17, 24, 39';
const accentGlow = isDark ? '255, 255, 255' : '0, 0, 0';
ctx.fillStyle = bgColor;
ctx.fillRect(0, 0, width, height);
// 1. Propagate Shockwaves
for (let s = shockwaves.length - 1; s >= 0; s--) {
const sw = shockwaves[s];
sw.radius += 400 * dt;
sw.power *= Math.pow(0.12, dt);
if (sw.radius > sw.maxRadius || sw.power < 0.01) {
shockwaves.splice(s, 1);
}
}
// 2. Physics Step (Hooke's Spring-Mass Lattice)
const SPRING_K = 26;
const DAMPING = 0.85;
for (let i = 0; i < nodes.length; i++) {
const n = nodes[i];
n.pulsePhase += dt * 3.2;
const dx = pointer.x - n.x;
const dy = pointer.y - n.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < pointer.radius && dist > 0) {
const ratio = 1 - dist / pointer.radius;
const force = ratio * (1600 + mouseSpeed * 180 + (pointer.isDown ? 2400 : 0));
const angle = Math.atan2(dy, dx);
n.vx -= Math.cos(angle) * force * dt;
n.vy -= Math.sin(angle) * force * dt;
n.tension = Math.min(1, n.tension + ratio * 0.5);
}
for (let s = 0; s < shockwaves.length; s++) {
const sw = shockwaves[s];
const swDx = n.x - sw.x;
const swDy = n.y - sw.y;
const swDist = Math.sqrt(swDx * swDx + swDy * swDy);
const delta = Math.abs(swDist - sw.radius);
if (delta < 55) {
const force = (1 - delta / 55) * sw.power * 2800;
const angle = Math.atan2(swDy, swDx);
n.vx += Math.cos(angle) * force * dt;
n.vy += Math.sin(angle) * force * dt;
n.tension = 1.0;
}
}
const hx = n.baseX - n.x;
const hy = n.baseY - n.y;
n.vx += hx * SPRING_K * dt;
n.vy += hy * SPRING_K * dt;
n.vx *= DAMPING;
n.vy *= DAMPING;
n.x += n.vx * dt * 60;
n.y += n.vy * dt * 60;
n.tension = Math.max(0, n.tension - dt * 0.9);
}
// 3. Spawn Random Synaptic Traveling Pulses
if (Math.random() < 0.3 && nodes.length > 0 && pulses.length < 40) {
const fromIdx = Math.floor(Math.random() * nodes.length);
const fromNode = nodes[fromIdx];
const possibleDirections = [
{ dc: 1, dr: 0 },
{ dc: -1, dr: 0 },
{ dc: 0, dr: 1 },
{ dc: 0, dr: -1 },
];
const dir = possibleDirections[Math.floor(Math.random() * possibleDirections.length)];
const targetCol = fromNode.col + dir.dc;
const targetRow = fromNode.row + dir.dr;
if (targetCol >= 0 && targetCol < cols && targetRow >= 0 && targetRow < rows) {
const toIdx = targetCol * rows + targetRow;
if (toIdx >= 0 && toIdx < nodes.length) {
pulses.push({
fromNode: fromIdx,
toNode: toIdx,
progress: 0,
speed: 1.6 + Math.random() * 2.2,
});
}
}
}
// 4. Render Grid Tension Strands
for (let c = 0; c < cols; c++) {
for (let r = 0; r < rows; r++) {
const idx = c * rows + r;
const n = nodes[idx];
if (!n) continue;
if (c < cols - 1) {
const rightIdx = (c + 1) * rows + r;
const nr = nodes[rightIdx];
if (nr) drawLatticeLink(ctx, n, nr, spacing, isDark, nodeColor);
}
if (r < rows - 1) {
const downIdx = c * rows + (r + 1);
const nd = nodes[downIdx];
if (nd) drawLatticeLink(ctx, n, nd, spacing, isDark, nodeColor);
}
}
}
// 5. Render Synaptic Data Pulses
for (let p = pulses.length - 1; p >= 0; p--) {
const pulse = pulses[p];
pulse.progress += dt * pulse.speed;
const n1 = nodes[pulse.fromNode];
const n2 = nodes[pulse.toNode];
if (!n1 || !n2 || pulse.progress >= 1) {
if (n2) n2.tension = Math.min(1, n2.tension + 0.35);
pulses.splice(p, 1);
continue;
}
const px = n1.x + (n2.x - n1.x) * pulse.progress;
const py = n1.y + (n2.y - n1.y) * pulse.progress;
ctx.fillStyle = isDark ? '#ffffff' : '#000000';
ctx.beginPath();
ctx.arc(px, py, 2.0, 0, Math.PI * 2);
ctx.fill();
}
// 6. Render Nodes & HUD Elements
for (let i = 0; i < nodes.length; i++) {
const n = nodes[i];
const dx = pointer.x - n.x;
const dy = pointer.y - n.y;
const dist = Math.sqrt(dx * dx + dy * dy);
const isNear = dist < pointer.radius;
const currentRadius = isNear
? n.radius * 2.2 + n.tension * 1.5
: n.radius + Math.sin(n.pulsePhase) * 0.25;
if (isNear || n.tension > 0.1) {
ctx.fillStyle = `rgba(${accentGlow}, ${Math.min(1, 0.25 + n.tension * 0.65)})`;
ctx.beginPath();
ctx.arc(n.x, n.y, currentRadius * 2.2, 0, Math.PI * 2);
ctx.fill();
}
ctx.fillStyle = isNear || n.tension > 0.1
? (isDark ? '#ffffff' : '#000000')
: `rgba(${nodeColor}, ${isDark ? 0.28 : 0.2})`;
ctx.beginPath();
ctx.arc(n.x, n.y, Math.max(0.8, currentRadius), 0, Math.PI * 2);
ctx.fill();
if (dist < 90) {
const radarRing = ((n.pulsePhase * 20) % 32) + 4;
const ringAlpha = (1 - radarRing / 36) * 0.35;
ctx.strokeStyle = `rgba(${accentGlow}, ${ringAlpha})`;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.arc(n.x, n.y, radarRing, 0, Math.PI * 2);
ctx.stroke();
ctx.font = '8px ui-monospace, SFMono-Regular, Consolas, monospace';
ctx.fillStyle = `rgba(${accentGlow}, 0.85)`;
ctx.fillText(n.label, n.x + 9, n.y - 9);
}
}
animId = requestAnimationFrame(render);
};
animId = requestAnimationFrame(render);
return () => cancelAnimationFrame(animId);
}, [isRunning, isDarkMode]);
const drawLatticeLink = (
ctx: CanvasRenderingContext2D,
n1: MatrixNode,
n2: MatrixNode,
restLen: number,
isDark: boolean,
nodeColor: string
) => {
const dx = n1.x - n2.x;
const dy = n1.y - n2.y;
const dist = Math.sqrt(dx * dx + dy * dy);
const stretch = Math.abs(dist - restLen) / restLen;
const isTensioned = n1.tension > 0.05 || n2.tension > 0.05 || stretch > 0.1;
if (isTensioned) {
const glow = Math.max(n1.tension, n2.tension, stretch * 2);
ctx.strokeStyle = isDark
? `rgba(255, 255, 255, ${Math.min(1, 0.25 + glow * 0.75)})`
: `rgba(0, 0, 0, ${Math.min(1, 0.25 + glow * 0.75)})`;
ctx.lineWidth = 0.8 + glow * 1.4;
} else {
ctx.strokeStyle = `rgba(${nodeColor}, ${isDark ? 0.08 : 0.05})`;
ctx.lineWidth = 0.65;
}
ctx.beginPath();
ctx.moveTo(n1.x, n1.y);
ctx.lineTo(n2.x, n2.y);
ctx.stroke();
};
const handlePointerMove = (e: React.MouseEvent<HTMLDivElement>) => {
const container = containerRef.current;
if (!container) return;
const rect = container.getBoundingClientRect();
pointerRef.current.x = e.clientX - rect.left;
pointerRef.current.y = e.clientY - rect.top;
};
const handlePointerDown = (e: React.MouseEvent<HTMLDivElement>) => {
const container = containerRef.current;
if (!container) return;
pointerRef.current.isDown = true;
const rect = container.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
shockwavesRef.current.push({
x,
y,
radius: 8,
maxRadius: 420,
power: 1.2,
});
};
const handlePointerUp = () => {
pointerRef.current.isDown = false;
};
const handlePointerLeave = () => {
pointerRef.current.x = -2000;
pointerRef.current.y = -2000;
pointerRef.current.isDown = false;
};
const triggerCentralImpulse = () => {
const { width, height } = dimensionsRef.current;
shockwavesRef.current.push({
x: width / 2,
y: height / 2,
radius: 10,
maxRadius: Math.max(width, height) * 0.85,
power: 1.4,
});
};
return (
<div
ref={containerRef}
onMouseMove={handlePointerMove}
onMouseDown={handlePointerDown}
onMouseUp={handlePointerUp}
onMouseLeave={handlePointerLeave}
className={cn(
"group relative flex h-full w-full select-none flex-col justify-between overflow-hidden bg-neutral-50 transition-colors duration-700 dark:bg-[#06070a]",
className
)}
>
{/* Absolute Edge-to-Edge Canvas Viewport */}
<canvas ref={canvasRef} className="absolute inset-0 block h-full w-full cursor-crosshair" />
{/* Content Deck Wrapper with Safe Padding */}
<div className="relative z-20 flex h-full w-full flex-col justify-between p-6 md:p-10">
{/* Top Header Deck */}
<header className="flex w-full items-center justify-between font-mono text-[11px] text-neutral-500 dark:text-neutral-400">
<div className="flex items-center gap-2">
<button
onClick={triggerCentralImpulse}
className="flex items-center gap-1.5 rounded-lg border border-neutral-300/80 bg-white/70 px-2.5 py-1.5 backdrop-blur-md transition-all hover:bg-neutral-100 dark:border-neutral-800 dark:bg-neutral-900/70 dark:hover:bg-neutral-800"
title="Trigger Shockwave"
>
<Sparkles className="size-3 text-neutral-800 dark:text-neutral-200" />
<span className="hidden sm:inline font-mono text-[10px]">PULSE</span>
</button>
<button
onClick={() => setIsRunning((prev) => !prev)}
className="flex items-center gap-1.5 rounded-lg border border-neutral-300/80 bg-white/70 px-2.5 py-1.5 backdrop-blur-md transition-all hover:bg-neutral-100 dark:border-neutral-800 dark:bg-neutral-900/70 dark:hover:bg-neutral-800"
>
{isRunning ? <Pause className="size-3" /> : <Play className="size-3" />}
<span className="font-mono text-[10px]">{isRunning ? "FREEZE" : "RUN"}</span>
</button>
</div>
</header>
{/* Center Hero Stencil Typography */}
<main className="pointer-events-none flex flex-col items-center justify-center text-center">
<h1 className="font-mono text-5xl font-black tracking-tighter uppercase sm:text-7xl md:text-9xl text-neutral-900 dark:text-white">
{title}
</h1>
</main>
<div />
</div>
</div>
);
}
export default KineticMatrix;