{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "glass-button",
  "title": "Glass Button",
  "description": "A glass button that swells when pressed and lights up from the point you press.",
  "dependencies": [
    "@radix-ui/react-slot",
    "clsx"
  ],
  "registryDependencies": [
    "@glassyui/glass-provider"
  ],
  "files": [
    {
      "path": "registry/glassyui/glass-button.tsx",
      "content": "'use client';\n\nimport { Slot, Slottable } from '@radix-ui/react-slot';\nimport clsx from 'clsx';\nimport { cloneElement, forwardRef, isValidElement, useCallback, useEffect, useMemo, useRef } from 'react';\nimport type { CSSProperties, ReactNode } from 'react';\nimport { mergeRefs } from './lib/merge-refs';\nimport { useGlassScenePane } from './lib/scene-context';\nimport { useLensFilter } from './lib/use-refraction-filter';\nimport { useOptionalPreferences } from './glass-provider';\n\nexport type ButtonVariant = 'primary' | 'prominent' | 'ghost' | 'soft';\n\nexport type ButtonSize = 'sm' | 'md' | 'lg';\n\ntype ButtonPreset = 'control' | 'showcase';\n\nexport interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {\n  /** Renders the single child element, such as a link, with the button's material. Links cannot be disabled. */\n  asChild?: boolean;\n  /** Chromium edge lens over the frost. On by default for filled variants. */\n  lens?: boolean;\n  preset?: ButtonPreset;\n  variant?: ButtonVariant;\n  size?: ButtonSize;\n}\n\nconst VARIANT_CLASS: Record<ButtonVariant, string> = {\n  primary: 'lg-button--primary',\n  prominent: 'lg-button--prominent',\n  ghost: 'lg-button--ghost',\n  soft: 'lg-button--soft',\n};\n\nconst SIZE_CLASS: Record<ButtonSize, string> = {\n  sm: 'lg-button--sm',\n  md: 'lg-button--md',\n  lg: 'lg-button--lg',\n};\n\n// The label sits above the material layers, so a slotted child's content moves into it.\nfunction withLabel(children: ReactNode) {\n  if (!isValidElement<{ children?: ReactNode }>(children)) return children;\n\n  return cloneElement(\n    children,\n    undefined,\n    <span className=\"lg-button__label\">{children.props.children}</span>\n  );\n}\n\nexport const GlassButton = forwardRef<HTMLButtonElement, ButtonProps>(\n  (\n    {\n      asChild = false,\n      className,\n      preset = 'control',\n      variant = 'primary',\n      lens = variant !== 'ghost',\n      size = 'md',\n      type,\n      disabled = false,\n      onPointerLeave,\n      onPointerCancel,\n      onPointerDown,\n      onPointerUp,\n      onBlur,\n      onKeyDown,\n      onKeyUp,\n      style,\n      children,\n      ...rest\n    },\n    forwardedRef\n  ) => {\n    const { preferences } = useOptionalPreferences();\n    const reduceMotion = Boolean(preferences?.reduceMotion);\n    const reduceTransparency = Boolean(preferences?.reduceTransparency);\n    const buttonRef = useRef<HTMLButtonElement | null>(null);\n    const bloomRef = useRef<HTMLSpanElement | null>(null);\n    const ref = useMemo(() => mergeRefs(forwardedRef, buttonRef), [forwardedRef]);\n\n    const releasePress = useCallback((target: HTMLButtonElement) => {\n      target.style.setProperty('--lg-press', '0');\n      target.dataset.pressed = 'false';\n    }, []);\n\n    // A press lights the glass from the point it lands and the light spreads\n    // outward, as it does through Liquid Glass under a fingertip.\n    const bloom = useCallback(\n      (target: HTMLElement, x: number, y: number) => {\n        const element = bloomRef.current;\n\n        if (!element || reduceMotion) return;\n\n        element.style.setProperty('--lg-bloom-x', `${x}px`);\n        element.style.setProperty('--lg-bloom-y', `${y}px`);\n        element.style.setProperty('--lg-bloom-size', `${Math.max(target.offsetWidth, target.offsetHeight) * 2.4}px`);\n        element.animate?.(\n          [\n            { opacity: 0, transform: 'scale(0.08)' },\n            { opacity: 1, transform: 'scale(0.42)', offset: 0.16 },\n            { opacity: 0, transform: 'scale(1)' },\n          ],\n          { duration: 720, easing: 'cubic-bezier(0.22, 0.8, 0.3, 1)' }\n        );\n      },\n      [reduceMotion]\n    );\n\n    const handlePointerLeave = useCallback(\n      (event: React.PointerEvent<HTMLButtonElement>) => {\n        onPointerLeave?.(event);\n        event.currentTarget.releasePointerCapture?.(event.pointerId);\n        releasePress(event.currentTarget);\n      },\n      [onPointerLeave, releasePress]\n    );\n\n    const handlePointerCancel = useCallback(\n      (event: React.PointerEvent<HTMLButtonElement>) => {\n        onPointerCancel?.(event);\n        event.currentTarget.releasePointerCapture?.(event.pointerId);\n        releasePress(event.currentTarget);\n      },\n      [onPointerCancel, releasePress]\n    );\n\n    const handlePointerDown = useCallback(\n      (event: React.PointerEvent<HTMLButtonElement>) => {\n        onPointerDown?.(event);\n\n        if (event.defaultPrevented || disabled) return;\n        const target = event.currentTarget;\n        const bounds = target.getBoundingClientRect();\n        // A scaled button maps client pixels back to its own, so the bloom starts under the finger.\n        const scale = bounds.width / Math.max(target.offsetWidth, 1) || 1;\n\n        target.setPointerCapture?.(event.pointerId);\n        target.style.setProperty('--lg-press', '1');\n        target.dataset.pressed = 'true';\n        bloom(target, (event.clientX - bounds.left) / scale, (event.clientY - bounds.top) / scale);\n      },\n      [bloom, disabled, onPointerDown]\n    );\n\n    const handlePointerUp = useCallback(\n      (event: React.PointerEvent<HTMLButtonElement>) => {\n        onPointerUp?.(event);\n        event.currentTarget.releasePointerCapture?.(event.pointerId);\n        event.currentTarget.style.setProperty('--lg-press', '0');\n        event.currentTarget.dataset.pressed = 'false';\n      },\n      [onPointerUp]\n    );\n\n    const handleBlur = useCallback(\n      (event: React.FocusEvent<HTMLButtonElement>) => {\n        onBlur?.(event);\n        releasePress(event.currentTarget);\n      },\n      [onBlur, releasePress]\n    );\n\n    const handleKeyDown = useCallback(\n      (event: React.KeyboardEvent<HTMLButtonElement>) => {\n        onKeyDown?.(event);\n\n        if (event.defaultPrevented || disabled) return;\n\n        if (event.key === ' ' || event.key === 'Enter') {\n          const target = event.currentTarget;\n\n          target.style.setProperty('--lg-press', '1');\n          target.dataset.pressed = 'true';\n\n          if (!event.repeat) bloom(target, target.offsetWidth / 2, target.offsetHeight / 2);\n        }\n      },\n      [bloom, disabled, onKeyDown]\n    );\n\n    const handleKeyUp = useCallback(\n      (event: React.KeyboardEvent<HTMLButtonElement>) => {\n        onKeyUp?.(event);\n\n        if (event.key === ' ' || event.key === 'Enter') {\n          event.currentTarget.style.setProperty('--lg-press', '0');\n          event.currentTarget.dataset.pressed = 'false';\n        }\n      },\n      [onKeyUp]\n    );\n\n    // A button disabled mid-press lets go of it.\n    useEffect(() => {\n      if (disabled && buttonRef.current) releasePress(buttonRef.current);\n    }, [disabled, releasePress]);\n\n    // Inside a GlassScene the canvas draws this glass. Ghost buttons have none.\n    useGlassScenePane(buttonRef, variant !== 'ghost');\n\n    const lensFilter = useLensFilter(buttonRef, {\n      enabled: lens && !disabled && !reduceTransparency,\n      signature: `${preferences?.theme}:${preferences?.mode}:${preferences?.quality}:${preferences?.thickness}`,\n    });\n\n    const Component = asChild ? Slot : 'button';\n\n    return (\n      <Component\n        {...rest}\n        ref={ref}\n        type={asChild ? type : (type ?? 'button')}\n        disabled={asChild ? undefined : disabled}\n        className={clsx('lg-button', VARIANT_CLASS[variant], SIZE_CLASS[size], className)}\n        data-lg-lens={lensFilter.active ? 'active' : undefined}\n        data-lg-preset={preset}\n        style={\n          // SAFETY: CSSProperties has no custom-property index; this adds one string-valued property.\n          lensFilter.filterStyle\n            ? ({ ...style, '--lg-lens-filter': lensFilter.filterStyle } as CSSProperties)\n            : style\n        }\n        onPointerLeave={handlePointerLeave}\n        onPointerCancel={handlePointerCancel}\n        onPointerDown={handlePointerDown}\n        onPointerUp={handlePointerUp}\n        onBlur={handleBlur}\n        onKeyDown={handleKeyDown}\n        onKeyUp={handleKeyUp}\n        data-pressed=\"false\"\n      >\n        <span className=\"lg-button__layers\" aria-hidden=\"true\">\n          {lensFilter.filterElement}\n          <span className=\"lg-button__light\" />\n          <span className=\"lg-button__distortion\" />\n          <span className=\"lg-button__tint\" />\n          <span className=\"lg-button__shine\" />\n          <span ref={bloomRef} className=\"lg-button__bloom\" />\n          <span className=\"lg-button__rim\" />\n        </span>\n        {asChild ? (\n          <Slottable>{withLabel(children)}</Slottable>\n        ) : (\n          <span className=\"lg-button__label\">{children}</span>\n        )}\n      </Component>\n    );\n  }\n);\n\nGlassButton.displayName = 'GlassButton';\n",
      "type": "registry:ui",
      "target": "components/glassyui/glass-button.tsx"
    },
    {
      "path": "registry/glassyui/lib/merge-refs.ts",
      "content": "import type { Ref } from 'react';\n\nexport function mergeRefs<T>(...refs: Array<Ref<T> | undefined>) {\n  return (node: T) => {\n    for (const ref of refs) {\n      if (!ref) continue;\n\n      if ('current' in ref) {\n        try {\n          ref.current = node;\n        } catch {\n          // Read-only refs cannot be assigned.\n        }\n      } else {\n        ref(node);\n      }\n    }\n  };\n}\n",
      "type": "registry:lib",
      "target": "components/glassyui/lib/merge-refs.ts"
    },
    {
      "path": "registry/glassyui/lib/refraction-map.ts",
      "content": "export type GeneratedRefractionStrength = 'soft' | 'strong';\n\ntype RefractionPreset = {\n  centerNeutralRatio: number;\n  defaultDisplacement: number;\n  edgeBandRatio: number;\n  edgeWidth: number;\n  /** Rim pull at the default displacement, as a share of the edge band. */\n  rimPull: number;\n};\n\nexport type RefractionMapOptions = {\n  devicePixelRatio?: number;\n  displacement?: number;\n  height: number;\n  radius: number;\n  strength: GeneratedRefractionStrength;\n  thickness?: number;\n  width: number;\n};\n\nexport type RefractionMapMetrics = {\n  cacheKey: string;\n  dispersion: number;\n  edgeBand: number;\n  /** Corner radius of the bend field, never tighter than the edge band. */\n  fieldRadius: number;\n  filterBlur: number;\n  filterPadding: number;\n  filterScale: number;\n  height: number;\n  mapHeight: number;\n  mapPixels: number;\n  mapWidth: number;\n  paddedHeight: number;\n  paddedWidth: number;\n  /** Inward offset at the rim, in CSS pixels. */\n  pull: number;\n  radius: number;\n  width: number;\n};\n\nexport type RefractionMapBuildResult = {\n  href: string;\n  metrics: RefractionMapMetrics;\n};\n\nexport const MAX_REFRACTION_MAP_PIXELS = 160000;\n\nconst REFRACTION_PRESETS: Record<GeneratedRefractionStrength, RefractionPreset> = {\n  soft: {\n    centerNeutralRatio: 0.68,\n    defaultDisplacement: 72,\n    edgeBandRatio: 0.18,\n    edgeWidth: 1.08,\n    rimPull: 0.22,\n  },\n  strong: {\n    centerNeutralRatio: 0.58,\n    defaultDisplacement: 112,\n    edgeBandRatio: 0.2,\n    edgeWidth: 1.22,\n    rimPull: 0.34,\n  },\n};\n\n/**\n * Scene blur under the rim, in CSS pixels. Chromium and Firefox sample the bend\n * nearest-neighbour, so the magnified rim would stair-step at 1x pixel density. The\n * map's blue channel keeps the blur off the flat centre.\n */\nconst RIM_BLUR = 0.8;\n\n/**\n * The bend falls off as (1 - t)^2 across the edge band, steepest at the rim. Any pull\n * under half the band keeps each sample further in than its outer neighbour, so the rim\n * compresses the view like thick glass and never mirrors it. The base pull is 0.34;\n * even the furthest colour stays below 0.4 to leave room for the map's 8-bit rounding.\n */\nconst MAX_RIM_PULL = 0.34;\n\nconst MAX_CHROMATIC_RIM_PULL = 0.4;\n\nconst refractionMapCache = new Map<string, string>();\n\nconst maxCachedRefractionMaps = 48;\n\nexport function clamp(value: number, min: number, max: number) {\n  return Math.min(max, Math.max(min, value));\n}\n\nexport function defaultRefractionDisplacement(strength: GeneratedRefractionStrength) {\n  return REFRACTION_PRESETS[strength].defaultDisplacement;\n}\n\nfunction smoothstep(edge0: number, edge1: number, value: number) {\n  const t = clamp((value - edge0) / Math.max(edge1 - edge0, 0.0001), 0, 1);\n\n  return t * t * (3 - 2 * t);\n}\n\n// Distance to a rounded rectangle plus its outward unit normal.\nfunction roundedRectEdge(x: number, y: number, width: number, height: number, radius: number) {\n  const px = x - width / 2;\n  const py = y - height / 2;\n  const qx = Math.abs(px) - (width / 2 - radius);\n  const qy = Math.abs(py) - (height / 2 - radius);\n  const signX = px < 0 ? -1 : 1;\n  const signY = py < 0 ? -1 : 1;\n\n  if (qx > 0 && qy > 0) {\n    const length = Math.hypot(qx, qy);\n\n    return {\n      distance: length - radius,\n      normalX: (signX * qx) / length,\n      normalY: (signY * qy) / length,\n    };\n  }\n\n  return qx > qy\n    ? { distance: qx - radius, normalX: signX, normalY: 0 }\n    : { distance: qy - radius, normalX: 0, normalY: signY };\n}\n\nfunction fitMapSize(width: number, height: number, pixelRatio: number) {\n  let mapWidth = clamp(Math.round(width * pixelRatio), 32, 1800);\n  let mapHeight = clamp(Math.round(height * pixelRatio), 32, 1200);\n\n  const pixels = mapWidth * mapHeight;\n\n  if (pixels <= MAX_REFRACTION_MAP_PIXELS) {\n    return { mapWidth, mapHeight };\n  }\n\n  const ratio = Math.sqrt(MAX_REFRACTION_MAP_PIXELS / pixels);\n  mapWidth = Math.max(32, Math.floor(mapWidth * ratio));\n  mapHeight = Math.max(32, Math.floor(mapHeight * ratio));\n\n  while (mapWidth * mapHeight > MAX_REFRACTION_MAP_PIXELS) {\n    if (mapWidth >= mapHeight) {\n      mapWidth -= 1;\n    } else {\n      mapHeight -= 1;\n    }\n  }\n\n  return { mapWidth, mapHeight };\n}\n\nexport function computeRefractionMapMetrics(options: RefractionMapOptions): RefractionMapMetrics {\n  const preset = REFRACTION_PRESETS[options.strength];\n  const width = Math.max(0, options.width);\n  const height = Math.max(0, options.height);\n  const minDimension = Math.min(width, height);\n  const radius = clamp(options.radius, 0, minDimension / 2);\n  const displacement = clamp(options.displacement ?? preset.defaultDisplacement, 0, 196);\n  const thickness = clamp(options.thickness ?? 1, 0.1, 4);\n  const rimWidth = 0.6 + 0.4 * thickness;\n  const dispersion = clamp((thickness - 1) * 0.2, 0, 0.6);\n  const blueFactor = 1 + dispersion / 2;\n  const neutralInset = (minDimension * (1 - preset.centerNeutralRatio)) / 2;\n\n  const edgeBand = clamp(\n    Math.min(minDimension * preset.edgeBandRatio, neutralInset) * preset.edgeWidth * rimWidth,\n    6,\n    Math.min(72 * rimWidth, (radius || 56) * 1.15 * rimWidth, minDimension * 0.48),\n  );\n\n  // Inside a tighter corner the inset contours would turn sharp and pinch the bend.\n  const fieldRadius = clamp(Math.max(radius, edgeBand), 0, minDimension / 2);\n\n  const pull =\n    edgeBand *\n    Math.min(\n      MAX_RIM_PULL,\n      MAX_CHROMATIC_RIM_PULL / blueFactor,\n      (preset.rimPull * displacement) / preset.defaultDisplacement,\n    );\n\n  // WebKit shifts a clipped, filtered scene unless the filter region reaches past the\n  // box as far as the primitives read: the blue pull plus three blur deviations.\n  const filterPadding = Math.ceil(pull * blueFactor + RIM_BLUR * 3) + 2;\n  const paddedWidth = width + filterPadding * 2;\n  const paddedHeight = height + filterPadding * 2;\n  const targetRatio = clamp((options.devicePixelRatio ?? 1) * 1.12, 1.15, 2.1);\n  const targetPixels = paddedWidth * paddedHeight * targetRatio * targetRatio;\n\n  const pixelRatio =\n    targetPixels > MAX_REFRACTION_MAP_PIXELS\n      ? Math.max(1, targetRatio * Math.sqrt(MAX_REFRACTION_MAP_PIXELS / targetPixels))\n      : targetRatio;\n\n  const { mapWidth, mapHeight } = fitMapSize(paddedWidth, paddedHeight, pixelRatio);\n\n  const cacheKey = [\n    mapWidth,\n    mapHeight,\n    Math.round(width),\n    Math.round(height),\n    Math.round(fieldRadius * 10),\n    Math.round(edgeBand * 10),\n    filterPadding,\n  ].join(':');\n\n  return {\n    cacheKey,\n    dispersion,\n    edgeBand,\n    fieldRadius,\n    filterBlur: RIM_BLUR,\n    filterPadding,\n    // Map channels hold a unit offset; the scale turns it into CSS pixels.\n    filterScale: Math.round(pull * 2 * 100) / 100,\n    height,\n    mapHeight,\n    mapPixels: mapWidth * mapHeight,\n    mapWidth,\n    paddedHeight,\n    paddedWidth,\n    pull,\n    radius,\n    width,\n  };\n}\n\n/**\n * Encodes a unit rim field: R/G = 0.5 + 0.5 * inward offset. Past the field's edge the\n * rim offset carries on unchanged, so the map stays smooth across the clipped border.\n * B is the share of softened scene: full through the outer half of the band, which\n * holds every point the steep rim samples, and gone by the flat centre.\n */\nexport async function buildRefractionMapDataUrl(\n  options: RefractionMapOptions,\n): Promise<RefractionMapBuildResult> {\n  const metrics = computeRefractionMapMetrics(options);\n  const cached = refractionMapCache.get(metrics.cacheKey);\n\n  if (cached) return { href: cached, metrics };\n\n  if (typeof document === 'undefined') {\n    throw new Error('Refraction maps require a browser document');\n  }\n\n  const canvas = document.createElement('canvas');\n  canvas.width = metrics.mapWidth;\n  canvas.height = metrics.mapHeight;\n\n  const ctx = canvas.getContext('2d');\n\n  if (!ctx) {\n    throw new Error('Canvas 2D context unavailable');\n  }\n\n  const {\n    edgeBand,\n    fieldRadius,\n    filterPadding,\n    height,\n    mapHeight,\n    mapWidth,\n    paddedHeight,\n    paddedWidth,\n    width,\n  } = metrics;\n\n  const data = new Uint8ClampedArray(mapWidth * mapHeight * 4);\n\n  for (let yIndex = 0; yIndex < mapHeight; yIndex += 1) {\n    const y = ((yIndex + 0.5) / mapHeight) * paddedHeight - filterPadding;\n\n    for (let xIndex = 0; xIndex < mapWidth; xIndex += 1) {\n      const x = ((xIndex + 0.5) / mapWidth) * paddedWidth - filterPadding;\n      const index = (yIndex * mapWidth + xIndex) * 4;\n      const edge = roundedRectEdge(x, y, width, height, fieldRadius);\n      const depth = clamp(-edge.distance / edgeBand, 0, 1);\n      const falloff = (1 - depth) ** 2;\n      const offsetX = -edge.normalX * falloff;\n      const offsetY = -edge.normalY * falloff;\n\n      data[index] = Math.round((0.5 + offsetX / 2) * 255);\n      data[index + 1] = Math.round((0.5 + offsetY / 2) * 255);\n      data[index + 2] = Math.round((1 - smoothstep(0.5, 1, depth)) * 255);\n      data[index + 3] = 255;\n    }\n  }\n\n  ctx.putImageData(new ImageData(data, mapWidth, mapHeight), 0, 0);\n  const href = canvas.toDataURL('image/png');\n  refractionMapCache.set(metrics.cacheKey, href);\n\n  if (refractionMapCache.size > maxCachedRefractionMaps) {\n    const oldestKey = refractionMapCache.keys().next().value;\n\n    if (oldestKey) refractionMapCache.delete(oldestKey);\n  }\n\n  return { href, metrics };\n}\n\n/** Corner radii in CSS pixels, clockwise from the top left. */\nexport type LensRadii = readonly [number, number, number, number];\n\nexport type LensMapOptions = {\n  /** Width of the curved rim in CSS pixels. */\n  bevel: number;\n  /** Multiplies how far the rim bends the backdrop. 0 leaves it flat. */\n  depth: number;\n  devicePixelRatio?: number;\n  height: number;\n  /** Refractive index of the glass. */\n  ior: number;\n  radii: LensRadii;\n  /** Height of the slab in CSS pixels. Thicker glass bends further. */\n  thickness: number;\n  width: number;\n};\n\nexport type LensMapMetrics = {\n  bevel: number;\n  cacheKey: string;\n  height: number;\n  mapHeight: number;\n  mapWidth: number;\n  /** The furthest the rim reads the backdrop from, in CSS pixels. */\n  reach: number;\n  width: number;\n};\n\nexport type LensMapBuildResult = {\n  href: string;\n  metrics: LensMapMetrics;\n};\n\nexport const MAX_LENS_MAP_PIXELS = 420000;\n\nexport const MAX_LENS_SURFACE_AREA = 720000;\n\n// The glass floats this far above its backdrop, as in the WebGL scene.\nconst LENS_AIR_GAP = 8;\n\n// The rim reads the backdrop from no further than this share of its width, and\n// the point it reads from advances by at least a quarter of every pixel across\n// the rim, so the view compresses toward the edge and never folds back on itself.\nconst LENS_REACH = 0.7;\n\nconst LENS_FOLD = 0.75;\n\nconst LENS_PROFILE_STEPS = 512;\n\n// The rim and the flat top are the same frosted glass, but smoked glass lets more\n// light through its rim. Blue holds the share of the rim's light: all of it near\n// the edge, none past four fifths of the rim or a sixth of the surface, whichever\n// comes first, so small pieces keep the top's shade in the middle.\nconst LENS_RIM_LIGHT = 0.8;\n\nconst LENS_RIM_LIGHT_SURFACE = 0.16;\n\nconst LENS_RIM_LIGHT_FALLOFF = 0.45;\n\nconst lensMapCache = new Map<string, LensMapBuildResult>();\n\nconst maxCachedLensMaps = 32;\n\n/**\n * How far inward a view ray lands on the backdrop after it crosses a slab of glass\n * whose top curves down into a rim, s pixels in from the edge. The top follows\n * (1 - (1 - u)^4)^(1/4) across the rim, Snell's law bends the ray into the glass and\n * again out of its flat underside, and it crosses an air gap to the backdrop. This is\n * the WebGL scene's `through()`, for a ray straight down.\n */\nfunction slabOffset(s: number, bevel: number, thickness: number, ior: number) {\n  const u = s / bevel;\n  const v = 1 - Math.max(u, 0.002);\n  let rise = 1;\n  let steepness = 0;\n\n  if (u < 1) {\n    const v4 = v ** 4;\n\n    rise = (1 - v4) ** 0.25;\n    steepness = v ** 3 * Math.max(1 - v4, 1e-6) ** -0.75;\n  }\n\n  const slope = Math.min(((0.7 * thickness) / bevel) * steepness, 12);\n  const height = thickness * (0.3 + 0.7 * rise);\n  const tilt = Math.hypot(slope, 1);\n  const sinTilt = slope / tilt;\n  const cosTilt = 1 / tilt;\n  const eta = 1 / ior;\n  const root = Math.sqrt(Math.max(1 - eta * eta * sinTilt * sinTilt, 0));\n  const bend = eta * cosTilt - root;\n  const along = bend * sinTilt;\n  const down = Math.max(eta - bend * cosTilt, 0.05);\n  const sinOut = ior * Math.abs(along);\n  const cosOut = Math.sqrt(Math.max(1 - sinOut * sinOut, 0));\n  const leaving = (ior * along) / Math.max(sinOut, 1);\n\n  return -(along * (height / down) + leaving * (LENS_AIR_GAP / Math.max(cosOut, 0.1)));\n}\n\n/**\n * The inward offset across the rim, from the edge to its inner boundary. The physical\n * offset is softly capped, then limited so it never falls faster than the pixels it\n * crosses, which would fold the view back on itself.\n */\nfunction lensProfile(bevel: number, thickness: number, ior: number, depth: number) {\n  const cap = LENS_REACH * bevel;\n  const step = bevel / LENS_PROFILE_STEPS;\n  const offsets = new Float32Array(LENS_PROFILE_STEPS + 1);\n\n  for (let index = 0; index < LENS_PROFILE_STEPS; index += 1) {\n    offsets[index] =\n      cap * Math.tanh((depth * slabOffset(index * step, bevel, thickness, ior)) / cap);\n  }\n\n  for (let index = LENS_PROFILE_STEPS - 1; index >= 0; index -= 1) {\n    offsets[index] = Math.min(offsets[index], offsets[index + 1] + LENS_FOLD * step);\n  }\n\n  return { offsets, reach: Math.max(...offsets, 0.01) };\n}\n\n// Distance to a rounded rectangle with its own radius in every corner, plus its outward unit normal.\nfunction roundedBoxEdge(x: number, y: number, width: number, height: number, radii: LensRadii) {\n  const px = x - width / 2;\n  const py = y - height / 2;\n  const radius = px < 0 ? (py < 0 ? radii[0] : radii[3]) : py < 0 ? radii[1] : radii[2];\n  const qx = Math.abs(px) - (width / 2 - radius);\n  const qy = Math.abs(py) - (height / 2 - radius);\n  const signX = px < 0 ? -1 : 1;\n  const signY = py < 0 ? -1 : 1;\n\n  if (qx > 0 && qy > 0) {\n    const length = Math.hypot(qx, qy);\n\n    return {\n      distance: length - radius,\n      normalX: (signX * qx) / length,\n      normalY: (signY * qy) / length,\n    };\n  }\n\n  return qx > qy\n    ? { distance: qx - radius, normalX: signX, normalY: 0 }\n    : { distance: qy - radius, normalX: 0, normalY: signY };\n}\n\nexport function computeLensMapMetrics(options: LensMapOptions) {\n  const width = Math.max(1, options.width);\n  const height = Math.max(1, options.height);\n  const bevel = clamp(options.bevel, 1, Math.min(width, height) / 2);\n  const pixelRatio = clamp(options.devicePixelRatio ?? 1, 1, 2);\n  let mapWidth = clamp(Math.round(width * pixelRatio), 32, 1800);\n  let mapHeight = clamp(Math.round(height * pixelRatio), 32, 1200);\n\n  if (mapWidth * mapHeight > MAX_LENS_MAP_PIXELS) {\n    const ratio = Math.sqrt(MAX_LENS_MAP_PIXELS / (mapWidth * mapHeight));\n\n    mapWidth = Math.max(32, Math.floor(mapWidth * ratio));\n    mapHeight = Math.max(32, Math.floor(mapHeight * ratio));\n  }\n\n  const cacheKey = [\n    mapWidth,\n    mapHeight,\n    Math.round(width),\n    Math.round(height),\n    ...options.radii.map((radius) => Math.round(radius * 10)),\n    Math.round(bevel * 10),\n    Math.round(options.thickness * 10),\n    Math.round(options.ior * 100),\n    Math.round(options.depth * 100),\n  ].join(':');\n\n  return { bevel, cacheKey, height, mapHeight, mapWidth, width };\n}\n\n/**\n * Encodes the rim of a slab of glass as a displacement field: R/G = 0.5 + 0.5 * the\n * offset the backdrop is read from, as a share of `reach`. The SVG feDisplacementMap\n * scale of twice `reach` turns it back into CSS pixels. B is the share of the rim's\n * light, which smoked glass dims less than its top. The field is flat past the rim.\n */\nexport function buildLensMapDataUrl(options: LensMapOptions): LensMapBuildResult {\n  const base = computeLensMapMetrics(options);\n  const cached = lensMapCache.get(base.cacheKey);\n\n  if (cached) {\n    lensMapCache.delete(base.cacheKey);\n    lensMapCache.set(base.cacheKey, cached);\n\n    return cached;\n  }\n\n  if (typeof document === 'undefined') {\n    throw new Error('Lens maps require a browser document');\n  }\n\n  const canvas = document.createElement('canvas');\n  canvas.width = base.mapWidth;\n  canvas.height = base.mapHeight;\n\n  const ctx = canvas.getContext('2d');\n\n  if (!ctx) {\n    throw new Error('Canvas 2D context unavailable');\n  }\n\n  const { bevel, height, mapHeight, mapWidth, width } = base;\n  const { offsets, reach } = lensProfile(bevel, options.thickness, options.ior, options.depth);\n\n  const litTo = Math.min(\n    LENS_RIM_LIGHT,\n    (LENS_RIM_LIGHT_SURFACE * Math.min(width, height)) / bevel,\n  );\n\n  const litFrom = litTo * LENS_RIM_LIGHT_FALLOFF;\n  const data = new Uint8ClampedArray(mapWidth * mapHeight * 4);\n\n  for (let yIndex = 0; yIndex < mapHeight; yIndex += 1) {\n    const y = ((yIndex + 0.5) / mapHeight) * height;\n\n    for (let xIndex = 0; xIndex < mapWidth; xIndex += 1) {\n      const x = ((xIndex + 0.5) / mapWidth) * width;\n      const index = (yIndex * mapWidth + xIndex) * 4;\n      const edge = roundedBoxEdge(x, y, width, height, options.radii);\n      const inset = Math.max(-edge.distance, 0);\n      let offset = 0;\n\n      if (inset < bevel) {\n        const position = (inset / bevel) * LENS_PROFILE_STEPS;\n        const lower = Math.floor(position);\n        const fraction = position - lower;\n\n        offset = offsets[lower] * (1 - fraction) + offsets[lower + 1] * fraction;\n      }\n\n      data[index] = Math.round(clamp(0.5 - (offset * edge.normalX) / (2 * reach), 0, 1) * 255);\n      data[index + 1] = Math.round(clamp(0.5 - (offset * edge.normalY) / (2 * reach), 0, 1) * 255);\n      data[index + 2] = Math.round((1 - smoothstep(litFrom, litTo, inset / bevel)) * 255);\n      data[index + 3] = 255;\n    }\n  }\n\n  ctx.putImageData(new ImageData(data, mapWidth, mapHeight), 0, 0);\n\n  const result = {\n    href: canvas.toDataURL('image/png'),\n    metrics: { ...base, reach },\n  };\n\n  lensMapCache.set(base.cacheKey, result);\n\n  if (lensMapCache.size > maxCachedLensMaps) {\n    const oldestKey = lensMapCache.keys().next().value;\n\n    if (oldestKey) lensMapCache.delete(oldestKey);\n  }\n\n  return result;\n}\n\nexport type MagnifierMapOptions = {\n  devicePixelRatio?: number;\n  size: number;\n};\n\nexport type MagnifierMapMetrics = {\n  /** Width of the ring that bends, in CSS pixels. */\n  band: number;\n  cacheKey: string;\n  /** Outward offset at the rim, in CSS pixels. */\n  depth: number;\n  mapSize: number;\n  size: number;\n};\n\nexport type MagnifierMapBuildResult = {\n  href: string;\n  metrics: MagnifierMapMetrics;\n};\n\nconst magnifierMapCache = new Map<string, string>();\n\nconst maxCachedMagnifierMaps = 8;\n\nexport function computeMagnifierMapMetrics(options: MagnifierMapOptions): MagnifierMapMetrics {\n  const size = Math.max(1, options.size);\n  const radius = size / 2;\n  const mapSize = clamp(Math.round(size * clamp(options.devicePixelRatio ?? 1, 1, 2)), 32, 400);\n\n  return {\n    band: radius * 0.46,\n    cacheKey: [mapSize, Math.round(size * 10)].join(':'),\n    depth: radius * 0.5,\n    mapSize,\n    size,\n  };\n}\n\n/**\n * Encodes an outward offset that grows toward the rim of a circle:\n * R/G = 0.5 + 0.5 * offset / depth. Each pixel samples a little further out than its\n * inner neighbour, so the view compresses into the rim like thick glass and never folds\n * back on itself, even over sharp text.\n */\nexport function buildMagnifierMapDataUrl(options: MagnifierMapOptions): MagnifierMapBuildResult {\n  const metrics = computeMagnifierMapMetrics(options);\n  const cached = magnifierMapCache.get(metrics.cacheKey);\n\n  if (cached) return { href: cached, metrics };\n\n  if (typeof document === 'undefined') {\n    throw new Error('Magnifier maps require a browser document');\n  }\n\n  const canvas = document.createElement('canvas');\n  canvas.width = metrics.mapSize;\n  canvas.height = metrics.mapSize;\n\n  const ctx = canvas.getContext('2d');\n\n  if (!ctx) {\n    throw new Error('Canvas 2D context unavailable');\n  }\n\n  const { band, mapSize, size } = metrics;\n  const radius = size / 2;\n  const data = new Uint8ClampedArray(mapSize * mapSize * 4);\n\n  for (let yIndex = 0; yIndex < mapSize; yIndex += 1) {\n    const dy = ((yIndex + 0.5) / mapSize) * size - radius;\n\n    for (let xIndex = 0; xIndex < mapSize; xIndex += 1) {\n      const dx = ((xIndex + 0.5) / mapSize) * size - radius;\n      const index = (yIndex * mapSize + xIndex) * 4;\n      const distance = Math.hypot(dx, dy) || 1;\n      // Flat in the centre, cubic through the ring so the bend starts without a crease.\n      const rim = clamp((distance - (radius - band)) / band, 0, 1) ** 3;\n\n      data[index] = Math.round((0.5 + (0.5 * rim * dx) / distance) * 255);\n      data[index + 1] = Math.round((0.5 + (0.5 * rim * dy) / distance) * 255);\n      data[index + 2] = 128;\n      data[index + 3] = 255;\n    }\n  }\n\n  ctx.putImageData(new ImageData(data, mapSize, mapSize), 0, 0);\n  const href = canvas.toDataURL('image/png');\n  magnifierMapCache.set(metrics.cacheKey, href);\n\n  if (magnifierMapCache.size > maxCachedMagnifierMaps) {\n    const oldestKey = magnifierMapCache.keys().next().value;\n\n    if (oldestKey) magnifierMapCache.delete(oldestKey);\n  }\n\n  return { href, metrics };\n}\n",
      "type": "registry:lib",
      "target": "components/glassyui/lib/refraction-map.ts"
    },
    {
      "path": "registry/glassyui/lib/use-refraction-filter.tsx",
      "content": "'use client';\n\nimport { useCallback, useEffect, useId, useMemo, useRef, useState, type RefObject } from 'react';\nimport {\n  buildLensMapDataUrl,\n  buildMagnifierMapDataUrl,\n  buildRefractionMapDataUrl,\n  clamp,\n  computeMagnifierMapMetrics,\n  MAX_LENS_SURFACE_AREA,\n  type GeneratedRefractionStrength,\n  type LensMapMetrics,\n  type LensRadii,\n  type MagnifierMapBuildResult,\n  type RefractionMapMetrics,\n} from './refraction-map';\n\ntype RefractionFilterOptions = {\n  displacement?: number;\n  enabled: boolean;\n  reduceTransparency?: boolean;\n  strength: GeneratedRefractionStrength;\n  thickness?: number;\n  viewportAligned?: boolean;\n};\n\ntype RefractionFilterState = {\n  href: string | null;\n  metrics: RefractionMapMetrics | null;\n};\n\nfunction isProductionRuntime() {\n  // SAFETY: only Node and bundler shims define `process`, so every step of the probe stays optional.\n  const runtime = globalThis as typeof globalThis & {\n    process?: { env?: { NODE_ENV?: string } };\n  };\n\n  return runtime.process?.env?.NODE_ENV === 'production';\n}\n\nfunction parseRadiusValue(value: string, width: number, height: number) {\n  const [first] = value.trim().split(/\\s+/);\n  const maxRadius = Math.min(width, height) / 2;\n\n  const parsed = Number.parseFloat(first);\n\n  if (first.endsWith('%')) {\n    return clamp((parsed / 100) * Math.min(width, height), 0, maxRadius);\n  }\n\n  return clamp(Number.isFinite(parsed) ? parsed : maxRadius, 0, maxRadius);\n}\n\nfunction readUniformRadius(el: HTMLElement, width: number, height: number) {\n  const style = window.getComputedStyle(el);\n\n  const rawRadii = [\n    style.borderTopLeftRadius,\n    style.borderTopRightRadius,\n    style.borderBottomRightRadius,\n    style.borderBottomLeftRadius,\n  ];\n\n  const radii = rawRadii.map((value) => parseRadiusValue(value, width, height));\n  const first = radii[0];\n  const mixed = radii.some((radius) => Math.abs(radius - first) > 0.5);\n\n  return mixed ? null : first;\n}\n\nfunction readCornerRadii(style: CSSStyleDeclaration, width: number, height: number): LensRadii {\n  return [\n    parseRadiusValue(style.borderTopLeftRadius, width, height),\n    parseRadiusValue(style.borderTopRightRadius, width, height),\n    parseRadiusValue(style.borderBottomRightRadius, width, height),\n    parseRadiusValue(style.borderBottomLeftRadius, width, height),\n  ];\n}\n\n/**\n * A scroll event reaches script a frame after the compositor has scrolled, so a scene moved from\n * script trails the page. Chromium filters a composited layer on its compositor, which lets a\n * scroll-driven animation carry the scene in step with the scroll and still bend it. WebKit leaves\n * a scene on its own compositing layer out of the SVG filter and Gecko has no scroll timelines, so\n * both keep moving the scene from script. Only Chromium has userAgentData.\n */\nfunction supportsCompositedSceneScroll() {\n  return (\n    typeof ViewTimeline !== 'undefined' &&\n    typeof navigator !== 'undefined' &&\n    'userAgentData' in navigator\n  );\n}\n\n// Across the timeline the scene moves down exactly as far as the scroll carries the surface up.\nconst sceneScrollKeyframes = (span: number): Keyframe[] => [\n  { translate: '0px 0px' },\n  { translate: `0px ${span}px` },\n];\n\n/**\n * A view timeline places its subject where it would sit if nothing were fixed or stuck, so it runs\n * on while such a surface stands still. Those surfaces keep following the scroll from script.\n */\nfunction standsStillWhileScrolling(el: Element, source: Element) {\n  for (let node: Element | null = el; node && node !== source; node = node.parentElement) {\n    const { position } = window.getComputedStyle(node);\n\n    if (position === 'fixed' || position === 'sticky') {\n      return true;\n    }\n  }\n\n  return false;\n}\n\nexport function useRefractionFilter(\n  surfaceRef: RefObject<HTMLElement | null>,\n  {\n    displacement,\n    enabled,\n    reduceTransparency = false,\n    strength,\n    thickness,\n    viewportAligned = false,\n  }: RefractionFilterOptions,\n) {\n  const rawId = useId().replaceAll(':', '');\n  const filterId = `lg-refraction-${rawId}`;\n  const generationRef = useRef(0);\n  const warnedMixedRadiusRef = useRef(false);\n  const [state, setState] = useState<RefractionFilterState>({ href: null, metrics: null });\n  const sceneRef = useRef<HTMLDivElement | null>(null);\n  // Set while the scene is kept aligned, so a scene that mounts is aligned before it first paints.\n  const alignSceneNowRef = useRef<(() => void) | null>(null);\n\n  const setScene = useCallback((node: HTMLDivElement | null) => {\n    sceneRef.current = node;\n    alignSceneNowRef.current?.();\n  }, []);\n\n  useEffect(() => {\n    if (!enabled || reduceTransparency || !viewportAligned) {\n      return;\n    }\n\n    const el = surfaceRef.current;\n\n    if (!el) {\n      return;\n    }\n\n    let alignmentFrame = 0;\n    let disposed = false;\n    // Aligning a scene that no one can see only costs a layout, a style pass and a repaint on\n    // every scroll frame, so it waits until the surface is within a quarter screen of the viewport.\n    let nearViewport = true;\n\n    const timeline = supportsCompositedSceneScroll()\n      ? new ViewTimeline({ axis: 'block', subject: el })\n      : null;\n\n    let sceneScroll: Animation | null = null;\n    let sceneScrollSpan = Number.NaN;\n\n    // Keeps the scene's scroll-driven animation spanning the timeline and returns how far along\n    // the block axis it has carried the scene, which the offset below then leaves out. Past\n    // either end of the timeline the animation holds still, but the unclamped distance keeps the\n    // offset constant, so plain scrolling rewrites nothing and the scene never waits on script.\n    const followScroll = () => {\n      const scene = sceneRef.current;\n      const source = timeline?.source ?? null;\n\n      if (\n        !timeline ||\n        !scene ||\n        !source ||\n        timeline.currentTime === null ||\n        standsStillWhileScrolling(el, source)\n      ) {\n        sceneScroll?.cancel();\n        sceneScroll = null;\n\n        return 0;\n      }\n\n      const start = timeline.startOffset.to('px').value;\n      const span = timeline.endOffset.to('px').value - start;\n\n      if (sceneScroll?.effect instanceof KeyframeEffect && sceneScroll.effect.target === scene) {\n        if (span !== sceneScrollSpan) {\n          sceneScroll.effect.setKeyframes(sceneScrollKeyframes(span));\n        }\n      } else {\n        sceneScroll?.cancel();\n        sceneScroll = scene.animate(sceneScrollKeyframes(span), { fill: 'both', timeline });\n      }\n\n      sceneScrollSpan = span;\n\n      return source.scrollTop - start;\n    };\n\n    const writeProperty = (property: string, value: string) => {\n      if (el.style.getPropertyValue(property) !== value) {\n        el.style.setProperty(property, value);\n      }\n    };\n\n    const alignScene = () => {\n      alignmentFrame = 0;\n\n      if (disposed) {\n        return;\n      }\n\n      const followed = followScroll();\n      const rect = el.getBoundingClientRect();\n      writeProperty('--lg-refraction-scene-x', `${-(rect.left + el.clientLeft)}px`);\n      writeProperty('--lg-refraction-scene-y', `${-(rect.top + el.clientTop) - followed}px`);\n      writeProperty('--lg-refraction-scene-width', `${document.documentElement.clientWidth}px`);\n      writeProperty('--lg-refraction-scene-height', `${document.documentElement.clientHeight}px`);\n    };\n\n    const scheduleAlignment = () => {\n      if (nearViewport && !alignmentFrame) {\n        alignmentFrame = requestAnimationFrame(alignScene);\n      }\n    };\n\n    alignSceneNowRef.current = () => {\n      cancelAnimationFrame(alignmentFrame);\n      alignScene();\n    };\n\n    const visibilityObserver =\n      typeof IntersectionObserver === 'undefined'\n        ? null\n        : new IntersectionObserver(\n            (entries) => {\n              const wasNear = nearViewport;\n              nearViewport = entries[entries.length - 1]?.isIntersecting ?? true;\n\n              // Coming into range aligns at once, in this task. A frame asked for from here would run\n              // after whatever the page queued for the next frame, and that could read the scene\n              // where it still was.\n              if (nearViewport && !wasNear) {\n                cancelAnimationFrame(alignmentFrame);\n                alignScene();\n              }\n            },\n            { rootMargin: '25% 0px' },\n          );\n\n    visibilityObserver?.observe(el);\n\n    const resizeObserver =\n      typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(scheduleAlignment);\n\n    for (let ancestor: HTMLElement | null = el; ancestor; ancestor = ancestor.parentElement) {\n      resizeObserver?.observe(ancestor);\n    }\n\n    const readExternalStyleSignature = () => {\n      const declarations: string[] = [];\n\n      for (let index = 0; index < el.style.length; index += 1) {\n        const property = el.style.item(index);\n\n        if (property.startsWith('--lg-refraction-scene-')) {\n          continue;\n        }\n\n        declarations.push(\n          `${property}:${el.style.getPropertyValue(property)}:${el.style.getPropertyPriority(property)}`,\n        );\n      }\n\n      return declarations.sort().join(';');\n    };\n\n    let externalStyleSignature = readExternalStyleSignature();\n\n    const mutationObserver =\n      typeof MutationObserver === 'undefined'\n        ? null\n        : new MutationObserver((records) => {\n            const hasExternalMutation = records.some((record) => {\n              if (\n                record.type === 'attributes' &&\n                record.target === el &&\n                record.attributeName === 'style'\n              ) {\n                const nextExternalStyleSignature = readExternalStyleSignature();\n\n                if (nextExternalStyleSignature === externalStyleSignature) {\n                  return false;\n                }\n\n                externalStyleSignature = nextExternalStyleSignature;\n              }\n\n              return true;\n            });\n\n            if (hasExternalMutation) {\n              scheduleAlignment();\n            }\n          });\n\n    mutationObserver?.observe(document.body, {\n      attributeFilter: ['class', 'hidden', 'open', 'style'],\n      attributes: true,\n      characterData: true,\n      childList: true,\n      subtree: true,\n    });\n    document.addEventListener('scroll', scheduleAlignment, {\n      capture: true,\n      passive: true,\n    });\n    document.addEventListener('load', scheduleAlignment, {\n      capture: true,\n      passive: true,\n    });\n    document.fonts?.addEventListener('loadingdone', scheduleAlignment);\n    window.addEventListener('resize', scheduleAlignment, { passive: true });\n    window.visualViewport?.addEventListener('resize', scheduleAlignment, { passive: true });\n    window.visualViewport?.addEventListener('scroll', scheduleAlignment, { passive: true });\n    scheduleAlignment();\n\n    return () => {\n      disposed = true;\n      alignSceneNowRef.current = null;\n      cancelAnimationFrame(alignmentFrame);\n      sceneScroll?.cancel();\n      resizeObserver?.disconnect();\n      mutationObserver?.disconnect();\n      visibilityObserver?.disconnect();\n      document.removeEventListener('scroll', scheduleAlignment, { capture: true });\n      document.removeEventListener('load', scheduleAlignment, { capture: true });\n      document.fonts?.removeEventListener('loadingdone', scheduleAlignment);\n      window.removeEventListener('resize', scheduleAlignment);\n      window.visualViewport?.removeEventListener('resize', scheduleAlignment);\n      window.visualViewport?.removeEventListener('scroll', scheduleAlignment);\n      el.style.removeProperty('--lg-refraction-scene-height');\n      el.style.removeProperty('--lg-refraction-scene-width');\n      el.style.removeProperty('--lg-refraction-scene-x');\n      el.style.removeProperty('--lg-refraction-scene-y');\n    };\n  }, [enabled, reduceTransparency, surfaceRef, viewportAligned]);\n\n  useEffect(() => {\n    generationRef.current += 1;\n    const generation = generationRef.current;\n\n    if (!enabled || reduceTransparency || typeof ResizeObserver === 'undefined') {\n      queueMicrotask(() => {\n        if (generationRef.current === generation) {\n          setState({ href: null, metrics: null });\n        }\n      });\n\n      return;\n    }\n\n    const el = surfaceRef.current;\n\n    if (!el) {\n      queueMicrotask(() => {\n        if (generationRef.current === generation) {\n          setState({ href: null, metrics: null });\n        }\n      });\n\n      return;\n    }\n\n    let raf = 0;\n    let disposed = false;\n\n    const clear = () => {\n      if (generationRef.current === generation) {\n        setState({ href: null, metrics: null });\n      }\n    };\n\n    const schedule = () => {\n      cancelAnimationFrame(raf);\n      raf = requestAnimationFrame(() => {\n        const rect = el.getBoundingClientRect();\n        const width = Math.max(0, rect.width);\n        const height = Math.max(0, rect.height);\n\n        if (!width || !height) {\n          clear();\n\n          return;\n        }\n\n        const radius = readUniformRadius(el, width, height);\n\n        if (radius === null) {\n          if (!isProductionRuntime() && !warnedMixedRadiusRef.current) {\n            console.warn(\n              'Glassy UI supplied-background refraction currently supports uniform rounded rectangles only.',\n            );\n            warnedMixedRadiusRef.current = true;\n          }\n\n          clear();\n\n          return;\n        }\n\n        void buildRefractionMapDataUrl({\n          devicePixelRatio: window.devicePixelRatio || 1,\n          displacement,\n          height,\n          radius,\n          strength,\n          thickness,\n          width,\n        })\n          .then((result) => {\n            if (!disposed && generationRef.current === generation) {\n              setState({ href: result.href, metrics: result.metrics });\n            }\n          })\n          .catch(() => {\n            if (!disposed) {\n              clear();\n            }\n          });\n      });\n    };\n\n    const observer = new ResizeObserver(schedule);\n    observer.observe(el);\n    schedule();\n\n    return () => {\n      disposed = true;\n      cancelAnimationFrame(raf);\n      observer.disconnect();\n    };\n  }, [displacement, enabled, reduceTransparency, strength, surfaceRef, thickness]);\n\n  const filterElement = useMemo(() => {\n    if (!state.href || !state.metrics) return null;\n\n    const {\n      dispersion,\n      filterBlur,\n      filterPadding,\n      filterScale,\n      height,\n      paddedHeight,\n      paddedWidth,\n      width,\n    } = state.metrics;\n\n    // Every colour follows the same two-axis rim normal, so the sides split as much as the ends.\n    const channel = (name: string, matrix: string, factor: number) => (\n      <>\n        <feDisplacementMap\n          in=\"scene\"\n          in2=\"map\"\n          scale={Math.round(filterScale * factor * 100) / 100}\n          xChannelSelector=\"R\"\n          yChannelSelector=\"G\"\n          result={`${name}-scene`}\n        />\n        <feColorMatrix in={`${name}-scene`} type=\"matrix\" values={matrix} result={`${name}-bent`} />\n      </>\n    );\n\n    return (\n      <svg aria-hidden=\"true\" className=\"lg-refraction-filter\" focusable=\"false\" viewBox=\"0 0 0 0\">\n        <defs>\n          <filter\n            id={filterId}\n            x={-filterPadding}\n            y={-filterPadding}\n            width={paddedWidth}\n            height={paddedHeight}\n            filterUnits=\"userSpaceOnUse\"\n            primitiveUnits=\"userSpaceOnUse\"\n            colorInterpolationFilters=\"sRGB\"\n          >\n            <feGaussianBlur in=\"SourceGraphic\" stdDeviation={filterBlur} result=\"blurred\" />\n            <feImage\n              href={state.href}\n              x={-filterPadding}\n              y={-filterPadding}\n              width={width + filterPadding * 2}\n              height={height + filterPadding * 2}\n              preserveAspectRatio=\"none\"\n              result=\"map\"\n            />\n            {/* The map's blue channel softens only the scene the rim magnifies. */}\n            <feColorMatrix\n              in=\"map\"\n              type=\"matrix\"\n              values=\"0 0 0 0 0  0 0 0 0 0  0 0 0 0 0  0 0 1 0 0\"\n              result=\"rim\"\n            />\n            <feComposite in=\"blurred\" in2=\"rim\" operator=\"in\" result=\"softRim\" />\n            <feComposite in=\"softRim\" in2=\"SourceGraphic\" operator=\"over\" result=\"scene\" />\n            {dispersion > 0 ? (\n              <>\n                {channel('red', '1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1', 1 - dispersion / 2)}\n                {channel('green', '0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 1', 1)}\n                {channel('blue', '0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 1', 1 + dispersion / 2)}\n                <feBlend in=\"red-bent\" in2=\"green-bent\" mode=\"screen\" result=\"red-green\" />\n                <feBlend in=\"red-green\" in2=\"blue-bent\" mode=\"screen\" result=\"rgb\" />\n                {/* Combine opaque colour, then restore coverage once so translucent scenes stay translucent. */}\n                <feComposite in=\"rgb\" in2=\"green-scene\" operator=\"in\" />\n              </>\n            ) : (\n              <feDisplacementMap\n                in=\"scene\"\n                in2=\"map\"\n                scale={filterScale}\n                xChannelSelector=\"R\"\n                yChannelSelector=\"G\"\n              />\n            )}\n          </filter>\n        </defs>\n      </svg>\n    );\n  }, [filterId, state.href, state.metrics]);\n\n  return {\n    active: Boolean(state.href && state.metrics),\n    filterElement,\n    filterStyle: state.href ? `url(#${filterId})` : undefined,\n    sceneRef: setScene,\n  };\n}\n\ntype LensFilterOptions = {\n  enabled: boolean;\n  /** Changes whenever theme, mode, quality or variant tokens may have changed. */\n  signature?: string;\n};\n\ntype LensFilterState = {\n  blur: number;\n  bodyBrightness: number;\n  dispersion: number;\n  href: string;\n  metrics: LensMapMetrics;\n  rimBrightness: number;\n  saturation: number;\n};\n\n/**\n * Only Chromium renders SVG references inside backdrop-filter. WebKit and\n * Gecko drop the whole declaration when url() is present, and CSS.supports()\n * reports true everywhere, so the engine is detected explicitly.\n */\nfunction supportsBackdropLens() {\n  return (\n    typeof navigator !== 'undefined' &&\n    'userAgentData' in navigator &&\n    typeof ResizeObserver !== 'undefined'\n  );\n}\n\nfunction readNumberToken(style: CSSStyleDeclaration, name: string) {\n  const value = Number.parseFloat(style.getPropertyValue(name));\n\n  return Number.isFinite(value) ? value : 0;\n}\n\nfunction readTokenOr(style: CSSStyleDeclaration, name: string, fallback: number) {\n  const value = Number.parseFloat(style.getPropertyValue(name));\n\n  return Number.isFinite(value) ? value : fallback;\n}\n\nfunction sameLens(previous: LensFilterState | null, next: LensFilterState) {\n  return (\n    previous !== null &&\n    previous.href === next.href &&\n    previous.blur === next.blur &&\n    previous.bodyBrightness === next.bodyBrightness &&\n    previous.dispersion === next.dispersion &&\n    previous.rimBrightness === next.rimBrightness &&\n    previous.saturation === next.saturation\n  );\n}\n\n// A rim as wide as the corners are round, up to a little wider than the WebGL scene's default.\nconst LENS_BEVEL = 36;\n\n// Slab height per pixel of rim, as in the scene (40 px of glass under a 28 px rim).\nconst LENS_THICKNESS_RATIO = 1.4;\n\n// The least frost the lens reads through, so bent edges never stair-step.\nconst MIN_FROST = 0.5;\n\n/**\n * Chromium-only lens for glass over live content. The backdrop is frosted, then read\n * through a slab of glass whose top curves down into a rounded rim, so content bends\n * and splits into colour along the edge and lies flat everywhere else. The\n * --lg-lens-* tokens shape it: depth, bevel, thickness, ior, dispersion, blur and\n * saturation, and --lg-glass-thickness scales the whole slab at once. Other engines,\n * zero depth and oversized surfaces keep the CSS glass.\n */\nexport function useLensFilter(\n  surfaceRef: RefObject<HTMLElement | null>,\n  { enabled, signature }: LensFilterOptions,\n) {\n  const rawId = useId().replaceAll(':', '');\n  const filterId = `lg-lens-${rawId}`;\n  const generationRef = useRef(0);\n  const [state, setState] = useState<LensFilterState | null>(null);\n\n  useEffect(() => {\n    generationRef.current += 1;\n    const generation = generationRef.current;\n    const el = surfaceRef.current;\n\n    if (!enabled || !el || !supportsBackdropLens()) {\n      queueMicrotask(() => {\n        if (generationRef.current === generation) {\n          setState(null);\n        }\n      });\n\n      return;\n    }\n\n    let frame = 0;\n    let disposed = false;\n\n    const measure = () => {\n      frame = 0;\n\n      if (disposed || generationRef.current !== generation) {\n        return;\n      }\n\n      // The lens layer fills the padding box, so borders and scrollbars stay out of the map.\n      const width = el.clientWidth;\n      const height = el.clientHeight;\n      const style = window.getComputedStyle(el);\n      const depth = readNumberToken(style, '--lg-lens-depth');\n\n      if (depth <= 0 || !width || !height || width * height > MAX_LENS_SURFACE_AREA) {\n        setState(null);\n\n        return;\n      }\n\n      const radii = readCornerRadii(style, width, height);\n      const roundest = Math.max(...radii);\n      const multiplier = clamp(readTokenOr(style, '--lg-glass-thickness', 1), 0.1, 4);\n      const bevelToken = readNumberToken(style, '--lg-lens-bevel');\n      const thicknessToken = readNumberToken(style, '--lg-lens-thickness');\n      const baseBevel = bevelToken > 0 ? bevelToken : clamp(roundest, 6, LENS_BEVEL);\n      // A thicker slab also has a wider rim, though not in proportion.\n      const bevel = clamp(baseBevel * (0.6 + 0.4 * multiplier), 4, Math.min(width, height) / 2);\n\n      const thickness =\n        (thicknessToken > 0 ? thicknessToken : baseBevel * LENS_THICKNESS_RATIO) * multiplier;\n\n      const { href, metrics } = buildLensMapDataUrl({\n        bevel,\n        depth,\n        devicePixelRatio: window.devicePixelRatio || 1,\n        height,\n        ior: Math.max(1, readTokenOr(style, '--lg-lens-ior', 1.5)),\n        radii,\n        thickness,\n        width,\n      });\n\n      const next = {\n        blur: Math.max(0, readNumberToken(style, '--lg-lens-blur')),\n        bodyBrightness: clamp(readTokenOr(style, '--lg-lens-body-brightness', 1), 0, 1),\n        dispersion: clamp(readNumberToken(style, '--lg-lens-dispersion') * multiplier, 0, 1),\n        href,\n        metrics,\n        rimBrightness: clamp(readTokenOr(style, '--lg-lens-rim-brightness', 1), 0, 1),\n        saturation: readNumberToken(style, '--lg-lens-saturation') || 1,\n      };\n\n      setState((previous) => (sameLens(previous, next) ? previous : next));\n    };\n\n    const schedule = () => {\n      if (!frame) {\n        frame = requestAnimationFrame(measure);\n      }\n    };\n\n    const observer = new ResizeObserver(schedule);\n    observer.observe(el);\n    schedule();\n\n    return () => {\n      disposed = true;\n      cancelAnimationFrame(frame);\n      observer.disconnect();\n    };\n  }, [enabled, signature, surfaceRef]);\n\n  const filterElement = useMemo(() => {\n    if (!state) return null;\n\n    const { blur, bodyBrightness, dispersion, href, metrics, rimBrightness, saturation } = state;\n    // The rim is the same frosted glass as the flat top: it bends and splits what the frost\n    // leaves and is never clearer, so content does not show through a sharp band at the edge.\n    const frost = Math.max(blur, MIN_FROST);\n    const padding = Math.ceil(frost * 3) + 2;\n    // Map channels hold a unit offset; the scale turns it into CSS pixels.\n    const scale = Math.round(2 * metrics.reach * 100) / 100;\n    const split = dispersion > 0.005;\n\n    // Each colour is read through the rim on its own, red bending least and blue most, then the three add back up.\n    const channel = (name: string, matrix: string, factor: number) => (\n      <>\n        <feColorMatrix in=\"frost\" type=\"matrix\" values={matrix} result={`${name}-only`} />\n        <feDisplacementMap\n          in={`${name}-only`}\n          in2=\"lens\"\n          scale={Math.round(scale * factor * 100) / 100}\n          xChannelSelector=\"R\"\n          yChannelSelector=\"G\"\n          result={`${name}-bent`}\n        />\n      </>\n    );\n\n    // Smoked glass darkens the flat top more than its rim, which lets the light through. A\n    // primitive is added only where it changes something: each one is another pass on the GPU.\n    const dim = (input: string, brightness: number, result: string) =>\n      brightness < 1 ? (\n        <feComponentTransfer in={input} result={result}>\n          <feFuncR type=\"linear\" slope={brightness} />\n          <feFuncG type=\"linear\" slope={brightness} />\n          <feFuncB type=\"linear\" slope={brightness} />\n        </feComponentTransfer>\n      ) : null;\n\n    // The map's blue channel weights the rim's light over the top's. Where both let the same\n    // light through, the bent frost alone is the glass.\n    const shaded = rimBrightness !== bodyBrightness;\n    const rimSource = rimBrightness < 1 ? 'rim-lit' : 'bent';\n    const topSource = bodyBrightness < 1 ? 'top-lit' : 'bent';\n    const glass = shaded ? 'mixed' : topSource;\n\n    return (\n      <svg aria-hidden=\"true\" className=\"lg-refraction-filter\" focusable=\"false\" viewBox=\"0 0 0 0\">\n        <defs>\n          <filter\n            id={filterId}\n            x={-padding}\n            y={-padding}\n            width={metrics.width + padding * 2}\n            height={metrics.height + padding * 2}\n            filterUnits=\"userSpaceOnUse\"\n            primitiveUnits=\"userSpaceOnUse\"\n            colorInterpolationFilters=\"sRGB\"\n          >\n            <feGaussianBlur in=\"SourceGraphic\" stdDeviation={frost} result=\"frost\" />\n            <feImage\n              href={href}\n              x={0}\n              y={0}\n              width={metrics.width}\n              height={metrics.height}\n              preserveAspectRatio=\"none\"\n              result=\"lens\"\n            />\n            {split ? (\n              <>\n                {channel('red', '1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0', 1 - dispersion / 2)}\n                {channel('green', '0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 1 0', 1)}\n                {channel('blue', '0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 1 0', 1 + dispersion / 2)}\n                <feBlend in=\"red-bent\" in2=\"green-bent\" mode=\"screen\" result=\"red-green\" />\n                <feBlend in=\"red-green\" in2=\"blue-bent\" mode=\"screen\" result=\"bent\" />\n              </>\n            ) : (\n              <feDisplacementMap\n                in=\"frost\"\n                in2=\"lens\"\n                scale={scale}\n                xChannelSelector=\"R\"\n                yChannelSelector=\"G\"\n                result=\"bent\"\n              />\n            )}\n            {shaded ? (\n              <>\n                <feColorMatrix\n                  in=\"lens\"\n                  type=\"matrix\"\n                  values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0\"\n                  result=\"rim\"\n                />\n                {dim('bent', rimBrightness, 'rim-lit')}\n                {dim('bent', bodyBrightness, 'top-lit')}\n                <feComposite in={rimSource} in2=\"rim\" operator=\"in\" result=\"rim-glass\" />\n                {/* The rim, weighted by its mask, over the top, which is opaque: the mask's\n                    complement is implied, so the top needs no mask of its own. */}\n                <feComposite in=\"rim-glass\" in2={topSource} operator=\"over\" result=\"mixed\" />\n              </>\n            ) : (\n              dim('bent', bodyBrightness, 'top-lit')\n            )}\n            {saturation === 1 ? null : (\n              <feColorMatrix in={glass} type=\"saturate\" values={String(saturation)} />\n            )}\n          </filter>\n        </defs>\n      </svg>\n    );\n  }, [filterId, state]);\n\n  return {\n    active: state !== null,\n    filterElement,\n    filterStyle: state ? `url(#${filterId})` : undefined,\n  };\n}\n\ntype MagnifierFilterOptions = {\n  enabled: boolean;\n  /** Diameter of the round lens in CSS pixels. */\n  size: number;\n};\n\n// Softens the compressed rim just enough to keep sharp edges from stair-stepping.\nconst MAGNIFIER_BLUR = 0.35;\n\n/**\n * Rim refraction for the round magnifier, applied with `filter` to its zoomed view, which\n * every engine with SVG filters on HTML supports. Near the rim each pixel samples content\n * from further out, so the view bends into the edge like a glass bead.\n *\n * The view extends `margin` past the lens on every side so the rim has content to sample.\n * WebKit only lines the filtered view up with the map when the view clips its own overflow\n * and the filter region reaches past it by as far as the primitives can read, so the\n * region is the view plus another margin and the map sits one margin in.\n */\nexport function useMagnifierFilter({ enabled, size }: MagnifierFilterOptions) {\n  const rawId = useId().replaceAll(':', '');\n  const filterId = `lg-magnifier-${rawId}`;\n  const [map, setMap] = useState<MagnifierMapBuildResult | null>(null);\n  // The rim samples up to `depth` past the lens, and the blur reads 3 deviations further.\n  const margin = Math.ceil(computeMagnifierMapMetrics({ size }).depth + MAGNIFIER_BLUR * 3) + 2;\n\n  useEffect(() => {\n    let disposed = false;\n\n    // The map is drawn on a canvas, so it can only be built in the browser.\n    queueMicrotask(() => {\n      if (disposed) return;\n\n      setMap(\n        enabled\n          ? buildMagnifierMapDataUrl({ devicePixelRatio: window.devicePixelRatio || 1, size })\n          : null,\n      );\n    });\n\n    return () => {\n      disposed = true;\n    };\n  }, [enabled, size]);\n\n  const filterElement = useMemo(() => {\n    if (!map) return null;\n\n    const { depth, size: mapCssSize } = map.metrics;\n\n    return (\n      <svg aria-hidden=\"true\" className=\"lg-refraction-filter\" focusable=\"false\" viewBox=\"0 0 0 0\">\n        <defs>\n          <filter\n            id={filterId}\n            x={-margin}\n            y={-margin}\n            width={mapCssSize + margin * 4}\n            height={mapCssSize + margin * 4}\n            filterUnits=\"userSpaceOnUse\"\n            primitiveUnits=\"userSpaceOnUse\"\n            colorInterpolationFilters=\"sRGB\"\n          >\n            <feGaussianBlur in=\"SourceGraphic\" stdDeviation={MAGNIFIER_BLUR} result=\"soft\" />\n            <feImage\n              href={map.href}\n              x={margin}\n              y={margin}\n              width={mapCssSize}\n              height={mapCssSize}\n              preserveAspectRatio=\"none\"\n              result=\"bend\"\n            />\n            <feDisplacementMap\n              in=\"soft\"\n              in2=\"bend\"\n              scale={depth * 2}\n              xChannelSelector=\"R\"\n              yChannelSelector=\"G\"\n            />\n          </filter>\n        </defs>\n      </svg>\n    );\n  }, [filterId, map, margin]);\n\n  return {\n    active: map !== null,\n    filterElement,\n    filterStyle: map ? `url(#${filterId})` : undefined,\n    margin,\n  };\n}\n",
      "type": "registry:hook",
      "target": "components/glassyui/lib/use-refraction-filter.tsx"
    },
    {
      "path": "registry/glassyui/lib/scene-context.ts",
      "content": "'use client';\n\nimport { createContext, useContext, useLayoutEffect } from 'react';\nimport type { RefObject } from 'react';\n\nexport type GlassSceneRegistry = {\n  elements: ReadonlySet<HTMLElement>;\n  register: (element: HTMLElement) => () => void;\n  subscribe: (listener: () => void) => () => void;\n};\n\n// The registry exists before any effect runs, because children register in\n// their effects and those run before the scene's own effect.\nexport function createGlassSceneRegistry(): GlassSceneRegistry {\n  const elements = new Set<HTMLElement>();\n  const listeners = new Set<() => void>();\n  const notify = () => listeners.forEach((listener) => listener());\n\n  return {\n    elements,\n    register(element) {\n      elements.add(element);\n      notify();\n\n      return () => {\n        elements.delete(element);\n        notify();\n      };\n    },\n    subscribe(listener) {\n      listeners.add(listener);\n\n      return () => {\n        listeners.delete(listener);\n      };\n    },\n  };\n}\n\nexport const GlassSceneContext = createContext<GlassSceneRegistry | null>(null);\n\n/**\n * Lets a glass element hand its material to the nearest GlassScene. Outside a\n * scene it does nothing. It registers before paint, so glass that mounts into\n * a live scene never shows its CSS material first.\n */\nexport function useGlassScenePane(ref: RefObject<HTMLElement | null>, enabled = true) {\n  const registry = useContext(GlassSceneContext);\n\n  useLayoutEffect(() => {\n    const element = ref.current;\n\n    if (!registry || !enabled || !element) return;\n\n    return registry.register(element);\n  }, [enabled, ref, registry]);\n}\n",
      "type": "registry:lib",
      "target": "components/glassyui/lib/scene-context.ts"
    }
  ],
  "meta": {
    "sourceModules": [
      "button"
    ]
  },
  "type": "registry:ui"
}