{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"pixel-highlight","type":"registry:ui","title":"Pixel Highlight","description":"Hovering or focusing fills the element with twinkling pixels that ripple out from the pointer, and ripple away on leave.","dependencies":[],"registryDependencies":["utils"],"files":[{"path":"registry/ui/pixel-highlight.tsx","type":"registry:ui","content":"\"use client\";\n\nimport { useEffect, useRef, type ReactNode } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface PixelHighlightProps {\n  children?: ReactNode;\n  /** Pixel colors, picked at random per pixel. */\n  colors?: string[];\n  /** Px between pixel centers. */\n  gap?: number;\n  /** Ripple speed in px per second. */\n  speed?: number;\n  /** Twinkle strength while lit, 0–1. */\n  twinkle?: number;\n  className?: string;\n}\n\nconst COLORS = [\"#c6ff3d\", \"#86efac\", \"#22d3ee\"];\n\nexport function PixelHighlight({\n  children,\n  colors = COLORS,\n  gap = 7,\n  speed = 900,\n  twinkle = 0.5,\n  className,\n}: PixelHighlightProps) {\n  const root = useRef<HTMLDivElement>(null);\n  const cv = useRef<HTMLCanvasElement>(null);\n\n  useEffect(() => {\n    const el = root.current!;\n    const canvas = cv.current!;\n    const ctx = canvas.getContext(\"2d\")!;\n    const reduced = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n    type Px = { x: number; y: number; c: string; a: number; ph: number; v: number; d: number };\n    let px: Px[] = [];\n    let w = 0;\n    let h = 0;\n    let on = false;\n    let t0 = 0;\n    let raf = 0;\n\n    const resize = () => {\n      const dpr = Math.min(window.devicePixelRatio || 1, 2);\n      w = canvas.offsetWidth;\n      h = canvas.offsetHeight;\n      canvas.width = Math.round(w * dpr);\n      canvas.height = Math.round(h * dpr);\n      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n      px = [];\n      for (let y = gap / 2; y < h; y += gap)\n        for (let x = gap / 2; x < w; x += gap)\n          px.push({ x, y, c: colors[(Math.random() * colors.length) | 0], a: 0.1 + Math.random() * 0.35, ph: Math.random() * 6.28, v: 0, d: 0 });\n      kick();\n    };\n\n    const frame = (now: number) => {\n      const el2 = now - t0;\n      const t = now / 1000;\n      const max = gap * 0.5;\n      let busy = on;\n      ctx.clearRect(0, 0, w, h);\n      for (const p of px) {\n        // Each pixel waits for the ripple front, then eases toward lit (hover) or off (leave).\n        if (reduced || el2 > p.d / (speed / 1000)) {\n          const goal = on ? 1 : 0;\n          p.v += (goal - p.v) * (reduced ? 1 : 0.18);\n          if (Math.abs(goal - p.v) < 0.01) p.v = goal;\n        }\n        if (p.v > 0) busy = true;\n        if (p.v <= 0) continue;\n        const tw = 1 - twinkle * (0.5 + 0.5 * Math.sin(t * 5 + p.ph));\n        const s = max * p.v * (reduced ? 1 : tw);\n        // Front-of-wave pixels flash brighter.\n        ctx.globalAlpha = Math.min(1, p.a + (1 - p.v) * 0.8);\n        ctx.fillStyle = p.c;\n        ctx.fillRect(p.x - s / 2, p.y - s / 2, s, s);\n      }\n      ctx.globalAlpha = 1;\n      raf = busy ? requestAnimationFrame(frame) : 0;\n    };\n    function kick() {\n      if (!raf) raf = requestAnimationFrame(frame);\n    }\n\n    const start = (active: boolean, ox: number, oy: number) => {\n      on = active;\n      t0 = performance.now();\n      for (const p of px) p.d = Math.hypot(p.x - ox, p.y - oy);\n      kick();\n    };\n    const local = (e: PointerEvent) => {\n      const r = el.getBoundingClientRect();\n      return [e.clientX - r.left, e.clientY - r.top] as const;\n    };\n    const enter = (e: PointerEvent) => start(true, ...local(e));\n    const leave = (e: PointerEvent) => {\n      if (!el.contains(document.activeElement)) start(false, ...local(e));\n    };\n    const focusIn = () => !on && start(true, w / 2, h / 2);\n    const focusOut = (e: FocusEvent) => {\n      if (!el.contains(e.relatedTarget as Node | null) && !el.matches(\":hover\")) start(false, w / 2, h / 2);\n    };\n\n    const ro = new ResizeObserver(resize);\n    ro.observe(canvas);\n    el.addEventListener(\"pointerenter\", enter);\n    el.addEventListener(\"pointerleave\", leave);\n    el.addEventListener(\"focusin\", focusIn);\n    el.addEventListener(\"focusout\", focusOut);\n    return () => {\n      cancelAnimationFrame(raf);\n      ro.disconnect();\n      el.removeEventListener(\"pointerenter\", enter);\n      el.removeEventListener(\"pointerleave\", leave);\n      el.removeEventListener(\"focusin\", focusIn);\n      el.removeEventListener(\"focusout\", focusOut);\n    };\n  }, [colors, gap, speed, twinkle]);\n\n  return (\n    <div ref={root} className={cn(\"relative isolate overflow-hidden\", className)}>\n      <canvas\n        ref={cv}\n        aria-hidden\n        style={{ position: \"absolute\", inset: 0, width: \"100%\", height: \"100%\", zIndex: -1, pointerEvents: \"none\" }}\n      />\n      {children}\n    </div>\n  );\n}\n"}]}