{
  "name": "path",
  "title": "Path",
  "type": "registry:component",
  "description": "A meandering ribbon trail from seeded waypoints; decorative, laid just over the ground.",
  "dependencies": [
    "@runek/core@^0.12.0",
    "three@^0.184.0"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "Path.tsx",
      "content": "import { rng, useWorld, type Vec3 } from '@runek/core'\nimport { useEffect, useMemo } from 'react'\nimport * as THREE from 'three'\n\nexport interface PathProps {\n  position?: Vec3\n  rotation?: Vec3\n  /** Length along local Z, in units. */\n  length?: number\n  width?: number\n  /** Lateral meander amplitude, in units. */\n  meander?: number\n  /** Total height climbed from the near end (local −Z) to the far end (+Z), in units. For a\n   *  trail that gains height as it winds; the ribbon rises linearly along its length. */\n  rise?: number\n  /** Explicit elevation profile, in units: evenly spaced samples from the near end (local −Z)\n   *  to the far end (+Z), linearly interpolated along the ribbon. Overrides `rise`. Author it\n   *  from the terrain the trail crosses so the ribbon hugs the ground it climbs. */\n  heights?: number[]\n  /** Defaults to the world palette's `ground` slot. */\n  color?: string\n  segments?: number\n  seed?: number\n}\n\n/** A meandering ribbon trail laid flat in the XZ plane, draped just above the\n *  ground. Decorative: you walk on the terrain beneath it, so it owns no collider. */\nexport function Path({\n  position = [0, 0, 0],\n  rotation = [0, 0, 0],\n  length = 12,\n  width = 1.4,\n  meander = 1.2,\n  rise = 0,\n  heights,\n  color,\n  segments = 48,\n  seed = 1,\n}: PathProps) {\n  const { unit, palette } = useWorld()\n  const pathColor = color ?? palette.ground\n  const L = length * unit\n  const W = width * unit\n  const amp = meander * unit\n  const Rise = rise * unit\n\n  const geometry = useMemo(() => {\n    const next = rng(seed)\n    const p1 = next() * Math.PI * 2\n    const p2 = next() * Math.PI * 2\n    const f1 = 1.2 + next() * 0.6\n    const f2 = 2.6 + next() * 1.0\n    const centerX = (t: number) =>\n      amp * (Math.sin(t * Math.PI * f1 + p1) * 0.6 + Math.sin(t * Math.PI * f2 + p2) * 0.4)\n    const profile = heights && heights.length > 1 ? heights : null\n    const elevation = (t: number) => {\n      if (!profile) return t * Rise\n      const s = t * (profile.length - 1)\n      const i = Math.min(profile.length - 2, Math.floor(s))\n      return (profile[i] + (profile[i + 1] - profile[i]) * (s - i)) * unit\n    }\n\n    const positions: number[] = []\n    const indices: number[] = []\n    for (let i = 0; i <= segments; i++) {\n      const t = i / segments\n      const z = -L / 2 + t * L\n      const cx = centerX(t)\n      const dt = 1 / segments\n      const tx = centerX(Math.min(1, t + dt)) - cx\n      const tz = dt * L\n      const tl = Math.hypot(tx, tz) || 1\n      const nx = -tz / tl\n      const nz = tx / tl\n      const y = elevation(t)\n      positions.push(cx + (nx * W) / 2, y, z + (nz * W) / 2)\n      positions.push(cx - (nx * W) / 2, y, z - (nz * W) / 2)\n    }\n    for (let i = 0; i < segments; i++) {\n      const a = i * 2\n      indices.push(a, a + 2, a + 1, a + 1, a + 2, a + 3)\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  }, [L, W, amp, Rise, heights, unit, segments, seed])\n\n  useEffect(() => () => geometry.dispose(), [geometry])\n\n  return (\n    <mesh\n      geometry={geometry}\n      position={[position[0], position[1] + 0.02 * unit, position[2]]}\n      rotation={rotation}\n      receiveShadow\n    >\n      <meshStandardMaterial color={pathColor} roughness={1} side={THREE.DoubleSide} />\n    </mesh>\n  )\n}\n",
      "type": "registry:component"
    }
  ]
}
