{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "grid-distortion",
  "type": "registry:ui",
  "title": "Grid distortion",
  "description": "Bends a grid or media surface around input and returns it smoothly.",
  "dependencies": [
    "motion"
  ],
  "categories": [
    "visual-ambient"
  ],
  "docs": "https://motion-lexicon.pages.dev/en/components/grid-distortion/",
  "meta": {
    "engines": [
      "motion",
      "css"
    ],
    "runtimeCost": "medium",
    "signature": "Field bends around interaction and returns without snapping",
    "sceneFamily": "spatial-dark",
    "motionRole": "ambient",
    "primaryState": "Still violet grid media",
    "assetProvenance": "self-contained"
  },
  "files": [
    {
      "path": "src/registry/components/grid-distortion.tsx",
      "type": "registry:ui",
      "target": "components/motion-lexicon/grid-distortion.tsx",
      "content": "\"use client\";\nimport { useEffect, useRef, type PointerEvent as ReactPointerEvent, type ReactNode } from \"react\";\nimport { useReducedMotion } from \"motion/react\";\nexport type GridDistortionProps = { art: ReactNode; alt: string; label?: string; className?: string };\nexport function GridDistortion({ art, alt, label = \"Grid distortion\", className = \"\" }: GridDistortionProps) { const canvasRef = useRef<HTMLCanvasElement>(null); const pointer = useRef({ x: -9999, y: -9999, active: false }); const requestRef = useRef<() => void>(() => undefined); const reduced = useReducedMotion() === true; useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; const context = canvas.getContext(\"2d\"); if (!context) return; let width = 1; let height = 1; let frame: number | null = null; let visible = true; const paint = () => { frame = null; if (!visible) return; context.clearRect(0, 0, width, height); context.strokeStyle = \"rgba(237,229,255,.32)\"; context.lineWidth = 1; const gap = 22; for (let x = 0; x <= width; x += gap) { context.beginPath(); for (let y = 0; y <= height; y += 6) { const dx = x - pointer.current.x; const dy = y - pointer.current.y; const force = pointer.current.active && !reduced ? Math.exp(-(dx * dx + dy * dy) / 8200) * 15 : 0; const bend = Math.sin(y / 18) * force; if (y === 0) context.moveTo(x + bend, y); else context.lineTo(x + bend, y); } context.stroke(); } for (let y = 0; y <= height; y += gap) { context.beginPath(); for (let x = 0; x <= width; x += 6) { const dx = x - pointer.current.x; const dy = y - pointer.current.y; const force = pointer.current.active && !reduced ? Math.exp(-(dx * dx + dy * dy) / 8200) * 15 : 0; const bend = Math.cos(x / 18) * force; if (x === 0) context.moveTo(x, y + bend); else context.lineTo(x, y + bend); } context.stroke(); } }; const request = () => { if (frame === null && visible) frame = requestAnimationFrame(paint); }; requestRef.current = request; const resize = new ResizeObserver(([entry]) => { const dpr = Math.min(window.devicePixelRatio || 1, 1.5); width = Math.max(1, entry.contentRect.width); height = Math.max(1, entry.contentRect.height); canvas.width = width * dpr; canvas.height = height * dpr; context.setTransform(dpr, 0, 0, dpr, 0, 0); request(); }); resize.observe(canvas); const observer = new IntersectionObserver(([entry]) => { visible = entry.isIntersecting && !document.hidden; if (visible) request(); else if (frame !== null) { cancelAnimationFrame(frame); frame = null; } }); observer.observe(canvas); request(); return () => { resize.disconnect(); observer.disconnect(); if (frame !== null) cancelAnimationFrame(frame); }; }, [reduced]); const move = (event: ReactPointerEvent<HTMLDivElement>) => { const rect = event.currentTarget.getBoundingClientRect(); pointer.current = { x: event.clientX - rect.left, y: event.clientY - rect.top, active: true }; requestRef.current(); }; return <figure className={`relative overflow-hidden rounded-[20px] border border-[#dbc5ef]/20 bg-[#21162e] shadow-[0_24px_56px_-38px_rgba(0,0,0,.9)] ${className}`}><div role=\"img\" aria-label={alt} tabIndex={0} onPointerMove={move} onPointerLeave={() => { pointer.current.active = false; requestRef.current(); }} onFocus={() => requestRef.current()} className=\"relative aspect-[16/10] overflow-hidden outline-none focus-visible:ring-2 focus-visible:ring-[#e6b5ff]\"><div aria-hidden className=\"absolute inset-0\">{art}</div><canvas ref={canvasRef} aria-hidden className=\"pointer-events-none absolute inset-0 size-full mix-blend-screen\" /><span className=\"absolute bottom-3 left-3 rounded-full border border-white/20 bg-black/25 px-2.5 py-1 font-mono text-[8px] tracking-[.14em] text-white\">{label}</span></div></figure>; }\n"
    }
  ]
}
