{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "day-night-sky",
  "description": "A shader sky that glides from day to night — sun, moon, drifting clouds and stars, with an optional scrub slider.",
  "dependencies": [
    "three"
  ],
  "files": [
    {
      "path": "registry/new-york/shaders/day-night-sky.tsx",
      "content": "\"use client\";\n\n/**\n * A shader sky that glides from day to night — sun, moon, drifting clouds and stars, with an optional scrub slider.\n */\n\n\nimport { useEffect, useRef, useState } from \"react\";\nimport * as THREE from \"three\";\n\nconst VERTEX_SHADER = /* glsl */ `\n  varying vec2 vUv;\n  void main() {\n    vUv = uv;\n    gl_Position = vec4(position.xy, 0.0, 1.0);\n  }\n`;\n\nconst FRAGMENT_SHADER = /* glsl */ `\n  precision highp float;\n\n  uniform vec2 uResolution;\n  uniform float uTime;\n  uniform float uT;          // 0 = day, 1 = night\n\n  varying vec2 vUv;\n\n  float hash21(vec2 p) {\n    p = fract(p * vec2(123.34, 456.21));\n    p += dot(p, p + 45.32);\n    return fract(p.x * p.y);\n  }\n  float vnoise(vec2 p) {\n    vec2 i = floor(p), f = fract(p);\n    vec2 u = f * f * (3.0 - 2.0 * f);\n    float a = hash21(i), b = hash21(i + vec2(1.0, 0.0));\n    float c = hash21(i + vec2(0.0, 1.0)), d = hash21(i + vec2(1.0, 1.0));\n    return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n  }\n  float fbm(vec2 p) {\n    float v = 0.0, a = 0.5;\n    mat2 m = mat2(1.6, 1.2, -1.2, 1.6);\n    for (int i = 0; i < 6; i++) { v += a * vnoise(p); p = m * p; a *= 0.5; }\n    return v;\n  }\n  float cloudShape(vec2 p) {\n    vec2 q = vec2(fbm(p), fbm(p + vec2(5.2, 1.3)));\n    vec2 r = vec2(fbm(p + 4.0 * q + vec2(1.7, 9.2)),\n                  fbm(p + 4.0 * q + vec2(8.3, 2.8)));\n    return fbm(p + 4.0 * r);\n  }\n\n  void main() {\n    vec2 uv = vUv;\n    float aspect = uResolution.x / uResolution.y;\n    float t = clamp(uT, 0.0, 1.0);\n\n    // sky gradient day → night, with a warm dusk band peaking mid-scrub\n    vec3 top = mix(vec3(0.26, 0.52, 0.92), vec3(0.02, 0.03, 0.12), t);\n    vec3 bot = mix(vec3(0.88, 0.92, 1.0), vec3(0.14, 0.10, 0.26), t);\n    float dusk = max(1.0 - abs(t - 0.5) * 2.0, 0.0);\n    bot = mix(bot, vec3(0.99, 0.62, 0.45), dusk * 0.6);\n    vec3 sky = mix(bot, top, smoothstep(0.0, 1.0, uv.y));\n\n    // sun (day) arcing to a crescent moon (night)\n    vec2 pos = mix(vec2(0.30, 0.72), vec2(0.72, 0.80), t);\n    vec2 d = uv - pos; d.x *= aspect;\n    float dist = length(d);\n    float r = 0.07;\n    float disc = 1.0 - smoothstep(r - 0.006, r, dist);\n    vec2 d2 = uv - (pos + vec2(0.022, 0.014)); d2.x *= aspect;\n    float carve = 1.0 - smoothstep(r - 0.006, r, length(d2));\n    float body = mix(disc, clamp(disc - carve, 0.0, 1.0), smoothstep(0.5, 0.8, t));\n    float glow = 0.02 / (dist * dist + 0.02);\n    vec3 bodyCol = mix(vec3(1.0, 0.95, 0.78), vec3(0.93, 0.95, 1.0), t);\n    sky += bodyCol * (body + glow * 0.3 * (1.0 - 0.5 * t));\n\n    // twinkling stars fade in toward night\n    vec2 sg = floor(uv * vec2(aspect, 1.0) * 150.0);\n    float star = step(0.986, hash21(sg));\n    float twinkle = 0.5 + 0.5 * sin(uTime * 3.0 + hash21(sg) * 6.2831);\n    sky += star * twinkle * smoothstep(0.4, 1.0, t) * smoothstep(0.2, 1.0, uv.y);\n\n    // drifting clouds, greying and thinning out at night\n    vec2 cp = uv * vec2(2.2, 1.7) + vec2(uTime * 0.025, 0.0);\n    float density = smoothstep(0.52, 0.8, cloudShape(cp));\n    float dl = cloudShape(cp + vec2(-0.08, 0.1));\n    float light = clamp((cloudShape(cp) - dl) * 4.0 + 0.55, 0.0, 1.0);\n    vec3 cloudCol = mix(mix(vec3(0.62, 0.63, 0.72), vec3(1.0), light),\n                        vec3(0.22, 0.22, 0.34), t);\n    sky = mix(sky, cloudCol, density * (1.0 - 0.45 * t));\n\n    gl_FragColor = vec4(sky, 1.0);\n  }\n`;\n\nconst DayNightSky = ({\n  className,\n  dpr = 2,\n  controls = false,\n}: {\n  className?: string;\n  dpr?: number;\n  controls?: boolean;\n}) => {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const [value, setValue] = useState(0.25);\n  const valueRef = useRef(value);\n  valueRef.current = value;\n\n  useEffect(() => {\n    const container = containerRef.current;\n    if (!container) return;\n\n    const camera = new THREE.Camera();\n    const scene = new THREE.Scene();\n    const geometry = new THREE.PlaneGeometry(2, 2);\n\n    const uniforms = {\n      uResolution: { value: new THREE.Vector2() },\n      uTime: { value: 0 },\n      uT: { value: valueRef.current },\n    };\n    const material = new THREE.ShaderMaterial({\n      uniforms,\n      vertexShader: VERTEX_SHADER,\n      fragmentShader: FRAGMENT_SHADER,\n    });\n    const mesh = new THREE.Mesh(geometry, material);\n    scene.add(mesh);\n\n    const renderer = new THREE.WebGLRenderer({ antialias: true });\n    renderer.setPixelRatio(Math.min(window.devicePixelRatio, dpr));\n    container.appendChild(renderer.domElement);\n    const canvas = renderer.domElement;\n    const onContextLost = (e: Event) => e.preventDefault();\n    canvas.addEventListener(\"webglcontextlost\", onContextLost);\n\n    let visible = true;\n    const intersectionObserver = new IntersectionObserver(\n      ([entry]) => {\n        visible = entry.isIntersecting;\n      },\n      { rootMargin: \"150px\", threshold: 0 },\n    );\n    intersectionObserver.observe(container);\n\n    let raf = 0;\n    let shaderTime = 0;\n    let last = performance.now();\n    const animate = () => {\n      raf = requestAnimationFrame(animate);\n      if (!visible) return;\n      const now = performance.now();\n      shaderTime += Math.min((now - last) / 1000, 0.05);\n      last = now;\n      uniforms.uTime.value = shaderTime;\n      uniforms.uT.value = controls\n        ? valueRef.current\n        : 0.5 - 0.5 * Math.cos(shaderTime * 0.25);\n      renderer.render(scene, camera);\n    };\n\n    const resize = () => {\n      const w = container.clientWidth;\n      const h = container.clientHeight;\n      if (w === 0 || h === 0) return;\n      renderer.setSize(w, h);\n      uniforms.uResolution.value.set(\n        renderer.domElement.width,\n        renderer.domElement.height,\n      );\n    };\n    resize();\n    const resizeObserver = new ResizeObserver(resize);\n    resizeObserver.observe(container);\n    animate();\n\n    return () => {\n      cancelAnimationFrame(raf);\n      resizeObserver.disconnect();\n      intersectionObserver.disconnect();\n      canvas.removeEventListener(\"webglcontextlost\", onContextLost);\n      if (canvas.parentNode === container) container.removeChild(canvas);\n      geometry.dispose();\n      material.dispose();\n      renderer.forceContextLoss();\n      renderer.dispose();\n    };\n  }, [dpr, controls]);\n\n  return (\n    <div className={className} style={{ position: \"relative\" }}>\n      <div ref={containerRef} aria-hidden className=\"size-full\" />\n      {controls && (\n        <div className=\"pointer-events-auto absolute inset-x-0 bottom-8 z-10 mx-auto flex w-[min(28rem,80%)] flex-col items-center gap-2\">\n          <div className=\"flex w-full justify-between text-xs text-white/80 drop-shadow\">\n            <span>Day</span>\n            <span>Night</span>\n          </div>\n          <input\n            type=\"range\"\n            min={0}\n            max={1}\n            step={0.001}\n            value={value}\n            onChange={(e) => setValue(parseFloat(e.target.value))}\n            aria-label=\"Scrub day to night\"\n            className=\"h-1.5 w-full cursor-pointer appearance-none rounded-full bg-white/40 accent-white\"\n          />\n        </div>\n      )}\n    </div>\n  );\n};\n\nexport default DayNightSky;\n",
      "type": "registry:component",
      "target": "components/pixel-perfect/day-night-sky.tsx"
    }
  ],
  "type": "registry:block"
}