{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "metaballs",
  "title": "Metaballs",
  "description": "Blobs that merge as they meet, taking their two colours from your theme.",
  "registryDependencies": [
    "utils",
    "https://ui.tinkererslabs.com/r/render-surface.json"
  ],
  "files": [
    {
      "path": "registry/default/metaballs/metaballs.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport {\n  RenderSurface,\n  createQuadProgram,\n  useThemeColors,\n  type QuadProgram,\n  type SurfaceColor,\n} from \"@/registry/default/render-surface/render-surface\"\nimport { cn } from \"@/lib/utils\"\n\nconst MAX_BALLS = 12\n\nexport type MetaballsProps = React.HTMLAttributes<HTMLDivElement> & {\n  count?: number\n  base?: string\n  tint?: string\n  speed?: number\n  /** Size of each blob, as a fraction of the shorter edge. */\n  radius?: number\n  /** How sharply the blobs end. Lower is gooier. */\n  edge?: number\n  /**\n   * Makes the pointer one of the blobs, so it merges with the rest as it moves\n   * through them. Tracked on the window rather than on this element, so it\n   * still works when the field is a backdrop behind other content.\n   */\n  pointer?: boolean\n  paused?: boolean\n  surfaceClassName?: string\n}\n\nconst FRAGMENT = `\nprecision highp float;\nvarying vec2 vUv;\nuniform vec2 uResolution;\nuniform vec3 uBase;\nuniform vec3 uTint;\nuniform float uRadius;\nuniform float uEdge;\nuniform int uCount;\nuniform vec2 uBalls[${MAX_BALLS}];\nuniform vec2 uPointer;\nuniform float uPointerStrength;\n\nvoid main() {\n  float aspect = uResolution.x / max(uResolution.y, 1.0);\n  vec2 p = (vUv - 0.5) * vec2(aspect, 1.0);\n\n  float field = 0.0;\n  for (int i = 0; i < ${MAX_BALLS}; i++) {\n    if (i >= uCount) break;\n    vec2 d = p - uBalls[i];\n    field += (uRadius * uRadius) / max(dot(d, d), 0.0001);\n  }\n\n  // The pointer is a slightly larger blob, and fades in rather than appearing,\n  // so arriving at the field does not make one pop into existence.\n  if (uPointerStrength > 0.001) {\n    vec2 d = p - uPointer;\n    field +=\n      (uRadius * uRadius * 1.7 * uPointerStrength) / max(dot(d, d), 0.0001);\n  }\n\n  float mask = smoothstep(1.0 - uEdge, 1.0 + uEdge, field);\n  gl_FragColor = vec4(mix(uBase, uTint, mask), 1.0);\n}\n`\n\ntype Scene = { quad: QuadProgram | null; balls: Float32Array }\n\nexport function Metaballs({\n  count = 7,\n  base = \"--background\",\n  tint = \"--primary\",\n  speed = 1,\n  radius = 0.16,\n  edge = 0.35,\n  pointer = false,\n  paused,\n  className,\n  surfaceClassName,\n  children,\n  ...rootProps\n}: MetaballsProps) {\n  const rootRef = React.useRef<HTMLDivElement>(null)\n  const total = Math.max(1, Math.min(MAX_BALLS, Math.round(count)))\n\n  const cursor = React.useRef({ x: 0, y: 0, want: 0, at: 0, sx: 0, sy: 0 })\n\n  React.useEffect(() => {\n    if (!pointer) return\n\n    const move = (event: PointerEvent) => {\n      const box = rootRef.current?.getBoundingClientRect()\n      if (!box || box.width === 0 || box.height === 0) return\n\n      const x = (event.clientX - box.left) / box.width\n      const y = (event.clientY - box.top) / box.height\n      const inside = x >= 0 && x <= 1 && y >= 0 && y <= 1\n\n      cursor.current.want = inside ? 1 : 0\n      if (!inside) return\n\n      // The same space the blobs move in: centred, and widened by the aspect.\n      cursor.current.x = (x - 0.5) * (box.width / Math.max(box.height, 1))\n      cursor.current.y = 0.5 - y\n    }\n\n    window.addEventListener(\"pointermove\", move, { passive: true })\n    return () => window.removeEventListener(\"pointermove\", move)\n  }, [pointer])\n\n  const tokens = React.useMemo(\n    () => [base, tint].filter((entry) => entry.startsWith(\"--\")),\n    [base, tint]\n  )\n\n  const resolved = useThemeColors(rootRef, tokens)\n  const baseColor = base.startsWith(\"--\") ? resolved[base] : undefined\n  const tintColor = tint.startsWith(\"--\") ? resolved[tint] : undefined\n  const ready = baseColor !== undefined && tintColor !== undefined\n\n  const colorsRef = React.useRef<{ base: SurfaceColor; tint: SurfaceColor }>({\n    base: [0, 0, 0],\n    tint: [1, 1, 1],\n  })\n\n  React.useEffect(() => {\n    if (baseColor && tintColor) {\n      colorsRef.current = { base: baseColor, tint: tintColor }\n    }\n  }, [baseColor, tintColor])\n\n  const setup = React.useCallback(\n    ({ context }: { context: WebGLRenderingContext }): Scene => ({\n      quad: createQuadProgram(context, FRAGMENT),\n      balls: new Float32Array(MAX_BALLS * 2),\n    }),\n    []\n  )\n\n  const draw = React.useCallback(\n    ({\n      context,\n      size,\n      state,\n      time,\n    }: {\n      context: WebGLRenderingContext\n      size: { width: number; height: number; dpr: number }\n      state: Scene\n      time: number\n    }) => {\n      const quad = state.quad\n      if (!quad) return\n\n      const t = time * speed\n\n      // The field is measured across the longer edge, so a wide box has to\n      // spread its blobs wider or they all bunch into the middle third and\n      // merge into one shape.\n      const aspect = size.width / Math.max(size.height, 1)\n\n      for (let index = 0; index < total; index += 1) {\n        const fx = 0.21 + index * 0.037\n        const fy = 0.17 + index * 0.043\n        state.balls[index * 2] =\n          Math.sin(t * fx * 6.28 + index * 1.7) * 0.34 * aspect\n        state.balls[index * 2 + 1] =\n          Math.cos(t * fy * 6.28 + index * 2.3) * 0.28\n      }\n\n      // Eased, so the blob follows the pointer rather than teleporting with it.\n      const chase = Math.min(1, 8 * (1 / 60))\n      cursor.current.sx += (cursor.current.x - cursor.current.sx) * chase\n      cursor.current.sy += (cursor.current.y - cursor.current.sy) * chase\n      cursor.current.at += (cursor.current.want - cursor.current.at) * 0.08\n\n      const { width, height, dpr } = size\n      context.useProgram(quad.program)\n      context.uniform2f(quad.uniform(\"uResolution\"), width * dpr, height * dpr)\n      context.uniform3fv(quad.uniform(\"uBase\"), colorsRef.current.base)\n      context.uniform3fv(quad.uniform(\"uTint\"), colorsRef.current.tint)\n      context.uniform1f(quad.uniform(\"uRadius\"), radius)\n      context.uniform1f(quad.uniform(\"uEdge\"), edge)\n      context.uniform1i(quad.uniform(\"uCount\"), total)\n      context.uniform2fv(quad.uniform(\"uBalls\"), state.balls)\n      context.uniform2f(\n        quad.uniform(\"uPointer\"),\n        cursor.current.sx,\n        cursor.current.sy\n      )\n      context.uniform1f(quad.uniform(\"uPointerStrength\"), cursor.current.at)\n\n      quad.paint(width, height, dpr)\n    },\n    [edge, radius, speed, total]\n  )\n\n  const teardown = React.useCallback((state: Scene) => {\n    state.quad?.dispose()\n  }, [])\n\n  return (\n    <div\n      ref={rootRef}\n      data-slot=\"metaballs\"\n      className={cn(\n        \"bg-background relative isolate overflow-hidden\",\n        className\n      )}\n      {...rootProps}\n    >\n      {ready && (\n        <RenderSurface<Scene, \"webgl\">\n          contextType=\"webgl\"\n          setup={setup}\n          draw={draw}\n          teardown={teardown}\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",
    "shader",
    "webgl"
  ],
  "type": "registry:ui"
}