{
  "slug": "dither",
  "name": "Dither",
  "description": "WebGL gradient shader with fluid warping, noise-driven rotation and optional animated grain.",
  "summary": "A WebGL 2 gradient shader built on OGL. Generates organic distortion waves with sine-based domain warping and noise-driven rotation, blends three colors, and adds film grain that can be static or animated. Includes contrast, gamma, saturation, zoom and light-mode controls.",
  "category": "backgrounds-and-effects",
  "tags": [
    "dither",
    "grain",
    "shader",
    "webgl",
    "ogl",
    "gradient",
    "warp",
    "noise",
    "motion"
  ],
  "dependencies": [
    "ogl"
  ],
  "version": "1.0.0",
  "createdDate": "2026-09-22",
  "updatedDate": "2026-09-22",
  "interactive": true,
  "supportsColor": true,
  "highlights": [
    "Lightweight OGL WebGL 2.0 renderer with full GPU pipeline lifecycle management",
    "Gradient noise rotation and sine-wave domain warp",
    "Hash-based film grain with no texture lookups (animated only when grainAnimated is true)",
    "Dynamic color blending between three customizable chromatic palette points",
    "Optional lightMode prop that remaps the output onto a white base"
  ],
  "anatomy": [
    "<div> (Responsive Canvas Container: manages ResizeObserver and IntersectionObserver lifecycle)",
    "<canvas> (OGL WebGL 2.0 Canvas: full-resolution viewport rendering pipeline)",
    "Triangle (Single full-screen triangle geometry running the vertex and fragment shaders)"
  ],
  "physics": {
    "engine": "OGL WebGL 2.0 Fragment Shader",
    "description": "Mathematical noise and sine warp domain displacement running in parallel across GPU fragment execution units.",
    "parameters": [
      {
        "label": "Warp Frequency",
        "value": "5.0 (warpFrequency)"
      },
      {
        "label": "Noise Scale",
        "value": "2.0 (noise input multiplier)"
      },
      {
        "label": "Rotation Amount",
        "value": "500 (noise-driven rotation range, plus or minus 250 degrees)"
      },
      {
        "label": "Grain Scale",
        "value": "2.0 (grainScale)"
      }
    ]
  },
  "accessibility": {
    "aria": "The shader container is aria-hidden=\"true\"; it is a purely decorative background.",
    "reducedMotion": "Pauses the animation loop when prefers-reduced-motion is set. The loop also stops while the canvas is offscreen or the tab is hidden."
  },
  "guidelines": {
    "recommended": [
      "Hero section dynamic ambient background in high-aesthetic developer platforms",
      "Fluid chromatic texture inside cards, dialogs, or generative AI interfaces",
      "Overlay layer for orbs, badges, or interactive creative canvases"
    ],
    "bestPractices": [
      "Use a subtle grainAmount (0.1 to 0.2) for a balanced texture",
      "Match color1 with your active theme primary accent for cohesive brand identity",
      "Ensure parent container has relative positioning and explicit dimensions"
    ]
  },
  "props": [
    {
      "name": "color1",
      "type": "string",
      "defaultValue": "'#FF9FFC'",
      "description": "Primary accent color for the upper gradient blend."
    },
    {
      "name": "color2",
      "type": "string",
      "defaultValue": "'#5227FF'",
      "description": "Secondary chromatic color for the mid warp flow."
    },
    {
      "name": "color3",
      "type": "string",
      "defaultValue": "'#B497CF'",
      "description": "Darker base color for the background depth."
    },
    {
      "name": "lightMode",
      "type": "boolean",
      "defaultValue": "false",
      "description": "Remaps the output onto a white base for light backgrounds."
    },
    {
      "name": "className",
      "type": "string",
      "defaultValue": "\"\"",
      "description": "Additional CSS classes for the canvas container."
    },
    {
      "name": "timeSpeed",
      "type": "number",
      "defaultValue": "0.25",
      "description": "Speed of the procedural fluid time evolution."
    },
    {
      "name": "colorBalance",
      "type": "number",
      "defaultValue": "0",
      "description": "Shifts the blend point between the three colors."
    },
    {
      "name": "warpStrength",
      "type": "number",
      "defaultValue": "1.0",
      "description": "Magnitude of domain warping displacement."
    },
    {
      "name": "warpFrequency",
      "type": "number",
      "defaultValue": "5",
      "description": "Frequency of the sine warp applied to the gradient."
    },
    {
      "name": "warpSpeed",
      "type": "number",
      "defaultValue": "2",
      "description": "Speed of the warp animation."
    },
    {
      "name": "warpAmplitude",
      "type": "number",
      "defaultValue": "50",
      "description": "Amplitude of the warp. Higher values give gentler waves."
    },
    {
      "name": "blendAngle",
      "type": "number",
      "defaultValue": "0",
      "description": "Rotation in degrees of the color blend axis."
    },
    {
      "name": "blendSoftness",
      "type": "number",
      "defaultValue": "0.05",
      "description": "Softness of the transitions between colors."
    },
    {
      "name": "rotationAmount",
      "type": "number",
      "defaultValue": "500",
      "description": "Strength of the noise-driven rotation of the gradient field."
    },
    {
      "name": "noiseScale",
      "type": "number",
      "defaultValue": "2",
      "description": "Scale of the noise that drives the rotation."
    },
    {
      "name": "grainAmount",
      "type": "number",
      "defaultValue": "0.1",
      "description": "Intensity of the simulated dither grain overlay."
    },
    {
      "name": "grainScale",
      "type": "number",
      "defaultValue": "2",
      "description": "Scale of the film grain pattern."
    },
    {
      "name": "grainAnimated",
      "type": "boolean",
      "defaultValue": "false",
      "description": "Whether the grain noise particles shift dynamically each frame."
    },
    {
      "name": "contrast",
      "type": "number",
      "defaultValue": "1.5",
      "description": "Output contrast."
    },
    {
      "name": "gamma",
      "type": "number",
      "defaultValue": "1",
      "description": "Output gamma correction."
    },
    {
      "name": "saturation",
      "type": "number",
      "defaultValue": "1",
      "description": "Output saturation. 0 renders grayscale."
    },
    {
      "name": "centerX",
      "type": "number",
      "defaultValue": "0",
      "description": "Horizontal offset of the gradient center."
    },
    {
      "name": "centerY",
      "type": "number",
      "defaultValue": "0",
      "description": "Vertical offset of the gradient center."
    },
    {
      "name": "zoom",
      "type": "number",
      "defaultValue": "0.9",
      "description": "Zoom level of the gradient field."
    }
  ],
  "usage": "import { Dither } from \"@/components/ui/dither\";\n\nexport function DitherDemo() {\n  return (\n    <div className=\"relative h-80 w-full\">\n      <Dither color1=\"#FF9FFC\" color2=\"#5227FF\" color3=\"#B497CF\" />\n    </div>\n  );\n}",
  "files": [
    {
      "name": "dither.tsx",
      "path": "registry/ui/dither.tsx",
      "code": "\"use client\";\n\nimport React, { useEffect, useRef } from \"react\";\nimport { Renderer, Program, Mesh, Triangle } from \"ogl\";\n\nexport interface GrainientProps {\n  timeSpeed?: number;\n  colorBalance?: number;\n  warpStrength?: number;\n  warpFrequency?: number;\n  warpSpeed?: number;\n  warpAmplitude?: number;\n  blendAngle?: number;\n  blendSoftness?: number;\n  rotationAmount?: number;\n  noiseScale?: number;\n  grainAmount?: number;\n  grainScale?: number;\n  grainAnimated?: boolean;\n  contrast?: number;\n  gamma?: number;\n  saturation?: number;\n  centerX?: number;\n  centerY?: number;\n  zoom?: number;\n  color1?: string;\n  color2?: string;\n  color3?: string;\n  lightMode?: boolean;\n  className?: string;\n}\n\nexport type DitherProps = GrainientProps;\n\nconst hexToRgb = (hex: string): [number, number, number] => {\n  const result = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);\n  if (!result) return [1, 1, 1];\n  return [\n    parseInt(result[1], 16) / 255,\n    parseInt(result[2], 16) / 255,\n    parseInt(result[3], 16) / 255,\n  ];\n};\n\nconst vertex = `#version 300 es\nin vec2 position;\nvoid main() {\n  gl_Position = vec4(position, 0.0, 1.0);\n}\n`;\n\nconst fragment = `#version 300 es\nprecision highp float;\nuniform vec2 iResolution;\nuniform float iTime;\nuniform float uTimeSpeed;\nuniform float uColorBalance;\nuniform float uWarpStrength;\nuniform float uWarpFrequency;\nuniform float uWarpSpeed;\nuniform float uWarpAmplitude;\nuniform float uBlendAngle;\nuniform float uBlendSoftness;\nuniform float uRotationAmount;\nuniform float uNoiseScale;\nuniform float uGrainAmount;\nuniform float uGrainScale;\nuniform float uGrainAnimated;\nuniform float uContrast;\nuniform float uGamma;\nuniform float uSaturation;\nuniform vec2 uCenterOffset;\nuniform float uZoom;\nuniform vec3 uColor1;\nuniform vec3 uColor2;\nuniform vec3 uColor3;\nuniform float uLightMode;\nout vec4 fragColor;\n#define S(a,b,t) smoothstep(a,b,t)\nmat2 Rot(float a){float s=sin(a),c=cos(a);return mat2(c,-s,s,c);} \nvec2 hash(vec2 p){p=vec2(dot(p,vec2(2127.1,81.17)),dot(p,vec2(1269.5,283.37)));return fract(sin(p)*43758.5453);} \nfloat noise(vec2 p){vec2 i=floor(p),f=fract(p),u=f*f*(3.0-2.0*f);float n=mix(mix(dot(-1.0+2.0*hash(i+vec2(0.0,0.0)),f-vec2(0.0,0.0)),dot(-1.0+2.0*hash(i+vec2(1.0,0.0)),f-vec2(1.0,0.0)),u.x),mix(dot(-1.0+2.0*hash(i+vec2(0.0,1.0)),f-vec2(0.0,1.0)),dot(-1.0+2.0*hash(i+vec2(1.0,1.0)),f-vec2(1.0,1.0)),u.x),u.y);return 0.5+0.5*n;}\nvoid mainImage(out vec4 o, vec2 C){\n  float t=iTime*uTimeSpeed;\n  vec2 uv=C/iResolution.xy;\n  float ratio=iResolution.x/iResolution.y;\n  vec2 tuv=uv-0.5+uCenterOffset;\n  tuv/=max(uZoom,0.001);\n\n  float degree=noise(vec2(t*0.1,tuv.x*tuv.y)*uNoiseScale);\n  tuv.y*=1.0/ratio;\n  tuv*=Rot(radians((degree-0.5)*uRotationAmount+180.0));\n  tuv.y*=ratio;\n\n  float frequency=uWarpFrequency;\n  float ws=max(uWarpStrength,0.001);\n  float amplitude=uWarpAmplitude/ws;\n  float warpTime=t*uWarpSpeed;\n  tuv.x+=sin(tuv.y*frequency+warpTime)/amplitude;\n  tuv.y+=sin(tuv.x*(frequency*1.5)+warpTime)/(amplitude*0.5);\n\n  vec3 colLav=uColor1;\n  vec3 colOrg=uColor2;\n  vec3 colDark=uColor3;\n  float b=uColorBalance;\n  float s=max(uBlendSoftness,0.0);\n  mat2 blendRot=Rot(radians(uBlendAngle));\n  float blendX=(tuv*blendRot).x;\n  float edge0=-0.3-b-s;\n  float edge1=0.2-b+s;\n  float v0=0.5-b+s;\n  float v1=-0.3-b-s;\n  vec3 layer1=mix(colDark,colOrg,S(edge0,edge1,blendX));\n  vec3 layer2=mix(colOrg,colLav,S(edge0,edge1,blendX));\n  vec3 col=mix(layer1,layer2,S(v0,v1,tuv.y));\n\n  vec2 grainUv=uv*max(uGrainScale,0.001);\n  if(uGrainAnimated>0.5){grainUv+=vec2(iTime*0.05);} \n  float grain=fract(sin(dot(grainUv,vec2(12.9898,78.233)))*43758.5453);\n  col+=(grain-0.5)*uGrainAmount;\n\n  col=(col-0.5)*uContrast+0.5;\n  float luma=dot(col,vec3(0.2126,0.7152,0.0722));\n  col=mix(vec3(luma),col,uSaturation);\n  col=pow(max(col,0.0),vec3(1.0/max(uGamma,0.001)));\n  col=clamp(col,0.0,1.0);\n  if(uLightMode>0.5){\n    float energy=max(max(col.r,col.g),col.b);\n    vec3 hue=col/max(energy,0.001);\n    float chroma=length(col-vec3(dot(col,vec3(0.333333))));\n    float coverage=clamp(0.12+chroma*1.15+energy*0.18,0.0,0.88);\n    col=mix(vec3(1.0),clamp(hue*0.58+col*0.18,0.0,1.0),coverage);\n  }\n\n  o=vec4(col,1.0);\n}\nvoid main(){\n  vec4 o=vec4(0.0);\n  mainImage(o,gl_FragCoord.xy);\n  fragColor=o;\n}\n`;\n\ntype GrainientCtx = {\n  renderer: InstanceType<typeof Renderer>;\n  program: InstanceType<typeof Program>;\n  mesh: InstanceType<typeof Mesh>;\n};\nconst ctxMap = new WeakMap<HTMLDivElement, GrainientCtx>();\n\nexport const Grainient: React.FC<GrainientProps> = ({\n  timeSpeed = 0.25,\n  colorBalance = 0.0,\n  warpStrength = 1.0,\n  warpFrequency = 5.0,\n  warpSpeed = 2.0,\n  warpAmplitude = 50.0,\n  blendAngle = 0.0,\n  blendSoftness = 0.05,\n  rotationAmount = 500.0,\n  noiseScale = 2.0,\n  grainAmount = 0.1,\n  grainScale = 2.0,\n  grainAnimated = false,\n  contrast = 1.5,\n  gamma = 1.0,\n  saturation = 1.0,\n  centerX = 0.0,\n  centerY = 0.0,\n  zoom = 0.9,\n  color1 = \"#FF9FFC\",\n  color2 = \"#5227FF\",\n  color3 = \"#B497CF\",\n  lightMode = false,\n  className = \"\",\n}) => {\n  const containerRef = useRef<HTMLDivElement | null>(null);\n\n  useEffect(() => {\n    const container = containerRef.current;\n    if (!container) return;\n\n    const renderer = new Renderer({\n      webgl: 2,\n      alpha: true,\n      antialias: false,\n      dpr: Math.min(window.devicePixelRatio || 1, 2),\n    });\n\n    const gl = renderer.gl;\n    const canvas = gl.canvas as HTMLCanvasElement;\n    canvas.style.width = \"100%\";\n    canvas.style.height = \"100%\";\n    canvas.style.display = \"block\";\n    container.appendChild(canvas);\n\n    const geometry = new Triangle(gl);\n    const program = new Program(gl, {\n      vertex,\n      fragment,\n      uniforms: {\n        iTime: { value: 0 },\n        iResolution: { value: new Float32Array([1, 1]) },\n        uTimeSpeed: { value: 0.25 },\n        uColorBalance: { value: 0.0 },\n        uWarpStrength: { value: 1.0 },\n        uWarpFrequency: { value: 5.0 },\n        uWarpSpeed: { value: 2.0 },\n        uWarpAmplitude: { value: 50.0 },\n        uBlendAngle: { value: 0.0 },\n        uBlendSoftness: { value: 0.05 },\n        uRotationAmount: { value: 500.0 },\n        uNoiseScale: { value: 2.0 },\n        uGrainAmount: { value: 0.1 },\n        uGrainScale: { value: 2.0 },\n        uGrainAnimated: { value: 0.0 },\n        uContrast: { value: 1.5 },\n        uGamma: { value: 1.0 },\n        uSaturation: { value: 1.0 },\n        uCenterOffset: { value: new Float32Array([0, 0]) },\n        uZoom: { value: 0.9 },\n        uColor1: { value: new Float32Array([1, 1, 1]) },\n        uColor2: { value: new Float32Array([1, 1, 1]) },\n        uColor3: { value: new Float32Array([1, 1, 1]) },\n        uLightMode: { value: 0.0 },\n      },\n    });\n\n    const mesh = new Mesh(gl, { geometry, program });\n    ctxMap.set(container, { renderer, program, mesh });\n\n    const setSize = () => {\n      const rect = container.getBoundingClientRect();\n      const w = Math.max(1, Math.floor(rect.width));\n      const h = Math.max(1, Math.floor(rect.height));\n      renderer.setSize(w, h);\n      const res = (program.uniforms.iResolution as { value: Float32Array })\n        .value;\n      res[0] = gl.drawingBufferWidth;\n      res[1] = gl.drawingBufferHeight;\n      renderer.render({ scene: mesh });\n    };\n\n    const ro = new ResizeObserver(setSize);\n    ro.observe(container);\n    setSize();\n\n    let raf = 0;\n    let isVisible = true;\n    let isPageVisible = !document.hidden;\n    const motionQuery = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n    let reduceMotion = motionQuery.matches;\n    const t0 = performance.now();\n\n    const loop = (t: number) => {\n      (program.uniforms.iTime as { value: number }).value = (t - t0) * 0.001;\n      renderer.render({ scene: mesh });\n      raf = requestAnimationFrame(loop);\n    };\n\n    const tryStart = () => {\n      if (!reduceMotion && isVisible && isPageVisible && raf === 0) {\n        raf = requestAnimationFrame(loop);\n      }\n    };\n    const tryStop = () => {\n      if (raf !== 0) {\n        cancelAnimationFrame(raf);\n        raf = 0;\n      }\n    };\n\n    const io = new IntersectionObserver(\n      ([entry]) => {\n        isVisible = entry.isIntersecting;\n        if (isVisible) {\n          tryStart();\n        } else {\n          tryStop();\n        }\n      },\n      { threshold: 0 },\n    );\n    io.observe(container);\n\n    const onVisibility = () => {\n      isPageVisible = !document.hidden;\n      if (isPageVisible) {\n        tryStart();\n      } else {\n        tryStop();\n      }\n    };\n    document.addEventListener(\"visibilitychange\", onVisibility);\n\n    const onMotionPreference = () => {\n      reduceMotion = motionQuery.matches;\n      if (reduceMotion) {\n        tryStop();\n      } else {\n        tryStart();\n      }\n    };\n    motionQuery.addEventListener(\"change\", onMotionPreference);\n\n    tryStart();\n\n    return () => {\n      tryStop();\n      ro.disconnect();\n      io.disconnect();\n      document.removeEventListener(\"visibilitychange\", onVisibility);\n      motionQuery.removeEventListener(\"change\", onMotionPreference);\n      ctxMap.delete(container);\n      try {\n        container.removeChild(canvas);\n      } catch {}\n      gl.getExtension(\"WEBGL_lose_context\")?.loseContext();\n    };\n  }, []);\n\n  useEffect(() => {\n    const container = containerRef.current;\n    if (!container) return;\n    const ctx = ctxMap.get(container);\n    if (!ctx) return;\n    const { renderer, program, mesh } = ctx;\n    const u = program.uniforms as Record<string, { value: unknown }>;\n\n    u.uTimeSpeed.value = timeSpeed;\n    u.uColorBalance.value = colorBalance;\n    u.uWarpStrength.value = warpStrength;\n    u.uWarpFrequency.value = warpFrequency;\n    u.uWarpSpeed.value = warpSpeed;\n    u.uWarpAmplitude.value = warpAmplitude;\n    u.uBlendAngle.value = blendAngle;\n    u.uBlendSoftness.value = blendSoftness;\n    u.uRotationAmount.value = rotationAmount;\n    u.uNoiseScale.value = noiseScale;\n    u.uGrainAmount.value = grainAmount;\n    u.uGrainScale.value = grainScale;\n    u.uGrainAnimated.value = grainAnimated ? 1.0 : 0.0;\n    u.uContrast.value = contrast;\n    u.uGamma.value = gamma;\n    u.uSaturation.value = saturation;\n    u.uCenterOffset.value = new Float32Array([centerX, centerY]);\n    u.uZoom.value = zoom;\n    u.uColor1.value = new Float32Array(hexToRgb(color1));\n    u.uColor2.value = new Float32Array(hexToRgb(color2));\n    u.uColor3.value = new Float32Array(hexToRgb(color3));\n    u.uLightMode.value = lightMode ? 1.0 : 0.0;\n    renderer.render({ scene: mesh });\n  }, [\n    timeSpeed,\n    colorBalance,\n    warpStrength,\n    warpFrequency,\n    warpSpeed,\n    warpAmplitude,\n    blendAngle,\n    blendSoftness,\n    rotationAmount,\n    noiseScale,\n    grainAmount,\n    grainScale,\n    grainAnimated,\n    contrast,\n    gamma,\n    saturation,\n    centerX,\n    centerY,\n    zoom,\n    color1,\n    color2,\n    color3,\n    lightMode,\n  ]);\n\n  return (\n    <div\n      ref={containerRef}\n      aria-hidden=\"true\"\n      className={`relative h-full w-full overflow-hidden ${className}`.trim()}\n    />\n  );\n};\n\nexport const Dither = Grainient;\nexport default Grainient;\n",
      "content": "\"use client\";\n\nimport React, { useEffect, useRef } from \"react\";\nimport { Renderer, Program, Mesh, Triangle } from \"ogl\";\n\nexport interface GrainientProps {\n  timeSpeed?: number;\n  colorBalance?: number;\n  warpStrength?: number;\n  warpFrequency?: number;\n  warpSpeed?: number;\n  warpAmplitude?: number;\n  blendAngle?: number;\n  blendSoftness?: number;\n  rotationAmount?: number;\n  noiseScale?: number;\n  grainAmount?: number;\n  grainScale?: number;\n  grainAnimated?: boolean;\n  contrast?: number;\n  gamma?: number;\n  saturation?: number;\n  centerX?: number;\n  centerY?: number;\n  zoom?: number;\n  color1?: string;\n  color2?: string;\n  color3?: string;\n  lightMode?: boolean;\n  className?: string;\n}\n\nexport type DitherProps = GrainientProps;\n\nconst hexToRgb = (hex: string): [number, number, number] => {\n  const result = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);\n  if (!result) return [1, 1, 1];\n  return [\n    parseInt(result[1], 16) / 255,\n    parseInt(result[2], 16) / 255,\n    parseInt(result[3], 16) / 255,\n  ];\n};\n\nconst vertex = `#version 300 es\nin vec2 position;\nvoid main() {\n  gl_Position = vec4(position, 0.0, 1.0);\n}\n`;\n\nconst fragment = `#version 300 es\nprecision highp float;\nuniform vec2 iResolution;\nuniform float iTime;\nuniform float uTimeSpeed;\nuniform float uColorBalance;\nuniform float uWarpStrength;\nuniform float uWarpFrequency;\nuniform float uWarpSpeed;\nuniform float uWarpAmplitude;\nuniform float uBlendAngle;\nuniform float uBlendSoftness;\nuniform float uRotationAmount;\nuniform float uNoiseScale;\nuniform float uGrainAmount;\nuniform float uGrainScale;\nuniform float uGrainAnimated;\nuniform float uContrast;\nuniform float uGamma;\nuniform float uSaturation;\nuniform vec2 uCenterOffset;\nuniform float uZoom;\nuniform vec3 uColor1;\nuniform vec3 uColor2;\nuniform vec3 uColor3;\nuniform float uLightMode;\nout vec4 fragColor;\n#define S(a,b,t) smoothstep(a,b,t)\nmat2 Rot(float a){float s=sin(a),c=cos(a);return mat2(c,-s,s,c);} \nvec2 hash(vec2 p){p=vec2(dot(p,vec2(2127.1,81.17)),dot(p,vec2(1269.5,283.37)));return fract(sin(p)*43758.5453);} \nfloat noise(vec2 p){vec2 i=floor(p),f=fract(p),u=f*f*(3.0-2.0*f);float n=mix(mix(dot(-1.0+2.0*hash(i+vec2(0.0,0.0)),f-vec2(0.0,0.0)),dot(-1.0+2.0*hash(i+vec2(1.0,0.0)),f-vec2(1.0,0.0)),u.x),mix(dot(-1.0+2.0*hash(i+vec2(0.0,1.0)),f-vec2(0.0,1.0)),dot(-1.0+2.0*hash(i+vec2(1.0,1.0)),f-vec2(1.0,1.0)),u.x),u.y);return 0.5+0.5*n;}\nvoid mainImage(out vec4 o, vec2 C){\n  float t=iTime*uTimeSpeed;\n  vec2 uv=C/iResolution.xy;\n  float ratio=iResolution.x/iResolution.y;\n  vec2 tuv=uv-0.5+uCenterOffset;\n  tuv/=max(uZoom,0.001);\n\n  float degree=noise(vec2(t*0.1,tuv.x*tuv.y)*uNoiseScale);\n  tuv.y*=1.0/ratio;\n  tuv*=Rot(radians((degree-0.5)*uRotationAmount+180.0));\n  tuv.y*=ratio;\n\n  float frequency=uWarpFrequency;\n  float ws=max(uWarpStrength,0.001);\n  float amplitude=uWarpAmplitude/ws;\n  float warpTime=t*uWarpSpeed;\n  tuv.x+=sin(tuv.y*frequency+warpTime)/amplitude;\n  tuv.y+=sin(tuv.x*(frequency*1.5)+warpTime)/(amplitude*0.5);\n\n  vec3 colLav=uColor1;\n  vec3 colOrg=uColor2;\n  vec3 colDark=uColor3;\n  float b=uColorBalance;\n  float s=max(uBlendSoftness,0.0);\n  mat2 blendRot=Rot(radians(uBlendAngle));\n  float blendX=(tuv*blendRot).x;\n  float edge0=-0.3-b-s;\n  float edge1=0.2-b+s;\n  float v0=0.5-b+s;\n  float v1=-0.3-b-s;\n  vec3 layer1=mix(colDark,colOrg,S(edge0,edge1,blendX));\n  vec3 layer2=mix(colOrg,colLav,S(edge0,edge1,blendX));\n  vec3 col=mix(layer1,layer2,S(v0,v1,tuv.y));\n\n  vec2 grainUv=uv*max(uGrainScale,0.001);\n  if(uGrainAnimated>0.5){grainUv+=vec2(iTime*0.05);} \n  float grain=fract(sin(dot(grainUv,vec2(12.9898,78.233)))*43758.5453);\n  col+=(grain-0.5)*uGrainAmount;\n\n  col=(col-0.5)*uContrast+0.5;\n  float luma=dot(col,vec3(0.2126,0.7152,0.0722));\n  col=mix(vec3(luma),col,uSaturation);\n  col=pow(max(col,0.0),vec3(1.0/max(uGamma,0.001)));\n  col=clamp(col,0.0,1.0);\n  if(uLightMode>0.5){\n    float energy=max(max(col.r,col.g),col.b);\n    vec3 hue=col/max(energy,0.001);\n    float chroma=length(col-vec3(dot(col,vec3(0.333333))));\n    float coverage=clamp(0.12+chroma*1.15+energy*0.18,0.0,0.88);\n    col=mix(vec3(1.0),clamp(hue*0.58+col*0.18,0.0,1.0),coverage);\n  }\n\n  o=vec4(col,1.0);\n}\nvoid main(){\n  vec4 o=vec4(0.0);\n  mainImage(o,gl_FragCoord.xy);\n  fragColor=o;\n}\n`;\n\ntype GrainientCtx = {\n  renderer: InstanceType<typeof Renderer>;\n  program: InstanceType<typeof Program>;\n  mesh: InstanceType<typeof Mesh>;\n};\nconst ctxMap = new WeakMap<HTMLDivElement, GrainientCtx>();\n\nexport const Grainient: React.FC<GrainientProps> = ({\n  timeSpeed = 0.25,\n  colorBalance = 0.0,\n  warpStrength = 1.0,\n  warpFrequency = 5.0,\n  warpSpeed = 2.0,\n  warpAmplitude = 50.0,\n  blendAngle = 0.0,\n  blendSoftness = 0.05,\n  rotationAmount = 500.0,\n  noiseScale = 2.0,\n  grainAmount = 0.1,\n  grainScale = 2.0,\n  grainAnimated = false,\n  contrast = 1.5,\n  gamma = 1.0,\n  saturation = 1.0,\n  centerX = 0.0,\n  centerY = 0.0,\n  zoom = 0.9,\n  color1 = \"#FF9FFC\",\n  color2 = \"#5227FF\",\n  color3 = \"#B497CF\",\n  lightMode = false,\n  className = \"\",\n}) => {\n  const containerRef = useRef<HTMLDivElement | null>(null);\n\n  useEffect(() => {\n    const container = containerRef.current;\n    if (!container) return;\n\n    const renderer = new Renderer({\n      webgl: 2,\n      alpha: true,\n      antialias: false,\n      dpr: Math.min(window.devicePixelRatio || 1, 2),\n    });\n\n    const gl = renderer.gl;\n    const canvas = gl.canvas as HTMLCanvasElement;\n    canvas.style.width = \"100%\";\n    canvas.style.height = \"100%\";\n    canvas.style.display = \"block\";\n    container.appendChild(canvas);\n\n    const geometry = new Triangle(gl);\n    const program = new Program(gl, {\n      vertex,\n      fragment,\n      uniforms: {\n        iTime: { value: 0 },\n        iResolution: { value: new Float32Array([1, 1]) },\n        uTimeSpeed: { value: 0.25 },\n        uColorBalance: { value: 0.0 },\n        uWarpStrength: { value: 1.0 },\n        uWarpFrequency: { value: 5.0 },\n        uWarpSpeed: { value: 2.0 },\n        uWarpAmplitude: { value: 50.0 },\n        uBlendAngle: { value: 0.0 },\n        uBlendSoftness: { value: 0.05 },\n        uRotationAmount: { value: 500.0 },\n        uNoiseScale: { value: 2.0 },\n        uGrainAmount: { value: 0.1 },\n        uGrainScale: { value: 2.0 },\n        uGrainAnimated: { value: 0.0 },\n        uContrast: { value: 1.5 },\n        uGamma: { value: 1.0 },\n        uSaturation: { value: 1.0 },\n        uCenterOffset: { value: new Float32Array([0, 0]) },\n        uZoom: { value: 0.9 },\n        uColor1: { value: new Float32Array([1, 1, 1]) },\n        uColor2: { value: new Float32Array([1, 1, 1]) },\n        uColor3: { value: new Float32Array([1, 1, 1]) },\n        uLightMode: { value: 0.0 },\n      },\n    });\n\n    const mesh = new Mesh(gl, { geometry, program });\n    ctxMap.set(container, { renderer, program, mesh });\n\n    const setSize = () => {\n      const rect = container.getBoundingClientRect();\n      const w = Math.max(1, Math.floor(rect.width));\n      const h = Math.max(1, Math.floor(rect.height));\n      renderer.setSize(w, h);\n      const res = (program.uniforms.iResolution as { value: Float32Array })\n        .value;\n      res[0] = gl.drawingBufferWidth;\n      res[1] = gl.drawingBufferHeight;\n      renderer.render({ scene: mesh });\n    };\n\n    const ro = new ResizeObserver(setSize);\n    ro.observe(container);\n    setSize();\n\n    let raf = 0;\n    let isVisible = true;\n    let isPageVisible = !document.hidden;\n    const motionQuery = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n    let reduceMotion = motionQuery.matches;\n    const t0 = performance.now();\n\n    const loop = (t: number) => {\n      (program.uniforms.iTime as { value: number }).value = (t - t0) * 0.001;\n      renderer.render({ scene: mesh });\n      raf = requestAnimationFrame(loop);\n    };\n\n    const tryStart = () => {\n      if (!reduceMotion && isVisible && isPageVisible && raf === 0) {\n        raf = requestAnimationFrame(loop);\n      }\n    };\n    const tryStop = () => {\n      if (raf !== 0) {\n        cancelAnimationFrame(raf);\n        raf = 0;\n      }\n    };\n\n    const io = new IntersectionObserver(\n      ([entry]) => {\n        isVisible = entry.isIntersecting;\n        if (isVisible) {\n          tryStart();\n        } else {\n          tryStop();\n        }\n      },\n      { threshold: 0 },\n    );\n    io.observe(container);\n\n    const onVisibility = () => {\n      isPageVisible = !document.hidden;\n      if (isPageVisible) {\n        tryStart();\n      } else {\n        tryStop();\n      }\n    };\n    document.addEventListener(\"visibilitychange\", onVisibility);\n\n    const onMotionPreference = () => {\n      reduceMotion = motionQuery.matches;\n      if (reduceMotion) {\n        tryStop();\n      } else {\n        tryStart();\n      }\n    };\n    motionQuery.addEventListener(\"change\", onMotionPreference);\n\n    tryStart();\n\n    return () => {\n      tryStop();\n      ro.disconnect();\n      io.disconnect();\n      document.removeEventListener(\"visibilitychange\", onVisibility);\n      motionQuery.removeEventListener(\"change\", onMotionPreference);\n      ctxMap.delete(container);\n      try {\n        container.removeChild(canvas);\n      } catch {}\n      gl.getExtension(\"WEBGL_lose_context\")?.loseContext();\n    };\n  }, []);\n\n  useEffect(() => {\n    const container = containerRef.current;\n    if (!container) return;\n    const ctx = ctxMap.get(container);\n    if (!ctx) return;\n    const { renderer, program, mesh } = ctx;\n    const u = program.uniforms as Record<string, { value: unknown }>;\n\n    u.uTimeSpeed.value = timeSpeed;\n    u.uColorBalance.value = colorBalance;\n    u.uWarpStrength.value = warpStrength;\n    u.uWarpFrequency.value = warpFrequency;\n    u.uWarpSpeed.value = warpSpeed;\n    u.uWarpAmplitude.value = warpAmplitude;\n    u.uBlendAngle.value = blendAngle;\n    u.uBlendSoftness.value = blendSoftness;\n    u.uRotationAmount.value = rotationAmount;\n    u.uNoiseScale.value = noiseScale;\n    u.uGrainAmount.value = grainAmount;\n    u.uGrainScale.value = grainScale;\n    u.uGrainAnimated.value = grainAnimated ? 1.0 : 0.0;\n    u.uContrast.value = contrast;\n    u.uGamma.value = gamma;\n    u.uSaturation.value = saturation;\n    u.uCenterOffset.value = new Float32Array([centerX, centerY]);\n    u.uZoom.value = zoom;\n    u.uColor1.value = new Float32Array(hexToRgb(color1));\n    u.uColor2.value = new Float32Array(hexToRgb(color2));\n    u.uColor3.value = new Float32Array(hexToRgb(color3));\n    u.uLightMode.value = lightMode ? 1.0 : 0.0;\n    renderer.render({ scene: mesh });\n  }, [\n    timeSpeed,\n    colorBalance,\n    warpStrength,\n    warpFrequency,\n    warpSpeed,\n    warpAmplitude,\n    blendAngle,\n    blendSoftness,\n    rotationAmount,\n    noiseScale,\n    grainAmount,\n    grainScale,\n    grainAnimated,\n    contrast,\n    gamma,\n    saturation,\n    centerX,\n    centerY,\n    zoom,\n    color1,\n    color2,\n    color3,\n    lightMode,\n  ]);\n\n  return (\n    <div\n      ref={containerRef}\n      aria-hidden=\"true\"\n      className={`relative h-full w-full overflow-hidden ${className}`.trim()}\n    />\n  );\n};\n\nexport const Dither = Grainient;\nexport default Grainient;\n",
      "type": "registry:ui",
      "target": "@ui/dither.tsx"
    }
  ],
  "type": "registry:ui",
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "title": "Dither"
}
