{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ballpit",
  "type": "registry:block",
  "title": "Ballpit",
  "description": "Ballpit",
  "files": [
    {
      "path": "components/usages/ballpitusage.tsx",
      "content": "\"use client\";\r\n\r\nimport React from \"react\";\r\n\r\nimport Ballpit from \"@/registry/open-source/ballpit\";\r\n\r\nexport default function Usage() {\r\n\treturn (\r\n\t\t<div className=\"relative w-full flex items-center justify-center\">\r\n\t\t\t<Ballpit\r\n\t\t\t\tcount={200}\r\n\t\t\t\tgravity={0.7}\r\n\t\t\t\tfriction={0.8}\r\n\t\t\t\twallBounce={0.95}\r\n\t\t\t\tfollowCursor={true}\r\n\t\t\t/>\r\n\t\t</div>\r\n\t);\r\n}\r\n",
      "type": "registry:block",
      "target": "~/example.tsx"
    },
    {
      "path": "components/usages/ballpitusage.tsx",
      "content": "\"use client\";\r\n\r\nimport React from \"react\";\r\n\r\nimport Ballpit from \"@/registry/open-source/ballpit\";\r\n\r\nexport default function Usage() {\r\n\treturn (\r\n\t\t<div className=\"relative w-full flex items-center justify-center\">\r\n\t\t\t<Ballpit\r\n\t\t\t\tcount={200}\r\n\t\t\t\tgravity={0.7}\r\n\t\t\t\tfriction={0.8}\r\n\t\t\t\twallBounce={0.95}\r\n\t\t\t\tfollowCursor={true}\r\n\t\t\t/>\r\n\t\t</div>\r\n\t);\r\n}\r\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/open-source/ballpit.tsx",
      "content": "import React, { useEffect, useRef } from \"react\";\n\nimport { gsap } from \"gsap\";\nimport { Observer } from \"gsap/Observer\";\nimport {\n\tACESFilmicToneMapping,\n\tAmbientLight,\n\tClock,\n\tColor,\n\tInstancedMesh,\n\tMathUtils,\n\tMeshPhysicalMaterial,\n\tObject3D,\n\tPerspectiveCamera,\n\tPlane,\n\tPMREMGenerator,\n\tPointLight,\n\tRaycaster,\n\tScene,\n\tShaderChunk,\n\tSphereGeometry,\n\tSRGBColorSpace,\n\tVector2,\n\tVector3,\n\tWebGLRenderer,\n\tWebGLRendererParameters,\n} from \"three\";\nimport { RoomEnvironment } from \"three/examples/jsm/environments/RoomEnvironment.js\";\n\n// Credit:\n// https://www.reactbits.dev/backgrounds/ballpit\n\ngsap.registerPlugin(Observer);\n\ninterface XConfig {\n\tcanvas?: HTMLCanvasElement;\n\tid?: string;\n\trendererOptions?: Partial<WebGLRendererParameters>;\n\tsize?: \"parent\" | { width: number; height: number };\n}\n\ninterface SizeData {\n\twidth: number;\n\theight: number;\n\twWidth: number;\n\twHeight: number;\n\tratio: number;\n\tpixelRatio: number;\n}\n\nclass X {\n\t#config: XConfig;\n\t#postprocessing: any;\n\t#resizeObserver?: ResizeObserver;\n\t#intersectionObserver?: IntersectionObserver;\n\t#resizeTimer?: number;\n\t#animationFrameId: number = 0;\n\t#clock: Clock = new Clock();\n\t#animationState = { elapsed: 0, delta: 0 };\n\t#isAnimating: boolean = false;\n\t#isVisible: boolean = false;\n\n\tcanvas!: HTMLCanvasElement;\n\tcamera!: PerspectiveCamera;\n\tcameraMinAspect?: number;\n\tcameraMaxAspect?: number;\n\tcameraFov!: number;\n\tmaxPixelRatio?: number;\n\tminPixelRatio?: number;\n\tscene!: Scene;\n\trenderer!: WebGLRenderer;\n\tsize: SizeData = {\n\t\twidth: 0,\n\t\theight: 0,\n\t\twWidth: 0,\n\t\twHeight: 0,\n\t\tratio: 0,\n\t\tpixelRatio: 0,\n\t};\n\n\trender: () => void = this.#render.bind(this);\n\tonBeforeRender: (state: { elapsed: number; delta: number }) => void =\n\t\t() => {};\n\tonAfterRender: (state: { elapsed: number; delta: number }) => void =\n\t\t() => {};\n\tonAfterResize: (size: SizeData) => void = () => {};\n\tisDisposed: boolean = false;\n\n\tconstructor(config: XConfig) {\n\t\tthis.#config = { ...config };\n\t\tthis.#initCamera();\n\t\tthis.#initScene();\n\t\tthis.#initRenderer();\n\t\tthis.resize();\n\t\tthis.#initObservers();\n\t}\n\n\t#initCamera() {\n\t\tthis.camera = new PerspectiveCamera();\n\t\tthis.cameraFov = this.camera.fov;\n\t}\n\n\t#initScene() {\n\t\tthis.scene = new Scene();\n\t}\n\n\t#initRenderer() {\n\t\tif (this.#config.canvas) {\n\t\t\tthis.canvas = this.#config.canvas;\n\t\t} else if (this.#config.id) {\n\t\t\tconst elem = document.getElementById(this.#config.id);\n\t\t\tif (elem instanceof HTMLCanvasElement) {\n\t\t\t\tthis.canvas = elem;\n\t\t\t} else {\n\t\t\t\tconsole.error(\"Three: Missing canvas or id parameter\");\n\t\t\t}\n\t\t} else {\n\t\t\tconsole.error(\"Three: Missing canvas or id parameter\");\n\t\t}\n\t\tthis.canvas!.style.display = \"block\";\n\t\tconst rendererOptions: WebGLRendererParameters = {\n\t\t\tcanvas: this.canvas,\n\t\t\tpowerPreference: \"high-performance\",\n\t\t\t...(this.#config.rendererOptions ?? {}),\n\t\t};\n\t\tthis.renderer = new WebGLRenderer(rendererOptions);\n\t\tthis.renderer.outputColorSpace = SRGBColorSpace;\n\t}\n\n\t#initObservers() {\n\t\tif (!(this.#config.size instanceof Object)) {\n\t\t\twindow.addEventListener(\"resize\", this.#onResize.bind(this));\n\t\t\tif (this.#config.size === \"parent\" && this.canvas.parentNode) {\n\t\t\t\tthis.#resizeObserver = new ResizeObserver(\n\t\t\t\t\tthis.#onResize.bind(this)\n\t\t\t\t);\n\t\t\t\tthis.#resizeObserver.observe(this.canvas.parentNode as Element);\n\t\t\t}\n\t\t}\n\t\tthis.#intersectionObserver = new IntersectionObserver(\n\t\t\tthis.#onIntersection.bind(this),\n\t\t\t{ root: null, rootMargin: \"0px\", threshold: 0 }\n\t\t);\n\t\tthis.#intersectionObserver.observe(this.canvas);\n\t\tdocument.addEventListener(\n\t\t\t\"visibilitychange\",\n\t\t\tthis.#onVisibilityChange.bind(this)\n\t\t);\n\t}\n\n\t#onResize() {\n\t\tif (this.#resizeTimer) clearTimeout(this.#resizeTimer);\n\t\tthis.#resizeTimer = window.setTimeout(this.resize.bind(this), 100);\n\t}\n\n\tresize() {\n\t\tlet w: number, h: number;\n\t\tif (this.#config.size instanceof Object) {\n\t\t\tw = this.#config.size.width;\n\t\t\th = this.#config.size.height;\n\t\t} else if (this.#config.size === \"parent\" && this.canvas.parentNode) {\n\t\t\tw = (this.canvas.parentNode as HTMLElement).offsetWidth;\n\t\t\th = (this.canvas.parentNode as HTMLElement).offsetHeight;\n\t\t} else {\n\t\t\tw = window.innerWidth;\n\t\t\th = window.innerHeight;\n\t\t}\n\t\tthis.size.width = w;\n\t\tthis.size.height = h;\n\t\tthis.size.ratio = w / h;\n\t\tthis.#updateCamera();\n\t\tthis.#updateRenderer();\n\t\tthis.onAfterResize(this.size);\n\t}\n\n\t#updateCamera() {\n\t\tthis.camera.aspect = this.size.width / this.size.height;\n\t\tif (this.camera.isPerspectiveCamera && this.cameraFov) {\n\t\t\tif (\n\t\t\t\tthis.cameraMinAspect &&\n\t\t\t\tthis.camera.aspect < this.cameraMinAspect\n\t\t\t) {\n\t\t\t\tthis.#adjustFov(this.cameraMinAspect);\n\t\t\t} else if (\n\t\t\t\tthis.cameraMaxAspect &&\n\t\t\t\tthis.camera.aspect > this.cameraMaxAspect\n\t\t\t) {\n\t\t\t\tthis.#adjustFov(this.cameraMaxAspect);\n\t\t\t} else {\n\t\t\t\tthis.camera.fov = this.cameraFov;\n\t\t\t}\n\t\t}\n\t\tthis.camera.updateProjectionMatrix();\n\t\tthis.updateWorldSize();\n\t}\n\n\t#adjustFov(aspect: number) {\n\t\tconst tanFov = Math.tan(MathUtils.degToRad(this.cameraFov / 2));\n\t\tconst newTan = tanFov / (this.camera.aspect / aspect);\n\t\tthis.camera.fov = 2 * MathUtils.radToDeg(Math.atan(newTan));\n\t}\n\n\tupdateWorldSize() {\n\t\tif (this.camera.isPerspectiveCamera) {\n\t\t\tconst fovRad = (this.camera.fov * Math.PI) / 180;\n\t\t\tthis.size.wHeight =\n\t\t\t\t2 * Math.tan(fovRad / 2) * this.camera.position.length();\n\t\t\tthis.size.wWidth = this.size.wHeight * this.camera.aspect;\n\t\t} else if ((this.camera as any).isOrthographicCamera) {\n\t\t\tconst cam = this.camera as any;\n\t\t\tthis.size.wHeight = cam.top - cam.bottom;\n\t\t\tthis.size.wWidth = cam.right - cam.left;\n\t\t}\n\t}\n\n\t#updateRenderer() {\n\t\tthis.renderer.setSize(this.size.width, this.size.height);\n\t\tthis.#postprocessing?.setSize(this.size.width, this.size.height);\n\t\tlet pr = window.devicePixelRatio;\n\t\tif (this.maxPixelRatio && pr > this.maxPixelRatio) {\n\t\t\tpr = this.maxPixelRatio;\n\t\t} else if (this.minPixelRatio && pr < this.minPixelRatio) {\n\t\t\tpr = this.minPixelRatio;\n\t\t}\n\t\tthis.renderer.setPixelRatio(pr);\n\t\tthis.size.pixelRatio = pr;\n\t}\n\n\tget postprocessing() {\n\t\treturn this.#postprocessing;\n\t}\n\tset postprocessing(value: any) {\n\t\tthis.#postprocessing = value;\n\t\tthis.render = value.render.bind(value);\n\t}\n\n\t#onIntersection(entries: IntersectionObserverEntry[]) {\n\t\tthis.#isAnimating = entries[0].isIntersecting;\n\t\tthis.#isAnimating ? this.#startAnimation() : this.#stopAnimation();\n\t}\n\n\t#onVisibilityChange() {\n\t\tif (this.#isAnimating) {\n\t\t\tdocument.hidden ? this.#stopAnimation() : this.#startAnimation();\n\t\t}\n\t}\n\n\t#startAnimation() {\n\t\tif (this.#isVisible) return;\n\t\tconst animateFrame = () => {\n\t\t\tthis.#animationFrameId = requestAnimationFrame(animateFrame);\n\t\t\tthis.#animationState.delta = this.#clock.getDelta();\n\t\t\tthis.#animationState.elapsed += this.#animationState.delta;\n\t\t\tthis.onBeforeRender(this.#animationState);\n\t\t\tthis.render();\n\t\t\tthis.onAfterRender(this.#animationState);\n\t\t};\n\t\tthis.#isVisible = true;\n\t\tthis.#clock.start();\n\t\tanimateFrame();\n\t}\n\n\t#stopAnimation() {\n\t\tif (this.#isVisible) {\n\t\t\tcancelAnimationFrame(this.#animationFrameId);\n\t\t\tthis.#isVisible = false;\n\t\t\tthis.#clock.stop();\n\t\t}\n\t}\n\n\t#render() {\n\t\tthis.renderer.render(this.scene, this.camera);\n\t}\n\n\tclear() {\n\t\tthis.scene.traverse((obj) => {\n\t\t\tif (\n\t\t\t\t(obj as any).isMesh &&\n\t\t\t\ttypeof (obj as any).material === \"object\" &&\n\t\t\t\t(obj as any).material !== null\n\t\t\t) {\n\t\t\t\tObject.keys((obj as any).material).forEach((key) => {\n\t\t\t\t\tconst matProp = (obj as any).material[key];\n\t\t\t\t\tif (\n\t\t\t\t\t\tmatProp &&\n\t\t\t\t\t\ttypeof matProp === \"object\" &&\n\t\t\t\t\t\ttypeof matProp.dispose === \"function\"\n\t\t\t\t\t) {\n\t\t\t\t\t\tmatProp.dispose();\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t\t(obj as any).material.dispose();\n\t\t\t\t(obj as any).geometry.dispose();\n\t\t\t}\n\t\t});\n\t\tthis.scene.clear();\n\t}\n\n\tdispose() {\n\t\tthis.#onResizeCleanup();\n\t\tthis.#stopAnimation();\n\t\tthis.clear();\n\t\tthis.#postprocessing?.dispose();\n\t\tthis.renderer.dispose();\n\t\tthis.isDisposed = true;\n\t}\n\n\t#onResizeCleanup() {\n\t\twindow.removeEventListener(\"resize\", this.#onResize.bind(this));\n\t\tthis.#resizeObserver?.disconnect();\n\t\tthis.#intersectionObserver?.disconnect();\n\t\tdocument.removeEventListener(\n\t\t\t\"visibilitychange\",\n\t\t\tthis.#onVisibilityChange.bind(this)\n\t\t);\n\t}\n}\n\ninterface WConfig {\n\tcount: number;\n\tmaxX: number;\n\tmaxY: number;\n\tmaxZ: number;\n\tmaxSize: number;\n\tminSize: number;\n\tsize0: number;\n\tgravity: number;\n\tfriction: number;\n\twallBounce: number;\n\tmaxVelocity: number;\n\tcontrolSphere0?: boolean;\n\tfollowCursor?: boolean;\n}\n\nclass W {\n\tconfig: WConfig;\n\tpositionData: Float32Array;\n\tvelocityData: Float32Array;\n\tsizeData: Float32Array;\n\tcenter: Vector3 = new Vector3();\n\n\tconstructor(config: WConfig) {\n\t\tthis.config = config;\n\t\tthis.positionData = new Float32Array(3 * config.count).fill(0);\n\t\tthis.velocityData = new Float32Array(3 * config.count).fill(0);\n\t\tthis.sizeData = new Float32Array(config.count).fill(1);\n\t\tthis.center = new Vector3();\n\t\tthis.#initializePositions();\n\t\tthis.setSizes();\n\t}\n\n\t#initializePositions() {\n\t\tconst { config, positionData } = this;\n\t\tthis.center.toArray(positionData, 0);\n\t\tfor (let i = 1; i < config.count; i++) {\n\t\t\tconst idx = 3 * i;\n\t\t\tpositionData[idx] = MathUtils.randFloatSpread(2 * config.maxX);\n\t\t\tpositionData[idx + 1] = MathUtils.randFloatSpread(2 * config.maxY);\n\t\t\tpositionData[idx + 2] = MathUtils.randFloatSpread(2 * config.maxZ);\n\t\t}\n\t}\n\n\tsetSizes() {\n\t\tconst { config, sizeData } = this;\n\t\tsizeData[0] = config.size0;\n\t\tfor (let i = 1; i < config.count; i++) {\n\t\t\tsizeData[i] = MathUtils.randFloat(config.minSize, config.maxSize);\n\t\t}\n\t}\n\n\tupdate(deltaInfo: { delta: number }) {\n\t\tconst { config, center, positionData, sizeData, velocityData } = this;\n\t\tlet startIdx = 0;\n\t\tif (config.controlSphere0) {\n\t\t\tstartIdx = 1;\n\t\t\tconst firstVec = new Vector3().fromArray(positionData, 0);\n\t\t\tfirstVec.lerp(center, 0.1).toArray(positionData, 0);\n\t\t\tnew Vector3(0, 0, 0).toArray(velocityData, 0);\n\t\t}\n\t\tfor (let idx = startIdx; idx < config.count; idx++) {\n\t\t\tconst base = 3 * idx;\n\t\t\tconst pos = new Vector3().fromArray(positionData, base);\n\t\t\tconst vel = new Vector3().fromArray(velocityData, base);\n\t\t\tvel.y -= deltaInfo.delta * config.gravity * sizeData[idx];\n\t\t\tvel.multiplyScalar(config.friction);\n\t\t\tvel.clampLength(0, config.maxVelocity);\n\t\t\tpos.add(vel);\n\t\t\tpos.toArray(positionData, base);\n\t\t\tvel.toArray(velocityData, base);\n\t\t}\n\t\tfor (let idx = startIdx; idx < config.count; idx++) {\n\t\t\tconst base = 3 * idx;\n\t\t\tconst pos = new Vector3().fromArray(positionData, base);\n\t\t\tconst vel = new Vector3().fromArray(velocityData, base);\n\t\t\tconst radius = sizeData[idx];\n\t\t\tfor (let jdx = idx + 1; jdx < config.count; jdx++) {\n\t\t\t\tconst otherBase = 3 * jdx;\n\t\t\t\tconst otherPos = new Vector3().fromArray(positionData, otherBase);\n\t\t\t\tconst otherVel = new Vector3().fromArray(velocityData, otherBase);\n\t\t\t\tconst diff = new Vector3().copy(otherPos).sub(pos);\n\t\t\t\tconst dist = diff.length();\n\t\t\t\tconst sumRadius = radius + sizeData[jdx];\n\t\t\t\tif (dist < sumRadius) {\n\t\t\t\t\tconst overlap = sumRadius - dist;\n\t\t\t\t\tconst correction = diff\n\t\t\t\t\t\t.normalize()\n\t\t\t\t\t\t.multiplyScalar(0.5 * overlap);\n\t\t\t\t\tconst velCorrection = correction\n\t\t\t\t\t\t.clone()\n\t\t\t\t\t\t.multiplyScalar(Math.max(vel.length(), 1));\n\t\t\t\t\tpos.sub(correction);\n\t\t\t\t\tvel.sub(velCorrection);\n\t\t\t\t\tpos.toArray(positionData, base);\n\t\t\t\t\tvel.toArray(velocityData, base);\n\t\t\t\t\totherPos.add(correction);\n\t\t\t\t\totherVel.add(\n\t\t\t\t\t\tcorrection\n\t\t\t\t\t\t\t.clone()\n\t\t\t\t\t\t\t.multiplyScalar(Math.max(otherVel.length(), 1))\n\t\t\t\t\t);\n\t\t\t\t\totherPos.toArray(positionData, otherBase);\n\t\t\t\t\totherVel.toArray(velocityData, otherBase);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (config.controlSphere0) {\n\t\t\t\tconst diff = new Vector3()\n\t\t\t\t\t.copy(new Vector3().fromArray(positionData, 0))\n\t\t\t\t\t.sub(pos);\n\t\t\t\tconst d = diff.length();\n\t\t\t\tconst sumRadius0 = radius + sizeData[0];\n\t\t\t\tif (d < sumRadius0) {\n\t\t\t\t\tconst correction = diff\n\t\t\t\t\t\t.normalize()\n\t\t\t\t\t\t.multiplyScalar(sumRadius0 - d);\n\t\t\t\t\tconst velCorrection = correction\n\t\t\t\t\t\t.clone()\n\t\t\t\t\t\t.multiplyScalar(Math.max(vel.length(), 2));\n\t\t\t\t\tpos.sub(correction);\n\t\t\t\t\tvel.sub(velCorrection);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (Math.abs(pos.x) + radius > config.maxX) {\n\t\t\t\tpos.x = Math.sign(pos.x) * (config.maxX - radius);\n\t\t\t\tvel.x = -vel.x * config.wallBounce;\n\t\t\t}\n\t\t\tif (config.gravity === 0) {\n\t\t\t\tif (Math.abs(pos.y) + radius > config.maxY) {\n\t\t\t\t\tpos.y = Math.sign(pos.y) * (config.maxY - radius);\n\t\t\t\t\tvel.y = -vel.y * config.wallBounce;\n\t\t\t\t}\n\t\t\t} else if (pos.y - radius < -config.maxY) {\n\t\t\t\tpos.y = -config.maxY + radius;\n\t\t\t\tvel.y = -vel.y * config.wallBounce;\n\t\t\t}\n\t\t\tconst maxBoundary = Math.max(config.maxZ, config.maxSize);\n\t\t\tif (Math.abs(pos.z) + radius > maxBoundary) {\n\t\t\t\tpos.z = Math.sign(pos.z) * (config.maxZ - radius);\n\t\t\t\tvel.z = -vel.z * config.wallBounce;\n\t\t\t}\n\t\t\tpos.toArray(positionData, base);\n\t\t\tvel.toArray(velocityData, base);\n\t\t}\n\t}\n}\n\nclass Y extends MeshPhysicalMaterial {\n\tuniforms: { [key: string]: { value: any } } = {\n\t\tthicknessDistortion: { value: 0.1 },\n\t\tthicknessAmbient: { value: 0 },\n\t\tthicknessAttenuation: { value: 0.1 },\n\t\tthicknessPower: { value: 2 },\n\t\tthicknessScale: { value: 10 },\n\t};\n\n\tconstructor(params: any) {\n\t\tsuper(params);\n\t\tthis.defines = { USE_UV: \"\" };\n\t\tthis.onBeforeCompile = (shader) => {\n\t\t\tObject.assign(shader.uniforms, this.uniforms);\n\t\t\tshader.fragmentShader =\n\t\t\t\t`\n        uniform float thicknessPower;\n        uniform float thicknessScale;\n        uniform float thicknessDistortion;\n        uniform float thicknessAmbient;\n        uniform float thicknessAttenuation;\n        ` + shader.fragmentShader;\n\t\t\tshader.fragmentShader = shader.fragmentShader.replace(\n\t\t\t\t\"void main() {\",\n\t\t\t\t`\n        void RE_Direct_Scattering(const in IncidentLight directLight, const in vec2 uv, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, inout ReflectedLight reflectedLight) {\n          vec3 scatteringHalf = normalize(directLight.direction + (geometryNormal * thicknessDistortion));\n          float scatteringDot = pow(saturate(dot(geometryViewDir, -scatteringHalf)), thicknessPower) * thicknessScale;\n          #ifdef USE_COLOR\n            vec3 scatteringIllu = (scatteringDot + thicknessAmbient) * vColor;\n          #else\n            vec3 scatteringIllu = (scatteringDot + thicknessAmbient) * diffuse;\n          #endif\n          reflectedLight.directDiffuse += scatteringIllu * thicknessAttenuation * directLight.color;\n        }\n\n        void main() {\n        `\n\t\t\t);\n\t\t\tconst lightsChunk = ShaderChunk.lights_fragment_begin.replaceAll(\n\t\t\t\t\"RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );\",\n\t\t\t\t`\n          RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );\n          RE_Direct_Scattering(directLight, vUv, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, reflectedLight);\n        `\n\t\t\t);\n\t\t\tshader.fragmentShader = shader.fragmentShader.replace(\n\t\t\t\t\"#include <lights_fragment_begin>\",\n\t\t\t\tlightsChunk\n\t\t\t);\n\t\t\tif (this.onBeforeCompile2) this.onBeforeCompile2(shader);\n\t\t};\n\t}\n\tonBeforeCompile2?: (shader: any) => void;\n}\n\nconst XConfig = {\n\tcount: 200,\n\tcolors: [0, 0, 0],\n\tambientColor: 0xffffff,\n\tambientIntensity: 1,\n\tlightIntensity: 200,\n\tmaterialParams: {\n\t\tmetalness: 0.5,\n\t\troughness: 0.5,\n\t\tclearcoat: 1,\n\t\tclearcoatRoughness: 0.15,\n\t},\n\tminSize: 0.5,\n\tmaxSize: 1,\n\tsize0: 1,\n\tgravity: 0.5,\n\tfriction: 0.9975,\n\twallBounce: 0.95,\n\tmaxVelocity: 0.15,\n\tmaxX: 5,\n\tmaxY: 5,\n\tmaxZ: 2,\n\tcontrolSphere0: false,\n\tfollowCursor: true,\n};\n\nconst U = new Object3D();\n\nlet globalPointerActive = false;\nconst pointerPosition = new Vector2();\n\ninterface PointerData {\n\tposition: Vector2;\n\tnPosition: Vector2;\n\thover: boolean;\n\ttouching: boolean;\n\tonEnter: (data: PointerData) => void;\n\tonMove: (data: PointerData) => void;\n\tonClick: (data: PointerData) => void;\n\tonLeave: (data: PointerData) => void;\n\tdispose?: () => void;\n}\n\nconst pointerMap = new Map<HTMLElement, PointerData>();\n\nfunction createPointerData(\n\toptions: Partial<PointerData> & { domElement: HTMLElement }\n): PointerData {\n\tconst defaultData: PointerData = {\n\t\tposition: new Vector2(),\n\t\tnPosition: new Vector2(),\n\t\thover: false,\n\t\ttouching: false,\n\t\tonEnter: () => {},\n\t\tonMove: () => {},\n\t\tonClick: () => {},\n\t\tonLeave: () => {},\n\t\t...options,\n\t};\n\tif (!pointerMap.has(options.domElement)) {\n\t\tpointerMap.set(options.domElement, defaultData);\n\t\tif (!globalPointerActive) {\n\t\t\tdocument.body.addEventListener(\n\t\t\t\t\"pointermove\",\n\t\t\t\tonPointerMove as EventListener\n\t\t\t);\n\t\t\tdocument.body.addEventListener(\n\t\t\t\t\"pointerleave\",\n\t\t\t\tonPointerLeave as EventListener\n\t\t\t);\n\t\t\tdocument.body.addEventListener(\n\t\t\t\t\"click\",\n\t\t\t\tonPointerClick as EventListener\n\t\t\t);\n\n\t\t\tdocument.body.addEventListener(\n\t\t\t\t\"touchstart\",\n\t\t\t\tonTouchStart as EventListener,\n\t\t\t\t{\n\t\t\t\t\tpassive: false,\n\t\t\t\t}\n\t\t\t);\n\t\t\tdocument.body.addEventListener(\n\t\t\t\t\"touchmove\",\n\t\t\t\tonTouchMove as EventListener,\n\t\t\t\t{\n\t\t\t\t\tpassive: false,\n\t\t\t\t}\n\t\t\t);\n\t\t\tdocument.body.addEventListener(\n\t\t\t\t\"touchend\",\n\t\t\t\tonTouchEnd as EventListener,\n\t\t\t\t{\n\t\t\t\t\tpassive: false,\n\t\t\t\t}\n\t\t\t);\n\t\t\tdocument.body.addEventListener(\n\t\t\t\t\"touchcancel\",\n\t\t\t\tonTouchEnd as EventListener,\n\t\t\t\t{\n\t\t\t\t\tpassive: false,\n\t\t\t\t}\n\t\t\t);\n\t\t\tglobalPointerActive = true;\n\t\t}\n\t}\n\tdefaultData.dispose = () => {\n\t\tpointerMap.delete(options.domElement);\n\t\tif (pointerMap.size === 0) {\n\t\t\tdocument.body.removeEventListener(\n\t\t\t\t\"pointermove\",\n\t\t\t\tonPointerMove as EventListener\n\t\t\t);\n\t\t\tdocument.body.removeEventListener(\n\t\t\t\t\"pointerleave\",\n\t\t\t\tonPointerLeave as EventListener\n\t\t\t);\n\t\t\tdocument.body.removeEventListener(\n\t\t\t\t\"click\",\n\t\t\t\tonPointerClick as EventListener\n\t\t\t);\n\n\t\t\tdocument.body.removeEventListener(\n\t\t\t\t\"touchstart\",\n\t\t\t\tonTouchStart as EventListener\n\t\t\t);\n\t\t\tdocument.body.removeEventListener(\n\t\t\t\t\"touchmove\",\n\t\t\t\tonTouchMove as EventListener\n\t\t\t);\n\t\t\tdocument.body.removeEventListener(\n\t\t\t\t\"touchend\",\n\t\t\t\tonTouchEnd as EventListener\n\t\t\t);\n\t\t\tdocument.body.removeEventListener(\n\t\t\t\t\"touchcancel\",\n\t\t\t\tonTouchEnd as EventListener\n\t\t\t);\n\t\t\tglobalPointerActive = false;\n\t\t}\n\t};\n\treturn defaultData;\n}\n\nfunction onPointerMove(e: PointerEvent) {\n\tpointerPosition.set(e.clientX, e.clientY);\n\tprocessPointerInteraction();\n}\n\nfunction processPointerInteraction() {\n\tfor (const [elem, data] of pointerMap) {\n\t\tconst rect = elem.getBoundingClientRect();\n\t\tif (isInside(rect)) {\n\t\t\tupdatePointerData(data, rect);\n\t\t\tif (!data.hover) {\n\t\t\t\tdata.hover = true;\n\t\t\t\tdata.onEnter(data);\n\t\t\t}\n\t\t\tdata.onMove(data);\n\t\t} else if (data.hover && !data.touching) {\n\t\t\tdata.hover = false;\n\t\t\tdata.onLeave(data);\n\t\t}\n\t}\n}\n\nfunction onTouchStart(e: TouchEvent) {\n\tif (e.touches.length > 0) {\n\t\te.preventDefault();\n\t\tpointerPosition.set(e.touches[0].clientX, e.touches[0].clientY);\n\t\tfor (const [elem, data] of pointerMap) {\n\t\t\tconst rect = elem.getBoundingClientRect();\n\t\t\tif (isInside(rect)) {\n\t\t\t\tdata.touching = true;\n\t\t\t\tupdatePointerData(data, rect);\n\t\t\t\tif (!data.hover) {\n\t\t\t\t\tdata.hover = true;\n\t\t\t\t\tdata.onEnter(data);\n\t\t\t\t}\n\t\t\t\tdata.onMove(data);\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunction onTouchMove(e: TouchEvent) {\n\tif (e.touches.length > 0) {\n\t\te.preventDefault();\n\t\tpointerPosition.set(e.touches[0].clientX, e.touches[0].clientY);\n\t\tfor (const [elem, data] of pointerMap) {\n\t\t\tconst rect = elem.getBoundingClientRect();\n\t\t\tupdatePointerData(data, rect);\n\t\t\tif (isInside(rect)) {\n\t\t\t\tif (!data.hover) {\n\t\t\t\t\tdata.hover = true;\n\t\t\t\t\tdata.touching = true;\n\t\t\t\t\tdata.onEnter(data);\n\t\t\t\t}\n\t\t\t\tdata.onMove(data);\n\t\t\t} else if (data.hover && data.touching) {\n\t\t\t\tdata.onMove(data);\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunction onTouchEnd() {\n\tfor (const [, data] of pointerMap) {\n\t\tif (data.touching) {\n\t\t\tdata.touching = false;\n\t\t\tif (data.hover) {\n\t\t\t\tdata.hover = false;\n\t\t\t\tdata.onLeave(data);\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunction onPointerClick(e: PointerEvent) {\n\tpointerPosition.set(e.clientX, e.clientY);\n\tfor (const [elem, data] of pointerMap) {\n\t\tconst rect = elem.getBoundingClientRect();\n\t\tupdatePointerData(data, rect);\n\t\tif (isInside(rect)) data.onClick(data);\n\t}\n}\n\nfunction onPointerLeave() {\n\tfor (const data of pointerMap.values()) {\n\t\tif (data.hover) {\n\t\t\tdata.hover = false;\n\t\t\tdata.onLeave(data);\n\t\t}\n\t}\n}\n\nfunction updatePointerData(data: PointerData, rect: DOMRect) {\n\tdata.position.set(\n\t\tpointerPosition.x - rect.left,\n\t\tpointerPosition.y - rect.top\n\t);\n\tdata.nPosition.set(\n\t\t(data.position.x / rect.width) * 2 - 1,\n\t\t(-data.position.y / rect.height) * 2 + 1\n\t);\n}\n\nfunction isInside(rect: DOMRect) {\n\treturn (\n\t\tpointerPosition.x >= rect.left &&\n\t\tpointerPosition.x <= rect.left + rect.width &&\n\t\tpointerPosition.y >= rect.top &&\n\t\tpointerPosition.y <= rect.top + rect.height\n\t);\n}\n\nclass Z extends InstancedMesh {\n\tconfig: typeof XConfig;\n\tphysics: W;\n\tambientLight: AmbientLight | undefined;\n\tlight: PointLight | undefined;\n\n\tconstructor(renderer: WebGLRenderer, params: Partial<typeof XConfig> = {}) {\n\t\tconst config = { ...XConfig, ...params };\n\t\tconst roomEnv = new RoomEnvironment();\n\t\tconst pmrem = new PMREMGenerator(renderer);\n\t\tconst envTexture = pmrem.fromScene(roomEnv).texture;\n\t\tconst geometry = new SphereGeometry();\n\t\tconst material = new Y({ envMap: envTexture, ...config.materialParams });\n\t\tmaterial.envMapRotation.x = -Math.PI / 2;\n\t\tsuper(geometry, material, config.count);\n\t\tthis.config = config;\n\t\tthis.physics = new W(config);\n\t\tthis.#setupLights();\n\t\tthis.setColors(config.colors);\n\t}\n\n\t#setupLights() {\n\t\tthis.ambientLight = new AmbientLight(\n\t\t\tthis.config.ambientColor,\n\t\t\tthis.config.ambientIntensity\n\t\t);\n\t\tthis.add(this.ambientLight);\n\t\tthis.light = new PointLight(\n\t\t\tthis.config.colors[0],\n\t\t\tthis.config.lightIntensity\n\t\t);\n\t\tthis.add(this.light);\n\t}\n\n\tsetColors(colors: number[]) {\n\t\tif (Array.isArray(colors) && colors.length > 1) {\n\t\t\tconst colorUtils = (function (colorsArr: number[]) {\n\t\t\t\tlet baseColors: number[] = colorsArr;\n\t\t\t\tlet colorObjects: Color[] = [];\n\t\t\t\tbaseColors.forEach((col) => {\n\t\t\t\t\tcolorObjects.push(new Color(col));\n\t\t\t\t});\n\t\t\t\treturn {\n\t\t\t\t\tsetColors: (cols: number[]) => {\n\t\t\t\t\t\tbaseColors = cols;\n\t\t\t\t\t\tcolorObjects = [];\n\t\t\t\t\t\tbaseColors.forEach((col) => {\n\t\t\t\t\t\t\tcolorObjects.push(new Color(col));\n\t\t\t\t\t\t});\n\t\t\t\t\t},\n\t\t\t\t\tgetColorAt: (ratio: number, out: Color = new Color()) => {\n\t\t\t\t\t\tconst clamped = Math.max(0, Math.min(1, ratio));\n\t\t\t\t\t\tconst scaled = clamped * (baseColors.length - 1);\n\t\t\t\t\t\tconst idx = Math.floor(scaled);\n\t\t\t\t\t\tconst start = colorObjects[idx];\n\t\t\t\t\t\tif (idx >= baseColors.length - 1) return start.clone();\n\t\t\t\t\t\tconst alpha = scaled - idx;\n\t\t\t\t\t\tconst end = colorObjects[idx + 1];\n\t\t\t\t\t\tout.r = start.r + alpha * (end.r - start.r);\n\t\t\t\t\t\tout.g = start.g + alpha * (end.g - start.g);\n\t\t\t\t\t\tout.b = start.b + alpha * (end.b - start.b);\n\t\t\t\t\t\treturn out;\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t})(colors);\n\t\t\tfor (let idx = 0; idx < this.count; idx++) {\n\t\t\t\tthis.setColorAt(idx, colorUtils.getColorAt(idx / this.count));\n\t\t\t\tif (idx === 0) {\n\t\t\t\t\tthis.light!.color.copy(colorUtils.getColorAt(idx / this.count));\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (!this.instanceColor) return;\n\t\t\tthis.instanceColor.needsUpdate = true;\n\t\t}\n\t}\n\n\tupdate(deltaInfo: { delta: number }) {\n\t\tthis.physics.update(deltaInfo);\n\t\tfor (let idx = 0; idx < this.count; idx++) {\n\t\t\tU.position.fromArray(this.physics.positionData, 3 * idx);\n\t\t\tif (idx === 0 && this.config.followCursor === false) {\n\t\t\t\tU.scale.setScalar(0);\n\t\t\t} else {\n\t\t\t\tU.scale.setScalar(this.physics.sizeData[idx]);\n\t\t\t}\n\t\t\tU.updateMatrix();\n\t\t\tthis.setMatrixAt(idx, U.matrix);\n\t\t\tif (idx === 0) this.light!.position.copy(U.position);\n\t\t}\n\t\tthis.instanceMatrix.needsUpdate = true;\n\t}\n}\n\ninterface CreateBallpitReturn {\n\tthree: X;\n\tspheres: Z;\n\tsetCount: (count: number) => void;\n\ttogglePause: () => void;\n\tdispose: () => void;\n}\n\nfunction createBallpit(\n\tcanvas: HTMLCanvasElement,\n\tconfig: any = {}\n): CreateBallpitReturn {\n\tconst threeInstance = new X({\n\t\tcanvas,\n\t\tsize: \"parent\",\n\t\trendererOptions: { antialias: true, alpha: true },\n\t});\n\tlet spheres: Z;\n\tthreeInstance.renderer.toneMapping = ACESFilmicToneMapping;\n\tthreeInstance.camera.position.set(0, 0, 20);\n\tthreeInstance.camera.lookAt(0, 0, 0);\n\tthreeInstance.cameraMaxAspect = 1.5;\n\tthreeInstance.resize();\n\tinitialize(config);\n\tconst raycaster = new Raycaster();\n\tconst plane = new Plane(new Vector3(0, 0, 1), 0);\n\tconst intersectionPoint = new Vector3();\n\tlet isPaused = false;\n\n\tcanvas.style.touchAction = \"none\";\n\tcanvas.style.userSelect = \"none\";\n\t(canvas.style as any).webkitUserSelect = \"none\";\n\n\tconst pointerData = createPointerData({\n\t\tdomElement: canvas,\n\t\tonMove() {\n\t\t\traycaster.setFromCamera(pointerData.nPosition, threeInstance.camera);\n\t\t\tthreeInstance.camera.getWorldDirection(plane.normal);\n\t\t\traycaster.ray.intersectPlane(plane, intersectionPoint);\n\t\t\tspheres.physics.center.copy(intersectionPoint);\n\t\t\tspheres.config.controlSphere0 = true;\n\t\t},\n\t\tonLeave() {\n\t\t\tspheres.config.controlSphere0 = false;\n\t\t},\n\t});\n\tfunction initialize(cfg: any) {\n\t\tif (spheres) {\n\t\t\tthreeInstance.clear();\n\t\t\tthreeInstance.scene.remove(spheres);\n\t\t}\n\t\tspheres = new Z(threeInstance.renderer, cfg);\n\t\tthreeInstance.scene.add(spheres);\n\t}\n\tthreeInstance.onBeforeRender = (deltaInfo) => {\n\t\tif (!isPaused) spheres.update(deltaInfo);\n\t};\n\tthreeInstance.onAfterResize = (size) => {\n\t\tspheres.config.maxX = size.wWidth / 2;\n\t\tspheres.config.maxY = size.wHeight / 2;\n\t};\n\treturn {\n\t\tthree: threeInstance,\n\t\tget spheres() {\n\t\t\treturn spheres;\n\t\t},\n\t\tsetCount(count: number) {\n\t\t\tinitialize({ ...spheres.config, count });\n\t\t},\n\t\ttogglePause() {\n\t\t\tisPaused = !isPaused;\n\t\t},\n\t\tdispose() {\n\t\t\tpointerData.dispose?.();\n\t\t\tthreeInstance.dispose();\n\t\t},\n\t};\n}\n\ninterface BallpitProps {\n\tclassName?: string;\n\tfollowCursor?: boolean;\n\t[key: string]: any;\n}\n\nconst Ballpit: React.FC<BallpitProps> = ({\n\tclassName = \"\",\n\tfollowCursor = true,\n\t...props\n}) => {\n\tconst canvasRef = useRef<HTMLCanvasElement>(null);\n\tconst spheresInstanceRef = useRef<CreateBallpitReturn | null>(null);\n\n\tuseEffect(() => {\n\t\tconst canvas = canvasRef.current;\n\t\tif (!canvas) return;\n\n\t\tspheresInstanceRef.current = createBallpit(canvas, {\n\t\t\tfollowCursor,\n\t\t\t...props,\n\t\t});\n\n\t\treturn () => {\n\t\t\tif (spheresInstanceRef.current) {\n\t\t\t\tspheresInstanceRef.current.dispose();\n\t\t\t}\n\t\t};\n\t\t// eslint-disable-next-line react-hooks/exhaustive-deps\n\t}, []);\n\n\treturn <canvas className={`${className} w-full h-full`} ref={canvasRef} />;\n};\n\nexport default Ballpit;\n",
      "type": "registry:ui"
    }
  ]
}