{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "glass-refraction",
  "type": "registry:ui",
  "title": "Glass Refraction",
  "description": "A liquid-glass pane that bends the content behind it, with a specular rim that leans toward the pointer.",
  "author": "WANSTUDIO <https://wanstudio.my>",
  "dependencies": [],
  "files": [
    {
      "path": "registry/wancraft/glass-refraction.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nexport type GlassSurface = \"lens\" | \"liquid\";\n\nexport interface GlassRefractionProps extends React.ComponentPropsWithoutRef<\"div\"> {\n  /** Refraction strength: the displacement scale in px (0 turns it off). */\n  distortion?: number;\n  /** Frost, in px. The fallback path (no SVG backdrop filters) adds 8px so it still reads as glass. */\n  blur?: number;\n  /** Strength of the surface tint, 0–1 (white on light themes, near-black on dark ones). */\n  tint?: number;\n  /** Specular edge highlight intensity, 0–1. */\n  edge?: number;\n  /** Corner radius, in px. Use half the size for a round lens. */\n  radius?: number;\n  /** `lens`: a convex bezel that bends content near the rim. `liquid`: a turbulent, rippled pane. */\n  surface?: GlassSurface;\n  /** Move the specular highlight toward the pointer (mouse and pen). */\n  followPointer?: boolean;\n  /** Overrides the theme tint colour (any CSS colour). */\n  tintColor?: string;\n}\n\n/* ---------------------------- Feature detection ---------------------------- */\n\nconst noopSubscribe = () => () => {};\n\n/**\n * `backdrop-filter: url(#svg-filter)` only renders in Chromium (Chrome, Edge, Opera, Arc…).\n * Firefox and Safari parse or ignore it without refracting, so they get the frosted fallback.\n */\nfunction supportsSvgBackdrop() {\n  if (typeof CSS === \"undefined\" || !CSS.supports(\"backdrop-filter\", \"url(#a)\")) return false;\n  const brands = (navigator as Navigator & { userAgentData?: { brands?: { brand: string }[] } }).userAgentData?.brands;\n  if (brands) return brands.some((b) => b.brand === \"Chromium\");\n  return /\\bChrome\\/\\d+/.test(navigator.userAgent) && !/Firefox|FxiOS|CriOS|EdgiOS/.test(navigator.userAgent);\n}\n\n/* ----------------------------- Reduced motion ------------------------------ */\n\nfunction subscribeReducedMotion(callback: () => void) {\n  const mq = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n  mq.addEventListener(\"change\", callback);\n  return () => mq.removeEventListener(\"change\", callback);\n}\n\n/** SSR-safe: hydrates as \"no preference\", then follows the media query. */\nfunction usePrefersReducedMotion() {\n  return React.useSyncExternalStore(\n    subscribeReducedMotion,\n    () => window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches,\n    () => false,\n  );\n}\n\n/* ------------------------------ Lens map ------------------------------------ */\n\nconst MAP_SCALE = 0.5; // the displacement map is drawn at half resolution and stretched\n\n/**\n * Builds a displacement map for a rounded rectangle: R/G encode an x/y offset (128 = none).\n * Inside a bezel band along the rim, samples are pulled from further inside the panel — strongest\n * at the edge, easing to zero — so content bends and magnifies toward the rim like thick glass.\n */\nfunction buildLensMap(width: number, height: number, radius: number): string {\n  const w = Math.max(2, Math.round(width * MAP_SCALE));\n  const h = Math.max(2, Math.round(height * MAP_SCALE));\n  const canvas = document.createElement(\"canvas\");\n  canvas.width = w;\n  canvas.height = h;\n  const ctx = canvas.getContext(\"2d\");\n  if (!ctx) return \"\";\n  const img = ctx.createImageData(w, h);\n  const r = Math.min(radius * MAP_SCALE, w / 2, h / 2);\n  const bezel = Math.max(6, Math.min(w, h) * 0.22);\n  const hw = w / 2;\n  const hh = h / 2;\n  for (let y = 0; y < h; y++) {\n    for (let x = 0; x < w; x++) {\n      // Signed distance to the rounded rect (negative inside) and its outward normal.\n      const px = x + 0.5 - hw;\n      const py = y + 0.5 - hh;\n      const qx = Math.abs(px) - (hw - r);\n      const qy = Math.abs(py) - (hh - r);\n      let nx: number;\n      let ny: number;\n      let dist: number;\n      if (qx > 0 && qy > 0) {\n        const len = Math.sqrt(qx * qx + qy * qy) || 1;\n        dist = len - r;\n        nx = qx / len;\n        ny = qy / len;\n      } else if (qx > qy) {\n        dist = qx - r;\n        nx = 1;\n        ny = 0;\n      } else {\n        dist = qy - r;\n        nx = 0;\n        ny = 1;\n      }\n      nx *= Math.sign(px) || 1;\n      ny *= Math.sign(py) || 1;\n      const depth = -dist; // 0 at the rim, growing inward\n      const t = depth < bezel ? 1 - Math.max(0, depth) / bezel : 0;\n      const m = t * t * (3 - 2 * t); // smooth falloff from rim to the flat centre\n      const o = (y * w + x) * 4;\n      // Pull samples inward: offset opposite to the outward normal.\n      img.data[o] = 128 - nx * m * 127;\n      img.data[o + 1] = 128 - ny * m * 127;\n      img.data[o + 2] = 128;\n      img.data[o + 3] = 255;\n    }\n  }\n  ctx.putImageData(img, 0, 0);\n  return canvas.toDataURL();\n}\n\n/* -------------------------------- Component -------------------------------- */\n\nconst ringMask: React.CSSProperties = {\n  // Keeps only the 1.5px padding ring: the content box is cut out of the full box.\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/**\n * A \"liquid glass\" pane that refracts whatever sits behind it. In Chromium the backdrop runs\n * through an inline SVG filter — feDisplacementMap driven by a generated bezel map (`lens`) or by\n * feTurbulence noise (`liquid`) — then a light blur and saturation. Other browsers get a frosted\n * `backdrop-filter: blur()` fallback. A specular rim highlight leans toward the pointer, and the\n * children render crisply on top, untouched by the filter.\n */\nexport function GlassRefraction({\n  distortion = 40,\n  blur = 2,\n  tint = 0.2,\n  edge = 0.6,\n  radius = 24,\n  surface = \"lens\",\n  followPointer = true,\n  tintColor,\n  className = \"\",\n  style,\n  children,\n  ...props\n}: GlassRefractionProps) {\n  const reduceMotion = usePrefersReducedMotion();\n  const svgBackdrop = React.useSyncExternalStore(noopSubscribe, supportsSvgBackdrop, () => false);\n  const rawId = React.useId();\n  const filterId = `glass-${rawId.replace(/[^a-zA-Z0-9_-]/g, \"\")}`;\n  const rootRef = React.useRef<HTMLDivElement>(null);\n  const [box, setBox] = React.useState({ width: 0, height: 0 });\n  const [lensMap, setLensMap] = React.useState(\"\");\n\n  const refract = svgBackdrop && distortion > 0;\n  const w = Math.round(box.width);\n  const h = Math.round(box.height);\n\n  // Track the pane's size: the SVG filter works in pixels, so it must match the element.\n  React.useEffect(() => {\n    const el = rootRef.current;\n    if (!el) return;\n    const ro = new ResizeObserver(([entry]) => {\n      const { width, height } = entry.contentRect;\n      setBox((prev) => (Math.abs(prev.width - width) < 1 && Math.abs(prev.height - height) < 1 ? prev : { width, height }));\n    });\n    ro.observe(el);\n    return () => ro.disconnect();\n  }, []);\n\n  // Regenerate the bezel map (only on the Chromium path, for the lens surface).\n  React.useEffect(() => {\n    if (!refract || surface !== \"lens\" || w < 2 || h < 2) return;\n    const frame = requestAnimationFrame(() => setLensMap(buildLensMap(w, h, radius)));\n    return () => cancelAnimationFrame(frame);\n  }, [refract, surface, w, h, radius]);\n\n  // Specular highlight: CSS variables updated from the window pointer, one write per frame.\n  React.useEffect(() => {\n    const el = rootRef.current;\n    if (!el || !followPointer || reduceMotion) return;\n    let raf = 0;\n    let cx = 0;\n    let cy = 0;\n    const apply = () => {\n      raf = 0;\n      const rect = el.getBoundingClientRect();\n      if (!rect.width || !rect.height) return;\n      // Clamp a little outside the pane, so the light slides along the rim nearest the pointer.\n      const x = Math.min(1.15, Math.max(-0.15, (cx - rect.left) / rect.width));\n      const y = Math.min(1.15, Math.max(-0.15, (cy - rect.top) / rect.height));\n      el.style.setProperty(\"--gr-x\", `${(x * 100).toFixed(1)}%`);\n      el.style.setProperty(\"--gr-y\", `${(y * 100).toFixed(1)}%`);\n    };\n    const onMove = (e: PointerEvent) => {\n      if (e.pointerType === \"touch\") return;\n      cx = e.clientX;\n      cy = e.clientY;\n      if (!raf && !document.hidden) raf = requestAnimationFrame(apply);\n    };\n    let listening = false;\n    const io = new IntersectionObserver(([entry]) => {\n      if (entry.isIntersecting && !listening) window.addEventListener(\"pointermove\", onMove, { passive: true });\n      if (!entry.isIntersecting && listening) window.removeEventListener(\"pointermove\", onMove);\n      listening = entry.isIntersecting;\n    });\n    io.observe(el);\n    return () => {\n      io.disconnect();\n      window.removeEventListener(\"pointermove\", onMove);\n      cancelAnimationFrame(raf);\n      el.style.removeProperty(\"--gr-x\");\n      el.style.removeProperty(\"--gr-y\");\n    };\n  }, [followPointer, reduceMotion]);\n\n  const frost = Math.max(0, blur);\n  const fallback = `blur(${frost + 8}px) saturate(1.6)`;\n  const backdrop = refract ? `url(#${filterId}) blur(${frost}px) saturate(1.5)` : fallback;\n  const tintAmount = `${Math.round(Math.min(1, Math.max(0, tint)) * 100)}%`;\n  const shine = Math.min(1, Math.max(0, edge));\n  const mapReady = surface === \"liquid\" || lensMap !== \"\";\n\n  return (\n    <div\n      ref={rootRef}\n      className={`relative ${className}`}\n      style={{\n        borderRadius: radius,\n        [\"--gr-x\" as string]: \"28%\",\n        [\"--gr-y\" as string]: \"0%\",\n        [\"--gr-edge\" as string]: shine,\n        [\"--gr-tint\" as string]: tintAmount,\n        ...(tintColor ? { [\"--gr-tint-color\" as string]: tintColor } : null),\n        ...style,\n      }}\n      data-refraction={refract ? \"svg\" : \"frost\"}\n      {...props}\n    >\n      {refract && w > 1 && h > 1 && (\n        <svg aria-hidden=\"true\" width=\"0\" height=\"0\" className=\"absolute\">\n          <filter id={filterId} colorInterpolationFilters=\"sRGB\">\n            {surface === \"lens\" ? (\n              lensMap && <feImage href={lensMap} x=\"0\" y=\"0\" width={w} height={h} preserveAspectRatio=\"none\" result=\"map\" />\n            ) : (\n              <>\n                <feTurbulence type=\"fractalNoise\" baseFrequency={(1.6 / Math.max(80, Math.min(w, h))).toFixed(4)} numOctaves={2} seed={7} result=\"noise\" />\n                <feGaussianBlur in=\"noise\" stdDeviation=\"2\" result=\"map\" />\n              </>\n            )}\n            <feDisplacementMap in=\"SourceGraphic\" in2=\"map\" scale={distortion} xChannelSelector=\"R\" yChannelSelector=\"G\" />\n          </filter>\n        </svg>\n      )}\n\n      {/* The glass: refracted (or frosted) backdrop plus a theme tint. */}\n      <div\n        aria-hidden=\"true\"\n        className=\"pointer-events-none absolute inset-0 overflow-hidden [--gr-tint-fallback:#ffffff] dark:[--gr-tint-fallback:#0c0e10]\"\n        style={{\n          borderRadius: \"inherit\",\n          backdropFilter: mapReady ? backdrop : fallback,\n          // Older Safari only knows the prefixed property; Chromium must not get a second value.\n          WebkitBackdropFilter: refract ? undefined : fallback,\n          backgroundColor: \"color-mix(in oklab, var(--gr-tint-color, var(--gr-tint-fallback)) var(--gr-tint), transparent)\",\n        }}\n      />\n\n      {/* Rim: a fixed hairline plus inner light and shade, then a soft drop shadow. */}\n      <div\n        aria-hidden=\"true\"\n        className=\"pointer-events-none absolute inset-0 border border-white/45 shadow-[inset_0_1px_0_rgb(255_255_255/0.55),inset_0_-1px_1px_rgb(0_0_0/0.08),0_18px_40px_-18px_rgb(0_0_0/0.35)] dark:border-white/[0.14] dark:shadow-[inset_0_1px_0_rgb(255_255_255/0.16),inset_0_-1px_1px_rgb(0_0_0/0.4),0_22px_50px_-20px_rgb(0_0_0/0.7)]\"\n        style={{ borderRadius: \"inherit\" }}\n      />\n\n      {/* Specular: a bright ring segment, a softer inner rim glow and a faint sheen, all centred on the light. */}\n      <div\n        aria-hidden=\"true\"\n        className=\"pointer-events-none absolute inset-0 p-[1.5px]\"\n        style={{\n          ...ringMask,\n          borderRadius: \"inherit\",\n          opacity: \"var(--gr-edge)\",\n          background: \"radial-gradient(60% 70% at var(--gr-x) var(--gr-y), rgb(255 255 255 / 0.95), rgb(255 255 255 / 0.25) 45%, transparent 75%)\",\n        }}\n      />\n      <div\n        aria-hidden=\"true\"\n        className=\"pointer-events-none absolute inset-0 p-[7px] blur-[3px]\"\n        style={{\n          ...ringMask,\n          borderRadius: \"inherit\",\n          opacity: \"calc(var(--gr-edge) * 0.55)\",\n          background: \"radial-gradient(55% 65% at var(--gr-x) var(--gr-y), rgb(255 255 255 / 0.7), transparent 70%)\",\n        }}\n      />\n      <div\n        aria-hidden=\"true\"\n        className=\"pointer-events-none absolute inset-0\"\n        style={{\n          borderRadius: \"inherit\",\n          opacity: \"calc(var(--gr-edge) * 0.5)\",\n          background: \"radial-gradient(70% 55% at var(--gr-x) var(--gr-y), rgb(255 255 255 / 0.22), transparent 70%)\",\n        }}\n      />\n\n      {/* Content stays crisp: the filters only ever touch the backdrop. */}\n      <div className=\"relative h-full w-full\">{children}</div>\n    </div>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "backgrounds"
  ],
  "docs": "Documentation: https://wancraft.my/components/glass-refraction"
}
