{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "pixel-liquid-bg",
  "title": "Pixel Liquid Background",
  "description": "A full Navier-Stokes fluid simulation background with pixelation, Bayer dithering, film-grain noise, and auto-demo mode. Reacts to cursor movement.",
  "dependencies": [
    "three"
  ],
  "devDependencies": [
    "@types/three"
  ],
  "files": [
    {
      "path": "registry/components/backgrounds/pixel-liquid-bg/index.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useRef } from \"react\";\nimport * as THREE from \"three\";\nimport { cn } from \"@/lib/utils\";\n\n/*\n  PixelLiquidBg — Navier-Stokes fluid sim with Bayer dithering, pixelation,\n  film-grain noise, and auto-demo mode that yields to cursor interaction.\n*/\n\nconst face_vert = /* glsl */ `\nattribute vec3 position;\nuniform vec2 px;\nuniform vec2 boundarySpace;\nvarying vec2 uv;\nprecision highp float;\nvoid main(){\n  vec3 pos = position;\n  vec2 scale = 1.0 - boundarySpace * 2.0;\n  pos.xy = pos.xy * scale;\n  uv = vec2(0.5) + pos.xy * 0.5;\n  gl_Position = vec4(pos, 1.0);\n}\n`;\n\nconst line_vert = /* glsl */ `\nattribute vec3 position;\nuniform vec2 px;\nprecision highp float;\nvarying vec2 uv;\nvoid main(){\n  vec3 pos = position;\n  uv = 0.5 + pos.xy * 0.5;\n  vec2 n = sign(pos.xy);\n  pos.xy = abs(pos.xy) - px * 1.0;\n  pos.xy *= n;\n  gl_Position = vec4(pos, 1.0);\n}\n`;\n\nconst mouse_vert = /* glsl */ `\nprecision highp float;\nattribute vec3 position;\nattribute vec2 uv;\nuniform vec2 center;\nuniform vec2 scale;\nuniform vec2 px;\nvarying vec2 vUv;\nvoid main(){\n  vec2 pos = position.xy * scale * 2.0 * px + center;\n  vUv = uv;\n  gl_Position = vec4(pos, 0.0, 1.0);\n}\n`;\n\nconst advection_frag = /* glsl */ `\nprecision highp float;\nuniform sampler2D velocity;\nuniform float dt;\nuniform bool isBFECC;\nuniform vec2 fboSize;\nuniform vec2 px;\nvarying vec2 uv;\nvoid main(){\n  vec2 ratio = max(fboSize.x, fboSize.y) / fboSize;\n  if(isBFECC == false){\n    vec2 vel = texture2D(velocity, uv).xy;\n    vec2 uv2 = uv - vel * dt * ratio;\n    vec2 newVel = texture2D(velocity, uv2).xy;\n    gl_FragColor = vec4(newVel, 0.0, 0.0);\n  } else {\n    vec2 spot_new = uv;\n    vec2 vel_old = texture2D(velocity, uv).xy;\n    vec2 spot_old = spot_new - vel_old * dt * ratio;\n    vec2 vel_new1 = texture2D(velocity, spot_old).xy;\n    vec2 spot_new2 = spot_old + vel_new1 * dt * ratio;\n    vec2 error = spot_new2 - spot_new;\n    vec2 spot_new3 = spot_new - error / 2.0;\n    vec2 vel_2 = texture2D(velocity, spot_new3).xy;\n    vec2 spot_old2 = spot_new3 - vel_2 * dt * ratio;\n    vec2 newVel2 = texture2D(velocity, spot_old2).xy;\n    gl_FragColor = vec4(newVel2, 0.0, 0.0);\n  }\n}\n`;\n\nconst color_frag = /* glsl */ `\nprecision highp float;\nuniform sampler2D velocity;\nuniform sampler2D palette;\nuniform sampler2D uBayer;\nuniform vec4 bgColor;\nuniform float uTime;\nuniform vec2 uRes;\nuniform float uPixelSize;\n\nvarying vec2 uv;\n\nfloat hash(vec2 p) {\n  return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);\n}\n\nfloat noise(vec2 p) {\n  vec2 i = floor(p);\n  vec2 f = fract(p);\n  vec2 u = f * f * (3.0 - 2.0 * f);\n  return mix(\n    mix(hash(i), hash(i + vec2(1.0, 0.0)), u.x),\n    mix(hash(i + vec2(0.0, 1.0)), hash(i + vec2(1.0, 1.0)), u.x),\n    u.y\n  );\n}\n\nvoid main(){\n  vec2 pixGrid = uRes / uPixelSize;\n  vec2 pixUV   = (floor(uv * pixGrid) + 0.5) / pixGrid;\n\n  vec2 vel  = texture2D(velocity, pixUV).xy;\n  float len = clamp(length(vel) * 2.2, 0.0, 1.0);\n\n  vec2 bayerUV = (mod(floor(gl_FragCoord.xy), 4.0) + 0.5) / 4.0;\n  float dither  = texture2D(uBayer, bayerUV).r - 0.5;\n\n  float noiseVal = noise(uv * 6.0 + uTime * 0.15) * 0.06 - 0.03;\n\n  float t = clamp(len + dither * 0.12 + noiseVal, 0.0, 1.0);\n\n  vec3 fluidColor = texture2D(palette, vec2(t, 0.5)).rgb;\n  vec3 col        = mix(bgColor.rgb, fluidColor, t);\n\n  float grain = hash(gl_FragCoord.xy + vec2(uTime * 137.0, uTime * 91.0));\n  col += (grain - 0.5) * 0.085;\n\n  float alpha = mix(bgColor.a, 1.0, t);\n  gl_FragColor = vec4(clamp(col, 0.0, 1.0), alpha);\n}\n`;\n\nconst divergence_frag = /* glsl */ `\nprecision highp float;\nuniform sampler2D velocity;\nuniform float dt;\nuniform vec2 px;\nvarying vec2 uv;\nvoid main(){\n  float x0 = texture2D(velocity, uv - vec2(px.x, 0.0)).x;\n  float x1 = texture2D(velocity, uv + vec2(px.x, 0.0)).x;\n  float y0 = texture2D(velocity, uv - vec2(0.0, px.y)).y;\n  float y1 = texture2D(velocity, uv + vec2(0.0, px.y)).y;\n  float divergence = (x1 - x0 + y1 - y0) / 2.0;\n  gl_FragColor = vec4(divergence / dt);\n}\n`;\n\nconst externalForce_frag = /* glsl */ `\nprecision highp float;\nuniform vec2 force;\nuniform vec2 center;\nuniform vec2 scale;\nuniform vec2 px;\nvarying vec2 vUv;\nvoid main(){\n  vec2 circle = (vUv - 0.5) * 2.0;\n  float d = 1.0 - min(length(circle), 1.0);\n  d *= d;\n  gl_FragColor = vec4(force * d, 0.0, 1.0);\n}\n`;\n\nconst poisson_frag = /* glsl */ `\nprecision highp float;\nuniform sampler2D pressure;\nuniform sampler2D divergence;\nuniform vec2 px;\nvarying vec2 uv;\nvoid main(){\n  float p0 = texture2D(pressure, uv + vec2(px.x * 2.0, 0.0)).r;\n  float p1 = texture2D(pressure, uv - vec2(px.x * 2.0, 0.0)).r;\n  float p2 = texture2D(pressure, uv + vec2(0.0, px.y * 2.0)).r;\n  float p3 = texture2D(pressure, uv - vec2(0.0, px.y * 2.0)).r;\n  float div = texture2D(divergence, uv).r;\n  float newP = (p0 + p1 + p2 + p3) / 4.0 - div;\n  gl_FragColor = vec4(newP);\n}\n`;\n\nconst pressure_frag = /* glsl */ `\nprecision highp float;\nuniform sampler2D pressure;\nuniform sampler2D velocity;\nuniform vec2 px;\nuniform float dt;\nvarying vec2 uv;\nvoid main(){\n  float p0 = texture2D(pressure, uv + vec2(px.x, 0.0)).r;\n  float p1 = texture2D(pressure, uv - vec2(px.x, 0.0)).r;\n  float p2 = texture2D(pressure, uv + vec2(0.0, px.y)).r;\n  float p3 = texture2D(pressure, uv - vec2(0.0, px.y)).r;\n  vec2 v      = texture2D(velocity, uv).xy;\n  vec2 gradP  = vec2(p0 - p1, p2 - p3) * 0.5;\n  v = v - gradP * dt;\n  gl_FragColor = vec4(v, 0.0, 1.0);\n}\n`;\n\nconst viscous_frag = /* glsl */ `\nprecision highp float;\nuniform sampler2D velocity;\nuniform sampler2D velocity_new;\nuniform float v;\nuniform vec2 px;\nuniform float dt;\nvarying vec2 uv;\nvoid main(){\n  vec2 old  = texture2D(velocity, uv).xy;\n  vec2 new0 = texture2D(velocity_new, uv + vec2(px.x * 2.0, 0.0)).xy;\n  vec2 new1 = texture2D(velocity_new, uv - vec2(px.x * 2.0, 0.0)).xy;\n  vec2 new2 = texture2D(velocity_new, uv + vec2(0.0, px.y * 2.0)).xy;\n  vec2 new3 = texture2D(velocity_new, uv - vec2(0.0, px.y * 2.0)).xy;\n  vec2 newv = 4.0 * old + v * dt * (new0 + new1 + new2 + new3);\n  newv /= 4.0 * (1.0 + v * dt);\n  gl_FragColor = vec4(newv, 0.0, 0.0);\n}\n`;\n\ntype Uniforms = Record<string, { value: unknown }>;\n\nconst DEFAULT_DARK_PALETTE = [\n  \"#000000\",\n  \"#2a0020\",\n  \"#8c0f60\",\n  \"#e8227a\",\n  \"#ff85b3\",\n];\nconst DEFAULT_LIGHT_PALETTE = [\n  \"#ffffff\",\n  \"#FD96E5\",\n  \"#F36AC3\",\n  \"#FE4396\",\n  \"#ff85b3\",\n];\n\nfunction writePaletteData(data: Uint8Array, stops: string[]) {\n  const arr = stops.length === 1 ? [stops[0], stops[0]] : stops;\n  for (let i = 0; i < arr.length; i++) {\n    const c = new THREE.Color(arr[i]);\n    data[i * 4] = Math.round(c.r * 255);\n    data[i * 4 + 1] = Math.round(c.g * 255);\n    data[i * 4 + 2] = Math.round(c.b * 255);\n    data[i * 4 + 3] = 255;\n  }\n}\n\nfunction makePaletteTexture(stops: string[]): THREE.DataTexture {\n  const arr = stops.length === 1 ? [stops[0], stops[0]] : stops;\n  const w = arr.length;\n  const data = new Uint8Array(w * 4);\n  writePaletteData(data, arr);\n  const tex = new THREE.DataTexture(data, w, 1, THREE.RGBAFormat);\n  tex.magFilter = THREE.LinearFilter;\n  tex.minFilter = THREE.LinearFilter;\n  tex.wrapS = THREE.ClampToEdgeWrapping;\n  tex.wrapT = THREE.ClampToEdgeWrapping;\n  tex.generateMipmaps = false;\n  tex.needsUpdate = true;\n  return tex;\n}\n\nfunction isDarkMode() {\n  return document.documentElement.classList.contains(\"dark\");\n}\n\nfunction getBgColor(dark: boolean) {\n  return dark ? new THREE.Vector4(0, 0, 0, 0) : new THREE.Vector4(1, 1, 1, 0);\n}\n\nfunction makeBayerTexture(): THREE.DataTexture {\n  const raw = [\n    0, 136, 34, 170, 204, 68, 238, 102, 51, 187, 17, 153, 255, 119, 221, 85,\n  ];\n  const data = new Uint8Array(16 * 4);\n  for (let i = 0; i < 16; i++) {\n    data[i * 4] = raw[i];\n    data[i * 4 + 1] = raw[i];\n    data[i * 4 + 2] = raw[i];\n    data[i * 4 + 3] = 255;\n  }\n  const tex = new THREE.DataTexture(data, 4, 4, THREE.RGBAFormat);\n  tex.magFilter = THREE.NearestFilter;\n  tex.minFilter = THREE.NearestFilter;\n  tex.wrapS = THREE.RepeatWrapping;\n  tex.wrapT = THREE.RepeatWrapping;\n  tex.generateMipmaps = false;\n  tex.needsUpdate = true;\n  return tex;\n}\n\nclass CommonGL {\n  width = 1;\n  height = 1;\n  pixelRatio = 1;\n  renderer: THREE.WebGLRenderer | null = null;\n  clock: THREE.Clock | null = null;\n  time = 0;\n  delta = 0;\n  container: HTMLElement | null = null;\n\n  init(container: HTMLElement) {\n    this.container = container;\n    this.pixelRatio = 1;\n    this.resize();\n    this.renderer = new THREE.WebGLRenderer({ antialias: false, alpha: true });\n    this.renderer.autoClear = false;\n    this.renderer.setClearColor(0x000000, 0);\n    this.renderer.setPixelRatio(this.pixelRatio);\n    this.renderer.setSize(this.width, this.height, false);\n    const el = this.renderer.domElement;\n    el.style.width = \"100%\";\n    el.style.height = \"100%\";\n    el.style.display = \"block\";\n    this.clock = new THREE.Clock();\n    this.clock.start();\n  }\n\n  resize() {\n    if (!this.container) return;\n    const r = this.container.getBoundingClientRect();\n    this.width = Math.max(1, Math.floor(r.width));\n    this.height = Math.max(1, Math.floor(r.height));\n    this.renderer?.setSize(this.width, this.height, false);\n  }\n\n  update() {\n    if (!this.clock) return;\n    this.delta = this.clock.getDelta();\n    this.time += this.delta;\n  }\n}\n\nclass MouseGL {\n  coords = new THREE.Vector2();\n  coords_old = new THREE.Vector2();\n  diff = new THREE.Vector2();\n  mouseMoved = false;\n  isInside = false;\n  isAutoActive = false;\n  autoIntensity = 2.0;\n  timer: ReturnType<typeof setTimeout> | null = null;\n  container: HTMLElement | null = null;\n  onInteract: (() => void) | null = null;\n\n  private _move = this._onMove.bind(this);\n  private _leave = () => {\n    this.isInside = false;\n  };\n  private _touch = this._onTouch.bind(this);\n\n  init(container: HTMLElement) {\n    this.container = container;\n    window.addEventListener(\"mousemove\", this._move);\n    window.addEventListener(\"touchmove\", this._touch, { passive: true });\n    window.addEventListener(\"touchstart\", this._touch, { passive: true });\n    document.addEventListener(\"mouseleave\", this._leave);\n  }\n\n  dispose() {\n    window.removeEventListener(\"mousemove\", this._move);\n    window.removeEventListener(\"touchmove\", this._touch);\n    window.removeEventListener(\"touchstart\", this._touch);\n    document.removeEventListener(\"mouseleave\", this._leave);\n  }\n\n  private _onMove(e: MouseEvent) {\n    if (!this.container) return;\n    const r = this.container.getBoundingClientRect();\n    this.isInside =\n      e.clientX >= r.left &&\n      e.clientX <= r.right &&\n      e.clientY >= r.top &&\n      e.clientY <= r.bottom;\n    if (!this.isInside) return;\n    this.onInteract?.();\n    this._set(e.clientX, e.clientY);\n  }\n\n  private _onTouch(e: TouchEvent) {\n    if (e.touches.length !== 1) return;\n    const t = e.touches[0];\n    this.onInteract?.();\n    this._set(t.clientX, t.clientY);\n  }\n\n  private _set(cx: number, cy: number) {\n    if (!this.container) return;\n    if (this.timer) clearTimeout(this.timer);\n    const r = this.container.getBoundingClientRect();\n    const nx = (cx - r.left) / r.width;\n    const ny = (cy - r.top) / r.height;\n    this.coords.set(nx * 2 - 1, -(ny * 2 - 1));\n    this.mouseMoved = true;\n    this.timer = setTimeout(() => {\n      this.mouseMoved = false;\n    }, 100);\n  }\n\n  setNormalized(x: number, y: number) {\n    this.coords.set(x, y);\n    this.mouseMoved = true;\n  }\n\n  update() {\n    this.diff.subVectors(this.coords, this.coords_old);\n    this.coords_old.copy(this.coords);\n    if (this.coords_old.x === 0 && this.coords_old.y === 0) this.diff.set(0, 0);\n    if (this.isAutoActive) this.diff.multiplyScalar(this.autoIntensity);\n  }\n}\n\nclass ShaderPass {\n  scene: THREE.Scene;\n  camera: THREE.Camera;\n  material: THREE.RawShaderMaterial | null = null;\n  geometry: THREE.BufferGeometry | null = null;\n  uniforms: Uniforms;\n  output: THREE.WebGLRenderTarget | null;\n  renderer: () => THREE.WebGLRenderer | null;\n\n  constructor(\n    renderer: () => THREE.WebGLRenderer | null,\n    vertShader: string,\n    fragShader: string,\n    uniforms: Uniforms,\n    output: THREE.WebGLRenderTarget | null = null,\n  ) {\n    this.renderer = renderer;\n    this.uniforms = uniforms;\n    this.output = output;\n    this.scene = new THREE.Scene();\n    this.camera = new THREE.Camera();\n    this.material = new THREE.RawShaderMaterial({\n      vertexShader: vertShader,\n      fragmentShader: fragShader,\n      uniforms,\n    });\n    this.geometry = new THREE.PlaneGeometry(2, 2);\n    this.scene.add(new THREE.Mesh(this.geometry, this.material));\n  }\n\n  render(to: THREE.WebGLRenderTarget | null = this.output) {\n    const r = this.renderer();\n    if (!r) return;\n    r.setRenderTarget(to);\n    r.render(this.scene, this.camera);\n    r.setRenderTarget(null);\n  }\n\n  dispose() {\n    this.material?.dispose();\n    this.geometry?.dispose();\n  }\n}\n\nclass AutoDriver {\n  enabled: boolean;\n  speed: number;\n  resumeDelay: number;\n  current = new THREE.Vector2();\n  target = new THREE.Vector2();\n  lastTime = performance.now();\n  private _tmp = new THREE.Vector2();\n  private _mouse: MouseGL;\n  private _getLastInteraction: () => number;\n\n  constructor(\n    mouse: MouseGL,\n    getLastInteraction: () => number,\n    speed = 0.4,\n    resumeDelay = 1200,\n  ) {\n    this._mouse = mouse;\n    this._getLastInteraction = getLastInteraction;\n    this.speed = speed;\n    this.resumeDelay = resumeDelay;\n    this.enabled = true;\n    this._pickTarget();\n  }\n\n  private _pickTarget() {\n    this.target.set(\n      (Math.random() * 2 - 1) * 0.8,\n      (Math.random() * 2 - 1) * 0.8,\n    );\n  }\n\n  update() {\n    if (!this.enabled) return;\n    const now = performance.now();\n    const idleMs = now - this._getLastInteraction();\n    if (idleMs < this.resumeDelay) {\n      this._mouse.isAutoActive = false;\n      return;\n    }\n    this._mouse.isAutoActive = true;\n    const dt = Math.min((now - this.lastTime) / 1000, 0.05);\n    this.lastTime = now;\n    const dir = this._tmp.subVectors(this.target, this.current);\n    const dist = dir.length();\n    if (dist < 0.02) {\n      this._pickTarget();\n      return;\n    }\n    dir.normalize();\n    this.current.addScaledVector(dir, Math.min(this.speed * dt, dist));\n    this._mouse.setNormalized(this.current.x, this.current.y);\n  }\n}\n\ninterface SimOpts {\n  resolution: number;\n  mouse_force: number;\n  cursor_size: number;\n  dt: number;\n  BFECC: boolean;\n  isBounce: boolean;\n  isViscous: boolean;\n  viscous: number;\n  iterations_viscous: number;\n  iterations_poisson: number;\n}\n\nclass FluidSim {\n  opts: SimOpts;\n  fboSize = new THREE.Vector2();\n  cellScale = new THREE.Vector2();\n  boundarySpace = new THREE.Vector2();\n  fbos: Record<string, THREE.WebGLRenderTarget | null> = {};\n  gl: CommonGL;\n  mouse: MouseGL;\n\n  advection!: { pass: ShaderPass; line: THREE.LineSegments };\n  externalForce!: {\n    scene: THREE.Scene;\n    camera: THREE.Camera;\n    mesh: THREE.Mesh;\n  };\n  viscousPass!: {\n    pass: ShaderPass;\n    output0: THREE.WebGLRenderTarget | null;\n    output1: THREE.WebGLRenderTarget | null;\n  };\n  divergencePass!: ShaderPass;\n  poissonPass!: {\n    pass: ShaderPass;\n    output0: THREE.WebGLRenderTarget | null;\n    output1: THREE.WebGLRenderTarget | null;\n  };\n  pressurePass!: ShaderPass;\n\n  constructor(gl: CommonGL, mouse: MouseGL, opts: Partial<SimOpts> = {}) {\n    this.gl = gl;\n    this.mouse = mouse;\n    this.opts = {\n      resolution: 0.4,\n      mouse_force: 10,\n      cursor_size: 100,\n      dt: 0.011,\n      BFECC: true,\n      isBounce: false,\n      isViscous: false,\n      viscous: 30,\n      iterations_viscous: 32,\n      iterations_poisson: 32,\n      ...opts,\n    };\n    this._calcSize();\n    this._createFBOs();\n    this._createPasses();\n  }\n\n  private _r = () => this.gl.renderer;\n\n  private _calcSize() {\n    const w = Math.max(1, Math.round(this.opts.resolution * this.gl.width));\n    const h = Math.max(1, Math.round(this.opts.resolution * this.gl.height));\n    this.cellScale.set(1 / w, 1 / h);\n    this.fboSize.set(w, h);\n  }\n\n  private _makeFBO() {\n    return new THREE.WebGLRenderTarget(this.fboSize.x, this.fboSize.y, {\n      type: THREE.HalfFloatType,\n      depthBuffer: false,\n      stencilBuffer: false,\n      minFilter: THREE.LinearFilter,\n      magFilter: THREE.LinearFilter,\n      wrapS: THREE.ClampToEdgeWrapping,\n      wrapT: THREE.ClampToEdgeWrapping,\n    });\n  }\n\n  private _createFBOs() {\n    for (const n of [\"vel_0\", \"vel_1\", \"vel_v0\", \"vel_v1\", \"div\", \"p0\", \"p1\"])\n      this.fbos[n] = this._makeFBO();\n  }\n\n  private _createPasses() {\n    const { fbos, cellScale, fboSize, opts, _r: r } = this;\n\n    const advUniforms: Uniforms = {\n      boundarySpace: { value: cellScale },\n      px: { value: cellScale },\n      fboSize: { value: fboSize },\n      velocity: { value: fbos.vel_0!.texture },\n      dt: { value: opts.dt },\n      isBFECC: { value: true },\n    };\n    const advPass = new ShaderPass(\n      r,\n      face_vert,\n      advection_frag,\n      advUniforms,\n      fbos.vel_1,\n    );\n    const bGeo = new THREE.BufferGeometry();\n    bGeo.setAttribute(\n      \"position\",\n      new THREE.BufferAttribute(\n        new Float32Array([\n          -1, -1, 0, -1, 1, 0, -1, 1, 0, 1, 1, 0, 1, 1, 0, 1, -1, 0, 1, -1, 0,\n          -1, -1, 0,\n        ]),\n        3,\n      ),\n    );\n    const bMat = new THREE.RawShaderMaterial({\n      vertexShader: line_vert,\n      fragmentShader: advection_frag,\n      uniforms: advUniforms,\n    });\n    const bLine = new THREE.LineSegments(bGeo, bMat);\n    advPass.scene.add(bLine);\n    this.advection = { pass: advPass, line: bLine };\n\n    const efScene = new THREE.Scene();\n    const efCam = new THREE.Camera();\n    const efMesh = new THREE.Mesh(\n      new THREE.PlaneGeometry(1, 1),\n      new THREE.RawShaderMaterial({\n        vertexShader: mouse_vert,\n        fragmentShader: externalForce_frag,\n        blending: THREE.AdditiveBlending,\n        depthWrite: false,\n        uniforms: {\n          px: { value: cellScale },\n          force: { value: new THREE.Vector2() },\n          center: { value: new THREE.Vector2() },\n          scale: {\n            value: new THREE.Vector2(opts.cursor_size, opts.cursor_size),\n          },\n        },\n      }),\n    );\n    efScene.add(efMesh);\n    this.externalForce = { scene: efScene, camera: efCam, mesh: efMesh };\n\n    const viscPass = new ShaderPass(\n      r,\n      face_vert,\n      viscous_frag,\n      {\n        boundarySpace: { value: cellScale },\n        velocity: { value: fbos.vel_1!.texture },\n        velocity_new: { value: fbos.vel_v0!.texture },\n        v: { value: opts.viscous },\n        px: { value: cellScale },\n        dt: { value: opts.dt },\n      },\n      fbos.vel_v1,\n    );\n    this.viscousPass = {\n      pass: viscPass,\n      output0: fbos.vel_v0,\n      output1: fbos.vel_v1,\n    };\n\n    this.divergencePass = new ShaderPass(\n      r,\n      face_vert,\n      divergence_frag,\n      {\n        boundarySpace: { value: cellScale },\n        velocity: { value: fbos.vel_v0!.texture },\n        px: { value: cellScale },\n        dt: { value: opts.dt },\n      },\n      fbos.div,\n    );\n\n    const poisPass = new ShaderPass(\n      r,\n      face_vert,\n      poisson_frag,\n      {\n        boundarySpace: { value: cellScale },\n        pressure: { value: fbos.p0!.texture },\n        divergence: { value: fbos.div!.texture },\n        px: { value: cellScale },\n      },\n      fbos.p1,\n    );\n    this.poissonPass = { pass: poisPass, output0: fbos.p0, output1: fbos.p1 };\n\n    this.pressurePass = new ShaderPass(\n      r,\n      face_vert,\n      pressure_frag,\n      {\n        boundarySpace: { value: cellScale },\n        pressure: { value: fbos.p0!.texture },\n        velocity: { value: fbos.vel_v0!.texture },\n        px: { value: cellScale },\n        dt: { value: opts.dt },\n      },\n      fbos.vel_0,\n    );\n  }\n\n  resize() {\n    this._calcSize();\n    for (const k in this.fbos)\n      this.fbos[k]!.setSize(this.fboSize.x, this.fboSize.y);\n  }\n\n  update(time: number) {\n    const { opts, mouse, fbos } = this;\n    const r = this.gl.renderer;\n    if (!r) return;\n\n    this.boundarySpace.copy(\n      opts.isBounce ? new THREE.Vector2() : this.cellScale,\n    );\n\n    {\n      const u = this.advection.pass.uniforms;\n      u.dt.value = opts.dt;\n      u.isBFECC.value = opts.BFECC;\n      this.advection.line.visible = opts.isBounce;\n      this.advection.pass.render();\n    }\n\n    {\n      const mf = opts.mouse_force;\n      const cs = opts.cursor_size;\n      const cx = this.cellScale.x;\n      const cy = this.cellScale.y;\n      const clampedX = Math.min(\n        Math.max(mouse.coords.x, -1 + cs * cx * 2 + cx * 2),\n        1 - cs * cx * 2 - cx * 2,\n      );\n      const clampedY = Math.min(\n        Math.max(mouse.coords.y, -1 + cs * cy * 2 + cy * 2),\n        1 - cs * cy * 2 - cy * 2,\n      );\n      const u = (this.externalForce.mesh.material as THREE.RawShaderMaterial)\n        .uniforms;\n      u.force.value.set((mouse.diff.x / 2) * mf, (mouse.diff.y / 2) * mf);\n      u.center.value.set(clampedX, clampedY);\n      u.scale.value.set(cs, cs);\n      r.setRenderTarget(fbos.vel_1);\n      r.render(this.externalForce.scene, this.externalForce.camera);\n      r.setRenderTarget(null);\n    }\n\n    let velFBO: THREE.WebGLRenderTarget | null = fbos.vel_1;\n    if (opts.isViscous) {\n      const { pass, output0, output1 } = this.viscousPass;\n      const u = pass.uniforms;\n      u.v.value = opts.viscous;\n      u.dt.value = opts.dt;\n      let fbo_in = output0,\n        fbo_out = output1;\n      for (let i = 0; i < opts.iterations_viscous; i++) {\n        if (i % 2 === 0) {\n          fbo_in = output0;\n          fbo_out = output1;\n        } else {\n          fbo_in = output1;\n          fbo_out = output0;\n        }\n        u.velocity_new.value = fbo_in!.texture;\n        pass.render(fbo_out);\n      }\n      velFBO = fbo_out;\n    }\n\n    (\n      this.divergencePass.uniforms as Uniforms & {\n        velocity: { value: THREE.Texture };\n      }\n    ).velocity.value = velFBO!.texture;\n    this.divergencePass.render();\n\n    {\n      const { pass, output0, output1 } = this.poissonPass;\n      let p_in = output0,\n        p_out = output1;\n      for (let i = 0; i < opts.iterations_poisson; i++) {\n        if (i % 2 === 0) {\n          p_in = output0;\n          p_out = output1;\n        } else {\n          p_in = output1;\n          p_out = output0;\n        }\n        pass.uniforms.pressure.value = p_in!.texture;\n        pass.render(p_out);\n      }\n      this.pressurePass.uniforms.pressure.value = p_out!.texture;\n      this.pressurePass.uniforms.velocity.value = velFBO!.texture;\n    }\n\n    this.pressurePass.render();\n    void time;\n  }\n\n  dispose() {\n    for (const k in this.fbos) this.fbos[k]?.dispose();\n    this.advection.pass.dispose();\n    this.divergencePass.dispose();\n    this.pressurePass.dispose();\n  }\n}\n\nexport interface PixelLiquidBgProps extends React.ComponentProps<\"div\"> {\n  darkPalette?: string[];\n  lightPalette?: string[];\n  /** pixelation grid size in px */\n  pixelSize?: number;\n  /** sim resolution multiplier 0–1; lower = faster */\n  resolution?: number;\n  mouseForce?: number;\n  cursorSize?: number;\n  /** auto-moves fluid when idle, yields to cursor */\n  autoDemo?: boolean;\n  children?: React.ReactNode;\n}\n\nexport function PixelLiquidBg({\n  darkPalette = DEFAULT_DARK_PALETTE,\n  lightPalette = DEFAULT_LIGHT_PALETTE,\n  pixelSize = 18,\n  resolution = 0.4,\n  mouseForce = 8,\n  cursorSize = 110,\n  autoDemo = true,\n  children,\n  className,\n  ...props\n}: PixelLiquidBgProps) {\n  const mountRef = useRef<HTMLDivElement>(null);\n\n  useEffect(() => {\n    const container = mountRef.current;\n    if (!container) return;\n\n    const gl = new CommonGL();\n    gl.init(container);\n    container.prepend(gl.renderer!.domElement);\n\n    const mouse = new MouseGL();\n    mouse.init(container);\n    mouse.autoIntensity = 2.4;\n\n    const dark = isDarkMode();\n    const palette = makePaletteTexture(dark ? darkPalette : lightPalette);\n    const bayerTex = makeBayerTexture();\n\n    const sim = new FluidSim(gl, mouse, {\n      resolution,\n      mouse_force: mouseForce,\n      cursor_size: cursorSize,\n      dt: 0.008,\n      BFECC: false,\n      isBounce: false,\n      isViscous: false,\n      iterations_poisson: 8,\n    });\n\n    const outputUniforms: Uniforms = {\n      velocity: { value: sim.fbos.vel_0!.texture },\n      palette: { value: palette },\n      uBayer: { value: bayerTex },\n      bgColor: { value: getBgColor(dark) },\n      uTime: { value: 0 },\n      uRes: { value: new THREE.Vector2(gl.width, gl.height) },\n      uPixelSize: { value: pixelSize },\n      boundarySpace: { value: new THREE.Vector2() },\n      px: { value: new THREE.Vector2() },\n    };\n    const outputScene = new THREE.Scene();\n    const outputCam = new THREE.Camera();\n    const outputMesh = new THREE.Mesh(\n      new THREE.PlaneGeometry(2, 2),\n      new THREE.RawShaderMaterial({\n        vertexShader: face_vert,\n        fragmentShader: color_frag,\n        transparent: true,\n        depthWrite: false,\n        uniforms: outputUniforms,\n      }),\n    );\n    outputScene.add(outputMesh);\n\n    const themeObserver = new MutationObserver(() => {\n      const nowDark = isDarkMode();\n      const stops = nowDark ? darkPalette : lightPalette;\n      writePaletteData(palette.image.data as Uint8Array, stops);\n      palette.needsUpdate = true;\n      const bg = getBgColor(nowDark);\n      (outputUniforms.bgColor.value as THREE.Vector4).set(\n        bg.x,\n        bg.y,\n        bg.z,\n        bg.w,\n      );\n    });\n    themeObserver.observe(document.documentElement, {\n      attributes: true,\n      attributeFilter: [\"class\"],\n    });\n\n    let lastInteraction = performance.now();\n    mouse.onInteract = () => {\n      lastInteraction = performance.now();\n    };\n    const driver = autoDemo\n      ? new AutoDriver(mouse, () => lastInteraction, 0.45, 1200)\n      : null;\n\n    const handleResize = () => {\n      gl.resize();\n      sim.resize();\n      (outputUniforms.uRes.value as THREE.Vector2).set(gl.width, gl.height);\n    };\n    const ro = new ResizeObserver(handleResize);\n    ro.observe(container);\n\n    let raf = 0;\n    let running = true;\n\n    const loop = () => {\n      if (!running) return;\n      raf = requestAnimationFrame(loop);\n      driver?.update();\n      mouse.update();\n      gl.update();\n      outputUniforms.uTime.value = gl.time;\n      sim.update(gl.time);\n      const r = gl.renderer;\n      if (r) {\n        r.setRenderTarget(null);\n        r.render(outputScene, outputCam);\n      }\n    };\n    loop();\n\n    const onVisibility = () => {\n      if (document.hidden) {\n        running = false;\n        cancelAnimationFrame(raf);\n      } else {\n        running = true;\n        loop();\n      }\n    };\n    document.addEventListener(\"visibilitychange\", onVisibility);\n\n    return () => {\n      running = false;\n      cancelAnimationFrame(raf);\n      ro.disconnect();\n      themeObserver.disconnect();\n      document.removeEventListener(\"visibilitychange\", onVisibility);\n      mouse.dispose();\n      sim.dispose();\n      palette.dispose();\n      bayerTex.dispose();\n      (outputMesh.material as THREE.Material).dispose();\n      outputMesh.geometry.dispose();\n      const canvas = gl.renderer?.domElement;\n      gl.renderer?.dispose();\n      if (canvas?.parentNode) canvas.parentNode.removeChild(canvas);\n    };\n  }, [\n    darkPalette,\n    lightPalette,\n    pixelSize,\n    resolution,\n    mouseForce,\n    cursorSize,\n    autoDemo,\n  ]);\n\n  return (\n    <div\n      ref={mountRef}\n      className={cn(\n        \"relative w-full h-full overflow-hidden bg-background\",\n        className,\n      )}\n      {...props}\n    >\n      {children && (\n        <div className=\"relative z-10 w-full h-full\">{children}</div>\n      )}\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/unlumen-ui/pixel-liquid-bg.tsx"
    }
  ],
  "type": "registry:ui"
}