{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ocean",
  "type": "registry:component",
  "title": "Ocean",
  "description": "Animated open sea for offshore scenes: six Gerstner swell components scaled by the significant wave height, wind chop and whitecaps in the per-pixel normals, a physical water material (IOR 1.333) with Fresnel, sun glints and environment reflections, and foam rings around legs, risers and piles (`foam`: [x, z, radius]). Slightly transparent looking down, so an underwater jacket reads through. Origin at mean sea level (y = 0); the polar LOD grid follows the camera and respects scene fog.",
  "dependencies": [
    "three",
    "@react-three/fiber",
    "@react-three/drei"
  ],
  "registryDependencies": [
    "https://deck-twin-library.demoareeb.workers.dev/r/core.json"
  ],
  "files": [
    {
      "path": "environment/ocean.tsx",
      "type": "registry:file",
      "target": "components/deck-twin/environment/ocean.tsx",
      "content": "// Animated open sea: Gerstner swell on a polar LOD grid, physical water with Fresnel, sun glints, whitecaps and foam rings; origin at mean sea level (ported from the Deck Twin app).\nimport { useEffect, useMemo, useRef } from 'react'\nimport * as THREE from 'three'\nimport { useFrame, useThree } from '@react-three/fiber'\nimport type { GroupProps } from '../lib/mesh'\n\nexport interface OceanProps {\n  /** significant wave height Hs, m */\n  waveHeight?: number\n  /** wind speed at 10 m, m/s (chop and whitecaps) */\n  wind?: number\n  /** sea surface radius, m */\n  radius?: number\n  /** keep the dense centre of the grid under the camera */\n  follow?: boolean\n  /** darker water for night scenes */\n  night?: boolean\n  color?: string\n  /** opacity looking straight down; grazing views are opaque */\n  opacity?: number\n  /** animation speed, 1 = real time */\n  speed?: number\n  /** foam ring centres [x, z, radius] in m, local frame (jacket legs, risers, piles) */\n  foam?: [number, number, number][]\n}\n\nexport const defaults = {\n  waveHeight: 1.5,\n  wind: 7,\n  radius: 4000,\n  follow: true,\n  night: false,\n  color: '#0c4a62',\n  opacity: 0.8,\n  speed: 1,\n  foam: [] as [number, number, number][],\n}\n\nconst FOAM_SLOTS = 32\nconst HS_REF = 1.5\n\nconst WAVES_GLSL = /* glsl */ `\nconst int NW = 6;\n// dir.x, dir.z, amplitude (m at Hs 1.5 m), wavelength (m)\nconst vec4 WAVES[6] = vec4[6](\n  vec4(0.94, 0.34, 0.50, 96.0),\n  vec4(0.70, 0.71, 0.32, 58.0),\n  vec4(0.99, -0.14, 0.20, 34.0),\n  vec4(0.52, 0.85, 0.11, 19.5),\n  vec4(0.86, -0.51, 0.065, 11.3),\n  vec4(0.28, 0.96, 0.04, 6.7)\n);\nconst float STEEP = 0.62;\nuniform float uTime;\nuniform float uAmp;\nuniform float uChop;\nuniform float uWhite;\nuniform float uNight;\nuniform float uOpacity;\nuniform vec2 uOffset;\nuniform mat3 uRot;\nfloat waveFade(float L, float spacing) { return smoothstep(2.5 * spacing, 5.0 * spacing, L); }\nfloat pixFade(float L, float pix) { return smoothstep(5.0 * pix, 14.0 * pix, L); }\nvec3 gerstner(vec2 p, float t, float spacing, out float h) {\n  vec3 d = vec3(0.0);\n  for (int i = 0; i < NW; i++) {\n    vec4 w = WAVES[i];\n    vec2 dir = normalize(w.xy);\n    float k = 6.2831853 / w.w;\n    float f = k * (dot(dir, p) - sqrt(9.81 / k) * t);\n    float fade = waveFade(w.w, spacing);\n    float q = STEEP * min(uAmp, 1.35) / (k * float(NW));\n    d.xz += q * dir * cos(f) * fade;\n    d.y += w.z * uAmp * fade * sin(f);\n  }\n  h = d.y;\n  return d;\n}\nvec3 gerstnerNormal(vec2 p, float t, float pix) {\n  vec3 n = vec3(0.0, 1.0, 0.0);\n  for (int i = 0; i < NW; i++) {\n    vec4 w = WAVES[i];\n    vec2 dir = normalize(w.xy);\n    float k = 6.2831853 / w.w;\n    float f = k * (dot(dir, p) - sqrt(9.81 / k) * t);\n    float fade = pixFade(w.w, pix);\n    float wa = k * w.z * uAmp * fade;\n    n.x -= dir.x * wa * cos(f);\n    n.z -= dir.y * wa * cos(f);\n    n.y -= (STEEP * min(uAmp, 1.35) / float(NW)) * sin(f) * fade;\n  }\n  // wind chop (normal only)\n  const vec4 DET[6] = vec4[6](\n    vec4(0.80, 0.60, 0.030, 4.3), vec4(-0.30, 0.95, 0.020, 2.9), vec4(0.97, -0.25, 0.015, 2.05),\n    vec4(0.45, -0.89, 0.010, 1.42), vec4(0.10, 0.99, 0.007, 0.97), vec4(0.70, 0.71, 0.005, 0.66)\n  );\n  for (int i = 0; i < 6; i++) {\n    vec4 w = DET[i];\n    vec2 dir = normalize(w.xy);\n    float k = 6.2831853 / w.w;\n    float f = k * dot(dir, p) - sqrt(9.81 * k) * t;\n    float wa = k * w.z * uChop * pixFade(w.w, pix);\n    n.x -= dir.x * wa * cos(f);\n    n.z -= dir.y * wa * cos(f);\n  }\n  return normalize(n);\n}\nfloat oHash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }\nfloat oNoise(vec2 p) {\n  vec2 i = floor(p), f = fract(p);\n  vec2 u = f * f * (3.0 - 2.0 * f);\n  return mix(mix(oHash(i), oHash(i + vec2(1, 0)), u.x), mix(oHash(i + vec2(0, 1)), oHash(i + vec2(1, 1)), u.x), u.y);\n}\nfloat oFbm(vec2 p) {\n  float v = 0.0, a = 0.5;\n  for (int i = 0; i < 4; i++) { v += a * oNoise(p); p = p * 2.07 + 11.3; a *= 0.5; }\n  return v;\n}\n`\n\nfunction polarGrid(radius: number) {\n  const segments = radius > 600 ? 384 : 256\n  const rings = radius > 600 ? 190 : 120\n  const first = Math.min(0.55, radius / 300)\n  let lo = 1.000001\n  let hi = 1.2\n  for (let i = 0; i < 60; i++) {\n    const q = (lo + hi) / 2\n    if ((first * (Math.pow(q, rings) - 1)) / (q - 1) > radius) hi = q\n    else lo = q\n  }\n  const q = (lo + hi) / 2\n  const pos = new Float32Array((1 + rings * segments) * 3)\n  let o = 3\n  for (let n = 1; n <= rings; n++) {\n    const r = (first * (Math.pow(q, n) - 1)) / (q - 1)\n    const off = n % 2 ? 0.5 : 0\n    for (let s = 0; s < segments; s++) {\n      const a = ((s + off) / segments) * Math.PI * 2\n      pos[o++] = Math.cos(a) * r\n      pos[o++] = 0\n      pos[o++] = Math.sin(a) * r\n    }\n  }\n  const idx: number[] = []\n  for (let s = 0; s < segments; s++) idx.push(0, 1 + ((s + 1) % segments), 1 + s)\n  for (let n = 1; n < rings; n++) {\n    const a0 = 1 + (n - 1) * segments\n    const b0 = 1 + n * segments\n    for (let s = 0; s < segments; s++) {\n      const s1 = (s + 1) % segments\n      idx.push(a0 + s, a0 + s1, b0 + s, a0 + s1, b0 + s1, b0 + s)\n    }\n  }\n  const nor = new Float32Array(pos.length)\n  for (let i = 1; i < nor.length; i += 3) nor[i] = 1\n  const g = new THREE.BufferGeometry()\n  g.setAttribute('position', new THREE.BufferAttribute(pos, 3))\n  g.setAttribute('normal', new THREE.BufferAttribute(nor, 3))\n  g.setIndex(idx)\n  g.computeBoundingBox()\n  g.boundingSphere = new THREE.Sphere(new THREE.Vector3(), radius)\n  return g\n}\n\ntype Uniforms = Record<'uTime' | 'uAmp' | 'uChop' | 'uWhite' | 'uNight' | 'uOpacity', { value: number }> & {\n  uOffset: { value: THREE.Vector2 }\n  uRot: { value: THREE.Matrix3 }\n  uFoam: { value: THREE.Vector3[] }\n}\n\nfunction makeMaterial(u: Uniforms, nFoam: number) {\n  const m = new THREE.MeshPhysicalMaterial({ color: defaults.color, roughness: 0.06, metalness: 0, ior: 1.333, transparent: true, envMapIntensity: 1 })\n  m.onBeforeCompile = (sh) => {\n    Object.assign(sh.uniforms, u)\n    sh.vertexShader = sh.vertexShader\n      .replace('#include <common>', `#include <common>\\nvarying vec3 vOWPos;\\nvarying vec2 vLocal;\\nvarying float vWaveH;\\n${WAVES_GLSL}`)\n      .replace(\n        '#include <begin_vertex>',\n        /* glsl */ `\n        vec3 transformed = vec3(position);\n        {\n          vec2 lp = position.xz + uOffset;\n          vec4 wp0 = modelMatrix * vec4(position, 1.0);\n          float camD = length(wp0.xz - cameraPosition.xz) + abs(cameraPosition.y - wp0.y) * 0.5;\n          float spacing = 0.036 * length(position.xz) + 0.25;\n          float h;\n          vec3 d = gerstner(lp, uTime, spacing, h);\n          d *= 1.0 - smoothstep(1200.0, 3200.0, camD);\n          transformed += d;\n          vLocal = lp + d.xz;\n          vOWPos = (modelMatrix * vec4(transformed, 1.0)).xyz;\n          vWaveH = h / (1.25 * max(uAmp, 0.3));\n        }`,\n      )\n    sh.fragmentShader = sh.fragmentShader\n      .replace(\n        '#include <common>',\n        `#include <common>\\n#define FOAM_N ${nFoam}\\n${nFoam ? `uniform vec3 uFoam[${nFoam}];` : ''}\\nvarying vec3 vOWPos;\\nvarying vec2 vLocal;\\nvarying float vWaveH;\\n${WAVES_GLSL}\\nfloat gFoam = 0.0;`,\n      )\n      .replace(\n        '#include <color_fragment>',\n        /* glsl */ `#include <color_fragment>\n        {\n          vec2 p = vLocal;\n          float ring = 0.0;\n          #if FOAM_N > 0\n          for (int i = 0; i < FOAM_N; i++) {\n            float d = length(p - uFoam[i].xy) - uFoam[i].z;\n            ring = max(ring, 1.0 - smoothstep(0.0, 3.2, d));\n          }\n          #endif\n          float nn = oFbm(p * 0.85 + vec2(uTime * 0.21, uTime * 0.13));\n          float nn2 = oFbm(p * 0.23 - vec2(uTime * 0.05, 0.0));\n          float foam = smoothstep(0.42, 0.85, ring * (0.45 + nn));\n          foam = max(foam, smoothstep(0.62 - 0.1 * uWhite, 1.0, vWaveH) * smoothstep(0.58 - 0.12 * uWhite, 0.78, nn2) * (0.25 + 0.6 * uWhite));\n          vec3 V = normalize(cameraPosition - vOWPos);\n          float fres = pow(1.0 - clamp(V.y, 0.0, 1.0), 4.0);\n          vec3 water = mix(diffuseColor.rgb, vec3(0.06, 0.32, 0.33), clamp(vWaveH * 0.5 + 0.25, 0.0, 1.0) * 0.35);\n          water *= mix(1.0, 0.3, uNight);\n          diffuseColor.rgb = mix(water, vec3(0.74, 0.8, 0.82) * mix(1.0, 0.25, uNight), foam * 0.7);\n          diffuseColor.a = mix(uOpacity, 1.0, max(clamp(fres * 2.0, 0.0, 1.0), foam));\n          gFoam = foam;\n        }`,\n      )\n      .replace(\n        '#include <roughnessmap_fragment>',\n        /* glsl */ `#include <roughnessmap_fragment>\n        roughnessFactor = mix(0.06, 0.24, smoothstep(40.0, 1800.0, length(cameraPosition - vOWPos))) + gFoam * 0.7;`,\n      )\n      .replace(\n        '#include <normal_fragment_begin>',\n        /* glsl */ `#include <normal_fragment_begin>\n        {\n          float pix = length(fwidth(vLocal)) + 0.02;\n          vec3 nW = gerstnerNormal(vLocal, uTime, pix);\n          nW = normalize(mix(nW, vec3(0.0, 1.0, 0.0), gFoam * 0.6));\n          normal = normalize((viewMatrix * vec4(normalize(uRot * nW), 0.0)).xyz);\n          vec3 toEye = normalize(vViewPosition);\n          vec3 up = normalize((viewMatrix * vec4(normalize(uRot * vec3(0.0, 1.0, 0.0)), 0.0)).xyz);\n          normal = normalize(mix(normal, up, smoothstep(0.16, -0.04, dot(normal, toEye))));\n          normal = normalize(normal + toEye * max(0.0, 0.05 - dot(normal, toEye)));\n        }`,\n      )\n  }\n  m.customProgramCacheKey = () => `deck-twin-ocean-${nFoam}`\n  m.name = 'ocean'\n  return m\n}\n\nconst _v = new THREE.Vector3()\n\nexport function Ocean({\n  waveHeight = defaults.waveHeight,\n  wind = defaults.wind,\n  radius = defaults.radius,\n  follow = defaults.follow,\n  night = defaults.night,\n  color = defaults.color,\n  opacity = defaults.opacity,\n  speed = defaults.speed,\n  foam = defaults.foam,\n  ...group\n}: OceanProps & GroupProps) {\n  const ref = useRef<THREE.Mesh>(null)\n  const camera = useThree((s) => s.camera)\n  const nFoam = Math.min(FOAM_SLOTS, foam.length)\n  const u = useMemo<Uniforms>(\n    () => ({\n      uTime: { value: 0 },\n      uAmp: { value: 1 },\n      uChop: { value: 1 },\n      uWhite: { value: 0.3 },\n      uNight: { value: 0 },\n      uOpacity: { value: defaults.opacity },\n      uOffset: { value: new THREE.Vector2() },\n      uRot: { value: new THREE.Matrix3() },\n      uFoam: { value: Array.from({ length: FOAM_SLOTS }, () => new THREE.Vector3(1e5, 1e5, 0)) },\n    }),\n    [],\n  )\n  const geometry = useMemo(() => polarGrid(Math.max(10, radius)), [radius])\n  const material = useMemo(() => makeMaterial(u, nFoam), [u, nFoam])\n  useEffect(() => () => geometry.dispose(), [geometry])\n  useEffect(() => () => material.dispose(), [material])\n  useEffect(() => {\n    material.color.set(color)\n    u.uAmp.value = Math.max(0.05, waveHeight / HS_REF)\n    u.uChop.value = 0.55 + wind / 14\n    u.uWhite.value = Math.min(1.4, Math.max(0, (wind - 4) / 10))\n    u.uNight.value = night ? 1 : 0\n    u.uOpacity.value = opacity\n  }, [material, u, color, waveHeight, wind, night, opacity])\n  const pool = useMemo(() => foam.map(([x, z, r]) => new THREE.Vector3(x, z, r)), [foam])\n  const lastPick = useRef(new THREE.Vector2(1e9, 1e9))\n  useEffect(() => {\n    lastPick.current.set(1e9, 1e9)\n  }, [pool])\n  useFrame((_, dt) => {\n    const m = ref.current\n    if (!m?.parent) return\n    u.uTime.value += Math.min(dt, 0.1) * speed\n    _v.copy(camera.position)\n    m.parent.worldToLocal(_v)\n    if (follow) m.position.set(_v.x, 0, _v.z)\n    else m.position.set(0, 0, 0)\n    u.uOffset.value.set(m.position.x, m.position.z)\n    m.updateMatrixWorld()\n    u.uRot.value.setFromMatrix4(m.matrixWorld)\n    if (pool.length && Math.hypot(_v.x - lastPick.current.x, _v.z - lastPick.current.y) > 8) {\n      lastPick.current.set(_v.x, _v.z)\n      const near = pool.length > FOAM_SLOTS ? [...pool].sort((a, b) => Math.hypot(a.x - _v.x, a.y - _v.z) - Math.hypot(b.x - _v.x, b.y - _v.z)) : pool\n      for (let i = 0; i < nFoam; i++) u.uFoam.value[i].copy(near[i])\n    }\n  })\n  return (\n    <group {...group}>\n      <mesh ref={ref} geometry={geometry} material={material} receiveShadow frustumCulled={false} renderOrder={-1} raycast={() => null} userData={{ noFit: true }} name=\"Ocean\" />\n    </group>\n  )\n}\n\nexport default Ocean\n"
    }
  ],
  "categories": [
    "environment"
  ],
  "meta": {
    "export": "Ocean",
    "anchor": "base",
    "standards": [
      "Gerstner (trochoidal) waves"
    ]
  }
}