{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "Ballpit-TS-TW",
	"title": "Ballpit",
	"description": "Physics ball pit simulation with bouncing colorful spheres.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:component",
			"path": "Ballpit/Ballpit.tsx",
			"content": "import { gsap } from 'gsap';\nimport { Observer } from 'gsap/Observer';\nimport React, { useEffect, useRef } from 'react';\nimport {\n  ACESFilmicToneMapping,\n  AmbientLight,\n  Color,\n  InstancedMesh,\n  MathUtils,\n  MeshPhysicalMaterial,\n  Object3D,\n  PerspectiveCamera,\n  Plane,\n  PMREMGenerator,\n  PointLight,\n  Raycaster,\n  Scene,\n  ShaderChunk,\n  SphereGeometry,\n  SRGBColorSpace,\n  Timer,\n  Vector2,\n  Vector3,\n  WebGLRenderer,\n  WebGLRendererParameters\n} from 'three';\nimport { RoomEnvironment } from 'three/examples/jsm/environments/RoomEnvironment.js';\n\ngsap.registerPlugin(Observer);\n\ninterface XConfig {\n  canvas?: HTMLCanvasElement;\n  id?: string;\n  rendererOptions?: Partial<WebGLRendererParameters>;\n  size?: 'parent' | { width: number; height: number };\n}\n\ninterface SizeData {\n  width: number;\n  height: number;\n  wWidth: number;\n  wHeight: number;\n  ratio: number;\n  pixelRatio: number;\n}\n\nclass X {\n  #config: XConfig;\n  #postprocessing: any;\n  #resizeObserver?: ResizeObserver;\n  #intersectionObserver?: IntersectionObserver;\n  #resizeTimer?: number;\n  #animationFrameId: number = 0;\n  #timer: Timer = new Timer();\n  #animationState = { elapsed: 0, delta: 0 };\n  #isAnimating: boolean = false;\n  #isVisible: boolean = false;\n\n  canvas!: HTMLCanvasElement;\n  camera!: PerspectiveCamera;\n  cameraMinAspect?: number;\n  cameraMaxAspect?: number;\n  cameraFov!: number;\n  maxPixelRatio?: number;\n  minPixelRatio?: number;\n  scene!: Scene;\n  renderer!: WebGLRenderer;\n  size: SizeData = {\n    width: 0,\n    height: 0,\n    wWidth: 0,\n    wHeight: 0,\n    ratio: 0,\n    pixelRatio: 0\n  };\n\n  render: () => void = this.#render.bind(this);\n  onBeforeRender: (state: { elapsed: number; delta: number }) => void = () => {};\n  onAfterRender: (state: { elapsed: number; delta: number }) => void = () => {};\n  onAfterResize: (size: SizeData) => void = () => {};\n  isDisposed: boolean = false;\n\n  constructor(config: XConfig) {\n    this.#config = { ...config };\n    this.#initCamera();\n    this.#initScene();\n    this.#initRenderer();\n    this.resize();\n    this.#initObservers();\n  }\n\n  #initCamera() {\n    this.camera = new PerspectiveCamera();\n    this.cameraFov = this.camera.fov;\n  }\n\n  #initScene() {\n    this.scene = new Scene();\n  }\n\n  #initRenderer() {\n    if (this.#config.canvas) {\n      this.canvas = this.#config.canvas;\n    } else if (this.#config.id) {\n      const elem = document.getElementById(this.#config.id);\n      if (elem instanceof HTMLCanvasElement) {\n        this.canvas = elem;\n      } else {\n        console.error('Three: Missing canvas or id parameter');\n      }\n    } else {\n      console.error('Three: Missing canvas or id parameter');\n    }\n    this.canvas!.style.display = 'block';\n    const rendererOptions: WebGLRendererParameters = {\n      canvas: this.canvas,\n      powerPreference: 'high-performance',\n      ...(this.#config.rendererOptions ?? {})\n    };\n    this.renderer = new WebGLRenderer(rendererOptions);\n    this.renderer.outputColorSpace = SRGBColorSpace;\n  }\n\n  #initObservers() {\n    if (!(this.#config.size instanceof Object)) {\n      window.addEventListener('resize', this.#onResize.bind(this));\n      if (this.#config.size === 'parent' && this.canvas.parentNode) {\n        this.#resizeObserver = new ResizeObserver(this.#onResize.bind(this));\n        this.#resizeObserver.observe(this.canvas.parentNode as Element);\n      }\n    }\n    this.#intersectionObserver = new IntersectionObserver(this.#onIntersection.bind(this), {\n      root: null,\n      rootMargin: '0px',\n      threshold: 0\n    });\n    this.#intersectionObserver.observe(this.canvas);\n    document.addEventListener('visibilitychange', this.#onVisibilityChange.bind(this));\n  }\n\n  #onResize() {\n    if (this.#resizeTimer) clearTimeout(this.#resizeTimer);\n    this.#resizeTimer = window.setTimeout(this.resize.bind(this), 100);\n  }\n\n  resize() {\n    let w: number, h: number;\n    if (this.#config.size instanceof Object) {\n      w = this.#config.size.width;\n      h = this.#config.size.height;\n    } else if (this.#config.size === 'parent' && this.canvas.parentNode) {\n      w = (this.canvas.parentNode as HTMLElement).offsetWidth;\n      h = (this.canvas.parentNode as HTMLElement).offsetHeight;\n    } else {\n      w = window.innerWidth;\n      h = window.innerHeight;\n    }\n    this.size.width = w;\n    this.size.height = h;\n    this.size.ratio = w / h;\n    this.#updateCamera();\n    this.#updateRenderer();\n    this.onAfterResize(this.size);\n  }\n\n  #updateCamera() {\n    this.camera.aspect = this.size.width / this.size.height;\n    if (this.camera.isPerspectiveCamera && this.cameraFov) {\n      if (this.cameraMinAspect && this.camera.aspect < this.cameraMinAspect) {\n        this.#adjustFov(this.cameraMinAspect);\n      } else if (this.cameraMaxAspect && this.camera.aspect > this.cameraMaxAspect) {\n        this.#adjustFov(this.cameraMaxAspect);\n      } else {\n        this.camera.fov = this.cameraFov;\n      }\n    }\n    this.camera.updateProjectionMatrix();\n    this.updateWorldSize();\n  }\n\n  #adjustFov(aspect: number) {\n    const tanFov = Math.tan(MathUtils.degToRad(this.cameraFov / 2));\n    const newTan = tanFov / (this.camera.aspect / aspect);\n    this.camera.fov = 2 * MathUtils.radToDeg(Math.atan(newTan));\n  }\n\n  updateWorldSize() {\n    if (this.camera.isPerspectiveCamera) {\n      const fovRad = (this.camera.fov * Math.PI) / 180;\n      this.size.wHeight = 2 * Math.tan(fovRad / 2) * this.camera.position.length();\n      this.size.wWidth = this.size.wHeight * this.camera.aspect;\n    } else if ((this.camera as any).isOrthographicCamera) {\n      const cam = this.camera as any;\n      this.size.wHeight = cam.top - cam.bottom;\n      this.size.wWidth = cam.right - cam.left;\n    }\n  }\n\n  #updateRenderer() {\n    this.renderer.setSize(this.size.width, this.size.height);\n    this.#postprocessing?.setSize(this.size.width, this.size.height);\n    let pr = window.devicePixelRatio;\n    if (this.maxPixelRatio && pr > this.maxPixelRatio) {\n      pr = this.maxPixelRatio;\n    } else if (this.minPixelRatio && pr < this.minPixelRatio) {\n      pr = this.minPixelRatio;\n    }\n    this.renderer.setPixelRatio(pr);\n    this.size.pixelRatio = pr;\n  }\n\n  get postprocessing() {\n    return this.#postprocessing;\n  }\n  set postprocessing(value: any) {\n    this.#postprocessing = value;\n    this.render = value.render.bind(value);\n  }\n\n  #onIntersection(entries: IntersectionObserverEntry[]) {\n    this.#isAnimating = entries[0].isIntersecting;\n    this.#isAnimating ? this.#startAnimation() : this.#stopAnimation();\n  }\n\n  #onVisibilityChange() {\n    if (this.#isAnimating) {\n      document.hidden ? this.#stopAnimation() : this.#startAnimation();\n    }\n  }\n\n  #startAnimation() {\n    if (this.#isVisible) return;\n    const animateFrame = () => {\n      this.#animationFrameId = requestAnimationFrame(animateFrame);\n      this.#timer.update();\n      this.#animationState.delta = this.#timer.getDelta();\n      this.#animationState.elapsed += this.#animationState.delta;\n      this.onBeforeRender(this.#animationState);\n      this.render();\n      this.onAfterRender(this.#animationState);\n    };\n    this.#isVisible = true;\n    this.#timer.reset();\n    animateFrame();\n  }\n\n  #stopAnimation() {\n    if (this.#isVisible) {\n      cancelAnimationFrame(this.#animationFrameId);\n      this.#isVisible = false;\n    }\n  }\n\n  #render() {\n    this.renderer.render(this.scene, this.camera);\n  }\n\n  clear() {\n    this.scene.traverse(obj => {\n      if ((obj as any).isMesh && typeof (obj as any).material === 'object' && (obj as any).material !== null) {\n        Object.keys((obj as any).material).forEach(key => {\n          const matProp = (obj as any).material[key];\n          if (matProp && typeof matProp === 'object' && typeof matProp.dispose === 'function') {\n            matProp.dispose();\n          }\n        });\n        (obj as any).material.dispose();\n        (obj as any).geometry.dispose();\n      }\n    });\n    this.scene.clear();\n  }\n\n  dispose() {\n    this.#onResizeCleanup();\n    this.#stopAnimation();\n    this.#timer.dispose();\n    this.clear();\n    this.#postprocessing?.dispose();\n    this.renderer.dispose();\n    this.renderer.forceContextLoss();\n    this.isDisposed = true;\n  }\n\n  #onResizeCleanup() {\n    window.removeEventListener('resize', this.#onResize.bind(this));\n    this.#resizeObserver?.disconnect();\n    this.#intersectionObserver?.disconnect();\n    document.removeEventListener('visibilitychange', this.#onVisibilityChange.bind(this));\n  }\n}\n\ninterface WConfig {\n  count: number;\n  maxX: number;\n  maxY: number;\n  maxZ: number;\n  maxSize: number;\n  minSize: number;\n  size0: number;\n  gravity: number;\n  friction: number;\n  wallBounce: number;\n  maxVelocity: number;\n  controlSphere0?: boolean;\n  followCursor?: boolean;\n}\n\nclass W {\n  config: WConfig;\n  positionData: Float32Array;\n  velocityData: Float32Array;\n  sizeData: Float32Array;\n  center: Vector3 = new Vector3();\n\n  constructor(config: WConfig) {\n    this.config = config;\n    this.positionData = new Float32Array(3 * config.count).fill(0);\n    this.velocityData = new Float32Array(3 * config.count).fill(0);\n    this.sizeData = new Float32Array(config.count).fill(1);\n    this.center = new Vector3();\n    this.#initializePositions();\n    this.setSizes();\n  }\n\n  #initializePositions() {\n    const { config, positionData } = this;\n    this.center.toArray(positionData, 0);\n    for (let i = 1; i < config.count; i++) {\n      const idx = 3 * i;\n      positionData[idx] = MathUtils.randFloatSpread(2 * config.maxX);\n      positionData[idx + 1] = MathUtils.randFloatSpread(2 * config.maxY);\n      positionData[idx + 2] = MathUtils.randFloatSpread(2 * config.maxZ);\n    }\n  }\n\n  setSizes() {\n    const { config, sizeData } = this;\n    sizeData[0] = config.size0;\n    for (let i = 1; i < config.count; i++) {\n      sizeData[i] = MathUtils.randFloat(config.minSize, config.maxSize);\n    }\n  }\n\n  update(deltaInfo: { delta: number }) {\n    const { config, center, positionData, sizeData, velocityData } = this;\n    let startIdx = 0;\n    if (config.controlSphere0) {\n      startIdx = 1;\n      const firstVec = new Vector3().fromArray(positionData, 0);\n      firstVec.lerp(center, 0.1).toArray(positionData, 0);\n      new Vector3(0, 0, 0).toArray(velocityData, 0);\n    }\n    for (let idx = startIdx; idx < config.count; idx++) {\n      const base = 3 * idx;\n      const pos = new Vector3().fromArray(positionData, base);\n      const vel = new Vector3().fromArray(velocityData, base);\n      vel.y -= deltaInfo.delta * config.gravity * sizeData[idx];\n      vel.multiplyScalar(config.friction);\n      vel.clampLength(0, config.maxVelocity);\n      pos.add(vel);\n      pos.toArray(positionData, base);\n      vel.toArray(velocityData, base);\n    }\n    for (let idx = startIdx; idx < config.count; idx++) {\n      const base = 3 * idx;\n      const pos = new Vector3().fromArray(positionData, base);\n      const vel = new Vector3().fromArray(velocityData, base);\n      const radius = sizeData[idx];\n      for (let jdx = idx + 1; jdx < config.count; jdx++) {\n        const otherBase = 3 * jdx;\n        const otherPos = new Vector3().fromArray(positionData, otherBase);\n        const otherVel = new Vector3().fromArray(velocityData, otherBase);\n        const diff = new Vector3().copy(otherPos).sub(pos);\n        const dist = diff.length();\n        const sumRadius = radius + sizeData[jdx];\n        if (dist < sumRadius) {\n          const overlap = sumRadius - dist;\n          const correction = diff.normalize().multiplyScalar(0.5 * overlap);\n          const velCorrection = correction.clone().multiplyScalar(Math.max(vel.length(), 1));\n          pos.sub(correction);\n          vel.sub(velCorrection);\n          pos.toArray(positionData, base);\n          vel.toArray(velocityData, base);\n          otherPos.add(correction);\n          otherVel.add(correction.clone().multiplyScalar(Math.max(otherVel.length(), 1)));\n          otherPos.toArray(positionData, otherBase);\n          otherVel.toArray(velocityData, otherBase);\n        }\n      }\n      if (config.controlSphere0) {\n        const diff = new Vector3().copy(new Vector3().fromArray(positionData, 0)).sub(pos);\n        const d = diff.length();\n        const sumRadius0 = radius + sizeData[0];\n        if (d < sumRadius0) {\n          const correction = diff.normalize().multiplyScalar(sumRadius0 - d);\n          const velCorrection = correction.clone().multiplyScalar(Math.max(vel.length(), 2));\n          pos.sub(correction);\n          vel.sub(velCorrection);\n        }\n      }\n      if (Math.abs(pos.x) + radius > config.maxX) {\n        pos.x = Math.sign(pos.x) * (config.maxX - radius);\n        vel.x = -vel.x * config.wallBounce;\n      }\n      if (config.gravity === 0) {\n        if (Math.abs(pos.y) + radius > config.maxY) {\n          pos.y = Math.sign(pos.y) * (config.maxY - radius);\n          vel.y = -vel.y * config.wallBounce;\n        }\n      } else if (pos.y - radius < -config.maxY) {\n        pos.y = -config.maxY + radius;\n        vel.y = -vel.y * config.wallBounce;\n      }\n      const maxBoundary = Math.max(config.maxZ, config.maxSize);\n      if (Math.abs(pos.z) + radius > maxBoundary) {\n        pos.z = Math.sign(pos.z) * (config.maxZ - radius);\n        vel.z = -vel.z * config.wallBounce;\n      }\n      pos.toArray(positionData, base);\n      vel.toArray(velocityData, base);\n    }\n  }\n}\n\nclass Y extends MeshPhysicalMaterial {\n  uniforms: { [key: string]: { value: any } } = {\n    thicknessDistortion: { value: 0.1 },\n    thicknessAmbient: { value: 0 },\n    thicknessAttenuation: { value: 0.1 },\n    thicknessPower: { value: 2 },\n    thicknessScale: { value: 10 }\n  };\n  defines: { USE_UV: string };\n\n  constructor(params: any) {\n    super(params);\n    this.defines = { USE_UV: '' };\n    this.onBeforeCompile = shader => {\n      Object.assign(shader.uniforms, this.uniforms);\n      shader.fragmentShader =\n        `\n        uniform float thicknessPower;\n        uniform float thicknessScale;\n        uniform float thicknessDistortion;\n        uniform float thicknessAmbient;\n        uniform float thicknessAttenuation;\n        ` + shader.fragmentShader;\n      shader.fragmentShader = shader.fragmentShader.replace(\n        'void main() {',\n        `\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      );\n      const lightsChunk = ShaderChunk.lights_fragment_begin.replaceAll(\n        'RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );',\n        `\n          RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );\n          RE_Direct_Scattering(directLight, vUv, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, reflectedLight);\n        `\n      );\n      shader.fragmentShader = shader.fragmentShader.replace('#include <lights_fragment_begin>', lightsChunk);\n      if (this.onBeforeCompile2) this.onBeforeCompile2(shader);\n    };\n  }\n  onBeforeCompile2?: (shader: any) => void;\n}\n\nconst XConfig = {\n  count: 200,\n  colors: [0, 0, 0],\n  ambientColor: 0xffffff,\n  ambientIntensity: 1,\n  lightIntensity: 200,\n  materialParams: {\n    metalness: 0.5,\n    roughness: 0.5,\n    clearcoat: 1,\n    clearcoatRoughness: 0.15\n  },\n  minSize: 0.5,\n  maxSize: 1,\n  size0: 1,\n  gravity: 0.5,\n  friction: 0.9975,\n  wallBounce: 0.95,\n  maxVelocity: 0.15,\n  maxX: 5,\n  maxY: 5,\n  maxZ: 2,\n  controlSphere0: false,\n  followCursor: true\n};\n\nconst U = new Object3D();\n\nlet globalPointerActive = false;\nconst pointerPosition = new Vector2();\n\ninterface PointerData {\n  position: Vector2;\n  nPosition: Vector2;\n  hover: boolean;\n  touching: boolean;\n  onEnter: (data: PointerData) => void;\n  onMove: (data: PointerData) => void;\n  onClick: (data: PointerData) => void;\n  onLeave: (data: PointerData) => void;\n  dispose?: () => void;\n}\n\nconst pointerMap = new Map<HTMLElement, PointerData>();\n\nfunction createPointerData(options: Partial<PointerData> & { domElement: HTMLElement }): PointerData {\n  const defaultData: PointerData = {\n    position: new Vector2(),\n    nPosition: new Vector2(),\n    hover: false,\n    touching: false,\n    onEnter: () => {},\n    onMove: () => {},\n    onClick: () => {},\n    onLeave: () => {},\n    ...options\n  };\n  if (!pointerMap.has(options.domElement)) {\n    pointerMap.set(options.domElement, defaultData);\n    if (!globalPointerActive) {\n      document.body.addEventListener('pointermove', onPointerMove as EventListener);\n      document.body.addEventListener('pointerleave', onPointerLeave as EventListener);\n      document.body.addEventListener('click', onPointerClick as EventListener);\n\n      document.body.addEventListener('touchstart', onTouchStart as EventListener, {\n        passive: false\n      });\n      document.body.addEventListener('touchmove', onTouchMove as EventListener, {\n        passive: false\n      });\n      document.body.addEventListener('touchend', onTouchEnd as EventListener, {\n        passive: false\n      });\n      document.body.addEventListener('touchcancel', onTouchEnd as EventListener, {\n        passive: false\n      });\n      globalPointerActive = true;\n    }\n  }\n  defaultData.dispose = () => {\n    pointerMap.delete(options.domElement);\n    if (pointerMap.size === 0) {\n      document.body.removeEventListener('pointermove', onPointerMove as EventListener);\n      document.body.removeEventListener('pointerleave', onPointerLeave as EventListener);\n      document.body.removeEventListener('click', onPointerClick as EventListener);\n\n      document.body.removeEventListener('touchstart', onTouchStart as EventListener);\n      document.body.removeEventListener('touchmove', onTouchMove as EventListener);\n      document.body.removeEventListener('touchend', onTouchEnd as EventListener);\n      document.body.removeEventListener('touchcancel', onTouchEnd as EventListener);\n      globalPointerActive = false;\n    }\n  };\n  return defaultData;\n}\n\nfunction onPointerMove(e: PointerEvent) {\n  pointerPosition.set(e.clientX, e.clientY);\n  processPointerInteraction();\n}\n\nfunction processPointerInteraction() {\n  for (const [elem, data] of pointerMap) {\n    const rect = elem.getBoundingClientRect();\n    if (isInside(rect)) {\n      updatePointerData(data, rect);\n      if (!data.hover) {\n        data.hover = true;\n        data.onEnter(data);\n      }\n      data.onMove(data);\n    } else if (data.hover && !data.touching) {\n      data.hover = false;\n      data.onLeave(data);\n    }\n  }\n}\n\nfunction onTouchStart(e: TouchEvent) {\n  if (e.touches.length > 0) {\n    e.preventDefault();\n    pointerPosition.set(e.touches[0].clientX, e.touches[0].clientY);\n    for (const [elem, data] of pointerMap) {\n      const rect = elem.getBoundingClientRect();\n      if (isInside(rect)) {\n        data.touching = true;\n        updatePointerData(data, rect);\n        if (!data.hover) {\n          data.hover = true;\n          data.onEnter(data);\n        }\n        data.onMove(data);\n      }\n    }\n  }\n}\n\nfunction onTouchMove(e: TouchEvent) {\n  if (e.touches.length > 0) {\n    e.preventDefault();\n    pointerPosition.set(e.touches[0].clientX, e.touches[0].clientY);\n    for (const [elem, data] of pointerMap) {\n      const rect = elem.getBoundingClientRect();\n      updatePointerData(data, rect);\n      if (isInside(rect)) {\n        if (!data.hover) {\n          data.hover = true;\n          data.touching = true;\n          data.onEnter(data);\n        }\n        data.onMove(data);\n      } else if (data.hover && data.touching) {\n        data.onMove(data);\n      }\n    }\n  }\n}\n\nfunction onTouchEnd() {\n  for (const [, data] of pointerMap) {\n    if (data.touching) {\n      data.touching = false;\n      if (data.hover) {\n        data.hover = false;\n        data.onLeave(data);\n      }\n    }\n  }\n}\n\nfunction onPointerClick(e: PointerEvent) {\n  pointerPosition.set(e.clientX, e.clientY);\n  for (const [elem, data] of pointerMap) {\n    const rect = elem.getBoundingClientRect();\n    updatePointerData(data, rect);\n    if (isInside(rect)) data.onClick(data);\n  }\n}\n\nfunction onPointerLeave() {\n  for (const data of pointerMap.values()) {\n    if (data.hover) {\n      data.hover = false;\n      data.onLeave(data);\n    }\n  }\n}\n\nfunction updatePointerData(data: PointerData, rect: DOMRect) {\n  data.position.set(pointerPosition.x - rect.left, pointerPosition.y - rect.top);\n  data.nPosition.set((data.position.x / rect.width) * 2 - 1, (-data.position.y / rect.height) * 2 + 1);\n}\n\nfunction isInside(rect: DOMRect) {\n  return (\n    pointerPosition.x >= rect.left &&\n    pointerPosition.x <= rect.left + rect.width &&\n    pointerPosition.y >= rect.top &&\n    pointerPosition.y <= rect.top + rect.height\n  );\n}\n\nclass Z extends InstancedMesh {\n  config: typeof XConfig;\n  physics: W;\n  ambientLight: AmbientLight | undefined;\n  light: PointLight | undefined;\n\n  constructor(renderer: WebGLRenderer, params: Partial<typeof XConfig> = {}) {\n    const config = { ...XConfig, ...params };\n    const roomEnv = new RoomEnvironment();\n    const pmrem = new PMREMGenerator(renderer);\n    const envTexture = pmrem.fromScene(roomEnv).texture;\n    const geometry = new SphereGeometry();\n    const material = new Y({ envMap: envTexture, ...config.materialParams });\n    material.envMapRotation.x = -Math.PI / 2;\n    super(geometry, material, config.count);\n    this.config = config;\n    this.physics = new W(config);\n    this.#setupLights();\n    this.setColors(config.colors);\n  }\n\n  #setupLights() {\n    this.ambientLight = new AmbientLight(this.config.ambientColor, this.config.ambientIntensity);\n    this.add(this.ambientLight);\n    this.light = new PointLight(this.config.colors[0], this.config.lightIntensity);\n    this.add(this.light);\n  }\n\n  setColors(colors: number[]) {\n    if (Array.isArray(colors) && colors.length > 1) {\n      const colorUtils = (function (colorsArr: number[]) {\n        let baseColors: number[] = colorsArr;\n        let colorObjects: Color[] = [];\n        baseColors.forEach(col => {\n          colorObjects.push(new Color(col));\n        });\n        return {\n          setColors: (cols: number[]) => {\n            baseColors = cols;\n            colorObjects = [];\n            baseColors.forEach(col => {\n              colorObjects.push(new Color(col));\n            });\n          },\n          getColorAt: (ratio: number, out: Color = new Color()) => {\n            const clamped = Math.max(0, Math.min(1, ratio));\n            const scaled = clamped * (baseColors.length - 1);\n            const idx = Math.floor(scaled);\n            const start = colorObjects[idx];\n            if (idx >= baseColors.length - 1) return start.clone();\n            const alpha = scaled - idx;\n            const end = colorObjects[idx + 1];\n            out.r = start.r + alpha * (end.r - start.r);\n            out.g = start.g + alpha * (end.g - start.g);\n            out.b = start.b + alpha * (end.b - start.b);\n            return out;\n          }\n        };\n      })(colors);\n      for (let idx = 0; idx < this.count; idx++) {\n        this.setColorAt(idx, colorUtils.getColorAt(idx / this.count));\n        if (idx === 0) {\n          this.light!.color.copy(colorUtils.getColorAt(idx / this.count));\n        }\n      }\n\n      if (!this.instanceColor) return;\n      this.instanceColor.needsUpdate = true;\n    }\n  }\n\n  update(deltaInfo: { delta: number }) {\n    this.physics.update(deltaInfo);\n    for (let idx = 0; idx < this.count; idx++) {\n      U.position.fromArray(this.physics.positionData, 3 * idx);\n      if (idx === 0 && this.config.followCursor === false) {\n        U.scale.setScalar(0);\n      } else {\n        U.scale.setScalar(this.physics.sizeData[idx]);\n      }\n      U.updateMatrix();\n      this.setMatrixAt(idx, U.matrix);\n      if (idx === 0) this.light!.position.copy(U.position);\n    }\n    this.instanceMatrix.needsUpdate = true;\n  }\n}\n\ninterface CreateBallpitReturn {\n  three: X;\n  spheres: Z;\n  setCount: (count: number) => void;\n  updateConfig: (newProps: { [key: string]: any }) => void;\n  togglePause: () => void;\n  dispose: () => void;\n}\n\nfunction createBallpit(canvas: HTMLCanvasElement, config: any = {}): CreateBallpitReturn {\n  const threeInstance = new X({\n    canvas,\n    size: 'parent',\n    rendererOptions: { antialias: true, alpha: true }\n  });\n  let spheres: Z;\n  threeInstance.renderer.toneMapping = ACESFilmicToneMapping;\n  threeInstance.camera.position.set(0, 0, 20);\n  threeInstance.camera.lookAt(0, 0, 0);\n  threeInstance.cameraMaxAspect = 1.5;\n  threeInstance.resize();\n  initialize(config);\n  const raycaster = new Raycaster();\n  const plane = new Plane(new Vector3(0, 0, 1), 0);\n  const intersectionPoint = new Vector3();\n  let isPaused = false;\n\n  canvas.style.touchAction = 'none';\n  canvas.style.userSelect = 'none';\n  (canvas.style as any).webkitUserSelect = 'none';\n\n  const pointerData = createPointerData({\n    domElement: canvas,\n    onMove() {\n      raycaster.setFromCamera(pointerData.nPosition, threeInstance.camera);\n      threeInstance.camera.getWorldDirection(plane.normal);\n      raycaster.ray.intersectPlane(plane, intersectionPoint);\n      spheres.physics.center.copy(intersectionPoint);\n      spheres.config.controlSphere0 = true;\n    },\n    onLeave() {\n      spheres.config.controlSphere0 = false;\n    }\n  });\n  function initialize(cfg: any) {\n    if (spheres) {\n      threeInstance.clear();\n      threeInstance.scene.remove(spheres);\n    }\n    spheres = new Z(threeInstance.renderer, cfg);\n    threeInstance.scene.add(spheres);\n  }\n  threeInstance.onBeforeRender = deltaInfo => {\n    if (!isPaused) spheres.update(deltaInfo);\n  };\n  threeInstance.onAfterResize = size => {\n    spheres.config.maxX = size.wWidth / 2;\n    spheres.config.maxY = size.wHeight / 2;\n  };\n  return {\n    three: threeInstance,\n    get spheres() {\n      return spheres;\n    },\n    setCount(count: number) {\n      initialize({ ...spheres.config, count });\n    },\n    updateConfig(newProps: { [key: string]: any }) {\n      if (newProps.count !== undefined && newProps.count !== spheres.config.count) {\n        initialize({ ...spheres.config, ...newProps });\n      } else {\n        Object.assign(spheres.config, newProps);\n        if (newProps.colors) {\n          spheres.setColors(spheres.config.colors);\n        }\n        if (newProps.minSize !== undefined || newProps.maxSize !== undefined || newProps.size0 !== undefined) {\n          spheres.physics.setSizes();\n        }\n      }\n    },\n    togglePause() {\n      isPaused = !isPaused;\n    },\n    dispose() {\n      pointerData.dispose?.();\n      threeInstance.dispose();\n    }\n  };\n}\n\ninterface BallpitProps {\n  className?: string;\n  followCursor?: boolean;\n  [key: string]: any;\n}\n\nconst Ballpit: React.FC<BallpitProps> = ({ className = '', followCursor = true, ...props }) => {\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  const spheresInstanceRef = useRef<CreateBallpitReturn | null>(null);\n  const isFirstRender = useRef(true);\n\n  useEffect(() => {\n    const canvas = canvasRef.current;\n    if (!canvas) return;\n\n    spheresInstanceRef.current = createBallpit(canvas, {\n      followCursor,\n      ...props\n    });\n\n    return () => {\n      if (spheresInstanceRef.current) {\n        spheresInstanceRef.current.dispose();\n        spheresInstanceRef.current = null;\n      }\n    };\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, []);\n\n  useEffect(() => {\n    if (isFirstRender.current) {\n      isFirstRender.current = false;\n      return;\n    }\n    if (spheresInstanceRef.current) {\n      spheresInstanceRef.current.updateConfig({ followCursor, ...props });\n    }\n  }, [props, followCursor]);\n\n  return <canvas className={`${className} w-full h-full`} ref={canvasRef} />;\n};\n\nexport default Ballpit;\n"
		}
	],
	"registryDependencies": [],
	"dependencies": [
		"gsap@^3.13.0",
		"three@^0.180.0"
	]
}