{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "sky",
  "type": "registry:component",
  "title": "Sky and lights",
  "description": "Drop-in sky and lighting rig. Day: a physical (Preetham) sky, a warm sun with soft shadows, a hemisphere fill, an offline environment map baked from the sky and Lightformers (no network), exponential haze and a horizon band that hides the sea and sky seam. Night: a star dome with a moon in the sun direction, cool moonlight, a warm fill standing in for deck floodlights (`deckLights`) and a dark environment map. Set `lights: false` to keep your own lights; `preview` adds a reference pile with chrome and matte spheres standing in the sea.",
  "dependencies": [
    "three",
    "@react-three/fiber",
    "@react-three/drei"
  ],
  "registryDependencies": [
    "https://deck-twin-library.demoareeb.workers.dev/r/core.json",
    "https://deck-twin-library.demoareeb.workers.dev/r/ocean.json"
  ],
  "files": [
    {
      "path": "environment/sky.tsx",
      "type": "registry:file",
      "target": "components/deck-twin/environment/sky.tsx",
      "content": "// Day or night sky and lighting rig: physical sky or star dome with moon, sun or moon light with shadows, hemisphere fill, offline environment map, haze and horizon band (ported from the Deck Twin app).\nimport { useEffect, useLayoutEffect, useMemo, useRef } from 'react'\nimport * as THREE from 'three'\nimport { useFrame, useThree } from '@react-three/fiber'\nimport { Environment, Lightformer, Sky as DreiSky } from '@react-three/drei'\nimport type { GroupProps } from '../lib/mesh'\nimport { Ocean } from './ocean'\n\nexport interface SkyProps {\n  mode?: 'day' | 'night'\n  /** sun (day) or moon (night) elevation above the horizon, degrees */\n  elevation?: number\n  /** compass bearing of the sun or moon, degrees from north (-Z) through east (+X) */\n  azimuth?: number\n  /** sun or moon light, hemisphere fill and environment map; false draws only the sky, haze and fog */\n  lights?: boolean\n  /** scene radius the sun shadow covers, m */\n  shadowRadius?: number\n  shadowCenter?: [number, number, number]\n  /** fog density multiplier, 0 = clear */\n  haze?: number\n  stars?: boolean\n  /** warm fill at night standing in for deck floodlights, 0 to 2 */\n  deckLights?: number\n  /** reference scene: a pile with chrome and matte spheres in a small sea */\n  preview?: boolean\n}\n\nexport const defaults = {\n  mode: 'day' as 'day' | 'night',\n  elevation: 33,\n  azimuth: 112,\n  lights: true,\n  shadowRadius: 40,\n  shadowCenter: [0, 0, 0] as [number, number, number],\n  haze: 1,\n  stars: true,\n  deckLights: 1,\n  preview: false,\n}\n\nconst HAZE = '#cfe0ee'\nconst NIGHT_HAZE = '#0d1522'\nconst DEEP = '#0a2e3a'\nconst NIGHT_DEEP = '#03080c'\nconst FOG_DENSITY = 0.0008\nconst SKY = { turbidity: 1.7, rayleigh: 2.9, mieCoefficient: 0.0025, mieDirectionalG: 0.8 }\n\nexport function sunDirection(elevation: number, azimuth: number) {\n  const e = THREE.MathUtils.degToRad(elevation)\n  const a = THREE.MathUtils.degToRad(azimuth)\n  return new THREE.Vector3(Math.cos(e) * Math.sin(a), Math.sin(e), -Math.cos(e) * Math.cos(a)).normalize()\n}\n\nconst DOME_VERT = /* glsl */ `\nvarying vec3 vDir;\nvoid main() {\n  vDir = position;\n  gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n}`\n\nfunction useDome(ref: React.RefObject<THREE.Mesh | null>, k: number, onFrame?: (t: number) => void) {\n  const camera = useThree((s) => s.camera)\n  useFrame(({ clock }) => {\n    const m = ref.current\n    if (!m) return\n    onFrame?.(clock.elapsedTime)\n    m.position.copy(camera.position)\n    if (m.parent) m.parent.worldToLocal(m.position)\n    m.scale.setScalar(Math.min(3000 * k, ((camera as THREE.PerspectiveCamera).far ?? 8000) * 0.85 * k))\n  })\n}\n\nfunction HorizonHaze({ haze, deep }: { haze: string; deep: string }) {\n  const ref = useRef<THREE.Mesh>(null)\n  const mat = useMemo(\n    () =>\n      new THREE.ShaderMaterial({\n        uniforms: { uHaze: { value: new THREE.Color() }, uDeep: { value: new THREE.Color() } },\n        vertexShader: DOME_VERT,\n        fragmentShader: /* glsl */ `\n          uniform vec3 uHaze;\n          uniform vec3 uDeep;\n          varying vec3 vDir;\n          void main() {\n            float y = normalize(vDir).y;\n            float above = 1.0 - smoothstep(0.0, 0.05, y);\n            above = above * above * (3.0 - 2.0 * above);\n            vec3 col = mix(uHaze, uDeep, smoothstep(-0.004, -0.09, y));\n            gl_FragColor = vec4(col, y < 0.0 ? 1.0 : above * 0.8);\n            #include <tonemapping_fragment>\n            #include <colorspace_fragment>\n          }`,\n        transparent: true,\n        depthWrite: false,\n        side: THREE.BackSide,\n        fog: false,\n      }),\n    [],\n  )\n  useEffect(() => {\n    mat.uniforms.uHaze.value.set(haze)\n    mat.uniforms.uDeep.value.set(deep)\n  }, [mat, haze, deep])\n  useEffect(() => () => mat.dispose(), [mat])\n  const geo = useMemo(() => new THREE.SphereGeometry(1, 64, 32), [])\n  useDome(ref, 1)\n  return <mesh ref={ref} geometry={geo} material={mat} renderOrder={-2} frustumCulled={false} raycast={() => null} />\n}\n\nfunction StarDome({ moon, stars }: { moon: THREE.Vector3; stars: boolean }) {\n  const ref = useRef<THREE.Mesh>(null)\n  const mat = useMemo(\n    () =>\n      new THREE.ShaderMaterial({\n        uniforms: { uHorizon: { value: new THREE.Color(NIGHT_HAZE) }, uZenith: { value: new THREE.Color('#020409') }, uMoon: { value: new THREE.Vector3(0, 1, 0) }, uTime: { value: 0 }, uStars: { value: 1 } },\n        vertexShader: DOME_VERT,\n        fragmentShader: /* glsl */ `\n          uniform vec3 uHorizon;\n          uniform vec3 uZenith;\n          uniform vec3 uMoon;\n          uniform float uTime;\n          uniform float uStars;\n          varying vec3 vDir;\n          float hash(vec3 p) { return fract(sin(dot(p, vec3(127.1, 311.7, 74.7))) * 43758.5453); }\n          void main() {\n            vec3 d = normalize(vDir);\n            float y = max(d.y, 0.0);\n            vec3 col = mix(uHorizon, uZenith, pow(y, 0.45));\n            vec3 g = d * 260.0;\n            vec3 c = floor(g);\n            float h = hash(c);\n            if (uStars > 0.5 && h > 0.985) {\n              vec3 j = vec3(hash(c + 1.7), hash(c + 3.1), hash(c + 5.3));\n              float r = length(fract(g) - j);\n              float tw = 0.75 + 0.25 * sin(uTime * (1.5 + 3.0 * h) + h * 60.0);\n              float b = (h - 0.985) / 0.015;\n              col += vec3(0.9, 0.93, 1.0) * smoothstep(0.22, 0.0, r) * (0.25 + 1.6 * b * b) * tw * smoothstep(0.004, 0.12, y);\n            }\n            float m = dot(d, normalize(uMoon));\n            col += vec3(1.0, 0.97, 0.9) * 2.2 * smoothstep(0.99996, 0.99999, m);\n            col += vec3(0.35, 0.42, 0.6) * 0.18 * pow(max(m, 0.0), 300.0);\n            gl_FragColor = vec4(col, 1.0);\n            #include <tonemapping_fragment>\n            #include <colorspace_fragment>\n          }`,\n        depthWrite: false,\n        side: THREE.BackSide,\n        fog: false,\n      }),\n    [],\n  )\n  useEffect(() => {\n    mat.uniforms.uMoon.value.copy(moon)\n    mat.uniforms.uStars.value = stars ? 1 : 0\n  }, [mat, moon, stars])\n  useEffect(() => () => mat.dispose(), [mat])\n  const geo = useMemo(() => new THREE.SphereGeometry(1, 64, 32), [])\n  useDome(ref, 0.96, (t) => (mat.uniforms.uTime.value = t))\n  return <mesh ref={ref} geometry={geo} material={mat} renderOrder={-3} frustumCulled={false} raycast={() => null} />\n}\n\nfunction EnvSea({ horizon, sea }: { horizon: string; sea: string }) {\n  const mat = useMemo(\n    () =>\n      new THREE.ShaderMaterial({\n        uniforms: { uHorizon: { value: new THREE.Color(horizon) }, uSea: { value: new THREE.Color(sea) } },\n        vertexShader: DOME_VERT,\n        fragmentShader: /* glsl */ `\n          uniform vec3 uHorizon;\n          uniform vec3 uSea;\n          varying vec3 vDir;\n          void main() {\n            float y = normalize(vDir).y;\n            if (y > 0.0) discard;\n            gl_FragColor = vec4(mix(uHorizon, uSea, smoothstep(0.02, -0.7, y)), 1.0);\n          }`,\n        side: THREE.BackSide,\n        depthWrite: false,\n        depthTest: false,\n      }),\n    [horizon, sea],\n  )\n  useEffect(() => () => mat.dispose(), [mat])\n  return (\n    <mesh material={mat} scale={400} renderOrder={1}>\n      <sphereGeometry args={[1, 48, 24]} />\n    </mesh>\n  )\n}\n\nfunction Reference({ night }: { night: boolean }) {\n  return (\n    <group>\n      <Ocean waveHeight={1.2} night={night} />\n      <mesh position={[0, 0.25, 0]} castShadow receiveShadow>\n        <cylinderGeometry args={[0.45, 0.45, 4.5, 32]} />\n        <meshStandardMaterial color=\"#d9a300\" roughness={0.5} metalness={0.2} />\n      </mesh>\n      <mesh position={[0, -11, 0]} userData={{ noFit: true }}>\n        <cylinderGeometry args={[0.45, 0.45, 18, 32]} />\n        <meshStandardMaterial color=\"#5b5a3e\" roughness={0.8} />\n      </mesh>\n      <mesh position={[0, 2.55, 0]} castShadow receiveShadow>\n        <boxGeometry args={[3, 0.1, 1.6]} />\n        <meshStandardMaterial color=\"#8a8f93\" roughness={0.6} metalness={0.4} />\n      </mesh>\n      <mesh position={[-0.75, 3.1, 0]} castShadow>\n        <sphereGeometry args={[0.5, 48, 32]} />\n        <meshStandardMaterial color=\"#ffffff\" roughness={0.04} metalness={1} />\n      </mesh>\n      <mesh position={[0.75, 3.1, 0]} castShadow>\n        <sphereGeometry args={[0.5, 48, 32]} />\n        <meshStandardMaterial color=\"#9a9a9a\" roughness={0.85} metalness={0} />\n      </mesh>\n    </group>\n  )\n}\n\nexport function Sky({\n  mode = defaults.mode,\n  elevation = defaults.elevation,\n  azimuth = defaults.azimuth,\n  lights = defaults.lights,\n  shadowRadius = defaults.shadowRadius,\n  shadowCenter = defaults.shadowCenter,\n  haze = defaults.haze,\n  stars = defaults.stars,\n  deckLights = defaults.deckLights,\n  preview = defaults.preview,\n  ...group\n}: SkyProps & GroupProps) {\n  const night = mode === 'night'\n  const scene = useThree((s) => s.scene)\n  const dir = useMemo(() => sunDirection(elevation, azimuth), [elevation, azimuth])\n  const sunPos = useMemo(() => dir.clone().multiplyScalar(1000), [dir])\n  const hazeCol = night ? NIGHT_HAZE : HAZE\n  const [cx, cy, cz] = shadowCenter\n  const r = Math.max(2, shadowRadius)\n\n  useLayoutEffect(() => {\n    const fog = scene.fog\n    const bg = scene.background\n    const myFog = haze > 0 ? new THREE.FogExp2(hazeCol, FOG_DENSITY * haze * (night ? 1.5 : 1)) : null\n    const myBg = new THREE.Color(hazeCol)\n    scene.fog = myFog\n    scene.background = myBg\n    return () => {\n      if (scene.fog === myFog) scene.fog = fog\n      if (scene.background === myBg) scene.background = bg\n    }\n  }, [scene, haze, hazeCol, night])\n\n  const target = useMemo(() => new THREE.Object3D(), [])\n  const light = useRef<THREE.DirectionalLight>(null)\n  useLayoutEffect(() => {\n    const l = light.current\n    if (!l) return\n    const cam = l.shadow.camera\n    const half = r * 1.3\n    cam.left = -half\n    cam.right = half\n    cam.top = half\n    cam.bottom = -half\n    cam.near = 1\n    cam.far = r * 6 + 40\n    cam.updateProjectionMatrix()\n    l.shadow.normalBias = 0.02 + r * 0.0006\n    l.shadow.needsUpdate = true\n  }, [r, lights])\n  const lightPos = useMemo(() => dir.clone().multiplyScalar(r * 3 + 20).add(new THREE.Vector3(cx, cy, cz)), [dir, r, cx, cy, cz])\n  const nightFill = 0.3 + 0.6 * deckLights\n\n  return (\n    <group {...group}>\n      <group userData={{ noFit: true }}>\n        {night ? <StarDome moon={dir} stars={stars} /> : <DreiSky distance={4000} sunPosition={sunPos} {...SKY} />}\n        <HorizonHaze haze={hazeCol} deep={night ? NIGHT_DEEP : DEEP} />\n      </group>\n      {lights && (\n        <>\n          <primitive object={target} position={[cx, cy, cz]} />\n          <directionalLight\n            ref={light}\n            target={target}\n            position={lightPos}\n            color={night ? '#9fb4d8' : '#fff1dc'}\n            intensity={night ? 0.45 : 3.1}\n            castShadow\n            shadow-mapSize={[2048, 2048]}\n            shadow-bias={-0.0002}\n          />\n          {night ? <hemisphereLight args={['#ffdcb4', '#2a3036', nightFill]} /> : <hemisphereLight args={['#b9d5f2', '#6d7c80', 0.4]} />}\n          <directionalLight position={[-20, -60, 10]} intensity={night ? 0.04 : 0.25} color=\"#9fb8c0\" />\n          {night ? (\n            <Environment key=\"night\" resolution={128} frames={1} environmentIntensity={0.3 + 0.1 * deckLights}>\n              <color attach=\"background\" args={['#04070c']} />\n              <EnvSea horizon=\"#1a2433\" sea=\"#05090d\" />\n              <Lightformer form=\"circle\" intensity={1.2} color=\"#c8d6ff\" position={dir.clone().multiplyScalar(80).toArray()} scale={6} target={[0, 0, 0]} />\n              <Lightformer form=\"rect\" intensity={0.12 * deckLights} color=\"#ffb877\" position={[0, 40, 0]} rotation-x={Math.PI / 2} scale={[120, 120, 1]} />\n            </Environment>\n          ) : (\n            <Environment key={`day-${elevation}-${azimuth}`} resolution={256} frames={1} environmentIntensity={0.75}>\n              <DreiSky distance={500} sunPosition={sunPos} {...SKY} />\n              <EnvSea horizon=\"#b8d0de\" sea=\"#3f7c93\" />\n              <Lightformer form=\"circle\" intensity={14} color=\"#fff0d6\" position={dir.clone().multiplyScalar(80).toArray()} scale={10} target={[0, 0, 0]} />\n            </Environment>\n          )}\n        </>\n      )}\n      {preview && <Reference night={night} />}\n    </group>\n  )\n}\n\nexport default Sky\n"
    }
  ],
  "categories": [
    "environment"
  ],
  "meta": {
    "export": "Sky",
    "anchor": "base",
    "standards": [
      "Preetham sky model (three.js Sky)"
    ]
  }
}