{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ripple-button",
  "type": "registry:component",
  "title": "Ripple Button",
  "description": "A button whose ripple is born exactly where you click: a luminous wavefront with rings that sweeps out to the farthest corner.",
  "author": "Marcelo Treviño Velázquez <https://furin.dev>",
  "dependencies": [],
  "categories": [
    "Buttons"
  ],
  "docs": "Installed in components/furin/ripple-button/. Docs, live controls and props table: https://furin.dev/components/ripple-button/",
  "files": [
    {
      "path": "registry/ripple-button/RippleButton.tsx",
      "content": "\"use client\";\nimport { useRef } from \"react\";\nimport type { ButtonHTMLAttributes, CSSProperties, PointerEvent, ReactNode } from \"react\";\nimport \"./RippleButton.css\";\n\nexport interface RippleButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, \"style\" | \"className\" | \"color\"> {\n  /** Texto del botón. */\n  children?: ReactNode;\n  /** Color del botón. */\n  color?: string;\n  /** Color de la onda. */\n  rippleColor?: string;\n  /** Color del texto. */\n  textColor?: string;\n  /** Milisegundos que dura la onda. */\n  duration?: number;\n  /** Cuántos anillos tiene la onda (1 a 3). */\n  rings?: number;\n  /** Radio de las esquinas, en px. */\n  radius?: number;\n  /** Alto del botón, en px. */\n  height?: number;\n  /** Clase extra para el botón. */\n  className?: string;\n  /** Estilos extra para el botón. */\n  style?: CSSProperties;\n}\n\n/** Botón con una onda que nace exactamente donde haces clic: un frente luminoso con anillos que se ensanchan hasta cubrir la esquina más lejana. */\nexport function RippleButton({\n  children = \"Continue\",\n  color = \"#3b4cf0\",\n  rippleColor = \"#ffffff\",\n  textColor = \"#ffffff\",\n  duration = 900,\n  rings = 2,\n  radius = 16,\n  height = 56,\n  className = \"\",\n  style,\n  onPointerDown,\n  onPointerMove,\n  onPointerLeave,\n  ...rest\n}: RippleButtonProps) {\n  const ref = useRef<HTMLButtonElement>(null);\n  const layer = useRef<HTMLSpanElement>(null);\n\n  const spawn = (e: PointerEvent<HTMLButtonElement>) => {\n    const b = ref.current, host = layer.current;\n    if (!b || !host) return;\n    const r = b.getBoundingClientRect();\n    // Con teclado no hay punto de clic: la onda nace en el centro.\n    const keyboard = e.clientX === 0 && e.clientY === 0;\n    const x = keyboard ? r.width / 2 : e.clientX - r.left;\n    const y = keyboard ? r.height / 2 : e.clientY - r.top;\n    // El radio llega a la esquina más lejana, así la onda siempre cubre todo el botón.\n    const far = Math.hypot(Math.max(x, r.width - x), Math.max(y, r.height - y));\n    const reduce = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n    const n = Math.max(1, Math.min(3, Math.round(rings)));\n    for (let i = 0; i < n; i++) {\n      const el = document.createElement(\"span\");\n      el.className = \"furin-ripple-button__wave\";\n      const d = far * 2;\n      el.style.cssText = `left:${x - far}px;top:${y - far}px;width:${d}px;height:${d}px`;\n      host.appendChild(el);\n      const delay = i * duration * 0.14;\n      const life = duration * (1 - i * 0.08);\n      const peak = [0.55, 0.32, 0.18][i];\n      const anim = reduce\n        ? el.animate([{ opacity: 0.25, transform: \"scale(1)\" }, { opacity: 0, transform: \"scale(1)\" }], { duration: 300, fill: \"both\" })\n        : el.animate(\n            [\n              { transform: \"scale(0.02)\", opacity: peak, offset: 0 },\n              { opacity: peak * 0.8, offset: 0.35 },\n              { transform: \"scale(1)\", opacity: 0, offset: 1 },\n            ],\n            { duration: life, delay, easing: \"cubic-bezier(0.16, 0.7, 0.25, 1)\", fill: \"both\" },\n          );\n      anim.onfinish = () => el.remove();\n    }\n    // Un destello breve bajo el dedo, para que el clic se sienta \"mojado\" de luz.\n    const flash = document.createElement(\"span\");\n    flash.className = \"furin-ripple-button__flash\";\n    flash.style.cssText = `left:${x - 60}px;top:${y - 60}px`;\n    host.appendChild(flash);\n    const fa = flash.animate([{ opacity: 0.7, transform: \"scale(0.4)\" }, { opacity: 0, transform: \"scale(1.5)\" }], { duration: 420, easing: \"ease-out\", fill: \"both\" });\n    fa.onfinish = () => flash.remove();\n  };\n\n  const move = (e: PointerEvent<HTMLButtonElement>) => {\n    const b = ref.current;\n    if (!b) return;\n    const r = b.getBoundingClientRect();\n    b.style.setProperty(\"--furin-ripple-button-mx\", `${e.clientX - r.left}px`);\n    b.style.setProperty(\"--furin-ripple-button-my\", `${e.clientY - r.top}px`);\n  };\n\n  const vars = {\n    \"--furin-ripple-button-color\": color,\n    \"--furin-ripple-button-ripple\": rippleColor,\n    \"--furin-ripple-button-text\": textColor,\n    \"--furin-ripple-button-r\": `${radius}px`,\n    \"--furin-ripple-button-h\": `${height}px`,\n  } as CSSProperties;\n\n  return (\n    <button\n      ref={ref}\n      type=\"button\"\n      className={`furin-ripple-button ${className}`}\n      style={{ ...vars, ...style }}\n      onPointerDown={(e) => { spawn(e); onPointerDown?.(e); }}\n      onPointerMove={(e) => { move(e); onPointerMove?.(e); }}\n      onPointerLeave={onPointerLeave}\n      {...rest}\n    >\n      <span ref={layer} className=\"furin-ripple-button__layer\" aria-hidden=\"true\" />\n      <span className=\"furin-ripple-button__label\">{children}</span>\n    </button>\n  );\n}\n\nexport default RippleButton;\n",
      "type": "registry:component",
      "target": "components/furin/ripple-button/RippleButton.tsx"
    },
    {
      "path": "registry/ripple-button/RippleButton.css",
      "content": ".furin-ripple-button {\n  --furin-ripple-button-mx: 50%;\n  --furin-ripple-button-my: 50%;\n  position: relative;\n  display: inline-grid;\n  place-items: center;\n  height: var(--furin-ripple-button-h);\n  padding: 0 34px;\n  border: 0;\n  border-radius: var(--furin-ripple-button-r);\n  overflow: hidden;\n  isolation: isolate;\n  color: var(--furin-ripple-button-text);\n  background:\n    linear-gradient(180deg, rgb(255 255 255 / 0.2), rgb(255 255 255 / 0) 48%),\n    var(--furin-ripple-button-color);\n  box-shadow:\n    inset 0 1px 0 rgb(255 255 255 / 0.35),\n    inset 0 -2px 0 rgb(0 0 0 / 0.18),\n    0 0 0 1px color-mix(in srgb, var(--furin-ripple-button-color) 70%, #000),\n    0 14px 26px -10px color-mix(in srgb, var(--furin-ripple-button-color) 75%, transparent);\n  font: 600 17px/1 Geist, -apple-system, BlinkMacSystemFont, \"SF Pro Text\", system-ui, sans-serif;\n  letter-spacing: -0.01em;\n  cursor: pointer;\n  user-select: none;\n  -webkit-tap-highlight-color: transparent;\n  outline: none;\n  transition: transform 0.45s cubic-bezier(0.3, 1.7, 0.5, 1), box-shadow 0.25s;\n}\n.furin-ripple-button:active { transform: scale(0.97); transition-duration: 0.1s; }\n.furin-ripple-button:focus-visible { box-shadow: inset 0 1px 0 rgb(255 255 255 / 0.35), 0 0 0 1px color-mix(in srgb, var(--furin-ripple-button-color) 70%, #000), 0 0 0 4px rgb(255 255 255 / 0.5); }\n.furin-ripple-button:disabled { opacity: 0.45; cursor: not-allowed; }\n/* Un reflejo suave que sigue al cursor. */\n.furin-ripple-button::before {\n  content: \"\";\n  position: absolute;\n  inset: 0;\n  z-index: -1;\n  background: radial-gradient(110px circle at var(--furin-ripple-button-mx) var(--furin-ripple-button-my), rgb(255 255 255 / 0.22), transparent 70%);\n  opacity: 0;\n  transition: opacity 0.3s;\n}\n.furin-ripple-button:hover::before { opacity: 1; }\n.furin-ripple-button__layer { position: absolute; inset: 0; z-index: -1; overflow: hidden; border-radius: inherit; pointer-events: none; }\n.furin-ripple-button__label { position: relative; text-shadow: 0 1px 1px rgb(0 0 0 / 0.18); }\n/* La onda: disco luminoso con el borde más fuerte que el centro, como un frente de agua. */\n.furin-ripple-button__wave {\n  position: absolute;\n  border-radius: 50%;\n  pointer-events: none;\n  background: radial-gradient(closest-side,\n    transparent 0%,\n    color-mix(in srgb, var(--furin-ripple-button-ripple) 16%, transparent) 52%,\n    color-mix(in srgb, var(--furin-ripple-button-ripple) 62%, transparent) 84%,\n    color-mix(in srgb, var(--furin-ripple-button-ripple) 8%, transparent) 96%,\n    transparent 100%);\n  will-change: transform, opacity;\n}\n.furin-ripple-button__flash {\n  position: absolute;\n  width: 120px;\n  height: 120px;\n  border-radius: 50%;\n  pointer-events: none;\n  background: radial-gradient(circle, color-mix(in srgb, var(--furin-ripple-button-ripple) 85%, transparent), transparent 66%);\n}\n@media (prefers-reduced-motion: reduce) { .furin-ripple-button { transition: none; } }\n",
      "type": "registry:file",
      "target": "components/furin/ripple-button/RippleButton.css"
    }
  ]
}
