{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "MetaBalls-TS-CSS",
	"title": "MetaBalls",
	"description": "Liquid metaball blobs that merge and separate with smooth implicit surface animation.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:file",
			"path": "MetaBalls.css",
			"target": "@components/MetaBalls.css",
			"content": ".metaballs-container {\n  position: relative;\n  width: 100%;\n  height: 100%;\n}\n"
		},
		{
			"type": "registry:component",
			"path": "MetaBalls.tsx",
			"content": "import React, { useEffect, useRef } from 'react';\nimport { Renderer, Program, Mesh, Triangle, Transform, Vec3, Camera } from 'ogl';\nimport './MetaBalls.css';\n\ntype MetaBallsProps = {\n  color?: string;\n  speed?: number;\n  enableMouseInteraction?: boolean;\n  hoverSmoothness?: number;\n  animationSize?: number;\n  ballCount?: number;\n  clumpFactor?: number;\n  cursorBallSize?: number;\n  cursorBallColor?: string;\n  enableTransparency?: boolean;\n};\n\nfunction parseHexColor(hex: string): [number, number, number] {\n  const c = hex.replace('#', '');\n  const r = parseInt(c.substring(0, 2), 16) / 255;\n  const g = parseInt(c.substring(2, 4), 16) / 255;\n  const b = parseInt(c.substring(4, 6), 16) / 255;\n  return [r, g, b];\n}\n\nfunction fract(x: number): number {\n  return x - Math.floor(x);\n}\n\nfunction hash31(p: number): number[] {\n  let r = [p * 0.1031, p * 0.103, p * 0.0973].map(fract);\n  const r_yzx = [r[1], r[2], r[0]];\n  const dotVal = r[0] * (r_yzx[0] + 33.33) + r[1] * (r_yzx[1] + 33.33) + r[2] * (r_yzx[2] + 33.33);\n  for (let i = 0; i < 3; i++) {\n    r[i] = fract(r[i] + dotVal);\n  }\n  return r;\n}\n\nfunction hash33(v: number[]): number[] {\n  let p = [v[0] * 0.1031, v[1] * 0.103, v[2] * 0.0973].map(fract);\n  const p_yxz = [p[1], p[0], p[2]];\n  const dotVal = p[0] * (p_yxz[0] + 33.33) + p[1] * (p_yxz[1] + 33.33) + p[2] * (p_yxz[2] + 33.33);\n  for (let i = 0; i < 3; i++) {\n    p[i] = fract(p[i] + dotVal);\n  }\n  const p_xxy = [p[0], p[0], p[1]];\n  const p_yxx = [p[1], p[0], p[0]];\n  const p_zyx = [p[2], p[1], p[0]];\n  const result: number[] = [];\n  for (let i = 0; i < 3; i++) {\n    result[i] = fract((p_xxy[i] + p_yxx[i]) * p_zyx[i]);\n  }\n  return result;\n}\n\nconst vertex = `#version 300 es\nprecision highp float;\nlayout(location = 0) in vec2 position;\nvoid main() {\n    gl_Position = vec4(position, 0.0, 1.0);\n}\n`;\n\nconst fragment = `#version 300 es\nprecision highp float;\nuniform vec3 iResolution;\nuniform float iTime;\nuniform vec3 iMouse;\nuniform vec3 iColor;\nuniform vec3 iCursorColor;\nuniform float iAnimationSize;\nuniform int iBallCount;\nuniform float iCursorBallSize;\nuniform vec3 iMetaBalls[50];\nuniform float iClumpFactor;\nuniform bool enableTransparency;\nout vec4 outColor;\nconst float PI = 3.14159265359;\n \nfloat getMetaBallValue(vec2 c, float r, vec2 p) {\n    vec2 d = p - c;\n    float dist2 = dot(d, d);\n    return (r * r) / dist2;\n}\n \nvoid main() {\n    vec2 fc = gl_FragCoord.xy;\n    float scale = iAnimationSize / iResolution.y;\n    vec2 coord = (fc - iResolution.xy * 0.5) * scale;\n    vec2 mouseW = (iMouse.xy - iResolution.xy * 0.5) * scale;\n    float m1 = 0.0;\n    for (int i = 0; i < 50; i++) {\n        if (i >= iBallCount) break;\n        m1 += getMetaBallValue(iMetaBalls[i].xy, iMetaBalls[i].z, coord);\n    }\n    float m2 = getMetaBallValue(mouseW, iCursorBallSize, coord);\n    float total = m1 + m2;\n    float f = smoothstep(-1.0, 1.0, (total - 1.3) / min(1.0, fwidth(total)));\n    vec3 cFinal = vec3(0.0);\n    if (total > 0.0) {\n        float alpha1 = m1 / total;\n        float alpha2 = m2 / total;\n        cFinal = iColor * alpha1 + iCursorColor * alpha2;\n    }\n    outColor = vec4(cFinal * f, enableTransparency ? f : 1.0);\n}\n`;\n\ntype BallParams = {\n  st: number;\n  dtFactor: number;\n  baseScale: number;\n  toggle: number;\n  radius: number;\n};\n\nconst MetaBalls: React.FC<MetaBallsProps> = ({\n  color = '#ffffff',\n  speed = 0.3,\n  enableMouseInteraction = true,\n  hoverSmoothness = 0.05,\n  animationSize = 30,\n  ballCount = 15,\n  clumpFactor = 1,\n  cursorBallSize = 3,\n  cursorBallColor = '#ffffff',\n  enableTransparency = false\n}) => {\n  const containerRef = useRef<HTMLDivElement>(null);\n\n  useEffect(() => {\n    const container = containerRef.current;\n    if (!container) return;\n\n    const dpr = 1;\n    const renderer = new Renderer({\n      dpr,\n      alpha: true,\n      premultipliedAlpha: false\n    });\n    const gl = renderer.gl;\n    gl.clearColor(0, 0, 0, enableTransparency ? 0 : 1);\n    container.appendChild(gl.canvas);\n\n    const camera = new Camera(gl, {\n      left: -1,\n      right: 1,\n      top: 1,\n      bottom: -1,\n      near: 0.1,\n      far: 10\n    });\n    camera.position.z = 1;\n\n    const geometry = new Triangle(gl);\n    const [r1, g1, b1] = parseHexColor(color);\n    const [r2, g2, b2] = parseHexColor(cursorBallColor);\n\n    const metaBallsUniform: Vec3[] = [];\n    for (let i = 0; i < 50; i++) {\n      metaBallsUniform.push(new Vec3(0, 0, 0));\n    }\n\n    const program = new Program(gl, {\n      vertex,\n      fragment,\n      uniforms: {\n        iTime: { value: 0 },\n        iResolution: { value: new Vec3(0, 0, 0) },\n        iMouse: { value: new Vec3(0, 0, 0) },\n        iColor: { value: new Vec3(r1, g1, b1) },\n        iCursorColor: { value: new Vec3(r2, g2, b2) },\n        iAnimationSize: { value: animationSize },\n        iBallCount: { value: ballCount },\n        iCursorBallSize: { value: cursorBallSize },\n        iMetaBalls: { value: metaBallsUniform },\n        iClumpFactor: { value: clumpFactor },\n        enableTransparency: { value: enableTransparency }\n      }\n    });\n\n    const mesh = new Mesh(gl, { geometry, program });\n    const scene = new Transform();\n    mesh.setParent(scene);\n\n    const maxBalls = 50;\n    const effectiveBallCount = Math.min(ballCount, maxBalls);\n    const ballParams: BallParams[] = [];\n    for (let i = 0; i < effectiveBallCount; i++) {\n      const idx = i + 1;\n      const h1 = hash31(idx);\n      const st = h1[0] * (2 * Math.PI);\n      const dtFactor = 0.1 * Math.PI + h1[1] * (0.4 * Math.PI - 0.1 * Math.PI);\n      const baseScale = 5.0 + h1[1] * (10.0 - 5.0);\n      const h2 = hash33(h1);\n      const toggle = Math.floor(h2[0] * 2.0);\n      const radiusVal = 0.5 + h2[2] * (2.0 - 0.5);\n      ballParams.push({ st, dtFactor, baseScale, toggle, radius: radiusVal });\n    }\n\n    const mouseBallPos = { x: 0, y: 0 };\n    let pointerInside = false;\n    let pointerX = 0;\n    let pointerY = 0;\n\n    function resize() {\n      if (!container) return;\n      const width = container.clientWidth;\n      const height = container.clientHeight;\n      renderer.setSize(width * dpr, height * dpr);\n      gl.canvas.style.width = `${width}px`;\n      gl.canvas.style.height = `${height}px`;\n      program.uniforms.iResolution.value.set(gl.canvas.width, gl.canvas.height, 0);\n    }\n    window.addEventListener('resize', resize);\n    resize();\n\n    function onPointerMove(e: PointerEvent) {\n      if (!enableMouseInteraction || !container) return;\n      const rect = container.getBoundingClientRect();\n      const px = e.clientX - rect.left;\n      const py = e.clientY - rect.top;\n      pointerX = (px / rect.width) * gl.canvas.width;\n      pointerY = (1 - py / rect.height) * gl.canvas.height;\n    }\n    function onPointerEnter() {\n      if (!enableMouseInteraction) return;\n      pointerInside = true;\n    }\n    function onPointerLeave() {\n      if (!enableMouseInteraction) return;\n      pointerInside = false;\n    }\n    container.addEventListener('pointermove', onPointerMove);\n    container.addEventListener('pointerenter', onPointerEnter);\n    container.addEventListener('pointerleave', onPointerLeave);\n\n    const startTime = performance.now();\n    let animationFrameId: number;\n    function update(t: number) {\n      animationFrameId = requestAnimationFrame(update);\n      const elapsed = (t - startTime) * 0.001;\n      program.uniforms.iTime.value = elapsed;\n\n      for (let i = 0; i < effectiveBallCount; i++) {\n        const p = ballParams[i];\n        const dt = elapsed * speed * p.dtFactor;\n        const th = p.st + dt;\n        const x = Math.cos(th);\n        const y = Math.sin(th + dt * p.toggle);\n        const posX = x * p.baseScale * clumpFactor;\n        const posY = y * p.baseScale * clumpFactor;\n        metaBallsUniform[i].set(posX, posY, p.radius);\n      }\n\n      let targetX: number, targetY: number;\n      if (pointerInside) {\n        targetX = pointerX;\n        targetY = pointerY;\n      } else {\n        const cx = gl.canvas.width * 0.5;\n        const cy = gl.canvas.height * 0.5;\n        const rx = gl.canvas.width * 0.15;\n        const ry = gl.canvas.height * 0.15;\n        targetX = cx + Math.cos(elapsed * speed) * rx;\n        targetY = cy + Math.sin(elapsed * speed) * ry;\n      }\n      mouseBallPos.x += (targetX - mouseBallPos.x) * hoverSmoothness;\n      mouseBallPos.y += (targetY - mouseBallPos.y) * hoverSmoothness;\n      program.uniforms.iMouse.value.set(mouseBallPos.x, mouseBallPos.y, 0);\n\n      renderer.render({ scene, camera });\n    }\n    animationFrameId = requestAnimationFrame(update);\n\n    return () => {\n      cancelAnimationFrame(animationFrameId);\n      window.removeEventListener('resize', resize);\n      container.removeEventListener('pointermove', onPointerMove);\n      container.removeEventListener('pointerenter', onPointerEnter);\n      container.removeEventListener('pointerleave', onPointerLeave);\n      container.removeChild(gl.canvas);\n      gl.getExtension('WEBGL_lose_context')?.loseContext();\n    };\n  }, [\n    color,\n    cursorBallColor,\n    speed,\n    enableMouseInteraction,\n    hoverSmoothness,\n    animationSize,\n    ballCount,\n    clumpFactor,\n    cursorBallSize,\n    enableTransparency\n  ]);\n\n  return <div ref={containerRef} className=\"metaballs-container\" />;\n};\n\nexport default MetaBalls;\n"
		}
	],
	"registryDependencies": [],
	"dependencies": [
		"ogl@^1.0.11"
	]
}