{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "liquid-glass",
  "title": "Liquid Glass",
  "description": "Low-level refractive glass wrapper used by liquid-refract components.",
  "registryDependencies": [
    "@glasscn/utils"
  ],
  "files": [
    {
      "path": "components/ui/glasscn/liquid-glass.tsx",
      "content": "\"use client\";\n\nimport {\n  type CSSProperties,\n  type HTMLAttributes,\n  forwardRef,\n  useCallback,\n  useEffect,\n  useId,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\ntype MapGeometry = {\n  width: number; // element width, CSS px, integer\n  height: number; // element height, CSS px, integer\n  radius: number; // corner radius, CSS px, already clamped to min(width, height) / 2\n  bezel: number; // fraction (0..1] of the half-min-dimension used as the refractive band\n};\n\nconst MAX_TEXTURE_SIZE = 480;\nconst EDGE_TAPER_PX = 1.25;\n\nconst displacementMapCache = new Map<string, string>();\n\n// CSS.supports(\"backdrop-filter\", \"url(#f)\") returns true in Safari and\n// Firefox (they parse the value) but neither browser actually renders SVG\n// filters through backdrop-filter. UA sniffing is the only reliable approach.\n// When WebKit ships support (track WebKit bug 245510), loosen this function.\nfunction supportsSvgBackdropFilter() {\n  if (typeof navigator === \"undefined\") return false;\n  const ua = navigator.userAgent;\n  const isChromium = /Chrom(e|ium)/.test(ua) || /Edg\\//.test(ua);\n  return isChromium && !/Firefox/.test(ua);\n}\n\n// NOTE: Per-corner radii and percentage radii are NOT supported. The map\n// reads only borderTopLeftRadius in px. If a future consumer needs\n// asymmetric corners, extend the corner SDF branch to accept four radii.\n//\n// NOTE: Map regeneration renders a ≤480 px texture and encodes a PNG for\n// each unique (width, height, radius, bezel). Continuously animating the\n// element's SIZE will thrash the cache; animate transform: scale instead.\nfunction createDisplacementMap({ width, height, radius, bezel }: MapGeometry) {\n  const cacheKey = `${width}:${height}:${radius}:${bezel}`;\n  const cached = displacementMapCache.get(cacheKey);\n  if (cached !== undefined) return cached;\n\n  const downscale = Math.min(1, MAX_TEXTURE_SIZE / Math.max(width, height));\n  const texWidth = Math.max(2, Math.round(width * downscale));\n  const texHeight = Math.max(2, Math.round(height * downscale));\n\n  const canvas = document.createElement(\"canvas\");\n  canvas.width = texWidth;\n  canvas.height = texHeight;\n  const ctx = canvas.getContext(\"2d\");\n  if (!ctx) return \"\";\n\n  const image = ctx.createImageData(texWidth, texHeight);\n  const data = image.data;\n  const halfWidth = width / 2;\n  const halfHeight = height / 2;\n  const r = Math.min(radius, halfWidth, halfHeight);\n  const bezelPx = Math.max(1, bezel * Math.min(halfWidth, halfHeight));\n\n  for (let ty = 0; ty < texHeight; ty += 1) {\n    for (let tx = 0; tx < texWidth; tx += 1) {\n      // Texel center in element coordinates, origin at element center.\n      const px = ((tx + 0.5) / texWidth) * width - halfWidth;\n      const py = ((ty + 0.5) / texHeight) * height - halfHeight;\n\n      // Signed distance + outward normal of the rounded-rectangle contour.\n      const qx = Math.abs(px) - (halfWidth - r);\n      const qy = Math.abs(py) - (halfHeight - r);\n      let signedDistance: number;\n      let dirX = 0;\n      let dirY = 0;\n      if (qx > 0 && qy > 0) {\n        const len = Math.hypot(qx, qy);\n        signedDistance = len - r;\n        dirX = (Math.sign(px) * qx) / len;\n        dirY = (Math.sign(py) * qy) / len;\n      } else if (qx > qy) {\n        signedDistance = qx - r;\n        dirX = Math.sign(px);\n      } else {\n        signedDistance = qy - r;\n        dirY = Math.sign(py);\n      }\n\n      const index = (ty * texWidth + tx) * 4;\n      const inside = -signedDistance; // px from the contour, positive inside\n\n      if (inside <= 0) {\n        data[index] = 128;\n        data[index + 1] = 128;\n        data[index + 2] = 128;\n        data[index + 3] = 255;\n        continue;\n      }\n\n      const t = Math.min(1, inside / bezelPx); // 0 at the edge → 1 at the bezel's inner end\n      let magnitude = 1 - convexSquircle(t); // max refraction at the edge, optically flat interior\n      magnitude *= Math.min(1, inside / EDGE_TAPER_PX); // avoid sampling artifacts on the outermost pixels\n\n      data[index] = Math.round(128 + dirX * magnitude * 127);\n      data[index + 1] = Math.round(128 + dirY * magnitude * 127);\n      data[index + 2] = 128;\n      data[index + 3] = 255;\n    }\n  }\n\n  ctx.putImageData(image, 0, 0);\n  const mapUrl = canvas.toDataURL(\"image/png\");\n  displacementMapCache.set(cacheKey, mapUrl);\n  return mapUrl;\n}\n\nfunction convexSquircle(x: number) {\n  return Math.pow(1 - Math.pow(1 - x, 4), 0.25);\n}\n\nexport type LiquidGlassProps = HTMLAttributes<HTMLDivElement> & {\n  /** Extra blur mixed into the backdrop-filter after the SVG refraction. */\n  blur?: number;\n  /** SVG displacement strength. Higher values bend the sampled backdrop more. */\n  refraction?: number;\n  /**\n   * @deprecated Texture size now derives from element size. Accepted for\n   * backwards-compatibility but has no effect.\n   */\n  mapSize?: number;\n  /** Fraction of the radius used as the curved edge where most refraction happens. */\n  bezel?: number;\n  /** Backdrop saturation. */\n  saturation?: number;\n};\n\nexport const LiquidGlass = forwardRef<HTMLDivElement, LiquidGlassProps>(function LiquidGlass(\n  {\n    blur = 2,\n    refraction = 15,\n    mapSize: _mapSize,\n    bezel = 0.34,\n    saturation = 1.28,\n    className,\n    style,\n    children,\n    ...props\n  },\n  ref,\n) {\n  const rawId = useId();\n  const filterId = useMemo(() => `liquid-glass-${rawId.replace(/:/g, \"\")}`, [rawId]);\n\n  // -------------------------------------------------------------------------\n  // Ref merge — we need the DOM node to measure geometry while still\n  // forwarding the ref to the consumer.\n  // -------------------------------------------------------------------------\n\n  const localRef = useRef<HTMLDivElement | null>(null);\n  const setRefs = useCallback(\n    (node: HTMLDivElement | null) => {\n      localRef.current = node;\n      if (typeof ref === \"function\") ref(node);\n      else if (ref) ref.current = node;\n    },\n    [ref],\n  );\n\n  // -------------------------------------------------------------------------\n  // Geometry measurement via ResizeObserver\n  // -------------------------------------------------------------------------\n\n  const [geometry, setGeometry] = useState<MapGeometry | null>(null);\n\n  useEffect(() => {\n    const el = localRef.current;\n    if (!el) return;\n    const measure = () => {\n      const rect = el.getBoundingClientRect();\n      const width = Math.round(rect.width);\n      const height = Math.round(rect.height);\n      if (!width || !height) return;\n      const parsed = Number.parseFloat(getComputedStyle(el).borderTopLeftRadius);\n      const radius = Math.min(Number.isFinite(parsed) ? parsed : Math.min(width, height) / 2, width / 2, height / 2);\n      setGeometry((prev) =>\n        prev && prev.width === width && prev.height === height && prev.radius === radius && prev.bezel === bezel\n          ? prev\n          : { width, height, radius, bezel },\n      );\n    };\n    measure();\n    const observer = new ResizeObserver(measure);\n    observer.observe(el);\n    return () => observer.disconnect();\n  }, [bezel]);\n\n  const mapUrl = useMemo(() => (geometry ? createDisplacementMap(geometry) : \"\"), [geometry]);\n\n  // -------------------------------------------------------------------------\n  // Browser capability gate\n  //\n  // Both SSR and the first client render take the fallback branch so hydration\n  // never mismatches. The state is set in an effect (client-only).\n  // -------------------------------------------------------------------------\n\n  const [supported, setSupported] = useState(false);\n\n  useEffect(() => {\n    setSupported(supportsSvgBackdropFilter());\n  }, []);\n\n  const refractionActive = supported && mapUrl !== \"\";\n  const backdropFilter = refractionActive\n    ? `url(#${filterId}) blur(${blur}px) saturate(${saturation})`\n    : `blur(${blur + 2}px) saturate(${saturation})`;\n\n  return (\n    <>\n      <div\n        ref={setRefs}\n        className={cn(\n          \"relative overflow-hidden rounded-full bg-white/[0.08]\",\n          className,\n        )}\n        style={{ ...style, backdropFilter, WebkitBackdropFilter: backdropFilter } as CSSProperties}\n        {...props}\n      >\n        {children}\n        <span\n          aria-hidden\n          className=\"pointer-events-none absolute inset-0 rounded-[inherit]\"\n          style={{\n            // Rim thickness, overridable per-consumer via --liquid-glass-rim-width.\n            padding: \"var(--liquid-glass-rim-width, 0.5px)\",\n            background:\n              // iOS 27 liquid-glass rim. Each gradient runs ALONG its edges and\n              // fades to nothing at the corners, uniform in between:\n              //   - to right  → white streak on the top + bottom runs\n              //   - to bottom → dark streak on the left + right runs\n              // Streak colors are overridable per-consumer via the\n              // --liquid-glass-rim-light / --liquid-glass-rim-dark variables;\n              // --liquid-glass-rim-fade sets how far from each corner the\n              // streak takes to reach full strength (smaller = longer streak).\n              \"linear-gradient(to right, rgba(255,255,255,0), var(--liquid-glass-rim-light, rgba(255,255,255,0.25)) var(--liquid-glass-rim-fade, 18%), var(--liquid-glass-rim-light, rgba(255,255,255,0.25)) calc(100% - var(--liquid-glass-rim-fade, 18%)), rgba(255,255,255,0)), \" +\n              \"linear-gradient(to bottom, rgba(0,0,0,0), var(--liquid-glass-rim-dark, rgba(0,0,0,0.2)) var(--liquid-glass-rim-fade, 18%), var(--liquid-glass-rim-dark, rgba(0,0,0,0.2)) calc(100% - var(--liquid-glass-rim-fade, 18%)), rgba(0,0,0,0))\",\n            WebkitMask: \"linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0)\",\n            WebkitMaskComposite: \"xor\",\n            mask: \"linear-gradient(#000 0 0) content-box exclude, linear-gradient(#000 0 0)\",\n          }}\n        />\n      </div>\n\n      {refractionActive && (\n        <svg className=\"absolute size-0 overflow-hidden\" aria-hidden>\n          <filter id={filterId} x=\"0\" y=\"0\" width=\"100%\" height=\"100%\" colorInterpolationFilters=\"sRGB\">\n            <feImage href={mapUrl} x=\"0\" y=\"0\" width=\"100%\" height=\"100%\" preserveAspectRatio=\"none\" result=\"map\" />\n            <feDisplacementMap\n              in=\"SourceGraphic\"\n              in2=\"map\"\n              scale={refraction}\n              xChannelSelector=\"R\"\n              yChannelSelector=\"G\"\n              result=\"displaced\"\n            />\n            <feGaussianBlur in=\"displaced\" stdDeviation=\"0.15\" />\n          </filter>\n        </svg>\n      )}\n    </>\n  );\n});\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:component"
}