{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "evil-eye-button",
  "type": "registry:ui",
  "title": "EvilEyeButton",
  "description": "An obsidian pill with a burning evil eye behind its label that tracks the cursor across the page, focuses on hover, and blinks.",
  "files": [
    {
      "path": "components/evil-buttons/evil-eye-button.tsx",
      "type": "registry:ui",
      "target": "components/evil-buttons/evil-eye-button.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface EvilEyeButtonProps\n  extends React.ButtonHTMLAttributes<HTMLButtonElement> {\n  /** Label used when no children are provided. */\n  label?: React.ReactNode;\n  /** Iris and glow color, as a hex string. */\n  eyeColor?: string;\n  /** Brightness multiplier for the flames. */\n  intensity?: number;\n  /** Size of the slit pupil. Lower values make it tighter. */\n  pupilSize?: number;\n  /** Width of the iris ring. */\n  irisWidth?: number;\n  /** Strength of the glow around the iris. */\n  glowIntensity?: number;\n  /** Zoom of the eye inside the button. Lower values zoom in. */\n  scale?: number;\n  /** Scale of the flame noise. */\n  noiseScale?: number;\n  /** How far the pupil tracks the cursor, 0 to 1. */\n  pupilFollow?: number;\n  /** Speed of the flames licking around the iris. */\n  flameSpeed?: number;\n  /** Blink every few seconds on its own, as well as on press. */\n  blink?: boolean;\n  /** Extra classes for the eye layer behind the label. */\n  eyeClassName?: string;\n  /** Extra classes for the label. */\n  labelClassName?: string;\n}\n\n/* ---------- Shader ---------- */\n\nconst VERTEX = `\nattribute vec2 aPosition;\nvoid main() {\n  gl_Position = vec4(aPosition, 0.0, 1.0);\n}\n`;\n\n// The React Bits evil eye: polar-mapped noise makes flames around an iris\n// ring, with a vertical slit pupil. uFocus narrows the slit and stokes the\n// flames while hovered.\nconst FRAGMENT = `\nprecision highp float;\nuniform float uTime;\nuniform vec2 uRes;\nuniform sampler2D uNoise;\nuniform float uPupilSize;\nuniform float uIrisWidth;\nuniform float uGlow;\nuniform float uIntensity;\nuniform float uScale;\nuniform float uNoiseScale;\nuniform vec2 uLook;\nuniform float uFocus;\nuniform vec3 uEyeColor;\nuniform vec3 uBgColor;\n\nvoid main() {\n  vec2 uv = (gl_FragCoord.xy * 2.0 - uRes) / uRes.y;\n  uv /= uScale;\n  float ft = uTime;\n\n  vec2 polarUv = vec2(length(uv) * 2.0, (2.0 * atan(uv.x, uv.y)) / 6.28 * 0.3);\n  vec4 noiseA = texture2D(uNoise, polarUv * vec2(0.2, 7.0) * uNoiseScale + vec2(-ft * 0.1, 0.0));\n  vec4 noiseB = texture2D(uNoise, polarUv * vec2(0.3, 4.0) * uNoiseScale + vec2(-ft * 0.2, 0.0));\n  vec4 noiseC = texture2D(uNoise, polarUv * vec2(0.1, 5.0) * uNoiseScale + vec2(-ft * 0.1, 0.0));\n\n  float distanceMask = 1.0 - length(uv);\n\n  float innerRing = clamp(-1.0 * ((distanceMask - 0.7) / uIrisWidth), 0.0, 1.0);\n  innerRing = (innerRing * distanceMask - 0.2) / 0.28;\n  innerRing += noiseA.r - 0.5;\n  innerRing *= 1.3;\n  innerRing = clamp(innerRing, 0.0, 1.0);\n\n  float outerRing = clamp(-1.0 * ((distanceMask - 0.5) / 0.2), 0.0, 1.0);\n  outerRing = (outerRing * distanceMask - 0.1) / 0.38;\n  outerRing += noiseC.r - 0.5;\n  outerRing *= 1.3;\n  outerRing = clamp(outerRing, 0.0, 1.0);\n\n  innerRing += outerRing;\n\n  float innerEye = distanceMask - 0.2;\n  innerEye *= noiseB.r * 2.0;\n\n  vec2 pupilUv = uv - uLook * 0.32;\n  float slit = mix(9.0, 14.0, uFocus);\n  float pupil = 1.0 - length(pupilUv * vec2(slit, 2.3));\n  pupil *= uPupilSize;\n  pupil = clamp(pupil, 0.0, 1.0);\n  pupil /= 0.35;\n\n  float outerEyeGlow = 1.0 - length(uv * vec2(0.5, 1.5));\n  outerEyeGlow = clamp(outerEyeGlow + 0.5, 0.0, 1.0);\n  outerEyeGlow += noiseC.r - 0.5;\n  float outerBgGlow = outerEyeGlow;\n  outerEyeGlow = pow(outerEyeGlow, 2.0);\n  outerEyeGlow += distanceMask;\n  outerEyeGlow *= uGlow;\n  outerEyeGlow = clamp(outerEyeGlow, 0.0, 1.0);\n  outerEyeGlow *= pow(1.0 - distanceMask, 2.0) * 2.5;\n\n  outerBgGlow += distanceMask;\n  outerBgGlow = pow(outerBgGlow, 0.5);\n  outerBgGlow *= 0.15;\n\n  float lit = clamp(max(innerRing + innerEye, outerEyeGlow + outerBgGlow) - pupil, 0.0, 3.0);\n  vec3 color = uEyeColor * uIntensity * (1.0 + uFocus * 0.12) * lit;\n  gl_FragColor = vec4(color + uBgColor, 1.0);\n}\n`;\n\n/** Tileable fractal value noise, built once and shared by every eye. */\nlet noiseCache: Uint8Array | null = null;\n\nfunction noiseTexture(size = 256) {\n  if (noiseCache) return noiseCache;\n  const data = new Uint8Array(size * size * 4);\n  const hash = (x: number, y: number, s: number) => {\n    let n = x * 374761393 + y * 668265263 + s * 1274126177;\n    n = Math.imul(n ^ (n >>> 13), 1274126177);\n    return ((n ^ (n >>> 16)) >>> 0) / 4294967296;\n  };\n  const noise = (px: number, py: number, freq: number, seed: number) => {\n    const fx = (px / size) * freq;\n    const fy = (py / size) * freq;\n    const ix = Math.floor(fx);\n    const iy = Math.floor(fy);\n    const tx = fx - ix;\n    const ty = fy - iy;\n    const w = freq | 0;\n    const wrap = (v: number) => ((v % w) + w) % w;\n    const v00 = hash(wrap(ix), wrap(iy), seed);\n    const v10 = hash(wrap(ix + 1), wrap(iy), seed);\n    const v01 = hash(wrap(ix), wrap(iy + 1), seed);\n    const v11 = hash(wrap(ix + 1), wrap(iy + 1), seed);\n    return (\n      v00 * (1 - tx) * (1 - ty) +\n      v10 * tx * (1 - ty) +\n      v01 * (1 - tx) * ty +\n      v11 * tx * ty\n    );\n  };\n  for (let y = 0; y < size; y++) {\n    for (let x = 0; x < size; x++) {\n      let v = 0;\n      let amp = 0.4;\n      let total = 0;\n      for (let o = 0; o < 8; o++) {\n        v += amp * noise(x, y, 32 * (1 << o), o * 31);\n        total += amp;\n        amp *= 0.65;\n      }\n      v = Math.max(0, Math.min(1, (v / total - 0.5) * 2.2 + 0.5));\n      const i = (y * size + x) * 4;\n      data[i] = data[i + 1] = data[i + 2] = Math.round(v * 255);\n      data[i + 3] = 255;\n    }\n  }\n  noiseCache = data;\n  return data;\n}\n\nfunction hexToRgb(hex: string): [number, number, number] {\n  const h = hex.replace(\"#\", \"\");\n  const full = h.length === 3 ? h.replace(/./g, \"$&$&\") : h;\n  const n = parseInt(full.slice(0, 6), 16) || 0;\n  return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255];\n}\n\nconst UNIFORMS = [\n  \"uTime\",\n  \"uRes\",\n  \"uNoise\",\n  \"uPupilSize\",\n  \"uIrisWidth\",\n  \"uGlow\",\n  \"uIntensity\",\n  \"uScale\",\n  \"uNoiseScale\",\n  \"uLook\",\n  \"uFocus\",\n  \"uEyeColor\",\n  \"uBgColor\",\n] as const;\n\ntype EyeGL = {\n  gl: WebGLRenderingContext;\n  u: Record<(typeof UNIFORMS)[number], WebGLUniformLocation | null>;\n};\n\nfunction createEyeGL(canvas: HTMLCanvasElement): EyeGL | null {\n  const gl = canvas.getContext(\"webgl\", {\n    alpha: false,\n    antialias: false,\n    powerPreference: \"low-power\",\n  });\n  if (!gl) return null;\n\n  const compile = (type: number, source: string) => {\n    const shader = gl.createShader(type)!;\n    gl.shaderSource(shader, source);\n    gl.compileShader(shader);\n    return gl.getShaderParameter(shader, gl.COMPILE_STATUS) ? shader : null;\n  };\n  const vs = compile(gl.VERTEX_SHADER, VERTEX);\n  const fs = compile(gl.FRAGMENT_SHADER, FRAGMENT);\n  if (!vs || !fs) return null;\n  const program = gl.createProgram()!;\n  gl.attachShader(program, vs);\n  gl.attachShader(program, fs);\n  gl.linkProgram(program);\n  if (!gl.getProgramParameter(program, gl.LINK_STATUS)) return null;\n  gl.useProgram(program);\n\n  gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());\n  gl.bufferData(\n    gl.ARRAY_BUFFER,\n    new Float32Array([-1, -1, 3, -1, -1, 3]),\n    gl.STATIC_DRAW,\n  );\n  const position = gl.getAttribLocation(program, \"aPosition\");\n  gl.enableVertexAttribArray(position);\n  gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0);\n\n  const texture = gl.createTexture();\n  gl.bindTexture(gl.TEXTURE_2D, texture);\n  gl.texImage2D(\n    gl.TEXTURE_2D,\n    0,\n    gl.RGBA,\n    256,\n    256,\n    0,\n    gl.RGBA,\n    gl.UNSIGNED_BYTE,\n    noiseTexture(256),\n  );\n  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);\n  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);\n  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);\n  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);\n\n  const u = Object.fromEntries(\n    UNIFORMS.map((name) => [name, gl.getUniformLocation(program, name)]),\n  ) as EyeGL[\"u\"];\n  gl.uniform1i(u.uNoise, 0);\n  return { gl, u };\n}\n\n/* ---------- Component ---------- */\n\n/** The face color; the shader paints it behind the eye so the socket blends in. */\nconst FACE = \"#0c0706\";\n\nexport const EvilEyeButton = React.forwardRef<\n  HTMLButtonElement,\n  EvilEyeButtonProps\n>(\n  (\n    {\n      children,\n      label = \"I see you\",\n      eyeColor = \"#ff6f37\",\n      intensity = 1.3,\n      pupilSize = 0.62,\n      irisWidth = 0.22,\n      glowIntensity = 0.5,\n      scale = 1.7,\n      noiseScale = 0.8,\n      pupilFollow = 0.8,\n      flameSpeed = 0.8,\n      blink = true,\n      eyeClassName,\n      labelClassName,\n      className,\n      style,\n      disabled,\n      type = \"button\",\n      onPointerEnter,\n      onPointerLeave,\n      onClick,\n      ...props\n    },\n    ref,\n  ) => {\n    const hostRef = React.useRef<HTMLSpanElement | null>(null);\n    const topLidRef = React.useRef<HTMLSpanElement | null>(null);\n    const bottomLidRef = React.useRef<HTMLSpanElement | null>(null);\n    const [painted, setPainted] = React.useState(false);\n\n    // Live tuning the render loop reads, so slider drags never rebuild the GL.\n    const live = React.useRef({\n      eyeColor,\n      intensity,\n      pupilSize,\n      irisWidth,\n      glowIntensity,\n      scale,\n      noiseScale,\n      pupilFollow,\n      flameSpeed,\n      focusTarget: 0,\n    });\n    Object.assign(live.current, {\n      eyeColor,\n      intensity,\n      pupilSize,\n      irisWidth,\n      glowIntensity,\n      scale,\n      noiseScale,\n      pupilFollow,\n      flameSpeed,\n    });\n\n    const blinkNow = React.useCallback(() => {\n      if (window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches) return;\n      const timing = { duration: 200 };\n      // Animate `scale`, the property Tailwind's scale-y-0 sets; animating\n      // transform instead would multiply with it and stay shut at zero.\n      const shut = [\n        { scale: \"1 0\", easing: \"cubic-bezier(0.5, 0, 0.75, 0)\" },\n        { scale: \"1 1\", offset: 0.4 },\n        { scale: \"1 1\", offset: 0.55, easing: \"cubic-bezier(0.25, 1, 0.5, 1)\" },\n        { scale: \"1 0\" },\n      ];\n      topLidRef.current?.animate(shut, timing);\n      bottomLidRef.current?.animate(shut, timing);\n    }, []);\n\n    React.useEffect(() => {\n      const host = hostRef.current;\n      if (!host) return;\n      // A fresh canvas per run: a lost context never comes back on the same\n      // canvas, which would freeze the eye after Strict Mode's remount.\n      const canvas = document.createElement(\"canvas\");\n      canvas.style.cssText = \"display:block;width:100%;height:100%\";\n      const eye = createEyeGL(canvas);\n      if (!eye) return;\n      host.appendChild(canvas);\n      const { gl, u } = eye;\n      const state = live.current;\n      const reduceMotion = window.matchMedia(\n        \"(prefers-reduced-motion: reduce)\",\n      ).matches;\n\n      let raf = 0;\n      let last = 0;\n      let time = 0;\n      let focus = 0;\n      let visible = true;\n      let first = true;\n      const look = { x: 0, y: 0, tx: 0, ty: 0 };\n\n      const draw = () => {\n        gl.uniform1f(u.uTime, time);\n        gl.uniform2f(u.uRes, canvas.width, canvas.height);\n        gl.uniform1f(u.uPupilSize, state.pupilSize);\n        gl.uniform1f(u.uIrisWidth, state.irisWidth);\n        gl.uniform1f(u.uGlow, state.glowIntensity);\n        gl.uniform1f(u.uIntensity, state.intensity);\n        gl.uniform1f(u.uScale, state.scale);\n        gl.uniform1f(u.uNoiseScale, state.noiseScale);\n        gl.uniform2f(u.uLook, look.x, look.y);\n        gl.uniform1f(u.uFocus, focus);\n        gl.uniform3f(u.uEyeColor, ...hexToRgb(state.eyeColor));\n        gl.uniform3f(u.uBgColor, ...hexToRgb(FACE));\n        gl.drawArrays(gl.TRIANGLES, 0, 3);\n        if (first) {\n          first = false;\n          setPainted(true);\n        }\n      };\n\n      const frame = (now: number) => {\n        const dt = last ? Math.min(0.05, (now - last) / 1000) : 0;\n        last = now;\n        time += dt * state.flameSpeed;\n        focus += (state.focusTarget - focus) * Math.min(1, dt * 7);\n        look.x += (look.tx - look.x) * Math.min(1, dt * 6);\n        look.y += (look.ty - look.y) * Math.min(1, dt * 6);\n        draw();\n        raf = requestAnimationFrame(frame);\n      };\n      const start = () => {\n        if (raf || reduceMotion || !visible || document.hidden) return;\n        last = 0;\n        raf = requestAnimationFrame(frame);\n      };\n      const stop = () => {\n        cancelAnimationFrame(raf);\n        raf = 0;\n      };\n\n      // The eye watches the cursor anywhere on the page, not just over itself.\n      const onPointerMove = (event: PointerEvent) => {\n        const rect = host.getBoundingClientRect();\n        const dx = event.clientX - (rect.left + rect.width / 2);\n        const dy = event.clientY - (rect.top + rect.height / 2);\n        const reach = 240;\n        const dist = Math.hypot(dx, dy);\n        const k = (Math.min(dist, reach) / reach / (dist || 1)) * state.pupilFollow;\n        look.tx = dx * k;\n        look.ty = -dy * k;\n      };\n      window.addEventListener(\"pointermove\", onPointerMove, { passive: true });\n\n      const resize = () => {\n        // At least 2x even on 1x screens: downsampling turns the flame noise\n        // into smooth licks instead of speckle.\n        const dpr = Math.min(Math.max(window.devicePixelRatio || 1, 2), 3);\n        canvas.width = Math.max(1, Math.round(host.clientWidth * dpr));\n        canvas.height = Math.max(1, Math.round(host.clientHeight * dpr));\n        gl.viewport(0, 0, canvas.width, canvas.height);\n        draw();\n      };\n      const resizeObserver = new ResizeObserver(resize);\n      resizeObserver.observe(host);\n      const intersection = new IntersectionObserver(([entry]) => {\n        visible = !!entry?.isIntersecting;\n        if (visible) start();\n        else stop();\n      });\n      intersection.observe(host);\n      const onVisibility = () => (document.hidden ? stop() : start());\n      document.addEventListener(\"visibilitychange\", onVisibility);\n      const onLost = (event: Event) => {\n        event.preventDefault();\n        stop();\n        setPainted(false);\n      };\n      canvas.addEventListener(\"webglcontextlost\", onLost);\n\n      resize();\n      start();\n\n      return () => {\n        stop();\n        window.removeEventListener(\"pointermove\", onPointerMove);\n        resizeObserver.disconnect();\n        intersection.disconnect();\n        document.removeEventListener(\"visibilitychange\", onVisibility);\n        canvas.removeEventListener(\"webglcontextlost\", onLost);\n        gl.getExtension(\"WEBGL_lose_context\")?.loseContext();\n        canvas.remove();\n        setPainted(false);\n      };\n    }, []);\n\n    // Idle blinks at uneven intervals, so it never feels like a metronome.\n    React.useEffect(() => {\n      if (!blink) return;\n      let timer = 0;\n      const schedule = () => {\n        timer = window.setTimeout(\n          () => {\n            if (!document.hidden) blinkNow();\n            schedule();\n          },\n          2800 + Math.random() * 4200,\n        );\n      };\n      schedule();\n      return () => window.clearTimeout(timer);\n    }, [blink, blinkNow]);\n\n    return (\n      <button\n        ref={ref}\n        type={type}\n        disabled={disabled}\n        onPointerEnter={(event) => {\n          onPointerEnter?.(event);\n          live.current.focusTarget = 1;\n        }}\n        onPointerLeave={(event) => {\n          onPointerLeave?.(event);\n          live.current.focusTarget = 0;\n        }}\n        onClick={(event) => {\n          onClick?.(event);\n          blinkNow();\n        }}\n        style={{ \"--eye\": eyeColor, ...style } as React.CSSProperties}\n        className={cn(\n          \"group/eye relative isolate inline-flex h-14 min-w-56 cursor-pointer items-center justify-center overflow-hidden rounded-full px-10 text-sm font-semibold tracking-[0.2em] text-white uppercase outline-none select-none\",\n          // Obsidian shell with an ember hairline, faint top highlight, and a soft drop.\n          \"bg-[#0c0706] shadow-[0_0_0_1px_color-mix(in_oklab,var(--eye)_30%,black),inset_0_1px_0_rgb(255_255_255/0.1),0_1px_2px_rgb(0_0_0/0.4),0_10px_24px_-10px_rgb(0_0_0/0.7)]\",\n          \"transition-[scale,box-shadow] duration-300 ease-[cubic-bezier(0.34,1.4,0.64,1)]\",\n          // Hover: the hairline heats up and the eye's light spills around the button.\n          \"hover:shadow-[0_0_0_1px_color-mix(in_oklab,var(--eye)_60%,black),inset_0_1px_0_rgb(255_255_255/0.12),0_1px_2px_rgb(0_0_0/0.4),0_10px_32px_-8px_color-mix(in_oklab,var(--eye)_50%,transparent)]\",\n          \"active:scale-[0.97] active:duration-100 active:ease-out motion-reduce:active:scale-100\",\n          \"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n          \"disabled:cursor-not-allowed disabled:opacity-50\",\n          className,\n        )}\n        {...props}\n      >\n        {/* The eye fills the button. */}\n        <span\n          aria-hidden\n          className={cn(\"pointer-events-none absolute inset-0 -z-10\", eyeClassName)}\n        >\n          <span\n            ref={hostRef}\n            className={cn(\n              \"absolute inset-0 transition-opacity duration-500\",\n              painted ? \"opacity-100\" : \"opacity-0\",\n            )}\n          />\n          {/* Static iris until the shader paints, or for good without WebGL. */}\n          {!painted ? (\n            <span className=\"absolute inset-0 bg-[radial-gradient(closest-side,black_0_10%,var(--eye)_16%,color-mix(in_oklab,var(--eye)_45%,black)_45%,transparent_80%)]\" />\n          ) : null}\n          {/* Lids close across the whole button, with curved lash lines. */}\n          <span\n            ref={topLidRef}\n            className=\"absolute inset-x-[-10%] top-0 h-[54%] origin-top scale-y-0 rounded-b-[50%] shadow-[0_1px_0_color-mix(in_oklab,var(--eye)_40%,transparent)]\"\n            style={{ background: FACE }}\n          />\n          <span\n            ref={bottomLidRef}\n            className=\"absolute inset-x-[-10%] bottom-0 h-[54%] origin-bottom scale-y-0 rounded-t-[50%] shadow-[0_-1px_0_color-mix(in_oklab,var(--eye)_40%,transparent)]\"\n            style={{ background: FACE }}\n          />\n        </span>\n\n        {/* Readability: a soft dark band behind the label, fading out before the\n            iris edge, and a rim vignette that frames the eye in the pill. */}\n        <span\n          aria-hidden\n          className=\"pointer-events-none absolute inset-0 -z-10 bg-[radial-gradient(30%_38%_at_50%_50%,rgb(12_7_6/0.4),transparent_100%),radial-gradient(ellipse_at_center,transparent_60%,rgb(12_7_6/0.7)_100%)]\"\n        />\n        {/* Glass lip along the top edge. */}\n        <span\n          aria-hidden\n          className=\"pointer-events-none absolute inset-0 -z-10 rounded-full bg-[linear-gradient(180deg,rgb(255_255_255/0.1),transparent_35%)]\"\n        />\n\n        <span\n          className={cn(\n            \"relative whitespace-nowrap [text-shadow:0_1px_2px_rgb(0_0_0/0.9),0_0_12px_color-mix(in_oklab,var(--eye)_55%,transparent)]\",\n            labelClassName,\n          )}\n        >\n          {children ?? label}\n        </span>\n      </button>\n    );\n  },\n);\n\nEvilEyeButton.displayName = \"EvilEyeButton\";\n\nexport default EvilEyeButton;\n"
    }
  ],
  "dependencies": [
    "clsx",
    "tailwind-merge"
  ]
}
