{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "liquid-button",
  "title": "Liquid Button",
  "description": "A button filled with live flowing liquid that speeds up on hover and presses in.",
  "dependencies": [
    "motion",
    "@radix-ui/react-slot",
    "class-variance-authority"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/clover-ui/liquid-button.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Slot, Slottable } from \"@radix-ui/react-slot\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { useReducedMotion } from \"motion/react\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport type ElementName = \"lava\" | \"ocean\" | \"stone\" | \"sky\" | \"obsidian\";\nexport type ElementColors = [string, string, string];\n\ntype ElementLook = {\n  colors: ElementColors;\n  text: string;\n  ring: string;\n  shape: number;\n  scale: number;\n  swirl: number;\n  distortion: number;\n  speed: number;\n  sparkle: number;\n  grain: number;\n};\n\nexport const elementPalettes: Record<ElementName, ElementLook> = {\n  lava: {\n    colors: [\"#d42a1a\", \"#9e1408\", \"#3a0300\"],\n    text: \"#ffffff\",\n    ring: \"#d42a1a\",\n    shape: 0,\n    scale: 34,\n    swirl: 0.9,\n    distortion: 0.35,\n    speed: 0.6,\n    sparkle: 0.04,\n    grain: 0,\n  },\n  ocean: {\n    colors: [\"#1d6fc4\", \"#0f4a94\", \"#031a40\"],\n    text: \"#ffffff\",\n    ring: \"#1d6fc4\",\n    shape: 0,\n    scale: 42,\n    swirl: 1.1,\n    distortion: 0.2,\n    speed: 0.45,\n    sparkle: 0.12,\n    grain: 0,\n  },\n  stone: {\n    colors: [\"#8a5a2b\", \"#5a3818\", \"#1f1309\"],\n    text: \"#fff7eb\",\n    ring: \"#8a5a2b\",\n    shape: 1,\n    scale: 26,\n    swirl: 0.35,\n    distortion: 0.5,\n    speed: 0.14,\n    sparkle: 0,\n    grain: 0.16,\n  },\n  sky: {\n    colors: [\"#3f6d96\", \"#2c5276\", \"#142a40\"],\n    text: \"#ffffff\",\n    ring: \"#3f6d96\",\n    shape: 1,\n    scale: 56,\n    swirl: 0.7,\n    distortion: 0.3,\n    speed: 0.75,\n    sparkle: 0,\n    grain: 0,\n  },\n  obsidian: {\n    colors: [\"#5c5c63\", \"#1f1f23\", \"#050506\"],\n    text: \"#fafafa\",\n    ring: \"#a1a1aa\",\n    shape: 0,\n    scale: 48,\n    swirl: 0.8,\n    distortion: 0.25,\n    speed: 0.4,\n    sparkle: 0.18,\n    grain: 0,\n  },\n};\n\nconst VERTEX = `attribute vec2 p;void main(){gl_Position=vec4(p,0.,1.);}`;\n\nconst FRAGMENT = `#ifdef GL_FRAGMENT_PRECISION_HIGH\nprecision highp float;\n#else\nprecision mediump float;\n#endif\nuniform float t;uniform float dpr;uniform vec2 seed;\nuniform float shape;uniform float scale;uniform float swirl;uniform float distortion;\nuniform float sparkle;uniform float grain;uniform vec3 c1;uniform vec3 c2;uniform vec3 c3;\nfloat h(vec2 p){return fract(sin(dot(p,vec2(127.1,311.7)))*43758.5453);}\nfloat n(vec2 p){vec2 i=floor(p),f=fract(p);f=f*f*(3.-2.*f);\nreturn mix(mix(h(i),h(i+vec2(1,0)),f.x),mix(h(i+vec2(0,1)),h(i+vec2(1,1)),f.x),f.y);}\nvoid main(){\nvec2 px=gl_FragCoord.xy;\nvec2 uv=px/(dpr*scale)+seed;\nfloat a=n(uv+t*.5)*6.2831;\nuv+=distortion*n(uv*2.-t*.5)*vec2(cos(a),sin(a));\nfor(int i=1;i<=5;i++){float fi=float(i);\nuv.x+=swirl/fi*cos(t+fi*1.5*uv.y);uv.y+=swirl/fi*cos(t+fi*uv.x);}\nfloat m;\nif(shape<.5){m=.5+.5*sin(uv.x)*cos(uv.y);}\nelse{float f=fract(uv.y*.5);m=smoothstep(0.,.55,f)*smoothstep(1.,.45,f);}\nvec3 col=mix(c3,c2,smoothstep(.12,.55,m));\ncol=mix(col,c1,smoothstep(.52,.92,m));\ncol+=c1*sparkle*pow(max(0.,sin(uv.x*2.1)*sin(uv.y*1.7)),12.);\ncol*=1.-grain*.5+grain*h(floor(px));\ngl_FragColor=vec4(col,1.);}`;\n\nfunction toRgb(hex: string): [number, number, number] {\n  const value = hex.replace(\"#\", \"\");\n  const full =\n    value.length === 3\n      ? value\n          .split(\"\")\n          .map((c) => c + c)\n          .join(\"\")\n      : value.slice(0, 6);\n  const number = parseInt(full, 16);\n  return [((number >> 16) & 255) / 255, ((number >> 8) & 255) / 255, (number & 255) / 255];\n}\n\nexport function elementGradient(colors: ElementColors) {\n  return `linear-gradient(0deg, ${colors[2]}, ${colors[1]}, ${colors[0]})`;\n}\n\ntype Surface = {\n  canvas: HTMLCanvasElement;\n  ctx: CanvasRenderingContext2D;\n  look: ElementLook;\n  rgb: [number, number, number][];\n  seed: [number, number];\n  time: number;\n  speed: number;\n  boost: number;\n  visible: boolean;\n  still: boolean;\n  dirty: boolean;\n  onReady: () => void;\n};\n\ntype Renderer = {\n  gl: WebGLRenderingContext;\n  canvas: HTMLCanvasElement;\n  uniforms: Record<string, WebGLUniformLocation | null>;\n  surfaces: Set<Surface>;\n  frame: number;\n  last: number;\n};\n\nlet renderer: Renderer | null | undefined;\n\nfunction getRenderer() {\n  if (renderer !== undefined) return renderer;\n  const canvas = document.createElement(\"canvas\");\n  const gl = canvas.getContext(\"webgl\", {\n    antialias: false,\n    alpha: false,\n    depth: false,\n    powerPreference: \"low-power\",\n  });\n  renderer = null;\n  if (!gl) return renderer;\n  const compile = (type: number, source: string) => {\n    const shader = gl.createShader(type)!;\n    gl.shaderSource(shader, source);\n    gl.compileShader(shader);\n    return shader;\n  };\n  const program = gl.createProgram()!;\n  gl.attachShader(program, compile(gl.VERTEX_SHADER, VERTEX));\n  gl.attachShader(program, compile(gl.FRAGMENT_SHADER, FRAGMENT));\n  gl.linkProgram(program);\n  if (!gl.getProgramParameter(program, gl.LINK_STATUS)) return renderer;\n  gl.useProgram(program);\n  gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());\n  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);\n  gl.enableVertexAttribArray(0);\n  gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);\n  const names = [\"t\", \"dpr\", \"seed\", \"shape\", \"scale\", \"swirl\", \"distortion\"];\n  const uniforms = Object.fromEntries(\n    [...names, \"sparkle\", \"grain\", \"c1\", \"c2\", \"c3\"].map((name) => [\n      name,\n      gl.getUniformLocation(program, name),\n    ]),\n  );\n  renderer = { gl, canvas, uniforms, surfaces: new Set(), frame: 0, last: 0 };\n  return renderer;\n}\n\nfunction paint(r: Renderer, surface: Surface) {\n  const { gl, canvas, uniforms: u } = r;\n  const { width, height } = surface.canvas;\n  if (!width || !height) return;\n  if (canvas.width < width || canvas.height < height) {\n    canvas.width = Math.max(canvas.width, width);\n    canvas.height = Math.max(canvas.height, height);\n  }\n  const { look } = surface;\n  gl.viewport(0, 0, width, height);\n  gl.uniform1f(u.t, surface.time);\n  gl.uniform1f(u.dpr, width / Math.max(1, surface.canvas.clientWidth));\n  gl.uniform2f(u.seed, surface.seed[0], surface.seed[1]);\n  gl.uniform1f(u.shape, look.shape);\n  gl.uniform1f(u.scale, look.scale);\n  gl.uniform1f(u.swirl, look.swirl);\n  gl.uniform1f(u.distortion, look.distortion);\n  gl.uniform1f(u.sparkle, look.sparkle);\n  gl.uniform1f(u.grain, look.grain);\n  surface.rgb.forEach((rgb, index) => gl.uniform3f(u[`c${index + 1}`], ...rgb));\n  gl.drawArrays(gl.TRIANGLES, 0, 3);\n  surface.ctx.drawImage(canvas, 0, canvas.height - height, width, height, 0, 0, width, height);\n  if (surface.dirty) {\n    surface.dirty = false;\n    surface.onReady();\n  }\n}\n\nfunction tick(now: number) {\n  const r = renderer;\n  if (!r) return;\n  const dt = r.last ? Math.min(0.05, (now - r.last) / 1000) : 0;\n  r.last = now;\n  let animating = false;\n  for (const surface of r.surfaces) {\n    if (!surface.visible) continue;\n    if (surface.still) {\n      if (surface.dirty) paint(r, surface);\n      continue;\n    }\n    surface.speed += (surface.boost - surface.speed) * Math.min(1, dt * 6);\n    surface.time += dt * surface.look.speed * surface.speed;\n    if (!document.hidden) paint(r, surface);\n    animating = true;\n  }\n  r.frame = animating ? requestAnimationFrame(tick) : 0;\n  if (!animating) r.last = 0;\n}\n\nfunction wake() {\n  const r = renderer;\n  if (r && !r.frame) r.frame = requestAnimationFrame(tick);\n}\n\nexport function useElementSurface(\n  canvasRef: React.RefObject<HTMLCanvasElement | null>,\n  element: ElementName,\n  colors: ElementColors | undefined,\n  still: boolean,\n) {\n  const [ready, setReady] = React.useState(false);\n  const surfaceRef = React.useRef<Surface | null>(null);\n  const look = elementPalettes[element];\n  const [a, b, c] = colors ?? look.colors;\n\n  React.useEffect(() => {\n    const canvas = canvasRef.current;\n    const ctx = canvas?.getContext(\"2d\");\n    const r = getRenderer();\n    if (!canvas || !ctx || !r) return;\n\n    const surface: Surface = {\n      canvas,\n      ctx,\n      look: elementPalettes.lava,\n      rgb: [],\n      seed: [Math.random() * 40, Math.random() * 40],\n      time: Math.random() * 20,\n      speed: 1,\n      boost: 1,\n      visible: true,\n      still: false,\n      dirty: true,\n      onReady: () => setReady(true),\n    };\n    surfaceRef.current = surface;\n\n    const resize = () => {\n      const dpr = Math.min(window.devicePixelRatio || 1, 1.5);\n      canvas.width = Math.max(1, Math.round(canvas.clientWidth * dpr));\n      canvas.height = Math.max(1, Math.round(canvas.clientHeight * dpr));\n      surface.dirty = true;\n      wake();\n    };\n    const sizer = new ResizeObserver(resize);\n    sizer.observe(canvas);\n    const viewer = new IntersectionObserver(([entry]) => {\n      surface.visible = entry.isIntersecting;\n      wake();\n    });\n    viewer.observe(canvas);\n\n    r.surfaces.add(surface);\n    resize();\n\n    return () => {\n      sizer.disconnect();\n      viewer.disconnect();\n      r.surfaces.delete(surface);\n      surfaceRef.current = null;\n    };\n  }, [canvasRef]);\n\n  React.useEffect(() => {\n    const surface = surfaceRef.current;\n    if (!surface) return;\n    surface.look = look;\n    surface.rgb = [toRgb(a), toRgb(b), toRgb(c)];\n    surface.still = still;\n    surface.dirty = true;\n    wake();\n  }, [look, a, b, c, still]);\n\n  const boost = React.useCallback((value: number) => {\n    if (surfaceRef.current) surfaceRef.current.boost = value;\n    wake();\n  }, []);\n\n  return { ready, boost };\n}\n\nconst liquidButtonVariants = cva(\n  [\n    \"group/element relative isolate inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 overflow-hidden font-semibold whitespace-nowrap select-none\",\n    \"transition-transform duration-150 ease-out outline-none active:scale-[0.96] active:duration-75\",\n    \"focus-visible:ring-offset-background focus-visible:ring-2 focus-visible:ring-[var(--element-ring)] focus-visible:ring-offset-2\",\n    \"disabled:pointer-events-none disabled:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0\",\n  ],\n  {\n    variants: {\n      size: {\n        sm: \"h-8 rounded-md px-3 text-xs\",\n        default: \"h-9 rounded-lg px-4 text-sm\",\n        lg: \"h-11 rounded-lg px-6 text-base\",\n      },\n    },\n    defaultVariants: { size: \"default\" },\n  },\n);\n\nexport type LiquidButtonSize = NonNullable<VariantProps<typeof liquidButtonVariants>[\"size\"]>;\n\nexport interface LiquidButtonProps\n  extends React.ComponentProps<\"button\">, VariantProps<typeof liquidButtonVariants> {\n  element?: ElementName;\n  colors?: ElementColors;\n  asChild?: boolean;\n  render?: React.ReactElement;\n}\n\nfunction LiquidButton({\n  element = \"lava\",\n  colors,\n  size,\n  asChild = false,\n  render,\n  className,\n  style,\n  children,\n  onPointerEnter,\n  onPointerLeave,\n  onFocus,\n  onBlur,\n  ...props\n}: LiquidButtonProps) {\n  const still = Boolean(useReducedMotion());\n  const canvasRef = React.useRef<HTMLCanvasElement>(null);\n  const palette = elementPalettes[element];\n  const { ready, boost } = useElementSurface(canvasRef, element, colors, still);\n  const slotted = asChild || Boolean(render);\n  const Comp = slotted ? Slot : \"button\";\n\n  return (\n    <Comp\n      data-element={element}\n      className={cn(liquidButtonVariants({ size }), className)}\n      style={\n        {\n          backgroundImage: elementGradient(colors ?? palette.colors),\n          color: palette.text,\n          \"--element-ring\": palette.ring,\n          ...style,\n        } as React.CSSProperties\n      }\n      onPointerEnter={(event: React.PointerEvent<HTMLButtonElement>) => {\n        onPointerEnter?.(event);\n        boost(2.6);\n      }}\n      onPointerLeave={(event: React.PointerEvent<HTMLButtonElement>) => {\n        onPointerLeave?.(event);\n        boost(1);\n      }}\n      onFocus={(event: React.FocusEvent<HTMLButtonElement>) => {\n        onFocus?.(event);\n        boost(2.6);\n      }}\n      onBlur={(event: React.FocusEvent<HTMLButtonElement>) => {\n        onBlur?.(event);\n        boost(1);\n      }}\n      {...(slotted ? {} : { type: \"button\" as const })}\n      {...props}\n    >\n      <canvas\n        ref={canvasRef}\n        aria-hidden\n        className={cn(\n          \"pointer-events-none absolute inset-0 -z-10 size-full transition-opacity duration-500\",\n          ready ? \"opacity-100\" : \"opacity-0\",\n        )}\n      />\n      <span\n        aria-hidden\n        className=\"pointer-events-none absolute inset-px -z-10 rounded-[inherit] bg-[linear-gradient(180deg,rgb(255_255_255/0.12),transparent_55%)] opacity-80 transition-opacity duration-300 group-hover/element:opacity-100\"\n      />\n      <Slottable>{render ? React.cloneElement(render, undefined, children) : children}</Slottable>\n    </Comp>\n  );\n}\n\nexport { LiquidButton, liquidButtonVariants };\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:component"
}