{
  "name": "ocean",
  "title": "Ocean",
  "type": "registry:component",
  "description": "Camera-following animated-shader ocean that reaches the world fog horizon (no textures); sits at the world ground.",
  "dependencies": [
    "@react-three/fiber@^9.6.1",
    "@runek/core@^0.12.0",
    "three@^0.184.0"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "Ocean.tsx",
      "content": "import { useFrame } from '@react-three/fiber'\nimport { useWorld, type Vec3 } from '@runek/core'\nimport { useMemo, useRef } from 'react'\nimport * as THREE from 'three'\n\nexport interface OceanProps {\n  position?: Vec3\n  rotation?: Vec3\n  /** Plane size `[width, depth]`, in units. With `follow` on (the default) this patch tracks\n   *  the camera, so keep it large enough to reach past the world fog. */\n  size?: [number, number]\n  /** Defaults to the world palette's `waterDeep` slot. */\n  colorDeep?: string\n  /** Defaults to the world palette's `waterShallow` slot. */\n  colorShallow?: string\n  /** Direction the sun glint comes from; pair with your Sky's `sunPosition`. */\n  sunPosition?: Vec3\n  waveHeight?: number\n  waveSpeed?: number\n  segments?: number\n  /** Track the camera horizontally for an endless sea (default true). Set false to pin it. */\n  follow?: boolean\n}\n\nconst VERTEX = /* glsl */ `\n  uniform float uTime;\n  uniform float uWaveHeight;\n  uniform float uWaveSpeed;\n  varying vec3 vWorldPos;\n  varying vec3 vNormal;\n  varying float vWave;\n\n  // Long, slow ocean swells (lower frequencies than a Lake's chop).\n  float waveH(vec2 q, float t) {\n    float w1 = sin(q.x * 0.045 + t) * cos(q.y * 0.035 + t * 0.8);\n    float w2 = sin(q.x * 0.018 - t * 0.7) * sin(q.y * 0.06 + t);\n    float w3 = sin((q.x + q.y) * 0.09 + t * 1.4) * 0.25;\n    return (w1 + w2 + w3) * 0.5;\n  }\n\n  void main() {\n    float t = uTime * uWaveSpeed;\n    // World-space horizontal coords of the undisplaced vertex, so the swells stay put in the\n    // world even as the mesh follows the camera (no swimming).\n    vec2 wxz = (modelMatrix * vec4(position.x, position.y, 0.0, 1.0)).xz;\n    float h = waveH(wxz, t);\n    vWave = h;\n\n    vec3 p = position;\n    p.z += h * uWaveHeight;\n\n    float eps = 1.5;\n    float hx = waveH(wxz + vec2(eps, 0.0), t);\n    float hy = waveH(wxz + vec2(0.0, eps), t);\n    vec3 n = normalize(vec3(-(hx - h) * uWaveHeight / eps, -(hy - h) * uWaveHeight / eps, 1.0));\n    vNormal = normalize(mat3(modelMatrix) * n);\n\n    vWorldPos = (modelMatrix * vec4(p, 1.0)).xyz;\n    gl_Position = projectionMatrix * viewMatrix * vec4(vWorldPos, 1.0);\n  }\n`\n\nconst FRAGMENT = /* glsl */ `\n  uniform vec3 uColorDeep;\n  uniform vec3 uColorShallow;\n  uniform vec3 uSunDir;\n  uniform vec3 uFogColor;\n  uniform float uFogNear;\n  uniform float uFogFar;\n  varying vec3 vWorldPos;\n  varying vec3 vNormal;\n  varying float vWave;\n\n  void main() {\n    vec3 n = normalize(vNormal);\n    vec3 viewDir = normalize(cameraPosition - vWorldPos);\n\n    float m = smoothstep(-1.0, 1.0, vWave);\n    vec3 col = mix(uColorDeep, uColorShallow, m);\n\n    // sky tint at grazing angles\n    float fresnel = pow(1.0 - max(dot(n, viewDir), 0.0), 3.0);\n    col = mix(col, vec3(0.62, 0.78, 0.9), fresnel * 0.5);\n\n    // sun glint\n    vec3 r = reflect(-uSunDir, n);\n    float glint = pow(max(dot(r, viewDir), 0.0), 80.0);\n    col += vec3(1.0, 0.95, 0.85) * glint * 0.8;\n\n    // crest brighten\n    col += pow(max(m - 0.6, 0.0), 2.0) * 0.35;\n\n    float alpha = 0.86 + fresnel * 0.14;\n\n    // Blend into the world's distance fog so the far sea melts into the sky/horizon.\n    float fogF = smoothstep(uFogNear, uFogFar, distance(cameraPosition, vWorldPos));\n    col = mix(col, uFogColor, fogF);\n    alpha = mix(alpha, 1.0, fogF);\n\n    gl_FragColor = vec4(col, alpha);\n  }\n`\n\n/** Procedural animated ocean (no textures), decorative (no collider). It follows the camera\n *  horizontally so it reads as open sea to the horizon, with world-space swells that stay put\n *  as it moves and a fog blend that melts the far water into the sky. The surface sits at the\n *  world ground baseline by default (CONTRACT §10); an explicit `position` height wins. */\nexport function Ocean({\n  position,\n  rotation = [0, 0, 0],\n  size = [400, 400],\n  colorDeep,\n  colorShallow,\n  sunPosition = [80, 30, 40],\n  waveHeight = 0.4,\n  waveSpeed = 0.6,\n  segments = 160,\n  follow = true,\n}: OceanProps) {\n  const { unit, palette, ground } = useWorld()\n  const deep = colorDeep ?? palette.waterDeep\n  const shallow = colorShallow ?? palette.waterShallow\n  const y = position?.[1] ?? ground\n  const w = size[0] * unit\n  const d = size[1] * unit\n  const meshRef = useRef<THREE.Mesh>(null)\n  const matRef = useRef<THREE.ShaderMaterial>(null)\n\n  const uniforms = useMemo(\n    () => ({\n      uTime: { value: 0 },\n      uWaveHeight: { value: waveHeight * unit },\n      uWaveSpeed: { value: waveSpeed },\n      uColorDeep: { value: new THREE.Color(deep) },\n      uColorShallow: { value: new THREE.Color(shallow) },\n      uSunDir: { value: new THREE.Vector3(...sunPosition).normalize() },\n      uFogColor: { value: new THREE.Color('#ffffff') },\n      uFogNear: { value: 1e6 },\n      uFogFar: { value: 1e7 },\n    }),\n    [waveHeight, waveSpeed, deep, shallow, sunPosition, unit],\n  )\n\n  useFrame((state, delta) => {\n    const mat = matRef.current\n    if (mat) {\n      mat.uniforms.uTime.value += delta\n      // Mirror the world's distance fog so the ocean horizon matches everything else.\n      const fog = state.scene.fog\n      if (fog && 'near' in fog) {\n        const linear = fog as THREE.Fog\n        mat.uniforms.uFogColor.value.copy(linear.color)\n        mat.uniforms.uFogNear.value = linear.near\n        mat.uniforms.uFogFar.value = linear.far\n      }\n    }\n    if (follow && meshRef.current) {\n      meshRef.current.position.x = state.camera.position.x + (position?.[0] ?? 0)\n      meshRef.current.position.z = state.camera.position.z + (position?.[2] ?? 0)\n    }\n  })\n\n  const initX = follow ? 0 : (position?.[0] ?? 0)\n  const initZ = follow ? 0 : (position?.[2] ?? 0)\n\n  return (\n    <mesh\n      ref={meshRef}\n      position={[initX, y, initZ]}\n      rotation={[-Math.PI / 2 + rotation[0], rotation[1], rotation[2]]}\n    >\n      <planeGeometry args={[w, d, segments, segments]} />\n      <shaderMaterial\n        ref={matRef}\n        uniforms={uniforms}\n        vertexShader={VERTEX}\n        fragmentShader={FRAGMENT}\n        transparent\n      />\n    </mesh>\n  )\n}\n",
      "type": "registry:component"
    }
  ]
}
