{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "constellation-field",
  "title": "Constellation Field",
  "description": "Drifting points joined by lines, brightening around the pointer.",
  "registryDependencies": [
    "utils",
    "https://ui.tinkererslabs.com/r/render-surface.json"
  ],
  "files": [
    {
      "path": "registry/default/constellation-field/constellation-field.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport {\n  RenderSurface,\n  useThemeColors,\n  type SurfaceColor,\n} from \"@/registry/default/render-surface/render-surface\"\nimport { cn } from \"@/lib/utils\"\n\nexport type ConstellationFieldProps = React.HTMLAttributes<HTMLDivElement> & {\n  /** Points per 10,000 CSS pixels of area, so the density survives a resize. */\n  density?: number\n  speed?: number\n  /** Points closer together than this are joined by a line. */\n  linkDistance?: number\n  /** How far the pointer reaches, in pixels. Zero turns the reaction off. */\n  pointerRadius?: number\n  color?: string\n  paused?: boolean\n  surfaceClassName?: string\n}\n\ntype Point = { x: number; y: number; vx: number; vy: number; r: number }\n\ntype Field = {\n  points: Point[]\n  pointer: { x: number; y: number; active: boolean }\n}\n\nfunction rgba([r, g, b]: SurfaceColor, alpha: number) {\n  return `rgba(${Math.round(r * 255)}, ${Math.round(g * 255)}, ${Math.round(b * 255)}, ${alpha})`\n}\n\nexport function ConstellationField({\n  density = 5,\n  speed = 1,\n  linkDistance = 120,\n  pointerRadius = 160,\n  color = \"--foreground\",\n  paused,\n  className,\n  surfaceClassName,\n  children,\n  ...rootProps\n}: ConstellationFieldProps) {\n  const rootRef = React.useRef<HTMLDivElement>(null)\n  const pointer = React.useRef({ x: 0, y: 0, active: false })\n\n  const tokens = React.useMemo(\n    () => (color.startsWith(\"--\") ? [color] : []),\n    [color]\n  )\n\n  React.useEffect(() => {\n    if (pointerRadius <= 0) return\n\n    const move = (event: PointerEvent) => {\n      const box = rootRef.current?.getBoundingClientRect()\n      if (!box) return\n\n      const x = event.clientX - box.left\n      const y = event.clientY - box.top\n\n      pointer.current.active =\n        x >= 0 && x <= box.width && y >= 0 && y <= box.height\n      pointer.current.x = x\n      pointer.current.y = y\n    }\n\n    window.addEventListener(\"pointermove\", move, { passive: true })\n    return () => window.removeEventListener(\"pointermove\", move)\n  }, [pointerRadius])\n\n  const resolved = useThemeColors(rootRef, tokens)\n  const ink = color.startsWith(\"--\") ? resolved[color] : undefined\n  const ready = ink !== undefined\n\n  const setup = React.useCallback(\n    ({ size }: { size: { width: number; height: number } }) => {\n      const total = Math.round((size.width * size.height * density) / 10000)\n\n      const points: Point[] = []\n      for (let index = 0; index < total; index += 1) {\n        const angle = (index * 2.399) % (Math.PI * 2)\n        points.push({\n          x: Math.random() * size.width,\n          y: Math.random() * size.height,\n          vx: Math.cos(angle) * (6 + (index % 7)),\n          vy: Math.sin(angle) * (6 + (index % 5)),\n          r: 0.8 + ((index * 0.37) % 1.4),\n        })\n      }\n\n      return { points, pointer: pointer.current } satisfies Field\n    },\n    [density]\n  )\n\n  const draw = React.useCallback(\n    ({\n      context,\n      size,\n      state,\n      delta,\n    }: {\n      context: CanvasRenderingContext2D\n      size: { width: number; height: number; dpr: number }\n      state: Field\n      delta: number\n    }) => {\n      if (!ink) return\n\n      const { width, height, dpr } = size\n      context.setTransform(dpr, 0, 0, dpr, 0, 0)\n      context.clearRect(0, 0, width, height)\n\n      const step = delta * speed\n      const reach = pointerRadius * pointerRadius\n\n      for (const point of state.points) {\n        point.x += point.vx * step\n        point.y += point.vy * step\n\n        if (point.x < 0) point.x += width\n        if (point.x > width) point.x -= width\n        if (point.y < 0) point.y += height\n        if (point.y > height) point.y -= height\n      }\n\n      context.lineWidth = 1\n\n      for (let a = 0; a < state.points.length; a += 1) {\n        const first = state.points[a]!\n\n        for (let b = a + 1; b < state.points.length; b += 1) {\n          const second = state.points[b]!\n          const dx = first.x - second.x\n          const dy = first.y - second.y\n          const distance = Math.hypot(dx, dy)\n\n          if (distance > linkDistance) continue\n\n          context.strokeStyle = rgba(ink, 0.18 * (1 - distance / linkDistance))\n          context.beginPath()\n          context.moveTo(first.x, first.y)\n          context.lineTo(second.x, second.y)\n          context.stroke()\n        }\n\n        let glow = 0\n        if (state.pointer.active && pointerRadius > 0) {\n          const dx = first.x - state.pointer.x\n          const dy = first.y - state.pointer.y\n          const squared = dx * dx + dy * dy\n          if (squared < reach) glow = 1 - squared / reach\n        }\n\n        context.fillStyle = rgba(ink, 0.45 + glow * 0.55)\n        context.beginPath()\n        context.arc(first.x, first.y, first.r + glow * 1.6, 0, Math.PI * 2)\n        context.fill()\n      }\n    },\n    [ink, linkDistance, pointerRadius, speed]\n  )\n\n  return (\n    <div\n      ref={rootRef}\n      data-slot=\"constellation-field\"\n      className={cn(\"relative isolate overflow-hidden\", className)}\n      {...rootProps}\n    >\n      {ready && (\n        <RenderSurface<Field, \"2d\">\n          setup={setup}\n          draw={draw}\n          paused={paused}\n          className={cn(\n            \"pointer-events-none absolute inset-0 -z-10\",\n            surfaceClassName\n          )}\n        />\n      )}\n      {children}\n    </div>\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "scene",
    "background",
    "particles",
    "pointer"
  ],
  "type": "registry:ui"
}