{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "loader",
  "type": "registry:component",
  "title": "Loader",
  "description": "Custom WebGL shader loaders with 3 shapes (sphere, swirl, ripple) × 3 styles (plain, blur, dither).",
  "dependencies": [],
  "files": [
    {
      "path": "registry/new-york/blocks/loader/loader.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { ShaderMount } from \"./shader-mount\";\nimport {\n  plainFragmentShader,\n  blurFragmentShader,\n  ditherFragmentShader,\n  hexToRgba,\n} from \"./shaders\";\n\ntype LoaderShape = \"sphere\" | \"swirl\" | \"ripple\";\ntype LoaderStyle = \"plain\" | \"blur\" | \"dither\";\n\nconst sizeConfig = {\n  sm: { width: 48, height: 48 },\n  default: { width: 80, height: 80 },\n  lg: { width: 120, height: 120 },\n};\n\nconst shapeMap: Record<LoaderShape, number> = {\n  sphere: 1,\n  swirl: 2,\n  ripple: 3,\n};\n\nconst shaderMap: Record<LoaderStyle, string> = {\n  plain: plainFragmentShader,\n  blur: blurFragmentShader,\n  dither: ditherFragmentShader,\n};\n\ninterface LoaderProps extends React.HTMLAttributes<HTMLDivElement> {\n  /** Base shape of the loader */\n  shape?: LoaderShape;\n  /** Visual style: plain (solid), blur (soft), or dither (pixelated) */\n  variant?: LoaderStyle;\n  /** Size preset (maps to width/height) */\n  size?: keyof typeof sizeConfig;\n  /** Custom width (overrides size preset) */\n  width?: number;\n  /** Custom height (overrides size preset) */\n  height?: number;\n  /** Animation speed multiplier */\n  speed?: number;\n  /** Primary color */\n  color?: string;\n  /** Background color */\n  colorBack?: string;\n}\n\nfunction Loader({\n  shape = \"sphere\",\n  variant = \"dither\",\n  size = \"default\",\n  width,\n  height,\n  speed = 1,\n  color = \"#00b3ff\",\n  colorBack = \"transparent\",\n  className,\n  style,\n  ...props\n}: LoaderProps) {\n  const sizeDefaults = sizeConfig[size];\n  const resolvedWidth = width ?? sizeDefaults.width;\n  const resolvedHeight = height ?? sizeDefaults.height;\n\n  const fragmentShader = shaderMap[variant];\n  const shapeValue = shapeMap[shape];\n  const scale = variant === \"blur\" ? 0.52 : 0.6;\n  const usesCssVariables = color.includes(\"var(\") || colorBack.includes(\"var(\");\n  const [cssVariableEpoch, setCssVariableEpoch] = React.useState(0);\n\n  React.useEffect(() => {\n    if (!usesCssVariables || typeof document === \"undefined\") {\n      return;\n    }\n\n    const bumpCssVariableEpoch = () => {\n      setCssVariableEpoch((value) => value + 1);\n    };\n\n    // Resolve once after mount, then re-resolve on theme/token changes.\n    bumpCssVariableEpoch();\n\n    const attributeFilter = [\"class\", \"style\", \"data-theme\"];\n    const observer = new MutationObserver(bumpCssVariableEpoch);\n    observer.observe(document.documentElement, {\n      attributes: true,\n      attributeFilter,\n    });\n    if (document.body) {\n      observer.observe(document.body, {\n        attributes: true,\n        attributeFilter,\n      });\n    }\n\n    const media = window.matchMedia(\"(prefers-color-scheme: dark)\");\n    if (typeof media.addEventListener === \"function\") {\n      media.addEventListener(\"change\", bumpCssVariableEpoch);\n    } else {\n      media.addListener(bumpCssVariableEpoch);\n    }\n\n    return () => {\n      observer.disconnect();\n      if (typeof media.removeEventListener === \"function\") {\n        media.removeEventListener(\"change\", bumpCssVariableEpoch);\n      } else {\n        media.removeListener(bumpCssVariableEpoch);\n      }\n    };\n  }, [usesCssVariables]);\n\n  const uniforms = React.useMemo(\n    () => ({\n      u_colorFront: hexToRgba(color),\n      u_colorBack: hexToRgba(colorBack),\n      u_shape: shapeValue,\n      u_scale: scale,\n      u_pxSize: 2, // Pixel size for dithering\n    }),\n    [color, colorBack, shapeValue, scale, cssVariableEpoch]\n  );\n\n  return (\n    <div\n      data-slot=\"loader\"\n      className={cn(\"relative overflow-hidden rounded-full\", className)}\n      style={{\n        width: resolvedWidth,\n        height: resolvedHeight,\n        ...style,\n      }}\n      {...props}\n    >\n      <ShaderMount\n        fragmentShader={fragmentShader}\n        uniforms={uniforms}\n        speed={speed}\n        width={resolvedWidth}\n        height={resolvedHeight}\n      />\n    </div>\n  );\n}\n\nexport { Loader, sizeConfig };\nexport type { LoaderProps, LoaderShape, LoaderStyle };\n",
      "type": "registry:component",
      "target": "components/ui/loader.tsx"
    },
    {
      "path": "registry/new-york/blocks/loader/shader-mount.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { vertexShaderSource } from \"./shaders\";\n\ninterface ShaderMountProps {\n  fragmentShader: string;\n  uniforms: Record<string, number | number[]>;\n  speed?: number;\n  width: number;\n  height: number;\n  className?: string;\n  style?: React.CSSProperties;\n}\n\nfunction createShader(\n  gl: WebGL2RenderingContext,\n  type: number,\n  source: string\n): WebGLShader | null {\n  const shader = gl.createShader(type);\n  if (!shader) return null;\n\n  gl.shaderSource(shader, source);\n  gl.compileShader(shader);\n\n  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n    console.error(\n      \"Shader compile error:\",\n      gl.getShaderInfoLog(shader)\n    );\n    gl.deleteShader(shader);\n    return null;\n  }\n\n  return shader;\n}\n\nfunction createProgram(\n  gl: WebGL2RenderingContext,\n  vertexSource: string,\n  fragmentSource: string\n): WebGLProgram | null {\n  const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexSource);\n  const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentSource);\n\n  if (!vertexShader || !fragmentShader) return null;\n\n  const program = gl.createProgram();\n  if (!program) return null;\n\n  gl.attachShader(program, vertexShader);\n  gl.attachShader(program, fragmentShader);\n  gl.linkProgram(program);\n\n  if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n    console.error(\"Program link error:\", gl.getProgramInfoLog(program));\n    gl.deleteProgram(program);\n    return null;\n  }\n\n  // Clean up shaders after linking\n  gl.detachShader(program, vertexShader);\n  gl.detachShader(program, fragmentShader);\n  gl.deleteShader(vertexShader);\n  gl.deleteShader(fragmentShader);\n\n  return program;\n}\n\nexport function ShaderMount({\n  fragmentShader,\n  uniforms,\n  speed = 1,\n  width,\n  height,\n  className,\n  style,\n}: ShaderMountProps) {\n  const canvasRef = React.useRef<HTMLCanvasElement>(null);\n  const glRef = React.useRef<WebGL2RenderingContext | null>(null);\n  const programRef = React.useRef<WebGLProgram | null>(null);\n  const uniformLocationsRef = React.useRef<\n    Record<string, WebGLUniformLocation | null>\n  >({});\n  const rafRef = React.useRef<number | null>(null);\n  const startTimeRef = React.useRef<number>(performance.now());\n  const currentSpeedRef = React.useRef<number>(speed);\n\n  // Update speed ref when prop changes\n  React.useEffect(() => {\n    currentSpeedRef.current = speed;\n  }, [speed]);\n\n  // Initialize WebGL\n  React.useEffect(() => {\n    const canvas = canvasRef.current;\n    if (!canvas) return;\n\n    const gl = canvas.getContext(\"webgl2\", {\n      alpha: true,\n      premultipliedAlpha: true,\n      antialias: true,\n    });\n\n    if (!gl) {\n      console.error(\"WebGL2 not supported\");\n      return;\n    }\n\n    glRef.current = gl;\n\n    // Create program\n    const program = createProgram(gl, vertexShaderSource, fragmentShader);\n    if (!program) return;\n\n    programRef.current = program;\n\n    // Setup position attribute\n    const positionBuffer = gl.createBuffer();\n    gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);\n    const positions = [-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1];\n    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);\n\n    const positionLocation = gl.getAttribLocation(program, \"a_position\");\n    gl.enableVertexAttribArray(positionLocation);\n    gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);\n\n    // Get uniform locations\n    const locations: Record<string, WebGLUniformLocation | null> = {\n      u_time: gl.getUniformLocation(program, \"u_time\"),\n      u_resolution: gl.getUniformLocation(program, \"u_resolution\"),\n      u_pixelRatio: gl.getUniformLocation(program, \"u_pixelRatio\"),\n    };\n\n    Object.keys(uniforms).forEach((key) => {\n      locations[key] = gl.getUniformLocation(program, key);\n    });\n\n    uniformLocationsRef.current = locations;\n\n    // Enable blending for transparency\n    gl.enable(gl.BLEND);\n    gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);\n\n    return () => {\n      if (rafRef.current) {\n        cancelAnimationFrame(rafRef.current);\n      }\n      if (gl && program) {\n        gl.deleteProgram(program);\n      }\n    };\n  }, [fragmentShader]);\n\n  // Render loop\n  React.useEffect(() => {\n    const gl = glRef.current;\n    const program = programRef.current;\n    const locations = uniformLocationsRef.current;\n\n    if (!gl || !program) return;\n\n    const pixelRatio = Math.min(window.devicePixelRatio, 2);\n    const canvasWidth = width * pixelRatio;\n    const canvasHeight = height * pixelRatio;\n\n    const canvas = canvasRef.current;\n    if (canvas) {\n      canvas.width = canvasWidth;\n      canvas.height = canvasHeight;\n    }\n\n    gl.viewport(0, 0, canvasWidth, canvasHeight);\n\n    let lastTime = performance.now();\n    let accumulatedTime = 0;\n\n    const render = (currentTime: number) => {\n      const deltaTime = currentTime - lastTime;\n      lastTime = currentTime;\n      accumulatedTime += deltaTime * currentSpeedRef.current * 0.001;\n\n      gl.clearColor(0, 0, 0, 0);\n      gl.clear(gl.COLOR_BUFFER_BIT);\n      gl.useProgram(program);\n\n      // Set built-in uniforms\n      if (locations.u_time) {\n        gl.uniform1f(locations.u_time, accumulatedTime);\n      }\n      if (locations.u_resolution) {\n        gl.uniform2f(locations.u_resolution, canvasWidth, canvasHeight);\n      }\n      if (locations.u_pixelRatio) {\n        gl.uniform1f(locations.u_pixelRatio, pixelRatio);\n      }\n\n      // Set custom uniforms\n      Object.entries(uniforms).forEach(([key, value]) => {\n        const location = locations[key];\n        if (!location) return;\n\n        if (Array.isArray(value)) {\n          switch (value.length) {\n            case 2:\n              gl.uniform2fv(location, value);\n              break;\n            case 3:\n              gl.uniform3fv(location, value);\n              break;\n            case 4:\n              gl.uniform4fv(location, value);\n              break;\n          }\n        } else {\n          gl.uniform1f(location, value);\n        }\n      });\n\n      gl.drawArrays(gl.TRIANGLES, 0, 6);\n\n      rafRef.current = requestAnimationFrame(render);\n    };\n\n    rafRef.current = requestAnimationFrame(render);\n\n    return () => {\n      if (rafRef.current) {\n        cancelAnimationFrame(rafRef.current);\n      }\n    };\n  }, [uniforms, width, height]);\n\n  return (\n    <canvas\n      ref={canvasRef}\n      className={className}\n      style={{\n        width,\n        height,\n        ...style,\n      }}\n    />\n  );\n}\n",
      "type": "registry:component",
      "target": "components/ui/shader-mount.tsx"
    },
    {
      "path": "registry/new-york/blocks/loader/shaders/index.ts",
      "content": "// Shader exports\nexport { vertexShaderSource } from \"./vertex\";\nexport { plainFragmentShader } from \"./plain\";\nexport { blurFragmentShader } from \"./blur\";\nexport { ditherFragmentShader } from \"./dither\";\nexport { hexToRgba } from \"./utils\";\n",
      "type": "registry:component",
      "target": "components/ui/shaders/index.ts"
    },
    {
      "path": "registry/new-york/blocks/loader/shaders/vertex.ts",
      "content": "// Shared vertex shader for all loader styles\nexport const vertexShaderSource = `#version 300 es\nprecision mediump float;\n\nlayout(location = 0) in vec4 a_position;\n\nvoid main() {\n  gl_Position = a_position;\n}\n`;\n",
      "type": "registry:component",
      "target": "components/ui/shaders/vertex.ts"
    },
    {
      "path": "registry/new-york/blocks/loader/shaders/utils.ts",
      "content": "// Shared GLSL utilities and shape functions\n\nexport const shaderConstants = `\n#define TWO_PI 6.28318530718\n#define PI 3.14159265358979323846\n`;\n\nexport const shapeUniforms = `\nuniform float u_time;\nuniform vec2 u_resolution;\nuniform float u_pixelRatio;\nuniform vec4 u_colorFront;\nuniform vec4 u_colorBack;\nuniform float u_shape; // 1=sphere, 2=swirl, 3=ripple\nuniform float u_scale;\n`;\n\n// Shape functions - all shapes expect normalized UV coordinates centered at origin\nexport const shapeFunctions = `\n// Sphere - 3D lit sphere with rotating light source\nfloat sphereShape(vec2 uv, float time) {\n  uv *= 2.0;\n  float d = 1.0 - pow(length(uv), 2.0);\n  vec3 pos = vec3(uv, sqrt(max(0.0, d)));\n  vec3 lightPos = normalize(vec3(cos(3.0 * time), 0.8, sin(2.5 * time)));\n  float shape = 0.5 + 0.5 * dot(lightPos, pos);\n  return shape * step(0.0, d);\n}\n\n// Swirl - spiral vortex pattern\nfloat swirlShape(vec2 uv, float time) {\n  float l = length(uv);\n  float angle = 6.0 * atan(uv.y, uv.x) + 8.0 * time;\n  float twist = 1.2;\n  float offset = 1.0 / pow(max(l, 1e-6), twist) + angle / TWO_PI;\n  float mid = smoothstep(0.0, 1.0, pow(l, twist));\n  return mix(0.0, fract(offset), mid);\n}\n\n// Ripple - concentric expanding waves\nfloat rippleShape(vec2 uv, float time) {\n  float dist = length(uv);\n  float waves = sin(pow(dist, 1.7) * 7.0 - 6.0 * time) * 0.5 + 0.5;\n  return waves;\n}\n\n// Get shape value based on u_shape uniform\nfloat getShape(vec2 uv, float time) {\n  if (u_shape < 1.5) {\n    return sphereShape(uv, time);\n  } else if (u_shape < 2.5) {\n    return swirlShape(uv, time);\n  } else {\n    return rippleShape(uv, time);\n  }\n}\n`;\n\nfunction createTransparentRgba(): [number, number, number, number] {\n  return [0, 0, 0, 0];\n}\n\nfunction parseHexColor(color: string): [number, number, number, number] | null {\n  const result = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(color.trim());\n  if (!result) {\n    return null;\n  }\n\n  return [\n    parseInt(result[1], 16) / 255,\n    parseInt(result[2], 16) / 255,\n    parseInt(result[3], 16) / 255,\n    1.0,\n  ];\n}\n\nfunction parseRgbColor(color: string): [number, number, number, number] | null {\n  const rgbResult =\n    /^rgba?\\(\\s*([\\d.]+)(?:\\s*,\\s*|\\s+)([\\d.]+)(?:\\s*,\\s*|\\s+)([\\d.]+)(?:\\s*[,/]\\s*([\\d.]+))?\\s*\\)$/i.exec(\n      color.trim()\n    );\n\n  if (!rgbResult) {\n    return null;\n  }\n\n  return [\n    Math.min(255, Math.max(0, Number(rgbResult[1]))) / 255,\n    Math.min(255, Math.max(0, Number(rgbResult[2]))) / 255,\n    Math.min(255, Math.max(0, Number(rgbResult[3]))) / 255,\n    rgbResult[4] ? Math.min(1, Math.max(0, Number(rgbResult[4]))) : 1.0,\n  ];\n}\n\nfunction resolveBrowserColor(color: string): string | null {\n  if (typeof window === \"undefined\" || typeof document === \"undefined\") {\n    return null;\n  }\n\n  const probe = document.createElement(\"span\");\n  probe.style.color = color;\n\n  // If the browser rejected the color value, style.color will be empty\n  if (!probe.style.color) {\n    return null;\n  }\n\n  probe.style.position = \"absolute\";\n  probe.style.visibility = \"hidden\";\n  probe.style.pointerEvents = \"none\";\n\n  document.body.appendChild(probe);\n  const resolvedColor = window.getComputedStyle(probe).color;\n  probe.remove();\n\n  return resolvedColor || null;\n}\n\n// Helper to convert CSS color values to RGBA array\nexport function hexToRgba(color: string): [number, number, number, number] {\n  const normalizedColor = color.trim();\n\n  if (!normalizedColor || normalizedColor === \"transparent\") {\n    return createTransparentRgba();\n  }\n\n  const fromHex = parseHexColor(normalizedColor);\n  if (fromHex) {\n    return fromHex;\n  }\n\n  const fromRgb = parseRgbColor(normalizedColor);\n  if (fromRgb) {\n    return fromRgb;\n  }\n\n  const resolvedColor = resolveBrowserColor(normalizedColor);\n  if (resolvedColor) {\n    const resolvedFromHex = parseHexColor(resolvedColor);\n    if (resolvedFromHex) {\n      return resolvedFromHex;\n    }\n\n    const resolvedFromRgb = parseRgbColor(resolvedColor);\n    if (resolvedFromRgb) {\n      return resolvedFromRgb;\n    }\n  }\n\n  return createTransparentRgba();\n}\n",
      "type": "registry:component",
      "target": "components/ui/shaders/utils.ts"
    },
    {
      "path": "registry/new-york/blocks/loader/shaders/plain.ts",
      "content": "// Plain style fragment shader - crisp core with soft bloom\nimport { shaderConstants, shapeUniforms, shapeFunctions } from \"./utils\";\n\nexport const plainFragmentShader = `#version 300 es\nprecision mediump float;\n\n${shaderConstants}\n${shapeUniforms}\n\nout vec4 fragColor;\n\n${shapeFunctions}\n\n// Gaussian-like blur by sampling shape at multiple offsets\nfloat blurredShape(vec2 uv, float time, float blurSize) {\n  float sum = 0.0;\n  float totalWeight = 0.0;\n  \n  // 9-tap blur kernel\n  for (float x = -1.0; x <= 1.0; x += 1.0) {\n    for (float y = -1.0; y <= 1.0; y += 1.0) {\n      vec2 offset = vec2(x, y) * blurSize;\n      float weight = 1.0 - length(vec2(x, y)) * 0.3;\n      sum += getShape(uv + offset, time) * weight;\n      totalWeight += weight;\n    }\n  }\n  \n  return sum / totalWeight;\n}\n\nvoid main() {\n  // Normalize coordinates to [-0.5, 0.5] centered\n  vec2 uv = (gl_FragCoord.xy - 0.5 * u_resolution) / min(u_resolution.x, u_resolution.y);\n  uv /= u_scale;\n  \n  float time = u_time;\n  \n  float core = getShape(uv, time);\n  float mask = 1.0 - smoothstep(0.7, 1.05, length(uv));\n  core *= mask;\n  \n  // Crisp core with a gentle bloom\n  float solid = smoothstep(0.2, 0.85, core);\n  float glow = blurredShape(uv, time, 0.018);\n  float halo = smoothstep(0.0, 0.6, glow) * 0.35;\n  float finalShape = clamp(solid + halo, 0.0, 1.0);\n  \n  // Subtle lighting for a more polished feel\n  float radial = 1.0 - smoothstep(0.2, 1.05, length(uv));\n  float highlight =\n      pow(max(0.0, 1.0 - length(uv - vec2(-0.25, 0.25))), 3.0);\n  float shade = clamp(0.75 + 0.25 * radial + 0.2 * highlight, 0.0, 1.15);\n  \n  // Mix colors with glow\n  vec3 fgColor = u_colorFront.rgb * u_colorFront.a;\n  float fgOpacity = u_colorFront.a * finalShape;\n  vec3 bgColor = u_colorBack.rgb * u_colorBack.a;\n  float bgOpacity = u_colorBack.a;\n  \n  vec3 color = fgColor * shade * finalShape;\n  float opacity = fgOpacity;\n  \n  color += bgColor * (1.0 - opacity);\n  opacity += bgOpacity * (1.0 - opacity);\n  \n  fragColor = vec4(color, opacity);\n}\n`;\n",
      "type": "registry:component",
      "target": "components/ui/shaders/plain.ts"
    },
    {
      "path": "registry/new-york/blocks/loader/shaders/blur.ts",
      "content": "// Blur style fragment shader - completely diffuse/foggy rendering\nimport { shaderConstants, shapeUniforms, shapeFunctions } from \"./utils\";\n\nexport const blurFragmentShader = `#version 300 es\nprecision mediump float;\n\n${shaderConstants}\n${shapeUniforms}\n\nout vec4 fragColor;\n\n${shapeFunctions}\n\n// Heavy blur sampling for diffuse effect\nfloat heavyBlur(vec2 uv, float time, float blurSize) {\n  float sum = 0.0;\n  float totalWeight = 0.0;\n  \n  // 25-tap blur kernel for heavier blur\n  for (float x = -2.0; x <= 2.0; x += 1.0) {\n    for (float y = -2.0; y <= 2.0; y += 1.0) {\n      vec2 offset = vec2(x, y) * blurSize;\n      float dist = length(vec2(x, y));\n      float weight = exp(-dist * dist * 0.15); // Gaussian falloff\n      sum += getShape(uv + offset, time) * weight;\n      totalWeight += weight;\n    }\n  }\n  \n  return sum / totalWeight;\n}\n\nvoid main() {\n  // Normalize coordinates to [-0.5, 0.5] centered\n  vec2 uv = (gl_FragCoord.xy - 0.5 * u_resolution) / min(u_resolution.x, u_resolution.y);\n  uv /= u_scale;\n  \n  float time = u_time;\n  float dist = length(uv);\n  \n  // Multiple heavy blur passes for completely diffuse result\n  float blurAmount = 0.05;\n  float shape1 = heavyBlur(uv, time, blurAmount);\n  float shape2 = heavyBlur(uv, time, blurAmount * 2.5);\n  float shape3 = heavyBlur(uv, time, blurAmount * 4.0);\n  float shape4 = heavyBlur(uv, time, blurAmount * 6.0);\n  \n  // Combine blur passes - heavily weighted toward outer blur\n  float shape = shape1 * 0.25 + shape2 * 0.3 + shape3 * 0.25 + shape4 * 0.2;\n  \n  // Very soft power curve for foggy appearance\n  // Boost intensity for swirl/ripple to fill more space\n  float powerCurve = (u_shape < 1.5) ? 0.6 : 0.5;\n  float foggy = pow(shape, powerCurve);\n  \n  // Apply shape-specific edge handling with extra padding\n  float edgeFade = 1.0;\n  if (u_shape < 1.5) {\n    // Sphere: soft radial fade with more breathing room\n    edgeFade = 1.0 - smoothstep(0.55, 0.88, dist);\n  } else if (u_shape < 2.5) {\n    // Swirl: diffuse edges without hard cutoff\n    edgeFade = 1.0 - smoothstep(0.6, 0.92, dist);\n  } else {\n    // Ripple: diffuse edges without hard cutoff\n    edgeFade = 1.0 - smoothstep(0.6, 0.92, dist);\n  }\n  foggy *= edgeFade;\n  \n  // Extra soft smoothstep for diffuse edges\n  float finalShape = smoothstep(0.0, 0.8, foggy);\n  \n  // Mix colors\n  vec3 fgColor = u_colorFront.rgb * u_colorFront.a;\n  float fgOpacity = u_colorFront.a * finalShape;\n  vec3 bgColor = u_colorBack.rgb * u_colorBack.a;\n  float bgOpacity = u_colorBack.a;\n  \n  vec3 color = fgColor * finalShape;\n  float opacity = fgOpacity;\n  \n  color += bgColor * (1.0 - opacity);\n  opacity += bgOpacity * (1.0 - opacity);\n  \n  fragColor = vec4(color, opacity);\n}\n`;\n",
      "type": "registry:component",
      "target": "components/ui/shaders/blur.ts"
    },
    {
      "path": "registry/new-york/blocks/loader/shaders/dither.ts",
      "content": "// Dither style fragment shader - Bayer matrix dithering for retro pixelated look\nimport { shaderConstants, shapeUniforms, shapeFunctions } from \"./utils\";\n\nexport const ditherFragmentShader = `#version 300 es\nprecision mediump float;\n\n${shaderConstants}\n${shapeUniforms}\nuniform float u_pxSize;\n\nout vec4 fragColor;\n\n${shapeFunctions}\n\n// 4x4 Bayer dithering matrix\nconst int bayer4x4[16] = int[16](\n  0, 8, 2, 10,\n  12, 4, 14, 6,\n  3, 11, 1, 9,\n  15, 7, 13, 5\n);\n\n// 8x8 Bayer dithering matrix for finer detail\nconst int bayer8x8[64] = int[64](\n  0, 32, 8, 40, 2, 34, 10, 42,\n  48, 16, 56, 24, 50, 18, 58, 26,\n  12, 44, 4, 36, 14, 46, 6, 38,\n  60, 28, 52, 20, 62, 30, 54, 22,\n  3, 35, 11, 43, 1, 33, 9, 41,\n  51, 19, 59, 27, 49, 17, 57, 25,\n  15, 47, 7, 39, 13, 45, 5, 37,\n  63, 31, 55, 23, 61, 29, 53, 21\n);\n\nfloat getBayerValue4x4(vec2 uv) {\n  ivec2 pos = ivec2(mod(uv, 4.0));\n  int index = pos.y * 4 + pos.x;\n  return float(bayer4x4[index]) / 16.0;\n}\n\nfloat getBayerValue8x8(vec2 uv) {\n  ivec2 pos = ivec2(mod(uv, 8.0));\n  int index = pos.y * 8 + pos.x;\n  return float(bayer8x8[index]) / 64.0;\n}\n\nvoid main() {\n  float pxSize = max(2.0, u_pxSize) * u_pixelRatio;\n  \n  // Pixelate the UV coordinates\n  vec2 pixelUV = gl_FragCoord.xy / pxSize;\n  vec2 pixelatedCoord = (floor(pixelUV) + 0.5) * pxSize;\n  \n  // Normalize to centered coordinates\n  vec2 uv = (pixelatedCoord - 0.5 * u_resolution) / min(u_resolution.x, u_resolution.y);\n  uv /= u_scale;\n  \n  float time = u_time;\n  float shape = getShape(uv, time);\n  \n  // Get Bayer dithering threshold\n  float dither = getBayerValue4x4(pixelUV);\n  dither -= 0.5;\n  \n  // Apply dithering threshold\n  float result = step(0.5, shape + dither);\n  \n  // Mix colors\n  vec3 fgColor = u_colorFront.rgb * u_colorFront.a;\n  float fgOpacity = u_colorFront.a;\n  vec3 bgColor = u_colorBack.rgb * u_colorBack.a;\n  float bgOpacity = u_colorBack.a;\n  \n  vec3 color = fgColor * result;\n  float opacity = fgOpacity * result;\n  \n  color += bgColor * (1.0 - opacity);\n  opacity += bgOpacity * (1.0 - opacity);\n  \n  fragColor = vec4(color, opacity);\n}\n`;\n",
      "type": "registry:component",
      "target": "components/ui/shaders/dither.ts"
    }
  ]
}