{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "bubble-cursor",
  "title": "Bubble Cursor",
  "description": "A liquid-glass bubble that follows the pointer across the whole page, rendered with a raymarched WebGL2 metaball trail with fresnel shading and iridescent glints, plus an optional light/dark theme toggle.",
  "dependencies": [
    "clsx",
    "tailwind-merge"
  ],
  "files": [
    {
      "path": "registry/new-york/bubble-cursor/bubble-cursor.tsx",
      "content": "\"use client\";\n\n// A liquid-glass bubble that follows the pointer, rendered with a\n// raymarched metaball trail in WebGL2 (fresnel shading + iridescent glints).\n//\n// This tracks the pointer across the whole page (not just its own wrapper),\n// which is the point of a custom cursor effect, so it intentionally listens\n// on `window` and is fully cleaned up on unmount.\n\nimport * as React from \"react\";\nimport { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nfunction cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n\nconst MAX_TRAIL = 20;\n\nconst VERT = `#version 300 es\nprecision highp float;\nlayout(location = 0) in vec2 aPos;\nvoid main () {\n  gl_Position = vec4(aPos, 0.0, 1.0);\n}`;\n\nconst FRAG = `#version 300 es\nprecision highp float;\nout vec4 outColor;\nuniform vec2 uResolution;\nuniform float uTime;\nuniform int uCount;\nuniform vec2 uTrail[${MAX_TRAIL}];\nuniform float uBaseRadius;\nuniform float uBlend;\nuniform float uShine;\nuniform float uRim;\nuniform float uIridescence;\nuniform float uIntensity;\nuniform vec3 uTint;\nuniform float uTintStrength;\nuniform vec3 uColorA;\nuniform vec3 uColorB;\nuniform float uOpacity;\n\nconst float EPS = 1e-4;\nconst int ITR = 16;\n\nfloat rnd3D (vec3 p) {\n  return fract(sin(dot(p, vec3(12.9898, 78.233, 37.719))) * 43758.5453123);\n}\n\nfloat noise3D (vec3 p) {\n  vec3 i = floor(p);\n  vec3 f = fract(p);\n\n  float a000 = rnd3D(i);\n  float a100 = rnd3D(i + vec3(1.0, 0.0, 0.0));\n  float a010 = rnd3D(i + vec3(0.0, 1.0, 0.0));\n  float a110 = rnd3D(i + vec3(1.0, 1.0, 0.0));\n  float a001 = rnd3D(i + vec3(0.0, 0.0, 1.0));\n  float a101 = rnd3D(i + vec3(1.0, 0.0, 1.0));\n  float a011 = rnd3D(i + vec3(0.0, 1.0, 1.0));\n  float a111 = rnd3D(i + vec3(1.0, 1.0, 1.0));\n\n  vec3 u = f * f * (3.0 - 2.0 * f);\n\n  float k0 = a000;\n  float k1 = a100 - a000;\n  float k2 = a010 - a000;\n  float k3 = a001 - a000;\n  float k4 = a000 - a100 - a010 + a110;\n  float k5 = a000 - a010 - a001 + a011;\n  float k6 = a000 - a100 - a001 + a101;\n  float k7 = -a000 + a100 + a010 - a110 + a001 - a101 - a011 + a111;\n\n  return k0 + k1 * u.x + k2 * u.y + k3 * u.z + k4 * u.x * u.y +\n    k5 * u.y * u.z + k6 * u.z * u.x + k7 * u.x * u.y * u.z;\n}\n\nfloat smoothMin (float d1, float d2, float k) {\n  float h = exp(-k * d1) + exp(-k * d2);\n  return -log(max(h, 1e-12)) / k;\n}\n\nfloat map (vec3 p) {\n  float radius = uBaseRadius * float(uCount);\n  float d = 1e5;\n  for (int i = 0; i < ${MAX_TRAIL}; i++) {\n    if (i >= uCount) break;\n    float sphere = length(p - vec3(uTrail[i], 0.0)) -\n      (radius - uBaseRadius * float(i));\n    d = smoothMin(d, sphere, uBlend);\n  }\n  return d;\n}\n\nvec3 generateNormal (vec3 p) {\n  return normalize(vec3(\n    map(p + vec3(EPS, 0.0, 0.0)) - map(p + vec3(-EPS, 0.0, 0.0)),\n    map(p + vec3(0.0, EPS, 0.0)) - map(p + vec3(0.0, -EPS, 0.0)),\n    map(p + vec3(0.0, 0.0, EPS)) - map(p + vec3(0.0, 0.0, -EPS))));\n}\n\nvec3 dropletColor (vec3 normal, vec3 rayDir) {\n  vec3 reflectDir = reflect(rayDir, normal);\n  float noisePosTime = noise3D(reflectDir * 2.0 + uTime);\n  float noiseNegTime = noise3D(reflectDir * 2.0 - uTime);\n  vec3 color0 = uColorA * noisePosTime;\n  vec3 color1 = uColorB * noiseNegTime;\n  return (color0 + color1) * uIntensity;\n}\n\nvoid main () {\n  vec2 frag = gl_FragCoord.xy;\n  float minRes = min(uResolution.x, uResolution.y);\n  vec2 p = (frag * 2.0 - uResolution) / minRes;\n\n  vec3 ray = vec3(p, 1.0);\n  vec3 rayDir = vec3(0.0, 0.0, -1.0);\n  float dist = 0.0;\n\n  for (int i = 0; i < ITR; ++i) {\n    dist = map(ray);\n    ray += rayDir * dist;\n    if (dist < EPS || dist > 8.0) break;\n  }\n\n  float cov = 1.0 - smoothstep(0.0, 3.0 / minRes, dist);\n  if (!(cov > 0.001)) {\n    outColor = vec4(0.0);\n    return;\n  }\n\n  vec3 n = generateNormal(ray);\n  vec3 glints = pow(max(dropletColor(n, rayDir), 0.0), vec3(7.0));\n  vec3 L = normalize(vec3(-0.5, 0.7, 0.6));\n  float spec = pow(max(dot(reflect(-L, n), vec3(0.0, 0.0, 1.0)), 0.0), 60.0);\n\n  float edge = pow(1.0 - clamp(n.z, 0.0, 1.0), 1.5);\n  vec3 filmTint = mix(vec3(0.9), uTint, clamp(uTintStrength, 0.0, 1.0));\n  float fade = cov * clamp(uOpacity, 0.0, 1.0);\n  vec3 light = glints * uIridescence * 0.65 + vec3(spec * uShine * 1.5) +\n    filmTint * (0.55 * max(uRim, 0.4) * edge + 0.03);\n  float a = fade * clamp(0.08 + 0.4 * edge, 0.0, 1.0);\n  outColor = vec4(light * fade, a);\n}`;\n\nfunction parseColor(color: string): [number, number, number] {\n  if (!color) return [1, 1, 1];\n  const rgbaMatch = color.match(/rgba?\\(\\s*(\\d+)\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)/i);\n  if (rgbaMatch) {\n    return [Number(rgbaMatch[1]) / 255, Number(rgbaMatch[2]) / 255, Number(rgbaMatch[3]) / 255];\n  }\n  let hex = color.trim().replace(\"#\", \"\");\n  if (hex.length === 3) {\n    hex = hex.split(\"\").map((c) => c + c).join(\"\");\n  }\n  if (hex.length >= 6) {\n    const r = parseInt(hex.slice(0, 2), 16);\n    const g = parseInt(hex.slice(2, 4), 16);\n    const b = parseInt(hex.slice(4, 6), 16);\n    if (!Number.isNaN(r) && !Number.isNaN(g) && !Number.isNaN(b)) {\n      return [r / 255, g / 255, b / 255];\n    }\n  }\n  return [1, 1, 1];\n}\n\nfunction compileShader(gl: WebGL2RenderingContext, type: number, source: string): WebGLShader {\n  const shader = gl.createShader(type)!;\n  gl.shaderSource(shader, source);\n  gl.compileShader(shader);\n  return shader;\n}\n\nexport interface BubbleCursorProps extends Omit<React.ComponentPropsWithoutRef<\"div\">, \"children\"> {\n  size?: number;\n  trailLength?: number;\n  followSpeed?: number;\n  blend?: number;\n  speed?: number;\n  shine?: number;\n  rim?: number;\n  iridescence?: number;\n  intensity?: number;\n  tint?: string;\n  tintStrength?: number;\n  colorA?: string;\n  colorB?: string;\n  opacity?: number;\n  zIndex?: number;\n  hideOnTouch?: boolean;\n  showThemeToggle?: boolean;\n}\n\nconst THEME_STORAGE_KEY = \"bubble-cursor-theme\";\n\nconst SunIcon = () => (\n  <svg aria-hidden width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\">\n    <circle cx=\"12\" cy=\"12\" r=\"4.5\" fill=\"currentColor\" />\n    <g stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\">\n      <line x1=\"12\" y1=\"1.5\" x2=\"12\" y2=\"4\" />\n      <line x1=\"12\" y1=\"20\" x2=\"12\" y2=\"22.5\" />\n      <line x1=\"1.5\" y1=\"12\" x2=\"4\" y2=\"12\" />\n      <line x1=\"20\" y1=\"12\" x2=\"22.5\" y2=\"12\" />\n      <line x1=\"4.5\" y1=\"4.5\" x2=\"6.2\" y2=\"6.2\" />\n      <line x1=\"17.8\" y1=\"17.8\" x2=\"19.5\" y2=\"19.5\" />\n      <line x1=\"4.5\" y1=\"19.5\" x2=\"6.2\" y2=\"17.8\" />\n      <line x1=\"17.8\" y1=\"6.2\" x2=\"19.5\" y2=\"4.5\" />\n    </g>\n  </svg>\n);\n\nconst MoonIcon = () => (\n  <svg aria-hidden width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\">\n    <path d=\"M20.5 14.5A9 9 0 1 1 9.5 3.5a7 7 0 0 0 11 11Z\" fill=\"currentColor\" />\n  </svg>\n);\n\nexport function BubbleCursor({\n  className,\n  style,\n  size = 30,\n  trailLength = 16,\n  followSpeed = 0.5,\n  blend = 14,\n  speed = 2,\n  shine = 0.25,\n  rim = 0.5,\n  iridescence = 1,\n  intensity = 0.9,\n  tint = \"rgba(255, 250, 240, 1)\",\n  tintStrength = 0,\n  colorA = \"rgba(214, 165, 74, 1)\",\n  colorB = \"rgba(140, 90, 40, 1)\",\n  opacity = 1,\n  zIndex = 9999,\n  hideOnTouch = true,\n  showThemeToggle = false,\n  ...props\n}: BubbleCursorProps) {\n  const wrapperRef = React.useRef<HTMLDivElement>(null);\n  const canvasRef = React.useRef<HTMLCanvasElement>(null);\n  const [supportsHover, setSupportsHover] = React.useState(true);\n  const [isDark, setIsDark] = React.useState(false);\n  const [themeChosen, setThemeChosen] = React.useState(false);\n  const scopeId = React.useId().replace(/[^a-zA-Z0-9]/g, \"\");\n\n  const optionsRef = React.useRef({\n    size,\n    trailLength,\n    followSpeed,\n    blend,\n    speed,\n    shine,\n    rim,\n    iridescence,\n    intensity,\n    tint,\n    tintStrength,\n    colorA,\n    colorB,\n    opacity,\n  });\n  optionsRef.current = {\n    size,\n    trailLength,\n    followSpeed,\n    blend,\n    speed,\n    shine,\n    rim,\n    iridescence,\n    intensity,\n    tint,\n    tintStrength,\n    colorA,\n    colorB,\n    opacity,\n  };\n  const startRef = React.useRef<() => void>(() => {});\n\n  React.useEffect(() => {\n    const mq = window.matchMedia(\"(hover: hover) and (pointer: fine)\");\n    setSupportsHover(mq.matches);\n    const onChange = () => setSupportsHover(mq.matches);\n    mq.addEventListener(\"change\", onChange);\n    return () => mq.removeEventListener(\"change\", onChange);\n  }, []);\n\n  React.useEffect(() => {\n    if (!showThemeToggle) return;\n    const stored = window.localStorage.getItem(THEME_STORAGE_KEY);\n    if (stored) {\n      setIsDark(stored === \"dark\");\n      setThemeChosen(true);\n    } else {\n      setIsDark(window.matchMedia(\"(prefers-color-scheme: dark)\").matches);\n    }\n  }, [showThemeToggle]);\n\n  React.useEffect(() => {\n    startRef.current();\n  }, [colorA, colorB, tint]);\n\n  function toggleTheme() {\n    setThemeChosen(true);\n    setIsDark((prev) => {\n      const next = !prev;\n      window.localStorage.setItem(THEME_STORAGE_KEY, next ? \"dark\" : \"light\");\n      return next;\n    });\n  }\n\n  React.useEffect(() => {\n    if (hideOnTouch && !supportsHover) return;\n\n    const wrapper = wrapperRef.current;\n    const canvas = canvasRef.current;\n    if (!wrapper || !canvas) return;\n\n    const gl = canvas.getContext(\"webgl2\", {\n      alpha: true,\n      depth: false,\n      stencil: false,\n      antialias: false,\n      premultipliedAlpha: true,\n    });\n    if (!gl || gl.isContextLost()) return;\n\n    const vertexShader = compileShader(gl, gl.VERTEX_SHADER, VERT);\n    const fragmentShader = compileShader(gl, gl.FRAGMENT_SHADER, FRAG);\n    const program = gl.createProgram()!;\n    gl.attachShader(program, vertexShader);\n    gl.attachShader(program, fragmentShader);\n    gl.linkProgram(program);\n\n    const uniforms: Record<string, WebGLUniformLocation> = {};\n    const uniformCount = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS);\n    for (let i = 0; i < uniformCount; i++) {\n      const info = gl.getActiveUniform(program, i);\n      if (!info) continue;\n      const location = gl.getUniformLocation(program, info.name);\n      if (location) uniforms[info.name] = location;\n    }\n\n    const quad = gl.createBuffer();\n    gl.bindBuffer(gl.ARRAY_BUFFER, quad);\n    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]), gl.STATIC_DRAW);\n    gl.enableVertexAttribArray(0);\n    gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);\n\n    // The canvas box is driven directly from window.innerWidth/innerHeight\n    // (not \"100%\"/\"100vw\" styles) so it always matches the coordinate space\n    // that pointer events (clientX/clientY) are reported in — mobile browsers\n    // resolve vw/vh against a viewport that can drift from innerWidth/Height\n    // while the address bar hides/shows.\n    function syncSize() {\n      if (!canvas) return;\n      const dpr = Math.min(window.devicePixelRatio || 1, 2);\n      const cssWidth = window.innerWidth;\n      const cssHeight = window.innerHeight;\n      const width = Math.max(1, Math.round(cssWidth * dpr));\n      const height = Math.max(1, Math.round(cssHeight * dpr));\n      if (canvas.width !== width || canvas.height !== height) {\n        canvas.width = width;\n        canvas.height = height;\n      }\n      canvas.style.width = `${cssWidth}px`;\n      canvas.style.height = `${cssHeight}px`;\n    }\n    syncSize();\n\n    const trailX = new Float32Array(MAX_TRAIL);\n    const trailY = new Float32Array(MAX_TRAIL);\n    const trailData = new Float32Array(MAX_TRAIL * 2);\n    let headX = window.innerWidth / 2;\n    let headY = window.innerHeight / 2;\n    let targetX = headX;\n    let targetY = headY;\n    trailX.fill(headX);\n    trailY.fill(headY);\n    let presence = 0;\n    let presenceTarget = 0;\n    let hasPointer = false;\n    let time = 0;\n\n    // This canvas is a fixed, full-viewport overlay, so Page Visibility (not\n    // an IntersectionObserver on the tiny wrapper anchor) is the correct\n    // signal for pausing the render loop.\n    let isVisible = document.visibilityState === \"visible\";\n    function onVisibilityChange() {\n      isVisible = document.visibilityState === \"visible\";\n      if (isVisible) start();\n    }\n    document.addEventListener(\"visibilitychange\", onVisibilityChange);\n\n    const motionQuery = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n    let reducedMotion = motionQuery.matches;\n    function onMotionChange() {\n      reducedMotion = motionQuery.matches;\n      start();\n    }\n    motionQuery.addEventListener(\"change\", onMotionChange);\n\n    function activeCount(): number {\n      const t = optionsRef.current.trailLength;\n      return Math.min(Math.max(Math.round(t), 2), MAX_TRAIL);\n    }\n\n    function render() {\n      if (!gl || !canvas) return;\n      const opts = optionsRef.current;\n      const dpr = canvas.width / Math.max(window.innerWidth, 1);\n      gl.bindFramebuffer(gl.FRAMEBUFFER, null);\n      gl.viewport(0, 0, canvas.width, canvas.height);\n      gl.disable(gl.SCISSOR_TEST);\n      gl.clearColor(0, 0, 0, 0);\n      gl.clear(gl.COLOR_BUFFER_BIT);\n      if (presence <= 0.004) return;\n\n      const count = activeCount();\n      const minRes = Math.min(canvas.width, canvas.height);\n      const headRadius = Math.max(opts.size, 4) * dpr * presence;\n      const baseRadius = (headRadius * 2) / (minRes * count);\n      const blendAmount = Math.max(opts.blend, 0.5);\n\n      let minX = Infinity;\n      let minY = Infinity;\n      let maxX = -Infinity;\n      let maxY = -Infinity;\n      for (let i = 0; i < count; i++) {\n        const dx = trailX[i] * dpr;\n        const dy = canvas.height - trailY[i] * dpr;\n        trailData[i * 2] = (dx * 2 - canvas.width) / minRes;\n        trailData[i * 2 + 1] = (dy * 2 - canvas.height) / minRes;\n        minX = Math.min(minX, dx);\n        maxX = Math.max(maxX, dx);\n        minY = Math.min(minY, dy);\n        maxY = Math.max(maxY, dy);\n      }\n\n      const pad = headRadius + ((Math.log(count + 1) / blendAmount) * minRes) / 2 + 32 * dpr;\n      const sx = Math.max(0, Math.floor(minX - pad));\n      const sy = Math.max(0, Math.floor(minY - pad));\n      gl.enable(gl.SCISSOR_TEST);\n      gl.scissor(sx, sy, Math.min(canvas.width - sx, Math.ceil(maxX - minX + pad * 2)), Math.min(canvas.height - sy, Math.ceil(maxY - minY + pad * 2)));\n\n      const colorA01 = parseColor(opts.colorA);\n      const colorB01 = parseColor(opts.colorB);\n      const tint01 = parseColor(opts.tint);\n\n      gl.useProgram(program);\n      gl.uniform2f(uniforms.uResolution, canvas.width, canvas.height);\n      gl.uniform1f(uniforms.uTime, time);\n      gl.uniform1i(uniforms.uCount, count);\n      gl.uniform2fv(uniforms[\"uTrail[0]\"], trailData);\n      gl.uniform1f(uniforms.uBaseRadius, baseRadius);\n      gl.uniform1f(uniforms.uBlend, blendAmount);\n      gl.uniform1f(uniforms.uShine, Math.max(opts.shine, 0));\n      gl.uniform1f(uniforms.uRim, Math.min(Math.max(opts.rim, 0), 2));\n      gl.uniform1f(uniforms.uIridescence, Math.max(opts.iridescence, 0));\n      gl.uniform1f(uniforms.uIntensity, Math.max(opts.intensity, 0));\n      gl.uniform3f(uniforms.uTint, tint01[0], tint01[1], tint01[2]);\n      gl.uniform1f(uniforms.uTintStrength, Math.min(Math.max(opts.tintStrength, 0), 1));\n      gl.uniform3f(uniforms.uColorA, colorA01[0], colorA01[1], colorA01[2]);\n      gl.uniform3f(uniforms.uColorB, colorB01[0], colorB01[1], colorB01[2]);\n      gl.uniform1f(uniforms.uOpacity, Math.min(Math.max(opts.opacity, 0), 1));\n      gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);\n      gl.disable(gl.SCISSOR_TEST);\n    }\n\n    let raf = 0;\n    let lastTime = performance.now();\n    let destroyed = false;\n    let running = false;\n\n    function frame(now: number) {\n      if (destroyed) return;\n      if (!isVisible) {\n        running = false;\n        return;\n      }\n      const opts = optionsRef.current;\n      const delta = Math.min((now - lastTime) / 1000, 1 / 30);\n      lastTime = now;\n      if (!reducedMotion) time += delta * Math.max(opts.speed, 0);\n\n      const follow = Math.min(Math.max(opts.followSpeed, 0.02), 1);\n      const kHead = reducedMotion || follow >= 1 ? 1 : 1 - Math.exp(-delta * (3 + follow * 30));\n      const kScale = reducedMotion ? 1 : 1 - Math.exp(-delta * 10);\n\n      headX += (targetX - headX) * kHead;\n      headY += (targetY - headY) * kHead;\n      for (let i = MAX_TRAIL - 1; i > 0; i--) {\n        trailX[i] = trailX[i - 1];\n        trailY[i] = trailY[i - 1];\n      }\n      trailX[0] = headX;\n      trailY[0] = headY;\n\n      let moved = Math.abs(targetX - headX) + Math.abs(targetY - headY);\n      for (let i = 1; i < MAX_TRAIL; i++) {\n        moved = Math.max(moved, Math.abs(trailX[i] - trailX[i - 1]) + Math.abs(trailY[i] - trailY[i - 1]));\n      }\n      presence += (presenceTarget - presence) * kScale;\n\n      render();\n\n      const settled = reducedMotion ? moved < 0.1 && Math.abs(presenceTarget - presence) < 0.002 : presence < 0.004 && presenceTarget === 0;\n      if (settled) {\n        presence = presenceTarget;\n        running = false;\n        return;\n      }\n      raf = requestAnimationFrame(frame);\n    }\n\n    function start() {\n      if (destroyed || running || !isVisible) return;\n      running = true;\n      lastTime = performance.now();\n      raf = requestAnimationFrame(frame);\n    }\n    startRef.current = start;\n\n    function onPointerMove(event: PointerEvent) {\n      targetX = event.clientX;\n      targetY = event.clientY;\n      if (!hasPointer) {\n        headX = targetX;\n        headY = targetY;\n        trailX.fill(targetX);\n        trailY.fill(targetY);\n        hasPointer = true;\n      }\n      presenceTarget = 1;\n      start();\n    }\n    function onPointerLeave() {\n      presenceTarget = 0;\n      hasPointer = false;\n      start();\n    }\n    function onResize() {\n      syncSize();\n      start();\n    }\n\n    window.addEventListener(\"pointermove\", onPointerMove, { passive: true });\n    window.addEventListener(\"pointerleave\", onPointerLeave, { passive: true });\n    window.addEventListener(\"resize\", onResize, { passive: true });\n    window.visualViewport?.addEventListener(\"resize\", onResize);\n\n    return () => {\n      destroyed = true;\n      cancelAnimationFrame(raf);\n      window.removeEventListener(\"pointermove\", onPointerMove);\n      window.removeEventListener(\"pointerleave\", onPointerLeave);\n      window.removeEventListener(\"resize\", onResize);\n      window.visualViewport?.removeEventListener(\"resize\", onResize);\n      motionQuery.removeEventListener(\"change\", onMotionChange);\n      document.removeEventListener(\"visibilitychange\", onVisibilityChange);\n      gl.deleteProgram(program);\n      gl.deleteShader(vertexShader);\n      gl.deleteShader(fragmentShader);\n      gl.deleteBuffer(quad);\n    };\n  }, [hideOnTouch, supportsHover]);\n\n  return (\n    <div ref={wrapperRef} className={cn(\"relative h-full min-h-[4px] w-full min-w-[4px]\", className)} style={style} {...props}>\n      {showThemeToggle && themeChosen ? (\n        <div\n          aria-hidden\n          className=\"pointer-events-none fixed inset-0\"\n          style={{ zIndex, background: isDark ? \"rgba(6, 7, 10, 0.42)\" : \"rgba(255, 255, 255, 0.9)\" }}\n        />\n      ) : null}\n      {!hideOnTouch || supportsHover ? (\n        <canvas ref={canvasRef} aria-hidden className=\"pointer-events-none fixed inset-0 h-full w-full\" style={{ zIndex }} />\n      ) : null}\n      {showThemeToggle ? (\n        <>\n          <style>{`\n            .bc-theme-btn-${scopeId} { transition: filter 0.15s ease, transform 0.15s ease; }\n            .bc-theme-btn-${scopeId}:hover { filter: brightness(1.15); }\n            .bc-theme-btn-${scopeId}:active { transform: scale(0.94); }\n          `}</style>\n          <button\n            type=\"button\"\n            className={cn(\"bc-theme-btn-\" + scopeId, \"fixed left-4 top-4 flex h-9 w-9 items-center justify-center rounded-full border p-0 shadow-[0_2px_12px_rgba(0,0,0,0.15)] backdrop-blur-[10px]\")}\n            onClick={toggleTheme}\n            aria-pressed={isDark}\n            aria-label={isDark ? \"Switch to light theme\" : \"Switch to dark theme\"}\n            style={{\n              zIndex: zIndex + 1,\n              borderColor: \"rgba(255, 255, 255, 0.25)\",\n              background: isDark ? \"rgba(20, 20, 24, 0.6)\" : \"rgba(255, 255, 255, 0.6)\",\n              color: isDark ? \"#f5f5f5\" : \"#1a1a1a\",\n            }}\n          >\n            {isDark ? <SunIcon /> : <MoonIcon />}\n          </button>\n        </>\n      ) : null}\n    </div>\n  );\n}\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:component"
}