{
  "name": "palm",
  "title": "Palm",
  "type": "registry:component",
  "description": "A coconut palm: leaning bowed trunk and a crown of drooping instanced fronds, deterministic from seed.",
  "dependencies": [
    "@react-three/rapier@^2.2.0",
    "@runek/core@^0.12.0",
    "three@^0.184.0"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "Palm.tsx",
      "content": "import { CylinderCollider, RigidBody } from '@react-three/rapier'\nimport { rng, useWorld, type WorldComponentProps } from '@runek/core'\nimport { useLayoutEffect, useMemo, useRef } from 'react'\nimport * as THREE from 'three'\n\nexport interface PalmProps extends WorldComponentProps {\n  /** Trunk height, in units. */\n  height?: number\n  /** Sideways lean of the crown, as a fraction of height. */\n  lean?: number\n  /** Frond count. */\n  fronds?: number\n  /** Frond length, in units. */\n  frondLength?: number\n  /** Defaults to the world palette's `bark` slot. */\n  trunkColor?: string\n  /** Defaults to the world palette's `foliage` slot. */\n  frondColor?: string\n  /** Grow a coconut cluster under the crown. */\n  coconuts?: boolean\n}\n\nconst TRUNK_SEGMENTS = 7\nconst FROND_STEPS = 6\nconst UP = new THREE.Vector3(0, 1, 0)\n\n/** Tapering ribbon that arcs out along +X and droops — one frond, folded to a shallow V. */\nfunction frondGeometry(length: number, width: number) {\n  const positions: number[] = []\n  const indices: number[] = []\n  for (let i = 0; i <= FROND_STEPS; i++) {\n    const t = i / FROND_STEPS\n    const x = t * length\n    const droop = t * t * length * 0.55\n    const w = width * (1 - t * 0.85)\n    const fold = w * 0.45\n    positions.push(x, -droop + fold, -w, x, -droop, 0, x, -droop + fold, w)\n  }\n  for (let i = 0; i < FROND_STEPS; i++) {\n    const a = i * 3\n    indices.push(a, a + 3, a + 1, a + 1, a + 3, a + 4)\n    indices.push(a + 1, a + 4, a + 2, a + 2, a + 4, a + 5)\n  }\n  const g = new THREE.BufferGeometry()\n  g.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3))\n  g.setIndex(indices)\n  g.computeVertexNormals()\n  return g\n}\n\n/**\n * A coconut palm: a leaning, gently bowed trunk topped with a crown of drooping fronds.\n * Trunk rings and fronds are instanced; the seed drives lean direction, frond spread, and\n * coconut placement. One trunk cylinder collider.\n */\nexport function Palm({\n  position = [0, 0, 0],\n  rotation = [0, 0, 0],\n  seed = 1,\n  height = 6,\n  lean = 0.18,\n  fronds = 11,\n  frondLength = 2.2,\n  trunkColor,\n  frondColor,\n  coconuts = true,\n}: PalmProps) {\n  const { unit, palette } = useWorld()\n  const bark = trunkColor ?? palette.bark\n  const leaf = frondColor ?? palette.foliage\n  const h = height * unit\n  const trunkRef = useRef<THREE.InstancedMesh>(null)\n  const frondsRef = useRef<THREE.InstancedMesh>(null)\n\n  const { rings, crown, crownYaw, nuts, frondPose } = useMemo(() => {\n    const next = rng(seed)\n    const yaw = next() * Math.PI * 2\n    const bow = lean * h\n    const tip = new THREE.Vector3(Math.cos(yaw) * bow, h, Math.sin(yaw) * bow)\n    const ctrl = new THREE.Vector3(Math.cos(yaw) * bow * 0.15, h * 0.55, Math.sin(yaw) * bow * 0.15)\n    const curve = new THREE.QuadraticBezierCurve3(new THREE.Vector3(0, 0, 0), ctrl, tip)\n    const rings: { pos: THREE.Vector3; quat: THREE.Quaternion; len: number; r: number }[] = []\n    for (let i = 0; i < TRUNK_SEGMENTS; i++) {\n      const t0 = i / TRUNK_SEGMENTS\n      const t1 = (i + 1) / TRUNK_SEGMENTS\n      const a = curve.getPoint(t0)\n      const b = curve.getPoint(t1)\n      const dir = b.clone().sub(a)\n      const len = dir.length() * 1.15 // overlap hides the joints\n      rings.push({\n        pos: a.clone().add(b).multiplyScalar(0.5),\n        quat: new THREE.Quaternion().setFromUnitVectors(UP, dir.normalize()),\n        len,\n        r: (0.16 - 0.07 * t0) * unit * (height / 6),\n      })\n    }\n    const frondPose = Array.from({ length: fronds }, (_, i) => ({\n      yaw: (i / fronds) * Math.PI * 2 + (next() - 0.5) * 0.5,\n      pitch: 0.15 + next() * 0.75,\n      scale: 0.8 + next() * 0.35,\n    }))\n    const nuts = Array.from({ length: 3 }, () => {\n      const a = next() * Math.PI * 2\n      const r = 0.16 * unit\n      return new THREE.Vector3(Math.cos(a) * r * 1.6, -0.12 * unit, Math.sin(a) * r * 1.6)\n    })\n    return { rings, crown: tip, crownYaw: yaw, nuts, frondPose }\n  }, [seed, h, lean, fronds, unit, height])\n\n  const frondGeo = useMemo(\n    () => frondGeometry(frondLength * unit, 0.28 * unit * (frondLength / 2.2)),\n    [frondLength, unit],\n  )\n\n  useLayoutEffect(() => {\n    const mesh = trunkRef.current\n    if (!mesh) return\n    const dummy = new THREE.Object3D()\n    rings.forEach((ring, i) => {\n      dummy.position.copy(ring.pos)\n      dummy.quaternion.copy(ring.quat)\n      dummy.scale.set(ring.r, ring.len, ring.r)\n      dummy.updateMatrix()\n      mesh.setMatrixAt(i, dummy.matrix)\n    })\n    mesh.instanceMatrix.needsUpdate = true\n  }, [rings])\n\n  useLayoutEffect(() => {\n    const mesh = frondsRef.current\n    if (!mesh) return\n    const dummy = new THREE.Object3D()\n    frondPose.forEach((f, i) => {\n      dummy.position.copy(crown)\n      dummy.rotation.set(0, -f.yaw, -f.pitch, 'YXZ')\n      dummy.scale.setScalar(f.scale)\n      dummy.updateMatrix()\n      mesh.setMatrixAt(i, dummy.matrix)\n    })\n    mesh.instanceMatrix.needsUpdate = true\n  }, [frondPose, crown])\n\n  return (\n    <RigidBody type=\"fixed\" colliders={false} position={position} rotation={rotation}>\n      <CylinderCollider args={[h / 2, 0.2 * unit]} position={[0, h / 2, 0]} />\n      <instancedMesh\n        key={`t${rings.length}`}\n        ref={trunkRef}\n        args={[undefined, undefined, rings.length]}\n        castShadow\n      >\n        <cylinderGeometry args={[0.82, 1, 1, 7]} />\n        <meshStandardMaterial color={bark} roughness={0.95} flatShading />\n      </instancedMesh>\n      <instancedMesh\n        key={`f${fronds}`}\n        ref={frondsRef}\n        args={[frondGeo, undefined, fronds]}\n        castShadow\n      >\n        <meshStandardMaterial color={leaf} roughness={0.9} side={THREE.DoubleSide} flatShading />\n      </instancedMesh>\n      {coconuts && (\n        <group position={[crown.x, crown.y, crown.z]} rotation={[0, -crownYaw, 0]}>\n          {nuts.map((n, i) => (\n            <mesh key={`${i * 7}`} position={n} castShadow>\n              <sphereGeometry args={[0.11 * unit, 8, 6]} />\n              <meshStandardMaterial color=\"#6b5233\" roughness={1} />\n            </mesh>\n          ))}\n        </group>\n      )}\n    </RigidBody>\n  )\n}\n",
      "type": "registry:component"
    }
  ]
}
