{
  "name": "pooltable",
  "title": "PoolTable",
  "type": "registry:component",
  "description": "A pool table with rails, cushions, six pockets, and sixteen instanced balls, racked or scattered mid-game.",
  "dependencies": [
    "@react-three/rapier@^2.2.0",
    "@runek/core@^0.13.0",
    "three@^0.184.0"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "PoolTable.tsx",
      "content": "import { CuboidCollider, RigidBody } from '@react-three/rapier'\nimport { pick, range, rng, sub, useWorld, type WorldComponentProps } from '@runek/core'\nimport { useLayoutEffect, useMemo, useRef } from 'react'\nimport { Color, type InstancedMesh, Object3D } from 'three'\n\n/** Balls `racked` in a triangle with the cue ball opposite, `scattered` mid-game, or `none`. */\nexport type PoolBalls = 'racked' | 'scattered' | 'none'\n\nexport interface PoolTableProps extends WorldComponentProps {\n  /** Outer length along local X, in units. */\n  length?: number\n  /** Outer width along local Z, in units. */\n  width?: number\n  /** Height of the playing surface, in units. */\n  height?: number\n  /** Seeded when unset. */\n  balls?: PoolBalls\n  /** The cloth. Seeded from a few classic colors when unset. */\n  color?: string\n  /** Rails and legs; defaults to the palette's `wood`. */\n  frameColor?: string\n}\n\nconst CLOTHS = ['#2f6b4a', '#2f6b4a', '#2b4f7a', '#7a2f3a']\n// Cue ball, then 1 to 15 (the eight is black). Fixed colors: the game's own, no palette slot fits.\nconst BALL_COLORS = [\n  '#f4f1e8',\n  '#f2c230',\n  '#2357b5',\n  '#d23b2f',\n  '#5b2d8c',\n  '#e8772b',\n  '#1f7a45',\n  '#7a2a22',\n  '#151515',\n  '#f2c230',\n  '#2357b5',\n  '#d23b2f',\n  '#5b2d8c',\n  '#e8772b',\n  '#1f7a45',\n  '#7a2a22',\n]\nconst BALL_R = 0.028\n\n/**\n * A pool table: wooden rails over cushioned cloth, six pockets, and solid legs, with the balls\n * racked in a triangle (the cue ball on the head spot) or scattered as if mid-game, all sixteen in\n * one instanced mesh. One cuboid collider for the table.\n */\nexport function PoolTable({\n  position = [0, 0, 0],\n  rotation = [0, 0, 0],\n  seed = 1,\n  length = 2.5,\n  width = 1.4,\n  height = 0.8,\n  balls,\n  color,\n  frameColor,\n}: PoolTableProps) {\n  const { unit, palette } = useWorld()\n  const u = unit\n  const L = length * u\n  const Wd = width * u\n  const H = height * u\n  const wood = frameColor ?? palette.wood\n  const rail = 0.12 * u\n  const bedL = L - rail * 2\n  const bedW = Wd - rail * 2\n  const r = BALL_R * u\n\n  const look = useMemo(() => {\n    const rr = rng(sub(seed, 0))\n    const rolled = {\n      cloth: pick(rr, CLOTHS),\n      balls: pick<PoolBalls>(rr, ['racked', 'scattered', 'scattered']),\n    }\n    const mode = balls ?? rolled.balls\n    const at: [number, number][] = []\n    if (mode === 'racked') {\n      at.push([-bedL / 4, 0])\n      // Rows of 1 to 5 behind the foot spot, the eight in the middle of the third row.\n      const order = [1, 9, 2, 10, 8, 3, 11, 4, 12, 5, 13, 6, 14, 7, 15]\n      let k = 0\n      const pos: [number, number][] = []\n      for (let row = 0; row < 5; row++)\n        for (let i = 0; i <= row; i++)\n          pos.push([bedL / 4 + row * r * 1.75, (i - row / 2) * r * 2.02])\n      for (const n of order) {\n        at[n] = pos[k++]\n      }\n    } else if (mode === 'scattered') {\n      for (let n = 0; n < 16; n++) {\n        let p: [number, number] = [0, 0]\n        for (let tries = 0; tries < 40; tries++) {\n          p = [range(rr, -0.45, 0.45) * bedL, range(rr, -0.42, 0.42) * bedW]\n          if (at.every((q) => Math.hypot(q[0] - p[0], q[1] - p[1]) > r * 2.2)) break\n        }\n        at.push(p)\n      }\n    }\n    return { cloth: color ?? rolled.cloth, at }\n  }, [seed, balls, color, bedL, bedW, r])\n\n  const ballMesh = useRef<InstancedMesh>(null)\n  useLayoutEffect(() => {\n    const mesh = ballMesh.current\n    if (!mesh) return\n    const o = new Object3D()\n    const c = new Color()\n    look.at.forEach(([x, z], i) => {\n      o.position.set(x, H + r, z)\n      o.updateMatrix()\n      mesh.setMatrixAt(i, o.matrix)\n      mesh.setColorAt(i, c.set(BALL_COLORS[i]))\n    })\n    mesh.count = look.at.length\n    mesh.instanceMatrix.needsUpdate = true\n    if (mesh.instanceColor) mesh.instanceColor.needsUpdate = true\n  }, [look, H, r])\n\n  const pockets: [number, number][] = [-1, 0, 1].flatMap((x) =>\n    [-1, 1].map((z) => [x * (bedL / 2), z * (bedW / 2)] as [number, number]),\n  )\n  const apron = 0.18 * u\n\n  return (\n    <RigidBody type=\"fixed\" colliders={false} position={position} rotation={rotation}>\n      <CuboidCollider\n        args={[L / 2, (H + 0.04 * u) / 2, Wd / 2]}\n        position={[0, (H + 0.04 * u) / 2, 0]}\n      />\n      {/* bed and cloth */}\n      <mesh position={[0, H - apron / 2, 0]} castShadow receiveShadow>\n        <boxGeometry args={[L - 0.04 * u, apron, Wd - 0.04 * u]} />\n        <meshStandardMaterial color={wood} roughness={0.55} />\n      </mesh>\n      <mesh position={[0, H + 0.001 * u, 0]} rotation={[-Math.PI / 2, 0, 0]} receiveShadow>\n        <planeGeometry args={[bedL, bedW]} />\n        <meshStandardMaterial color={look.cloth} roughness={0.95} />\n      </mesh>\n      {/* cushions, then the wooden rails around them */}\n      {[-1, 1].map((s) => (\n        <group key={s}>\n          <mesh position={[0, H + 0.02 * u, s * (bedW / 2 + 0.02 * u)]} castShadow>\n            <boxGeometry args={[bedL - 0.1 * u, 0.04 * u, 0.04 * u]} />\n            <meshStandardMaterial color={look.cloth} roughness={0.95} />\n          </mesh>\n          <mesh position={[s * (bedL / 2 + 0.02 * u), H + 0.02 * u, 0]} castShadow>\n            <boxGeometry args={[0.04 * u, 0.04 * u, bedW - 0.1 * u]} />\n            <meshStandardMaterial color={look.cloth} roughness={0.95} />\n          </mesh>\n          <mesh\n            position={[0, H + 0.025 * u, s * (Wd / 2 - (rail - 0.04 * u) / 2)]}\n            castShadow\n            receiveShadow\n          >\n            <boxGeometry args={[L, 0.05 * u, rail - 0.04 * u]} />\n            <meshStandardMaterial color={wood} roughness={0.45} />\n          </mesh>\n          <mesh\n            position={[s * (L / 2 - (rail - 0.04 * u) / 2), H + 0.025 * u, 0]}\n            castShadow\n            receiveShadow\n          >\n            <boxGeometry args={[rail - 0.04 * u, 0.05 * u, Wd]} />\n            <meshStandardMaterial color={wood} roughness={0.45} />\n          </mesh>\n        </group>\n      ))}\n      {pockets.map(([x, z]) => (\n        <mesh key={`${x}:${z}`} position={[x, H + 0.03 * u, z]}>\n          <cylinderGeometry args={[0.055 * u, 0.05 * u, 0.062 * u, 16]} />\n          <meshStandardMaterial color=\"#0d0d0e\" roughness={0.9} />\n        </mesh>\n      ))}\n      {/* legs */}\n      {[-1, 1].flatMap((sx) =>\n        [-1, 1].map((sz) => (\n          <mesh\n            key={`${sx}${sz}`}\n            position={[sx * (L / 2 - 0.2 * u), (H - apron) / 2, sz * (Wd / 2 - 0.16 * u)]}\n            castShadow\n          >\n            <boxGeometry args={[0.14 * u, H - apron, 0.14 * u]} />\n            <meshStandardMaterial color={wood} roughness={0.55} />\n          </mesh>\n        )),\n      )}\n      {look.at.length > 0 && (\n        <instancedMesh\n          key={look.at.length}\n          ref={ballMesh}\n          args={[undefined, undefined, look.at.length]}\n          castShadow\n        >\n          <sphereGeometry args={[r, 16, 12]} />\n          <meshPhysicalMaterial roughness={0.15} clearcoat={1} />\n        </instancedMesh>\n      )}\n    </RigidBody>\n  )\n}\n\nPoolTable.groundSitting = true\n",
      "type": "registry:component"
    }
  ]
}
