{
  "name": "tent",
  "title": "Tent",
  "type": "registry:component",
  "description": "An A-frame ridge tent: striped wind-rippled fabric, entrance flaps pinned open, guy ropes and stakes; solid sides and back, open front.",
  "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": "Tent.tsx",
      "content": "import { useFrame } from '@react-three/fiber'\nimport { CuboidCollider, RigidBody } from '@react-three/rapier'\nimport { useWorld, type WorldComponentProps } from '@runek/core'\nimport { useEffect, useMemo } from 'react'\nimport { BufferAttribute, BufferGeometry, DoubleSide, Quaternion, Vector3 } from 'three'\n\nexport interface TentProps extends WorldComponentProps {\n  /** Width across the tent (local X), in units. */\n  width?: number\n  /** Depth front-to-back (local Z); the entrance faces +Z. */\n  depth?: number\n  /** Ridge height, in units. */\n  height?: number\n  /** Fabric color; defaults to the palette's `fabric`. */\n  color?: string\n  /** Alternate stripe color; defaults to the palette's `wall`. */\n  stripeColor?: string\n  /** Pole color; defaults to the palette's `woodDark`. */\n  poleColor?: string\n  /** Billow depth as a fraction of the width. */\n  wind?: number\n  /** Wind ripple speed. */\n  windSpeed?: number\n  /** Static inward drape of the fabric between ridge and ground, as a fraction of the width. */\n  sag?: number\n  /** Solid walls (the two sides + the back) you can't walk through; the front stays open. */\n  collider?: boolean\n}\n\nconst V = (x: number, y: number, z: number) => new Vector3(x, y, z)\nconst UP = new Vector3(0, 1, 0)\n\n/**\n * An A-frame ridge tent: two sloped striped-fabric sides and a closed back, open at the front (+Z)\n * where two flaps are pinned back from the entrance. The fabric drapes inward between ridge and\n * ground and billows in the wind per-frame (pinned at the ridge and ground). The ridge pole pokes\n * out past the fabric, stayed by guy ropes to stakes at the front corners and the back. The two\n * sides and the back are solid (set `collider={false}` for a soft tent); the open front lets you\n * walk in. For a round dwelling with a conical roof, use `Hut`.\n */\nexport function Tent({\n  position = [0, 0, 0],\n  rotation = [0, 0, 0],\n  width = 3,\n  depth = 3.6,\n  height = 2.2,\n  color,\n  stripeColor,\n  poleColor,\n  wind = 0.05,\n  windSpeed = 2.5,\n  sag = 0.03,\n  collider = true,\n}: TentProps) {\n  const { unit, palette } = useWorld()\n  const fabric = color ?? palette.fabric\n  const stripe = stripeColor ?? palette.wall\n  const pole = poleColor ?? palette.woodDark\n  const W = width * unit\n  const H = height * unit\n  const D = depth * unit\n  const hw = W / 2\n  const hd = D / 2\n  const poleR = 0.05 * unit\n  const slant = Math.hypot(hw, H)\n  const lean = Math.atan2(hw, H) // door-pole tilt from vertical\n\n  // Fabric as one mesh: right slope, left slope, closed back, and two entrance flaps pinned open.\n  // Each vertex carries its panel's flat normal so the per-frame billow pushes it in and out like\n  // cloth; alternating cell columns are indexed into two material groups for crisp stripes.\n  const { geometry, base, normals } = useMemo(() => {\n    const pos: number[] = []\n    const nrm: number[] = []\n    const idxA: number[] = [] // fabric-colored triangles\n    const idxB: number[] = [] // stripe-colored triangles\n    const addQuad = (\n      a: Vector3,\n      b: Vector3,\n      c: Vector3,\n      d: Vector3,\n      nu: number,\n      nv: number,\n      mat: 'striped' | 'fabric' | 'stripe',\n    ) => {\n      const n = new Vector3().subVectors(b, a).cross(new Vector3().subVectors(d, a)).normalize()\n      const start = pos.length / 3\n      const cols = nu + 1\n      for (let iv = 0; iv <= nv; iv++) {\n        for (let iu = 0; iu <= nu; iu++) {\n          const u = iu / nu\n          const v = iv / nv\n          const x = (1 - u) * (1 - v) * a.x + u * (1 - v) * b.x + u * v * c.x + (1 - u) * v * d.x\n          const y = (1 - u) * (1 - v) * a.y + u * (1 - v) * b.y + u * v * c.y + (1 - u) * v * d.y\n          const z = (1 - u) * (1 - v) * a.z + u * (1 - v) * b.z + u * v * c.z + (1 - u) * v * d.z\n          pos.push(x, y, z)\n          nrm.push(n.x, n.y, n.z)\n        }\n      }\n      for (let iv = 0; iv < nv; iv++) {\n        for (let iu = 0; iu < nu; iu++) {\n          const i0 = start + iv * cols + iu\n          const striped =\n            mat === 'striped' ? (Math.floor(iu / 2) % 2 === 1 ? 'stripe' : 'fabric') : mat\n          const out = striped === 'stripe' ? idxB : idxA\n          out.push(i0, i0 + cols, i0 + 1, i0 + 1, i0 + cols, i0 + cols + 1)\n        }\n      }\n    }\n\n    // right slope (outward +X), left slope (outward −X), back wall (outward −Z, top collapsed)\n    addQuad(V(0, H, -hd), V(0, H, hd), V(hw, 0, hd), V(hw, 0, -hd), 12, 6, 'striped')\n    addQuad(V(0, H, hd), V(0, H, -hd), V(-hw, 0, -hd), V(-hw, 0, hd), 12, 6, 'striped')\n    addQuad(V(-hw, 0, -hd), V(hw, 0, -hd), V(0, H, -hd), V(0, H, -hd), 8, 6, 'striped')\n\n    // entrance flaps: the door triangle split at the ridge, each half swung out around its\n    // door-pole line so the tent reads open; one flap per color for the alternating look\n    const ridge = V(0, H, hd)\n    const mid = V(0, 0, hd)\n    for (const side of [1, -1] as const) {\n      const foot = V(side * hw, 0, hd)\n      const axis = new Vector3().subVectors(foot, ridge).normalize()\n      const out = new Vector3()\n        .subVectors(mid, ridge)\n        .applyAxisAngle(axis, side * 1.15)\n        .add(ridge)\n      const free = V(out.x + side * hw * 0.02, out.y, out.z)\n      if (side > 0) addQuad(ridge, foot, free, out, 4, 4, 'fabric')\n      else addQuad(ridge, out, free, foot, 4, 4, 'stripe')\n    }\n\n    const g = new BufferGeometry()\n    g.setAttribute('position', new BufferAttribute(new Float32Array(pos), 3))\n    g.setIndex([...idxA, ...idxB])\n    g.addGroup(0, idxA.length, 0)\n    g.addGroup(idxA.length, idxB.length, 1)\n\n    // static inward drape between the pinned ridge/ground/edges, so the panels read as hung cloth\n    const drape = sag * W\n    for (let i = 0; i < pos.length / 3; i++) {\n      const y = pos[i * 3 + 1]\n      const z = pos[i * 3 + 2]\n      const pin = Math.sin((Math.PI * y) / H) * Math.sin((Math.PI * (z + hd)) / Math.max(D, 0.001))\n      pos[i * 3] -= nrm[i * 3] * drape * Math.max(0, pin)\n      pos[i * 3 + 1] -= nrm[i * 3 + 1] * drape * Math.max(0, pin)\n      pos[i * 3 + 2] -= nrm[i * 3 + 2] * drape * Math.max(0, pin)\n    }\n    g.attributes.position = new BufferAttribute(new Float32Array(pos), 3)\n    g.computeVertexNormals()\n    return { geometry: g, base: Float32Array.from(pos), normals: Float32Array.from(nrm) }\n  }, [W, H, D, hw, hd, sag])\n\n  useEffect(() => () => geometry.dispose(), [geometry])\n\n  useFrame((state) => {\n    const t = state.clock.elapsedTime * windSpeed\n    const amp = wind * W\n    const p = geometry.attributes.position\n    for (let i = 0; i < p.count; i++) {\n      const bx = base[i * 3]\n      const by = base[i * 3 + 1]\n      const bz = base[i * 3 + 2]\n      const ripple = Math.sin(bx * 1.6 + bz * 1.2 - t) * 0.6 + Math.sin(by * 2 + bz - t * 0.7) * 0.4\n      // pinned at ridge (top) and ground; billows most in the middle of the panel\n      const d = ripple * amp * Math.sin((Math.PI * by) / H)\n      p.setXYZ(i, bx + normals[i * 3] * d, by + normals[i * 3 + 1] * d, bz + normals[i * 3 + 2] * d)\n    }\n    p.needsUpdate = true\n    geometry.computeVertexNormals()\n  })\n\n  // Guy ropes from the ridge-pole tips down to stakes: two framing the entrance, one at the back.\n  const ridgeOver = 0.35 * unit\n  const ropes = useMemo(() => {\n    const make = (from: Vector3, to: Vector3) => {\n      const dir = new Vector3().subVectors(to, from)\n      const len = dir.length()\n      const quat = new Quaternion().setFromUnitVectors(UP, dir.normalize())\n      const mid = new Vector3().addVectors(from, to).multiplyScalar(0.5)\n      return { mid, quat, len, stake: to }\n    }\n    return [\n      make(V(0, H, hd + ridgeOver), V(W * 0.55, 0.05, hd + H * 0.55)),\n      make(V(0, H, hd + ridgeOver), V(-W * 0.55, 0.05, hd + H * 0.55)),\n      make(V(0, H, -hd - ridgeOver), V(0, 0.05, -hd - H * 0.6)),\n    ]\n  }, [W, H, hd, ridgeOver])\n\n  const wallT = 0.05 * unit\n\n  return (\n    <RigidBody type=\"fixed\" colliders={false} position={position} rotation={rotation}>\n      {/* solid sloped sides + back; the front (+Z) is left open for the entrance */}\n      {collider && (\n        <>\n          <CuboidCollider\n            args={[wallT, slant / 2, hd]}\n            position={[hw / 2, H / 2, 0]}\n            rotation={[0, 0, lean]}\n          />\n          <CuboidCollider\n            args={[wallT, slant / 2, hd]}\n            position={[-hw / 2, H / 2, 0]}\n            rotation={[0, 0, -lean]}\n          />\n          <CuboidCollider args={[hw, H / 2, wallT]} position={[0, H / 2, -hd]} />\n        </>\n      )}\n\n      <mesh geometry={geometry} castShadow receiveShadow>\n        <meshStandardMaterial\n          attach=\"material-0\"\n          color={fabric}\n          side={DoubleSide}\n          roughness={0.95}\n        />\n        <meshStandardMaterial\n          attach=\"material-1\"\n          color={stripe}\n          side={DoubleSide}\n          roughness={0.95}\n        />\n      </mesh>\n\n      {/* ridge pole, poking out past the fabric at both ends */}\n      <mesh position={[0, H, 0]} rotation={[Math.PI / 2, 0, 0]} castShadow>\n        <cylinderGeometry args={[poleR, poleR, D + ridgeOver * 2, 8]} />\n        <meshStandardMaterial color={pole} roughness={0.8} />\n      </mesh>\n\n      {/* two door poles framing the entrance (+Z) */}\n      <mesh position={[hw / 2, H / 2, hd]} rotation={[0, 0, lean]} castShadow>\n        <cylinderGeometry args={[poleR, poleR, slant, 8]} />\n        <meshStandardMaterial color={pole} roughness={0.8} />\n      </mesh>\n      <mesh position={[-hw / 2, H / 2, hd]} rotation={[0, 0, -lean]} castShadow>\n        <cylinderGeometry args={[poleR, poleR, slant, 8]} />\n        <meshStandardMaterial color={pole} roughness={0.8} />\n      </mesh>\n\n      {/* guy ropes + stakes */}\n      {ropes.map((r, i) => (\n        <group key={i}>\n          <mesh position={r.mid} quaternion={r.quat}>\n            <cylinderGeometry args={[0.018 * unit, 0.018 * unit, r.len, 5]} />\n            <meshStandardMaterial color={palette.sand} roughness={1} />\n          </mesh>\n          <mesh position={[r.stake.x, 0.12 * unit, r.stake.z]} castShadow>\n            <cylinderGeometry args={[0.035 * unit, 0.05 * unit, 0.3 * unit, 6]} />\n            <meshStandardMaterial color={pole} roughness={0.9} />\n          </mesh>\n        </group>\n      ))}\n    </RigidBody>\n  )\n}\n",
      "type": "registry:component"
    }
  ]
}
