# Shader Lens Blur

> Interactive WebGL lens blur shader for React and shadcn/ui built on Three.js, with pointer-reactive distortion and gradients.

Source: https://www.cult-ui.com/docs/components/shader-lens-blur

## Example

```tsx title="shader-lens-blur-demo.tsx"
"use client"

import { useCallback, useId } from "react"
import { useAtom } from "jotai"
import { Circle, CircleOff, Sliders, Square, Triangle } from "lucide-react"
import { motion } from "motion/react"

import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Label } from "@/components/ui/label"
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@/components/ui/popover"
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"
import { Separator } from "@/components/ui/separator"
import { Slider } from "@/components/ui/slider"
import { Switch } from "@/components/ui/switch"

import ColorPicker from "../ui/color-picker"
import ShaderLensBlur, { configAtom } from "../ui/shader-lens-blur"

function ShaderLensBlurDemo() {
  return (
    <div className="grid grid-cols-1 gap-8">
      <ShaderLensBlur />
      <ShaderBlurConfig />
    </div>
  )
}

function ShaderBlurConfig() {
  const [config, setConfig] = useAtom(configAtom)
  const id = useId()
  const variationId = `${id}-variation`
  const enableHoverId = `${id}-enable-hover`
  const invertMouseId = `${id}-invert-mouse`
  const widthId = `${id}-width`
  const heightId = `${id}-height`

  const handleVariationChange = useCallback(
    (value: string) => {
      setConfig((prev) => ({ ...prev, variation: parseInt(value) }))
    },
    [setConfig]
  )

  const handleColorChange = useCallback(
    (key: "color1" | "color2" | "color3" | "color4", value: string) => {
      setConfig((prev) => ({ ...prev, [key]: value }))
    },
    [setConfig]
  )

  const handleDimensionChange = useCallback(
    (key: "width" | "height", value: number) => {
      setConfig((prev) => ({ ...prev, [key]: value }))
    },
    [setConfig]
  )

  const variationIcons = [
    { icon: Square, label: "Square" },
    { icon: Circle, label: "Solid Circle" },
    { icon: CircleOff, label: "Hollow Circle" },
    { icon: Triangle, label: "Triangle" },
  ]

  return (
    <Card className="border-border bg-card text-card-foreground">
      <CardHeader className="border-border border-b">
        <CardTitle className="flex items-center text-lg font-semibold">
          <Sliders className="mr-2 h-5 w-5" />
          Shader Configuration
        </CardTitle>
      </CardHeader>
      <CardContent className="space-y-6 p-6">
        <div className="grid grid-cols-1 gap-6 md:grid-cols-2">
          <div className="space-y-6">
            <div className="space-y-4">
              <Label
                htmlFor={variationId}
                className="text-muted-foreground text-sm font-medium"
              >
                Variation
              </Label>
              <Select
                value={config.variation.toString()}
                onValueChange={handleVariationChange}
              >
                <SelectTrigger
                  id={variationId}
                  className="border-border bg-background w-full"
                >
                  <SelectValue placeholder="Select variation" />
                </SelectTrigger>
                <SelectContent className="border-border bg-popover text-popover-foreground">
                  {variationIcons.map((variation, index) => (
                    <SelectItem
                      key={variation.label}
                      value={index.toString()}
                      className="cursor-pointer"
                    >
                      <div className="flex items-center space-x-2">
                        <variation.icon className="h-5 w-5" />
                        <span>- {variation.label}</span>
                      </div>
                    </SelectItem>
                  ))}
                </SelectContent>
              </Select>
            </div>

            <Separator />

            <div className="space-y-4">
              <h3 className="text-sm font-medium">Colors</h3>
              <div className="grid grid-cols-2 gap-4">
                {(["color1", "color2", "color3", "color4"] as const).map(
                  (color) => (
                    <div key={color} className="space-y-2">
                      <Label
                        htmlFor={color}
                        className="text-muted-foreground block text-sm font-medium"
                      >
                        {color}
                      </Label>
                      <Popover>
                        <PopoverTrigger asChild>
                          <motion.button
                            whileHover={{ scale: 1.05 }}
                            whileTap={{ scale: 0.95 }}
                            className="border-border focus-visible:ring-ring focus-visible:ring-offset-background h-10 w-full rounded-md border focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none"
                            style={{ backgroundColor: config[color] }}
                          />
                        </PopoverTrigger>
                        <PopoverContent className="border-border w-64 p-3">
                          <ColorPicker
                            color={config[color]}
                            onChange={(value) =>
                              handleColorChange(color, value)
                            }
                          />
                        </PopoverContent>
                      </Popover>
                    </div>
                  )
                )}
              </div>
            </div>
          </div>

          <div className="space-y-6">
            <div className="space-y-4">
              <h3 className="text-sm font-medium">Options</h3>
              <div className="flex flex-col space-y-4">
                <div className="flex items-center justify-between">
                  <Label
                    htmlFor={enableHoverId}
                    className="text-muted-foreground text-sm font-medium"
                  >
                    Enable Hover
                  </Label>
                  <Switch
                    id={enableHoverId}
                    checked={config.enableHover}
                    onCheckedChange={(checked) =>
                      setConfig((prev) => ({ ...prev, enableHover: checked }))
                    }
                  />
                </div>
                <div className="flex items-center justify-between">
                  <Label
                    htmlFor={invertMouseId}
                    className="text-muted-foreground text-sm font-medium"
                  >
                    Invert Mouse
                  </Label>
                  <Switch
                    id={invertMouseId}
                    checked={config.invertMouse}
                    onCheckedChange={(checked) =>
                      setConfig((prev) => ({ ...prev, invertMouse: checked }))
                    }
                  />
                </div>
              </div>
            </div>

            <Separator />

            <div className="space-y-4">
              <h3 className="text-sm font-medium">Dimensions</h3>
              <div className="space-y-6">
                <div className="space-y-2">
                  <div className="flex items-center justify-between">
                    <Label
                      htmlFor={widthId}
                      className="text-muted-foreground text-sm font-medium"
                    >
                      Width
                    </Label>
                    <span className="text-muted-foreground text-sm">
                      {config.width}px
                    </span>
                  </div>
                  <Slider
                    id={widthId}
                    min={100}
                    max={1000}
                    step={10}
                    value={[parseInt(config.width.toString())]}
                    onValueChange={([value]) =>
                      handleDimensionChange("width", value)
                    }
                  />
                </div>
                <div className="space-y-2">
                  <div className="flex items-center justify-between">
                    <Label
                      htmlFor={heightId}
                      className="text-muted-foreground text-sm font-medium"
                    >
                      Height
                    </Label>
                    <span className="text-muted-foreground text-sm">
                      {config.height}px
                    </span>
                  </div>
                  <Slider
                    id={heightId}
                    min={100}
                    max={1000}
                    step={10}
                    value={[parseInt(config.height.toString())]}
                    onValueChange={([value]) =>
                      handleDimensionChange("height", value)
                    }
                  />
                </div>
              </div>
            </div>
          </div>
        </div>
      </CardContent>
    </Card>
  )
}
export default ShaderLensBlurDemo
```

ShaderLensBlur is a React component for shadcn/ui built on Three.js. Pointer movement distorts its shapes and color gradients, which suits landing page heroes, product backdrops, and creative portfolio sections.

## Installation

### CLI

```bash
npx shadcn@latest add @cult-ui/shader-lens-blur
```

### Manual

**Install the required dependencies:**

```bash
npm install jotai three motion next-themes
```

**Copy and paste the following code into your project.**

```tsx title="shader-lens-blur.tsx"
"use client"

import { useCallback, useEffect, useRef, useState } from "react"
import { atom, useAtom } from "jotai"
import { motion } from "motion/react"
import { useTheme } from "next-themes"
import * as THREE from "three"

const fragmentShader = `
varying vec2 v_texcoord;

uniform vec2 u_mouse;
uniform vec2 u_resolution;
uniform float u_pixelRatio;
uniform float u_time;
uniform vec3 u_color1;
uniform vec3 u_color2;
uniform vec3 u_color3;
uniform vec3 u_color4;
uniform float u_hoverStrength;
uniform bool u_invertMouse;
uniform bool u_isDarkMode;

#define PI 3.1415926535897932384626433832795
#define TWO_PI 6.2831853071795864769252867665590

vec2 coord(in vec2 p) {
    p = p / u_resolution.xy;
    if (u_resolution.x > u_resolution.y) {
        p.x *= u_resolution.x / u_resolution.y;
        p.x += (u_resolution.y - u_resolution.x) / u_resolution.y / 2.0;
    } else {
        p.y *= u_resolution.y / u_resolution.x;
        p.y += (u_resolution.x - u_resolution.y) / u_resolution.x / 2.0;
    }
    p -= 0.5;
    p *= vec2(-1.0, 1.0);
    return p;
}

#define st0 coord(gl_FragCoord.xy)
#define mx coord(u_mouse * u_pixelRatio)

float sdRoundRect(vec2 p, vec2 b, float r) {
    vec2 d = abs(p - 0.5) * 4.2 - b + vec2(r);
    return min(max(d.x, d.y), 0.0) + length(max(d, 0.0)) - r;
}

float sdCircle(in vec2 st, in vec2 center) {
    return length(st - center) * 2.0;
}

float sdPoly(in vec2 p, in float w, in int sides) {
    float a = atan(p.x, p.y) + PI;
    float r = TWO_PI / float(sides);
    float d = cos(floor(0.5 + a / r) * r - a) * length(max(abs(p) * 1.0, 0.0));
    return d * 2.0 - w;
}

float aastep(float threshold, float value) {
    float afwidth = length(vec2(dFdx(value), dFdy(value))) * 0.70710678118654757;
    return smoothstep(threshold - afwidth, threshold + afwidth, value);
}

float fill(float x, float size, float edge) {
    return 1.0 - smoothstep(size - edge, size + edge, x);
}

float stroke(float x, float size, float w, float edge) {
    float d = smoothstep(size - edge, size + edge, x + w * 0.5) - smoothstep(size - edge, size + edge, x - w * 0.5);
    return clamp(d, 0.0, 1.0);
}

void main() {
    vec2 st = st0 + 0.5;
    vec2 posMouse = mx + 0.5;

    float size = 1.2 + sin(u_time) * 0.1;
    float roundness = 0.4 + sin(u_time * 0.5) * 0.1;
    float borderSize = 0.05 + sin(u_time * 0.7) * 0.02;
    float circleSize = 0.3 + sin(u_time * 0.8) * 0.05;
    float circleEdge = 0.5 + sin(u_time * 0.6) * 0.1;

    float sdfCircle = fill(
        sdCircle(st, posMouse),
        circleSize,
        circleEdge
    );

    float sdf;
    if (VAR == 0) {
        sdf = sdRoundRect(st, vec2(size), roundness);
        sdf = stroke(sdf, 0.0, borderSize, sdfCircle) * 4.0;
    } else if (VAR == 1) {
        sdf = sdCircle(st, vec2(0.5));
        sdf = fill(sdf, 0.6, sdfCircle) * 1.2;
    } else if (VAR == 2) {
        sdf = sdCircle(st, vec2(0.5));
        sdf = stroke(sdf, 0.58, 0.02, sdfCircle) * 4.0;
    } else if (VAR == 3) {
        sdf = sdPoly(st - vec2(0.5, 0.45), 0.3, 3);
        sdf = fill(sdf, 0.05, sdfCircle) * 1.4;
    }

    vec3 gradient = mix(
        mix(u_color1, u_color2, 0.5 + 0.5 * cos(u_time + st.x + 0.0)),
        mix(u_color3, u_color4, 0.5 + 0.5 * cos(u_time + st.y + 2.0)),
        0.5 + 0.5 * cos(u_time + st.x + st.y + 4.0)
    );

    vec3 shapeColor = sdf * gradient;

    float mouseEffect = u_invertMouse ? 1.0 - sdfCircle : sdfCircle;
    shapeColor = mix(shapeColor, vec3(1.0) - shapeColor, u_hoverStrength * mouseEffect);

    if (u_isDarkMode) {
        shapeColor = mix(shapeColor, vec3(1.0), 0.2);
    } else {
        shapeColor = mix(shapeColor, vec3(0.0), 0.1);
    }

    gl_FragColor = vec4(shapeColor, sdf);
}
`

interface ShaderConfig {
  variation: number
  color1: string
  color2: string
  color3: string
  color4: string
  enableHover: boolean
  invertMouse: boolean
  width: string
  height: string
}

const initialState: ShaderConfig = {
  variation: 3,
  color1: "#D5F981",
  color2: "#A1BBE7",
  color3: "#F2BAE2",
  color4: "#68E8FA",
  enableHover: true,
  invertMouse: true,
  width: "100%",
  height: "400px",
}

export const configAtom = atom<ShaderConfig>(initialState)

export function ShaderLensBlur() {
  const [config] = useAtom(configAtom)
  const containerRef = useRef<HTMLDivElement>(null)
  const canvasRef = useRef<HTMLCanvasElement>(null)
  const rendererRef = useRef<THREE.WebGLRenderer | null>(null)
  const sceneRef = useRef<THREE.Scene | null>(null)
  const cameraRef = useRef<THREE.OrthographicCamera | null>(null)
  const materialRef = useRef<THREE.ShaderMaterial | null>(null)
  const rafRef = useRef<number | null>(null)
  const pixelRatioRef = useRef(1)
  const [isInteracting, setIsInteracting] = useState(false)
  const { theme } = useTheme()

  const updateSize = useCallback(() => {
    if (
      !containerRef.current ||
      !canvasRef.current ||
      !rendererRef.current ||
      !cameraRef.current ||
      !materialRef.current
    )
      return

    const { clientWidth: w, clientHeight: h } = containerRef.current
    const aspect = w / h

    cameraRef.current.left = -aspect
    cameraRef.current.right = aspect
    cameraRef.current.top = 1
    cameraRef.current.bottom = -1
    cameraRef.current.updateProjectionMatrix()

    const pixelRatio = Math.min(window.devicePixelRatio || 1, 2)
    pixelRatioRef.current = pixelRatio

    rendererRef.current.setPixelRatio(pixelRatio)
    rendererRef.current.setSize(w, h)

    const drawingBufferSize = rendererRef.current.getDrawingBufferSize(
      new THREE.Vector2()
    )
    materialRef.current.uniforms.u_resolution.value.copy(drawingBufferSize)
    materialRef.current.uniforms.u_pixelRatio.value = pixelRatio
  }, [])

  const updateMousePosition = useCallback(
    (x: number, y: number) => {
      if (!containerRef.current || !materialRef.current) return
      const rect = containerRef.current.getBoundingClientRect()
      const mouseX = x - rect.left
      const mouseY = y - rect.top
      if (isInteracting || config.enableHover) {
        materialRef.current.uniforms.u_mouse.value.set(
          mouseX,
          rect.height - mouseY
        )
      }
    },
    [isInteracting, config.enableHover]
  )

  const animate = useCallback(
    (time: number) => {
      if (
        !rendererRef.current ||
        !sceneRef.current ||
        !cameraRef.current ||
        !materialRef.current
      )
        return

      const pixelRatio = Math.min(window.devicePixelRatio || 1, 2)
      if (pixelRatio !== pixelRatioRef.current) {
        updateSize()
      }

      materialRef.current.uniforms.u_time.value = time * 0.001
      materialRef.current.uniforms.u_hoverStrength.value =
        isInteracting || config.enableHover ? 0.3 : 0

      rendererRef.current.render(sceneRef.current, cameraRef.current)
      rafRef.current = requestAnimationFrame(animate)
    },
    [config.enableHover, isInteracting, updateSize]
  )

  useEffect(() => {
    if (!containerRef.current || !canvasRef.current) return

    const scene = new THREE.Scene()
    sceneRef.current = scene

    const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0.1, 1000)
    camera.position.z = 1
    cameraRef.current = camera

    const renderer = new THREE.WebGLRenderer({
      canvas: canvasRef.current,
      antialias: true,
      alpha: true,
    })
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
    renderer.setClearColor(0x000000, 0)
    rendererRef.current = renderer

    const geometry = new THREE.PlaneGeometry(2, 2)
    const material = new THREE.ShaderMaterial({
      vertexShader: `
        varying vec2 v_texcoord;
        void main() {
          gl_Position = vec4(position, 1.0);
          v_texcoord = uv;
        }
      `,
      fragmentShader,
      uniforms: {
        u_mouse: { value: new THREE.Vector2() },
        u_resolution: { value: new THREE.Vector2() },
        u_pixelRatio: { value: Math.min(window.devicePixelRatio, 2) },
        u_time: { value: 0 },
        u_color1: { value: new THREE.Color(config.color1) },
        u_color2: { value: new THREE.Color(config.color2) },
        u_color3: { value: new THREE.Color(config.color3) },
        u_color4: { value: new THREE.Color(config.color4) },
        u_hoverStrength: { value: 0 },
        u_invertMouse: { value: config.invertMouse },
        u_isDarkMode: { value: theme === "dark" },
      },
      defines: {
        VAR: config.variation,
      },
      transparent: true,
      blending: THREE.NormalBlending,
    })
    materialRef.current = material

    const mesh = new THREE.Mesh(geometry, material)
    scene.add(mesh)

    updateSize()
    rafRef.current = requestAnimationFrame(animate)

    const resizeObserver = new ResizeObserver(updateSize)
    resizeObserver.observe(containerRef.current)

    return () => {
      if (rafRef.current) cancelAnimationFrame(rafRef.current)
      if (rendererRef.current) rendererRef.current.dispose()
      if (containerRef.current) resizeObserver.unobserve(containerRef.current)
    }
  }, [config, updateSize, animate, theme])

  useEffect(() => {
    const container = containerRef.current
    if (!container) return

    const handlePointerMove = (event: PointerEvent) =>
      updateMousePosition(event.clientX, event.clientY)
    const handleTouchMove = (event: TouchEvent) => {
      if (event.touches.length > 0) {
        const touch = event.touches[0]
        updateMousePosition(touch.clientX, touch.clientY)
      }
    }
    const handlePointerDown = () => setIsInteracting(true)
    const handlePointerUp = () => setIsInteracting(false)
    const handleTouchStart = () => setIsInteracting(true)
    const handleTouchEnd = () => setIsInteracting(false)

    container.addEventListener("pointermove", handlePointerMove)
    container.addEventListener("touchmove", handleTouchMove)
    container.addEventListener("pointerdown", handlePointerDown)
    container.addEventListener("pointerup", handlePointerUp)
    container.addEventListener("touchstart", handleTouchStart)
    container.addEventListener("touchend", handleTouchEnd)

    return () => {
      container.removeEventListener("pointermove", handlePointerMove)
      container.removeEventListener("touchmove", handleTouchMove)
      container.removeEventListener("pointerdown", handlePointerDown)
      container.removeEventListener("pointerup", handlePointerUp)
      container.removeEventListener("touchstart", handleTouchStart)
      container.removeEventListener("touchend", handleTouchEnd)
    }
  }, [updateMousePosition])

  useEffect(() => {
    if (materialRef.current) {
      materialRef.current.uniforms.u_color1.value.set(config.color1)
      materialRef.current.uniforms.u_color2.value.set(config.color2)
      materialRef.current.uniforms.u_color3.value.set(config.color3)
      materialRef.current.uniforms.u_color4.value.set(config.color4)
      materialRef.current.uniforms.u_invertMouse.value = config.invertMouse
      materialRef.current.uniforms.u_isDarkMode.value = theme === "dark"
      materialRef.current.defines.VAR = config.variation
      materialRef.current.needsUpdate = true
    }
  }, [config, theme])

  return (
    <motion.div
      ref={containerRef}
      initial={{ opacity: 0 }}
      animate={{ opacity: 1 }}
      transition={{ duration: 0.5 }}
      className="relative"
      style={{
        width: config.width,
        height: config.height,
        backgroundColor: "black",
        borderRadius: "8px",
      }}
    >
      <canvas ref={canvasRef} className="h-full w-full touch-none" />
      <div
        className={`absolute bottom-4 left-4 text-sm ${
          theme === "dark" ? "text-neutral-300" : "text-neutral-200"
        }`}
      >
        {config.enableHover
          ? `${
              config.invertMouse ? "Inverted mouse" : "Normal mouse"
            } interaction`
          : "Interact with the animation"}{" "}
        using mouse or touch
      </div>
    </motion.div>
  )
}

export default ShaderLensBlur
// "use client"

// // npm install jotai three
// import React, { useCallback, useEffect, useRef, useState } from "react"
// import { motion } from "motion/react"
// import { atom, useAtom } from "jotai"
// import * as THREE from "three"

// const fragmentShader = `
// varying vec2 v_texcoord;

// uniform vec2 u_mouse;
// uniform vec2 u_resolution;
// uniform float u_pixelRatio;
// uniform float u_time;
// uniform vec3 u_color1;
// uniform vec3 u_color2;
// uniform vec3 u_color3;
// uniform vec3 u_color4;
// uniform float u_hoverStrength;
// uniform bool u_invertMouse;

// #define PI 3.1415926535897932384626433832795
// #define TWO_PI 6.2831853071795864769252867665590

// vec2 coord(in vec2 p) {
//     p = p / u_resolution.xy;
//     if (u_resolution.x > u_resolution.y) {
//         p.x *= u_resolution.x / u_resolution.y;
//         p.x += (u_resolution.y - u_resolution.x) / u_resolution.y / 2.0;
//     } else {
//         p.y *= u_resolution.y / u_resolution.x;
//         p.y += (u_resolution.x - u_resolution.y) / u_resolution.x / 2.0;
//     }
//     p -= 0.5;
//     p *= vec2(-1.0, 1.0);
//     return p;
// }

// #define st0 coord(gl_FragCoord.xy)
// #define mx coord(u_mouse)

// float sdRoundRect(vec2 p, vec2 b, float r) {
//     vec2 d = abs(p - 0.5) * 4.2 - b + vec2(r);
//     return min(max(d.x, d.y), 0.0) + length(max(d, 0.0)) - r;
// }

// float sdCircle(in vec2 st, in vec2 center) {
//     return length(st - center) * 2.0;
// }

// float sdPoly(in vec2 p, in float w, in int sides) {
//     float a = atan(p.x, p.y) + PI;
//     float r = TWO_PI / float(sides);
//     float d = cos(floor(0.5 + a / r) * r - a) * length(max(abs(p) * 1.0, 0.0));
//     return d * 2.0 - w;
// }

// float aastep(float threshold, float value) {
//     float afwidth = length(vec2(dFdx(value), dFdy(value))) * 0.70710678118654757;
//     return smoothstep(threshold - afwidth, threshold + afwidth, value);
// }

// float fill(float x, float size, float edge) {
//     return 1.0 - smoothstep(size - edge, size + edge, x);
// }

// float stroke(float x, float size, float w, float edge) {
//     float d = smoothstep(size - edge, size + edge, x + w * 0.5) - smoothstep(size - edge, size + edge, x - w * 0.5);
//     return clamp(d, 0.0, 1.0);
// }

// void main() {
//     vec2 st = st0 + 0.5;
//     vec2 posMouse = mx + 0.5;

//     float size = 1.2 + sin(u_time) * 0.1;
//     float roundness = 0.4 + sin(u_time * 0.5) * 0.1;
//     float borderSize = 0.05 + sin(u_time * 0.7) * 0.02;
//     float circleSize = 0.3 + sin(u_time * 0.8) * 0.05;
//     float circleEdge = 0.5 + sin(u_time * 0.6) * 0.1;

//     float sdfCircle = fill(
//         sdCircle(st, posMouse),
//         circleSize,
//         circleEdge
//     );

//     float sdf;
//     if (VAR == 0) {
//         sdf = sdRoundRect(st, vec2(size), roundness);
//         sdf = stroke(sdf, 0.0, borderSize, sdfCircle) * 4.0;
//     } else if (VAR == 1) {
//         sdf = sdCircle(st, vec2(0.5));
//         sdf = fill(sdf, 0.6, sdfCircle) * 1.2;
//     } else if (VAR == 2) {
//         sdf = sdCircle(st, vec2(0.5));
//         sdf = stroke(sdf, 0.58, 0.02, sdfCircle) * 4.0;
//     } else if (VAR == 3) {
//         sdf = sdPoly(st - vec2(0.5, 0.45), 0.3, 3);
//         sdf = fill(sdf, 0.05, sdfCircle) * 1.4;
//     }

//     vec3 gradient = mix(
//         mix(u_color1, u_color2, 0.5 + 0.5 * cos(u_time + st.x + 0.0)),
//         mix(u_color3, u_color4, 0.5 + 0.5 * cos(u_time + st.y + 2.0)),
//         0.5 + 0.5 * cos(u_time + st.x + st.y + 4.0)
//     );

//     vec3 shapeColor = sdf * gradient;

//     float mouseEffect = u_invertMouse ? 1.0 - sdfCircle : sdfCircle;
//     shapeColor = mix(shapeColor, vec3(1.0) - shapeColor, u_hoverStrength * mouseEffect);

//     gl_FragColor = vec4(shapeColor, sdf);
// }
// `

// interface ShaderConfig {
//   variation: number
//   color1: string
//   color2: string
//   color3: string
//   color4: string
//   enableHover: boolean
//   invertMouse: boolean
//   width: string
//   height: string
// }

// const initialState: ShaderConfig = {
//   variation: 0,
//   color1: "#ff0000",
//   color2: "#00ff00",
//   color3: "#0000ff",
//   color4: "#ffff00",
//   enableHover: true,
//   invertMouse: true,
//   width: "100%",
//   height: "400px",
// }

// export const configAtom = atom<ShaderConfig>(initialState)

// export function ShaderLensBlur() {
//   const [config] = useAtom(configAtom)
//   const containerRef = useRef<HTMLDivElement>(null)
//   const canvasRef = useRef<HTMLCanvasElement>(null)
//   const rendererRef = useRef<THREE.WebGLRenderer | null>(null)
//   const sceneRef = useRef<THREE.Scene | null>(null)
//   const cameraRef = useRef<THREE.OrthographicCamera | null>(null)
//   const materialRef = useRef<THREE.ShaderMaterial | null>(null)
//   const rafRef = useRef<number | null>(null)
//   const [isInteracting, setIsInteracting] = useState(false)

//   const updateSize = useCallback(() => {
//     if (
//       !containerRef.current ||
//       !canvasRef.current ||
//       !rendererRef.current ||
//       !cameraRef.current ||
//       !materialRef.current
//     )
//       return

//     const { clientWidth: w, clientHeight: h } = containerRef.current
//     const aspect = w / h

//     cameraRef.current.left = -aspect
//     cameraRef.current.right = aspect
//     cameraRef.current.top = 1
//     cameraRef.current.bottom = -1
//     cameraRef.current.updateProjectionMatrix()

//     rendererRef.current.setSize(w, h)
//     materialRef.current.uniforms.u_resolution.value.set(w, h)
//   }, [])

//   const updateMousePosition = useCallback(
//     (x: number, y: number) => {
//       if (!containerRef.current || !materialRef.current) return
//       const rect = containerRef.current.getBoundingClientRect()
//       const mouseX = x - rect.left
//       const mouseY = y - rect.top
//       if (isInteracting || config.enableHover) {
//         materialRef.current.uniforms.u_mouse.value.set(
//           mouseX,
//           rect.height - mouseY
//         )
//       }
//     },
//     [isInteracting, config.enableHover]
//   )

//   const animate = useCallback(
//     (time: number) => {
//       if (
//         !rendererRef.current ||
//         !sceneRef.current ||
//         !cameraRef.current ||
//         !materialRef.current
//       )
//         return

//       materialRef.current.uniforms.u_time.value = time * 0.001
//       materialRef.current.uniforms.u_hoverStrength.value =
//         isInteracting || config.enableHover ? 0.3 : 0

//       rendererRef.current.render(sceneRef.current, cameraRef.current)
//       rafRef.current = requestAnimationFrame(animate)
//     },
//     [config.enableHover, isInteracting]
//   )

//   useEffect(() => {
//     if (!containerRef.current || !canvasRef.current) return

//     const scene = new THREE.Scene()
//     sceneRef.current = scene

//     const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0.1, 1000)
//     camera.position.z = 1
//     cameraRef.current = camera

//     const renderer = new THREE.WebGLRenderer({
//       canvas: canvasRef.current,
//       antialias: true,
//       alpha: true,
//     })
//     renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
//     renderer.setClearColor(0x000000, 0)
//     rendererRef.current = renderer

//     const geometry = new THREE.PlaneGeometry(2, 2)
//     const material = new THREE.ShaderMaterial({
//       vertexShader: `
//         varying vec2 v_texcoord;
//         void main() {
//           gl_Position = vec4(position, 1.0);
//           v_texcoord = uv;
//         }
//       `,
//       fragmentShader,
//       uniforms: {
//         u_mouse: { value: new THREE.Vector2() },
//         u_resolution: { value: new THREE.Vector2() },
//         u_pixelRatio: { value: Math.min(window.devicePixelRatio, 2) },
//         u_time: { value: 0 },
//         u_color1: { value: new THREE.Color(config.color1) },
//         u_color2: { value: new THREE.Color(config.color2) },
//         u_color3: { value: new THREE.Color(config.color3) },
//         u_color4: { value: new THREE.Color(config.color4) },
//         u_hoverStrength: { value: 0 },
//         u_invertMouse: { value: config.invertMouse },
//       },
//       defines: {
//         VAR: config.variation,
//       },
//       transparent: true,
//       blending: THREE.NormalBlending,
//     })
//     materialRef.current = material

//     const mesh = new THREE.Mesh(geometry, material)
//     scene.add(mesh)

//     updateSize()
//     rafRef.current = requestAnimationFrame(animate)

//     const resizeObserver = new ResizeObserver(updateSize)
//     resizeObserver.observe(containerRef.current)

//     return () => {
//       if (rafRef.current) cancelAnimationFrame(rafRef.current)
//       if (rendererRef.current) rendererRef.current.dispose()
//       if (containerRef.current) resizeObserver.unobserve(containerRef.current)
//     }
//   }, [config, updateSize, animate])

//   useEffect(() => {
//     const container = containerRef.current
//     if (!container) return

//     const handlePointerMove = (event: PointerEvent) =>
//       updateMousePosition(event.clientX, event.clientY)
//     const handleTouchMove = (event: TouchEvent) => {
//       if (event.touches.length > 0) {
//         const touch = event.touches[0]
//         updateMousePosition(touch.clientX, touch.clientY)
//       }
//     }

//     container.addEventListener("pointermove", handlePointerMove)
//     container.addEventListener("touchmove", handleTouchMove)
//     container.addEventListener("pointerdown", () => setIsInteracting(true))
//     container.addEventListener("pointerup", () => setIsInteracting(false))
//     container.addEventListener("touchstart", () => setIsInteracting(true))
//     container.addEventListener("touchend", () => setIsInteracting(false))

//     return () => {
//       container.removeEventListener("pointermove", handlePointerMove)
//       container.removeEventListener("touchmove", handleTouchMove)
//       container.removeEventListener("pointerdown", () => setIsInteracting(true))
//       container.removeEventListener("pointerup", () => setIsInteracting(false))
//       container.removeEventListener("touchstart", () => setIsInteracting(true))
//       container.removeEventListener("touchend", () => setIsInteracting(false))
//     }
//   }, [updateMousePosition])

//   useEffect(() => {
//     if (materialRef.current) {
//       materialRef.current.uniforms.u_color1.value.set(config.color1)
//       materialRef.current.uniforms.u_color2.value.set(config.color2)
//       materialRef.current.uniforms.u_color3.value.set(config.color3)
//       materialRef.current.uniforms.u_color4.value.set(config.color4)
//       materialRef.current.uniforms.u_invertMouse.value = config.invertMouse
//       materialRef.current.defines.VAR = config.variation
//       materialRef.current.needsUpdate = true
//     }
//   }, [config])

//   return (
//     <motion.div
//       ref={containerRef}
//       initial={{ opacity: 0 }}
//       animate={{ opacity: 1 }}
//       transition={{ duration: 0.5 }}
//       className="relative"
//       style={{
//         width: config.width,
//         height: config.height,
//         backgroundColor: "transparent",
//         borderRadius: "8px",
//       }}
//     >
//       <canvas ref={canvasRef} className="w-full h-full touch-none" />
//       <div className="absolute bottom-4 left-4 text-white text-sm">
//         {config.enableHover
//           ? `${
//               config.invertMouse ? "Inverted mouse" : "Normal mouse"
//             } interaction`
//           : "Interact with the animation"}{" "}
//         using mouse or touch
//       </div>
//     </motion.div>
//   )
// }

// export default ShaderLensBlur
```

**Update the import paths to match your project setup.**

## Usage

```tsx
import ShaderLensBlur from "@/components/ui/shader-lens-blur";
```

### Basic

Render the default interactive lens blur canvas:

```tsx
<div className="w-full max-w-2xl">
  <ShaderLensBlur />
</div>
```

### Programmatic Configuration

`ShaderLensBlur` reads its configuration from the exported `configAtom`. This makes it easy to drive from your own controls, presets, or app state.

```tsx
"use client";

import { useAtom } from "jotai";
import ShaderLensBlur, { configAtom } from "@/components/ui/shader-lens-blur";

export function ShaderLensBlurControlled() {
  const [config, setConfig] = useAtom(configAtom);

  return (
    <div className="space-y-4">
      <ShaderLensBlur />
      <button
        className="rounded-md border px-3 py-2 text-sm"
        onClick={() =>
          setConfig((prev) => ({
            ...prev,
            variation: (prev.variation + 1) % 4,
            invertMouse: !prev.invertMouse,
          }))
        }
      >
        Toggle Preset
      </button>
      <p className="text-sm text-muted-foreground">
        Current variation: {config.variation}
      </p>
    </div>
  );
}
```

### Set Explicit Dimensions

The component supports custom canvas dimensions through the same atom config:

```tsx
setConfig((prev) => ({
  ...prev,
  width: "100%",
  height: "520px",
}));
```

## API Reference

### Exports

| Export | Type | Description |
| --- | --- | --- |
| `ShaderLensBlur` | `React.ComponentType` | Main interactive shader component |
| `configAtom` | `Atom<ShaderConfig>` | Shared state for colors, variation, interaction mode, and dimensions |

### `ShaderConfig` Shape

| Key | Type | Default | Description |
| --- | --- | --- | --- |
| `variation` | `number` | `3` | Shape mode (`0` square-ish frame, `1` solid circle, `2` ring, `3` triangle) |
| `color1` | `string` | `#D5F981` | Gradient color stop 1 |
| `color2` | `string` | `#A1BBE7` | Gradient color stop 2 |
| `color3` | `string` | `#F2BAE2` | Gradient color stop 3 |
| `color4` | `string` | `#68E8FA` | Gradient color stop 4 |
| `enableHover` | `boolean` | `true` | Enables pointer tracking without requiring press/drag |
| `invertMouse` | `boolean` | `true` | Inverts the pointer influence mask |
| `width` | `string` | `"100%"` | Container width style |
| `height` | `string` | `"400px"` | Container height style |

## Behavior Notes

- Uses a `THREE.WebGLRenderer` with an orthographic full-screen quad and custom fragment shader.
- Pointer input is translated from container-space into shader-space and corrected for device pixel ratio.
- Shader colors and variation update live when `configAtom` changes.
- Theme awareness is built in (`next-themes`) and influences subtle color mixing behavior in the shader.

## Accessibility & UX Tips

- Provide a visible parent label or heading so users understand what interaction is expected.
- Keep height at least `280px` for comfortable pointer/touch interaction.
- For touch-first contexts, consider `enableHover: false` so effects primarily respond during active touch/press.

## Troubleshooting

### Blur hotspot feels offset from pointer

- Ensure you are using the latest component version (includes DPR-correct pointer mapping).
- Avoid additional CSS transforms (for example `scale(...)`) on the canvas container.
- If wrapping inside animated layout components, verify the container reports stable bounds (`getBoundingClientRect`).

### Nothing renders

- Confirm WebGL is available in the browser/environment.
- Confirm required dependencies are installed and import paths were updated.
- If using theme features, ensure your app is wrapped with `ThemeProvider` from `next-themes`.

## References

<Citations>
  <CitationTitle>Inspiration</CitationTitle>
  <CitationList>
    <CitationItem>
      <CitationLink
        href="https://github.com/guilanier/codrops-sdf-lensblur"
        target="_blank"
        rel="noopener noreferrer"
      >
        codrops-sdf-lensblur
      </CitationLink>
    </CitationItem>
  </CitationList>
</Citations>
