{
  "name": "bridge",
  "title": "Bridge",
  "type": "registry:component",
  "description": "Plank deck spanning a gap, optionally arched, with railings and slab colliders that follow the deck.",
  "dependencies": [
    "@react-three/rapier@^2.2.0",
    "@runek/core@^0.12.0"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "Bridge.tsx",
      "content": "import { CuboidCollider, RigidBody } from '@react-three/rapier'\nimport { rng, useWorld, type Vec3 } from '@runek/core'\nimport { useMemo } from 'react'\n\nexport interface BridgeProps {\n  position?: Vec3\n  rotation?: Vec3\n  /** Span along local X, in units. */\n  length?: number\n  /** Walkway width along local Z, in units. */\n  width?: number\n  /** Arch rise at the center, in units (0 = flat). */\n  arch?: number\n  /** Side railings. */\n  rails?: boolean\n  /** Deck color; defaults to the world palette's `wood` slot. */\n  color?: string\n  seed?: number\n}\n\nconst archY = (t: number, rise: number) => Math.sin(t * Math.PI) * rise\n\n/** A plank deck that spans a gap, optionally arched, with railings. The deck is\n *  the gameplay surface, so colliders follow it as a few slabs, not one per plank. */\nexport function Bridge({\n  position = [0, 0, 0],\n  rotation = [0, 0, 0],\n  length = 6,\n  width = 2,\n  arch = 0,\n  rails = true,\n  color,\n  seed = 1,\n}: BridgeProps) {\n  const { unit, palette } = useWorld()\n  const deckColor = color ?? palette.wood\n  const railColor = palette.woodDark\n  const L = length * unit\n  const W = width * unit\n  const rise = arch * unit\n  const deckT = 0.1 * unit\n  const railH = 0.9 * unit\n\n  const plankCount = Math.max(6, Math.round(length / 0.3))\n  const planks = useMemo(() => {\n    const next = rng(seed)\n    const pw = L / plankCount\n    return Array.from({ length: plankCount }, (_, i) => {\n      const t = (i + 0.5) / plankCount\n      return { x: -L / 2 + (i + 0.5) * pw, y: archY(t, rise), w: pw * (0.86 + next() * 0.1) }\n    })\n  }, [L, plankCount, rise, seed])\n\n  const SLABS = 6\n  const colliders = useMemo(\n    () =>\n      Array.from({ length: SLABS }, (_, i) => {\n        const t = (i + 0.5) / SLABS\n        return { x: -L / 2 + t * L, y: archY(t, rise) }\n      }),\n    [L, rise],\n  )\n\n  const posts = useMemo(() => {\n    if (!rails) return []\n    const n = Math.max(2, Math.round(length / 1.2))\n    return Array.from({ length: n + 1 }, (_, i) => {\n      const t = i / n\n      return { x: -L / 2 + t * L, y: archY(t, rise) }\n    })\n  }, [rails, length, L, rise])\n\n  return (\n    <RigidBody type=\"fixed\" colliders={false} position={position} rotation={rotation}>\n      {colliders.map((c) => (\n        <CuboidCollider\n          key={`col-${c.x.toFixed(3)}`}\n          args={[L / SLABS / 2, deckT / 2, W / 2]}\n          position={[c.x, c.y, 0]}\n        />\n      ))}\n\n      {planks.map((p) => (\n        <mesh key={`plank-${p.x.toFixed(3)}`} castShadow receiveShadow position={[p.x, p.y, 0]}>\n          <boxGeometry args={[p.w, deckT, W]} />\n          <meshStandardMaterial color={deckColor} roughness={0.85} />\n        </mesh>\n      ))}\n\n      {posts.map((p) =>\n        [-1, 1].map((s) => (\n          <mesh\n            key={`post-${p.x.toFixed(3)}-${s}`}\n            castShadow\n            position={[p.x, p.y + railH / 2, (s * W) / 2]}\n          >\n            <boxGeometry args={[0.08 * unit, railH, 0.08 * unit]} />\n            <meshStandardMaterial color={railColor} roughness={0.85} />\n          </mesh>\n        )),\n      )}\n\n      {rails &&\n        [-1, 1].map((s) => (\n          <mesh key={`toprail-${s}`} castShadow position={[0, rise * 0.5 + railH, (s * W) / 2]}>\n            <boxGeometry args={[L, 0.07 * unit, 0.07 * unit]} />\n            <meshStandardMaterial color={railColor} roughness={0.85} />\n          </mesh>\n        ))}\n    </RigidBody>\n  )\n}\n",
      "type": "registry:component"
    }
  ]
}
