{
  "name": "sailboat",
  "title": "Sailboat",
  "type": "registry:component",
  "description": "A small procedural sailboat: a station-built low-poly hull, mast, boom, and mainsail, floating at the waterline with a gentle moored bob.",
  "dependencies": [
    "@react-three/fiber@^9.6.1",
    "@react-three/rapier@^2.2.0",
    "@runek/core@^0.12.0",
    "three@^0.184.0"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "Sailboat.tsx",
      "content": "import { useFrame } from '@react-three/fiber'\nimport { CuboidCollider, RigidBody } from '@react-three/rapier'\nimport { rng, useWorld, type WorldComponentProps } from '@runek/core'\nimport { useMemo, useRef } from 'react'\nimport { BufferAttribute, BufferGeometry, DoubleSide, type Group } from 'three'\n\nexport interface SailboatProps extends WorldComponentProps {\n  /** Hull length along local Z (bow at +Z), in units. */\n  length?: number\n  /** Hull maximum beam (width along X), in units. */\n  beam?: number\n  /** Deck height above the waterline (local y=0), in units. */\n  freeboard?: number\n  /** Keel depth below the waterline, in units. */\n  draft?: number\n  /** Mast height above the deck, in units. */\n  mastHeight?: number\n  /** Boom length aft of the mast, in units. */\n  boomLength?: number\n  /** Raise the mainsail. */\n  sail?: boolean\n  /** Gentle moored bobbing on the swell. */\n  bob?: boolean\n  /** Solid hull collider. */\n  collider?: boolean\n  /** Render the hull/rig as a bare visual, with no `RigidBody` or collider — for a parent\n   *  controller (e.g. a steerable vehicle) that owns the physics body. When false, `position`\n   *  and `rotation` place the visual directly and the `collider` prop is ignored. Default true. */\n  physics?: boolean\n  /** Hull color; defaults to the world palette's `wood`. */\n  color?: string\n  /** Deck, mast, and boom color; defaults to the palette's `woodDark`. */\n  trimColor?: string\n  /** Sail color. */\n  sailColor?: string\n}\n\ntype V3 = [number, number, number]\n\n/** A low-poly hull from stations along the keel: pointed bow, transom stern, V-section. The\n *  local origin sits at the waterline, so the deck rides at +freeboard and the keel at -draft. */\nfunction buildHull(L: number, B: number, fb: number, dr: number): BufferGeometry {\n  const N = 14\n  const t0 = 0.12\n  const hbMult = (t: number) => Math.sin(Math.PI * t) ** 0.6\n  const rocker = (t: number) => Math.sin(Math.PI * t) ** 0.5\n  const st = Array.from({ length: N }, (_, i) => {\n    const t = t0 + (i / (N - 1)) * (1 - t0)\n    return { z: -L / 2 + (i / (N - 1)) * L, hb: (B / 2) * hbMult(t), ky: -dr * rocker(t) }\n  })\n  const pos: number[] = []\n  const push = (...vs: V3[]) => {\n    for (const v of vs) pos.push(v[0], v[1], v[2])\n  }\n  for (let i = 0; i < N - 1; i++) {\n    const s = st[i]\n    const n = st[i + 1]\n    const Ls: V3 = [-s.hb, fb, s.z]\n    const Rs: V3 = [s.hb, fb, s.z]\n    const Ks: V3 = [0, s.ky, s.z]\n    const Ln: V3 = [-n.hb, fb, n.z]\n    const Rn: V3 = [n.hb, fb, n.z]\n    const Kn: V3 = [0, n.ky, n.z]\n    push(Ls, Ks, Kn, Ls, Kn, Ln) // port side\n    push(Rs, Rn, Kn, Rs, Kn, Ks) // starboard side\n    push(Ls, Ln, Rn, Ls, Rn, Rs) // deck\n  }\n  const s0 = st[0]\n  push([-s0.hb, fb, s0.z], [0, s0.ky, s0.z], [s0.hb, fb, s0.z]) // transom\n  const g = new BufferGeometry()\n  g.setAttribute('position', new BufferAttribute(new Float32Array(pos), 3))\n  g.computeVertexNormals()\n  return g\n}\n\nfunction triGeo(a: V3, b: V3, c: V3): BufferGeometry {\n  const g = new BufferGeometry()\n  g.setAttribute('position', new BufferAttribute(new Float32Array([...a, ...b, ...c]), 3))\n  g.computeVertexNormals()\n  return g\n}\n\n/** A small procedural sailboat: a station-built hull, a mast, a boom, and a swung-out mainsail.\n *  Floats at the local waterline and bobs gently on the swell. A static prop for now; the\n *  steerable variant is app-side player logic, not a new component. */\nexport function Sailboat({\n  position = [0, 0, 0],\n  rotation = [0, 0, 0],\n  seed = 1,\n  length = 5.5,\n  beam = 2,\n  freeboard = 0.5,\n  draft = 0.6,\n  mastHeight = 4.6,\n  boomLength = 3.2,\n  sail = true,\n  bob = true,\n  collider = true,\n  physics = true,\n  color,\n  trimColor,\n  sailColor = '#eee6d0',\n}: SailboatProps) {\n  const { unit, palette } = useWorld()\n  const hullColor = color ?? palette.wood\n  const trim = trimColor ?? palette.woodDark\n  const boat = useRef<Group>(null)\n\n  const U = unit\n  const L = length * U\n  const B = beam * U\n  const fb = freeboard * U\n  const dr = draft * U\n  const mastH = mastHeight * U\n  const boomL = boomLength * U\n  const goose = 0.7 * U\n  const mastZ = 0.12 * L\n  const boomAngle = 0.2\n\n  const hull = useMemo(() => buildHull(L, B, fb, dr), [L, B, fb, dr])\n  const main = useMemo(\n    () => triGeo([0, mastH, 0], [0, goose, 0], [0, goose, -boomL]),\n    [mastH, goose, boomL],\n  )\n\n  const phase = useMemo(() => rng(seed)() * Math.PI * 2, [seed])\n  useFrame((state) => {\n    if (!bob || !boat.current) return\n    const t = state.clock.elapsedTime * 1.1 + phase\n    boat.current.position.y = Math.sin(t) * 0.05 * U\n    boat.current.rotation.z = Math.sin(t * 0.8) * 0.03\n    boat.current.rotation.x = Math.sin(t * 0.6 + 1) * 0.02\n  })\n\n  // The hull, mast, boom, and sail. `boat` is the group the bob animates, so any placement\n  // (the RigidBody, or the bare-visual wrapper) goes on a parent, never on this group.\n  const rig = (\n    <group ref={boat}>\n      {/* hull */}\n      <mesh geometry={hull} castShadow receiveShadow>\n        <meshStandardMaterial color={hullColor} side={DoubleSide} flatShading roughness={0.8} />\n      </mesh>\n\n      {/* mast */}\n      <mesh position={[0, fb + mastH / 2, mastZ]} castShadow>\n        <cylinderGeometry args={[0.05 * U, 0.06 * U, mastH, 8]} />\n        <meshStandardMaterial color={trim} roughness={0.7} />\n      </mesh>\n\n      {/* boom + mainsail, swung out a touch and pivoting at the mast */}\n      <group position={[0, fb, mastZ]} rotation={[0, boomAngle, 0]}>\n        <mesh position={[0, goose, -boomL / 2]} rotation={[Math.PI / 2, 0, 0]} castShadow>\n          <cylinderGeometry args={[0.04 * U, 0.04 * U, boomL, 6]} />\n          <meshStandardMaterial color={trim} roughness={0.7} />\n        </mesh>\n        {sail && (\n          <mesh geometry={main} castShadow>\n            <meshStandardMaterial color={sailColor} side={DoubleSide} roughness={0.9} />\n          </mesh>\n        )}\n      </group>\n    </group>\n  )\n\n  // Bare visual: a parent controller owns the physics body and places this.\n  if (!physics) {\n    return (\n      <group position={position} rotation={rotation}>\n        {rig}\n      </group>\n    )\n  }\n\n  return (\n    <RigidBody type=\"fixed\" colliders={false} position={position} rotation={rotation}>\n      {collider && (\n        <CuboidCollider\n          args={[B * 0.45, (fb + dr) / 2, L * 0.45]}\n          position={[0, (fb - dr) / 2, 0]}\n        />\n      )}\n      {rig}\n    </RigidBody>\n  )\n}\n",
      "type": "registry:component"
    }
  ]
}
