{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"blind-light","type":"registry:ui","title":"Blind Light","description":"Warm light slanting in from one side through window blinds, with dust motes glinting in the beams. WebGL.","dependencies":[],"registryDependencies":[],"files":[{"path":"registry/ui/blind-light.tsx","type":"registry:ui","content":"\"use client\";\n\nimport { useEffect, useRef } from \"react\";\n\nexport interface BlindLightProps {\n  /** Light color. */\n  color?: string;\n  /** Animation speed multiplier. */\n  speed?: number;\n  /** Which edge the light enters from. */\n  side?: \"left\" | \"right\";\n  /** Downward tilt of the rays, in radians. */\n  angle?: number;\n  /** Number of blind slats across the container height. */\n  slats?: number;\n  /** Brightness. */\n  intensity?: number;\n  /** Amount of floating dust, 0-1. */\n  dust?: number;\n  className?: string;\n}\n\nconst FRAG = `\nprecision highp float;\nuniform vec2 uRes;\nuniform float uTime;\nuniform vec3 uColor;\nuniform float uSide;\nuniform float uAngle;\nuniform float uSlats;\nuniform float uIntensity;\nuniform float uDust;\n\nfloat hash(vec2 p) {\n  vec3 p3 = fract(vec3(p.xyx) * 0.1031);\n  p3 += dot(p3, p3.yzx + 33.33);\n  return fract((p3.x + p3.y) * p3.z);\n}\nfloat noise(vec2 p) {\n  vec2 i = floor(p), f = fract(p);\n  f = f * f * (3.0 - 2.0 * f);\n  return mix(mix(hash(i), hash(i + vec2(1, 0)), f.x), mix(hash(i + vec2(0, 1)), hash(i + vec2(1, 1)), f.x), f.y);\n}\n\nvoid main() {\n  vec2 uv = gl_FragCoord.xy / uRes;\n  if (uSide > 0.5) uv.x = 1.0 - uv.x;\n  vec2 p = vec2(uv.x * uRes.x / uRes.y, uv.y);\n  float t = uTime;\n\n  float ang = uAngle + 0.025 * sin(t * 0.3);\n  vec2 nrm = vec2(sin(ang), cos(ang));\n  float u = dot(p, nrm) + 0.1;              // position across the beams\n  float dist = p.x / cos(ang);              // distance travelled from the window\n\n  // blinds: soft-edged slats whose openings breathe slowly; penumbra widens with distance\n  float s = u * uSlats;\n  float id = floor(s);\n  float f = fract(s);\n  float open = 0.22 + 0.3 * noise(vec2(id * 1.7, t * 0.2));\n  float soft = 0.04 + dist * 0.16;\n  float lo = 0.5 - open * 0.5, hi = 0.5 + open * 0.5;\n  float beam = smoothstep(lo - soft, lo + soft, f) * (1.0 - smoothstep(hi - soft, hi + soft, f));\n  beam *= 0.75 + 0.25 * noise(vec2(id * 3.1, t * 0.6));\n  beam *= 0.8 + 0.2 * noise(vec2(u * 70.0, dist * 1.5 - t * 0.15));\n\n  float fall = exp(-dist * 1.1) * smoothstep(0.28, 0.5, u) * (1.0 - smoothstep(1.0, 1.25, u));\n  float light = beam * fall * 0.75 + exp(-dist * 1.8) * 0.08 * fall;\n\n  // dust motes, only visible where the light hits them\n  float dust = 0.0;\n  for (int i = 0; i < 3; i++) {\n    float fi = float(i);\n    float sc = 14.0 + fi * 9.0;\n    vec2 q = p * sc + vec2(t * (0.25 + fi * 0.1), t * 0.12 * (fi - 1.0) + sin(t * 0.2 + fi) * 0.6);\n    vec2 cid = floor(q);\n    float h = hash(cid + fi * 17.0);\n    if (h < 0.35 * uDust) {\n      vec2 pos = vec2(hash(cid + 3.3), hash(cid + 8.8)) - 0.5;\n      pos *= 0.6;\n      pos += 0.12 * vec2(sin(t * 0.7 + h * 40.0), cos(t * 0.5 + h * 60.0));\n      float d = length(fract(q) - 0.5 - pos);\n      float tw = 0.5 + 0.5 * sin(t * 1.8 + h * 90.0);\n      dust += (1.0 - smoothstep(0.0, 0.07, d)) * (0.4 + 0.6 * tw) * (1.0 - fi * 0.25);\n    }\n  }\n  float motes = dust * (beam * fall * 2.0 + 0.04);\n\n  vec3 col = uColor * light * 0.9 + mix(uColor, vec3(1.0), 0.5) * motes;\n  col = 1.0 - exp(-col * uIntensity * 1.3);\n  gl_FragColor = vec4(col, max(col.r, max(col.g, col.b)));\n}\n`;\n\nconst VERT = \"attribute vec2 p;void main(){gl_Position=vec4(p,0.,1.);}\";\n\ntype Ptr = { x: number; y: number; hover: number };\ntype Uni = (name: string) => WebGLUniformLocation | null;\n\n/** Full-screen shader canvas inside `host`: DPR-capped resize, offscreen pause, reduced motion, eased pointer. */\nfunction runShader(host: HTMLElement, frag: string, speed: () => number, update: (gl: WebGLRenderingContext, u: Uni) => void) {\n  const canvas = document.createElement(\"canvas\");\n  canvas.style.cssText = \"position:absolute;inset:0;width:100%;height:100%;display:block\";\n  const gl = canvas.getContext(\"webgl\", { antialias: false });\n  if (!gl) return null;\n  const prog = gl.createProgram()!;\n  for (const [type, src] of [\n    [gl.VERTEX_SHADER, VERT],\n    [gl.FRAGMENT_SHADER, frag],\n  ] as const) {\n    const sh = gl.createShader(type)!;\n    gl.shaderSource(sh, src);\n    gl.compileShader(sh);\n    if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) console.warn(gl.getShaderInfoLog(sh));\n    gl.attachShader(prog, sh);\n  }\n  gl.bindAttribLocation(prog, 0, \"p\");\n  gl.linkProgram(prog);\n  if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {\n    gl.getExtension(\"WEBGL_lose_context\")?.loseContext();\n    return null;\n  }\n  gl.useProgram(prog);\n  gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());\n  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);\n  gl.enableVertexAttribArray(0);\n  gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);\n  host.appendChild(canvas);\n\n  const locs = new Map<string, WebGLUniformLocation | null>();\n  const u: Uni = (n) => {\n    if (!locs.has(n)) locs.set(n, gl.getUniformLocation(prog, n));\n    return locs.get(n) ?? null;\n  };\n  const zone = host.parentElement ?? host;\n  const reduced = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n  const ptr: Ptr = { x: 0.5, y: 0.5, hover: 0 };\n  const goal: Ptr = { ...ptr };\n  let raf = 0;\n  let last = 0;\n  let time = 0;\n\n  const draw = () => {\n    gl.uniform2f(u(\"uRes\"), canvas.width, canvas.height);\n    gl.uniform1f(u(\"uTime\"), time);\n    gl.uniform2f(u(\"uMouse\"), ptr.x, ptr.y);\n    gl.uniform1f(u(\"uHover\"), ptr.hover);\n    update(gl, u);\n    gl.drawArrays(gl.TRIANGLES, 0, 3);\n  };\n  const loop = (now: number) => {\n    const dt = Math.min((now - last) / 1000, 0.1);\n    last = now;\n    const k = 1 - Math.exp(-dt * 4);\n    for (const key of [\"x\", \"y\", \"hover\"] as const) ptr[key] += (goal[key] - ptr[key]) * k;\n    time += dt * speed();\n    draw();\n    raf = requestAnimationFrame(loop);\n  };\n  const run = (on: boolean) => {\n    if (on && !raf && !reduced) {\n      last = performance.now();\n      raf = requestAnimationFrame(loop);\n    } else if (!on && raf) {\n      cancelAnimationFrame(raf);\n      raf = 0;\n    }\n  };\n  const resize = () => {\n    const dpr = Math.min(window.devicePixelRatio || 1, 2);\n    canvas.width = Math.max(1, Math.round(host.clientWidth * dpr));\n    canvas.height = Math.max(1, Math.round(host.clientHeight * dpr));\n    gl.viewport(0, 0, canvas.width, canvas.height);\n    draw();\n  };\n  const move = (e: PointerEvent) => {\n    const r = host.getBoundingClientRect();\n    goal.x = (e.clientX - r.left) / r.width;\n    goal.y = 1 - (e.clientY - r.top) / r.height;\n    goal.hover = 1;\n  };\n  const leave = () => {\n    goal.hover = 0;\n  };\n  const ro = new ResizeObserver(resize);\n  const io = new IntersectionObserver(([e]) => run(e.isIntersecting));\n  ro.observe(host);\n  io.observe(host);\n  zone.addEventListener(\"pointermove\", move);\n  zone.addEventListener(\"pointerleave\", leave);\n  return {\n    redraw: () => {\n      if (!raf) draw();\n    },\n    dispose: () => {\n      run(false);\n      ro.disconnect();\n      io.disconnect();\n      zone.removeEventListener(\"pointermove\", move);\n      zone.removeEventListener(\"pointerleave\", leave);\n      gl.getExtension(\"WEBGL_lose_context\")?.loseContext();\n      canvas.remove();\n    },\n  };\n}\n\nconst rgb = (hex: string) => {\n  const n = parseInt(hex.replace(\"#\", \"\").padEnd(6, \"0\").slice(0, 6), 16);\n  return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255];\n};\n\nexport function BlindLight({\n  color = \"#ffe2b8\",\n  speed = 1,\n  side = \"left\",\n  angle = 0.35,\n  slats = 7,\n  intensity = 1,\n  dust = 0.6,\n  className,\n}: BlindLightProps) {\n  const ref = useRef<HTMLDivElement>(null);\n  const opts = useRef({ color, speed, side, angle, slats, intensity, dust });\n  const redraw = useRef<(() => void) | undefined>(undefined);\n\n  useEffect(() => {\n    opts.current = { color, speed, side, angle, slats, intensity, dust };\n    redraw.current?.();\n  }, [color, speed, side, angle, slats, intensity, dust]);\n\n  useEffect(() => {\n    const r = runShader(\n      ref.current!,\n      FRAG,\n      () => opts.current.speed,\n      (gl, u) => {\n        const o = opts.current;\n        gl.uniform3fv(u(\"uColor\"), rgb(o.color));\n        gl.uniform1f(u(\"uSide\"), o.side === \"right\" ? 1 : 0);\n        gl.uniform1f(u(\"uAngle\"), Math.max(-1.2, Math.min(1.2, o.angle)));\n        gl.uniform1f(u(\"uSlats\"), Math.max(1, o.slats));\n        gl.uniform1f(u(\"uIntensity\"), o.intensity);\n        gl.uniform1f(u(\"uDust\"), o.dust);\n      },\n    );\n    redraw.current = r?.redraw;\n    return () => r?.dispose();\n  }, []);\n\n  return (\n    <div\n      ref={ref}\n      aria-hidden\n      className={className}\n      style={{ position: \"absolute\", inset: 0, overflow: \"hidden\", pointerEvents: \"none\" }}\n    />\n  );\n}\n"}]}