{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "liquid-metal",
  "title": "Liquid Metal",
  "description": "A WebGL metal surface behind a button or chip, with reflections that drift so the control reads as a real, premium material.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "utils",
    "https://miseui.junsu4621.workers.dev/r/motion.json"
  ],
  "files": [
    {
      "path": "registry/miseui/ui/liquid-metal.tsx",
      "content": "import type * as React from \"react\"\nimport { useEffect, useRef, useState } from \"react\"\nimport { useReducedMotion } from \"motion/react\"\nimport { clampUnit } from \"@/registry/miseui/lib/motion\"\nimport { cn } from \"@/registry/miseui/lib/utils\"\n\ntype LiquidMetalTint = \"silver\" | \"mise\"\ntype Rgb = readonly [number, number, number]\n\nconst DEFAULT_INTENSITY = 0.7\nconst DEFAULT_SPEED = 1\n/** Retina is plenty for a soft reflection; above that the shader cost grows with nothing to show for it. */\nconst MAX_PIXEL_RATIO = 2\nconst MILLISECONDS_PER_SECOND = 1000\nconst CHANNEL_MAX = 255\nconst MISE_TOKEN = \"--mise\"\n/** Slightly cool silver, and light-theme --mise for when the token cannot be resolved (no 2D canvas). */\nconst SILVER_RGB: Rgb = [0.86, 0.87, 0.9]\nconst MISE_FALLBACK_RGB: Rgb = [0.93, 0.6, 0.27]\n\n/** One triangle covering clip space; no vertex buffer needed. */\nconst VERTEX_SHADER = `#version 300 es\nvoid main() {\n  gl_Position = vec4(vec2((gl_VertexID & 1) * 4 - 1, (gl_VertexID & 2) * 2 - 1), 0.0, 1.0);\n}`\n\n/**\n * Two octaves of value noise warp each other and drift, like a room sliding across a curved surface. The field\n * becomes cosine bands, bright and dark (environment streaks), over a sky-to-ground gradient, and the edges catch\n * extra light, a cheap stand-in for Fresnel on a bevel. Cosines and quintic noise matter: a triangle wave or\n * cubic fade leaves creases. The coordinates lean over and the warp stays small because a button is one noise\n * cell tall: stronger warp folds the field into hard vertical lines, while streaks that run along the surface read as chrome.\n */\nconst FRAGMENT_SHADER = `#version 300 es\nprecision highp float;\nout vec4 fragColor;\nuniform vec2 u_resolution;\nuniform float u_time;\nuniform float u_intensity;\nuniform vec3 u_tint;\nfloat hash(vec2 p) {\n  p = fract(p * vec2(123.34, 456.21));\n  p += dot(p, p + 45.32);\n  return fract(p.x * p.y);\n}\nfloat valueNoise(vec2 p) {\n  vec2 cell = floor(p);\n  vec2 f = fract(p);\n  vec2 u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);\n  float a = hash(cell), b = hash(cell + vec2(1.0, 0.0));\n  float c = hash(cell + vec2(0.0, 1.0)), d = hash(cell + vec2(1.0, 1.0));\n  return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\nfloat fbm(vec2 p) {\n  return valueNoise(p) * 0.7 + valueNoise(p * 2.0 + 7.3) * 0.3;\n}\nvoid main() {\n  vec2 uv = gl_FragCoord.xy / u_resolution;\n  float aspect = u_resolution.x / u_resolution.y;\n  vec2 p = vec2(uv.x * aspect, uv.y);\n  float t = u_time;\n  vec2 q = vec2(p.x * 0.45 + p.y * 0.3, p.y * 1.25 - p.x * 0.1);\n  float warp = fbm(q * 0.9 + vec2(t * 0.1, -t * 0.07));\n  float field = fbm(q * 1.2 + vec2(warp * 0.55 - t * 0.14, warp * 0.3 + t * 0.08));\n  float band = pow(0.5 + 0.5 * cos(6.2832 * (field * 1.5 + t * 0.03)), 2.5);\n  float dark = pow(0.5 + 0.5 * cos(6.2832 * (field * 0.9 - t * 0.02) + 1.7), 2.0);\n  float sky = 0.74 + 0.3 * uv.y;\n  float edge = min(min(uv.x, 1.0 - uv.x) * aspect, min(uv.y, 1.0 - uv.y));\n  float fresnel = pow(1.0 - smoothstep(0.0, 0.5, edge), 2.0);\n  float shade = sky * (0.76 + 0.1 * field) - u_intensity * 0.16 * dark + u_intensity * (0.36 * band + 0.2 * fresnel);\n  fragColor = vec4(u_tint * shade + vec3(0.24) * u_intensity * band, 1.0);\n}`\n\ninterface LiquidMetalProps extends React.ComponentProps<\"div\"> {\n  /** The control to dress: a button, chip or icon with a transparent background. */\n  children: React.ReactNode\n  /** 0..1, how strong the moving highlights are. Values outside the range are clamped. */\n  intensity?: number\n  /** Flow speed multiplier; 1 is a slow, calm drift. */\n  speed?: number\n  /** \"silver\" is neutral metal; \"mise\" reads the --mise token so the metal takes the accent colour. */\n  tint?: LiquidMetalTint\n}\n\ntype MetalOptions = Required<Pick<LiquidMetalProps, \"intensity\" | \"speed\" | \"tint\">> & { enabled: boolean; reduceMotion: boolean }\n\nlet webgl2Probe: boolean | undefined\n/** Probed once per page: WebGL2 availability does not change between renders. */\nfunction supportsWebGL2() {\n  if (webgl2Probe === undefined) {\n    const probe = typeof document === \"undefined\" ? null : document.createElement(\"canvas\").getContext(\"webgl2\")\n    webgl2Probe = Boolean(probe)\n    probe?.getExtension(\"WEBGL_lose_context\")?.loseContext()\n  }\n  return webgl2Probe\n}\n\n/** Resolves any CSS colour (oklch included) to 0..1 RGB by painting it on a 1 px canvas. */\nfunction resolveCssColor(value: string): Rgb | null {\n  const ctx = document.createElement(\"canvas\").getContext(\"2d\")\n  if (!ctx || !value) return null\n  ctx.fillStyle = \"transparent\"\n  ctx.fillStyle = value\n  ctx.fillRect(0, 0, 1, 1)\n  const [r, g, b, alpha] = ctx.getImageData(0, 0, 1, 1).data\n  return alpha === 0 ? null : [r / CHANNEL_MAX, g / CHANNEL_MAX, b / CHANNEL_MAX]\n}\n\nfunction tintRgb(host: HTMLElement, tint: LiquidMetalTint): Rgb {\n  if (tint === \"silver\") return SILVER_RGB\n  return resolveCssColor(getComputedStyle(host).getPropertyValue(MISE_TOKEN).trim()) ?? MISE_FALLBACK_RGB\n}\n\nfunction createProgram(gl: WebGL2RenderingContext) {\n  const program = gl.createProgram()\n  if (!program) return null\n  const stages = [[gl.VERTEX_SHADER, VERTEX_SHADER], [gl.FRAGMENT_SHADER, FRAGMENT_SHADER]] as const\n  for (const [type, source] of stages) {\n    const shader = gl.createShader(type)\n    if (!shader) return null\n    gl.shaderSource(shader, source)\n    gl.compileShader(shader)\n    if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) return null\n    gl.attachShader(program, shader)\n  }\n  gl.linkProgram(program)\n  return gl.getProgramParameter(program, gl.LINK_STATUS) ? program : null\n}\n\n/** Owns the GL context: draws once, then loops only while the canvas is on screen and motion is allowed. */\nfunction useMetalCanvas(\n  canvasRef: React.RefObject<HTMLCanvasElement | null>,\n  hostRef: React.RefObject<HTMLDivElement | null>,\n  { enabled, intensity, speed, tint, reduceMotion }: MetalOptions,\n) {\n  useEffect(() => {\n    const canvas = canvasRef.current\n    const host = hostRef.current\n    if (!enabled || !canvas || !host) return\n    const gl = canvas.getContext(\"webgl2\", { antialias: false, premultipliedAlpha: true })\n    const program = gl ? createProgram(gl) : null\n    if (!gl || !program) return\n    gl.useProgram(program)\n    gl.uniform1f(gl.getUniformLocation(program, \"u_intensity\"), intensity)\n    const tintLocation = gl.getUniformLocation(program, \"u_tint\")\n    gl.uniform3fv(tintLocation, tintRgb(host, tint))\n    const resolutionLocation = gl.getUniformLocation(program, \"u_resolution\")\n    const timeLocation = gl.getUniformLocation(program, \"u_time\")\n    const draw = (seconds: number) => {\n      gl.uniform1f(timeLocation, seconds * speed)\n      gl.drawArrays(gl.TRIANGLES, 0, 3)\n    }\n    const fit = () => {\n      const ratio = Math.min(window.devicePixelRatio || 1, MAX_PIXEL_RATIO)\n      const { width, height } = canvas.getBoundingClientRect()\n      canvas.width = Math.max(1, Math.round(width * ratio))\n      canvas.height = Math.max(1, Math.round(height * ratio))\n      gl.viewport(0, 0, canvas.width, canvas.height)\n      gl.uniform2f(resolutionLocation, canvas.width, canvas.height)\n    }\n    let frame = 0\n    let lastSeconds = 0\n    const start = performance.now()\n    const loop = (now: number) => {\n      lastSeconds = (now - start) / MILLISECONDS_PER_SECOND\n      draw(lastSeconds)\n      frame = requestAnimationFrame(loop)\n    }\n    const play = () => {\n      if (frame === 0 && !reduceMotion) frame = requestAnimationFrame(loop)\n    }\n    const pause = () => {\n      cancelAnimationFrame(frame)\n      frame = 0\n    }\n    fit()\n    draw(0)\n    // The tint comes from a token, and tokens change with the theme or palette on <html>. Repaint so the\n    // metal never keeps an old colour, including under reduced motion where no loop would redraw it.\n    const retint =\n      tint === \"silver\" || typeof MutationObserver === \"undefined\"\n        ? null\n        : new MutationObserver(() => {\n            gl.uniform3fv(tintLocation, tintRgb(host, tint))\n            draw(lastSeconds)\n          })\n    retint?.observe(document.documentElement, { attributes: true, attributeFilter: [\"class\", \"data-palette\"] })\n    // Setting canvas.width clears it. The loop repaints on its next frame, but under reduced motion, or\n    // while off screen, there is no next frame, so the canvas would stay blank.\n    const sizer =\n      typeof ResizeObserver === \"undefined\"\n        ? null\n        : new ResizeObserver(() => {\n            fit()\n            draw(lastSeconds)\n          })\n    sizer?.observe(canvas)\n    const watcher =\n      typeof IntersectionObserver === \"undefined\"\n        ? null\n        : new IntersectionObserver(([entry]) => (entry?.isIntersecting ? play() : pause()))\n    if (watcher) watcher.observe(canvas)\n    else play()\n    return () => {\n      pause()\n      retint?.disconnect()\n      sizer?.disconnect()\n      watcher?.disconnect()\n      gl.deleteProgram(program)\n    }\n  }, [canvasRef, hostRef, enabled, intensity, speed, tint, reduceMotion])\n}\n\nfunction LiquidMetal({\n  children,\n  intensity = DEFAULT_INTENSITY,\n  speed = DEFAULT_SPEED,\n  tint = \"silver\",\n  className,\n  ...props\n}: LiquidMetalProps) {\n  const reduceMotion = useReducedMotion() ?? false\n  const [supported] = useState(supportsWebGL2)\n  const hostRef = useRef<HTMLDivElement>(null)\n  const canvasRef = useRef<HTMLCanvasElement>(null)\n  const level = clampUnit(intensity)\n  useMetalCanvas(canvasRef, hostRef, { enabled: supported, intensity: level, speed, tint, reduceMotion })\n  return (\n    <div\n      ref={hostRef}\n      data-slot=\"liquid-metal\"\n      data-tint={tint}\n      data-intensity={level}\n      data-fallback={supported ? undefined : \"true\"}\n      data-reduced={reduceMotion ? \"true\" : \"false\"}\n      className={cn(\"relative isolate inline-flex\", className)}\n      {...props}\n    >\n      {supported ? (\n        <canvas ref={canvasRef} aria-hidden=\"true\" className=\"pointer-events-none absolute inset-0 -z-10 size-full rounded-[inherit]\" />\n      ) : null}\n      {children}\n    </div>\n  )\n}\n\nexport { LiquidMetal, type LiquidMetalProps, type LiquidMetalTint }\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "effects"
  ],
  "type": "registry:ui"
}