{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ripple-button",
  "title": "Ripple Button",
  "description": "Expands a ripple from the exact point you pressed.",
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/components/velora/ripple-button.tsx",
      "content": "\"use client\";\n\nimport { useState } from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\ninterface RippleButtonProps\n  extends React.ButtonHTMLAttributes<HTMLButtonElement> {\n  /** Milliseconds for a ripple to finish expanding */\n  duration?: number;\n  /** Any CSS colour; defaults to a translucent version of the text colour */\n  rippleColor?: string;\n  children: React.ReactNode;\n}\n\ninterface Ripple {\n  id: number;\n  x: number;\n  y: number;\n  size: number;\n}\n\n/**\n * Button that expands a ripple from the exact point you pressed.\n * Ripples are skipped entirely under `prefers-reduced-motion`.\n */\nexport function RippleButton({\n  duration = 600,\n  rippleColor = \"currentColor\",\n  children,\n  className,\n  onClick,\n  ...props\n}: RippleButtonProps) {\n  const [ripples, setRipples] = useState<Ripple[]>([]);\n\n  const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {\n    const reduced = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n    if (!reduced) {\n      const rect = event.currentTarget.getBoundingClientRect();\n      // Diameter that reaches the furthest corner from the press point.\n      const size = Math.max(rect.width, rect.height) * 2;\n      // Keyboard activation (detail 0) has no pointer position: use the centre.\n      const keyboard = event.detail === 0;\n      const ripple: Ripple = {\n        id: Date.now() + Math.random(),\n        x: (keyboard ? rect.width / 2 : event.clientX - rect.left) - size / 2,\n        y: (keyboard ? rect.height / 2 : event.clientY - rect.top) - size / 2,\n        size,\n      };\n      setRipples((current) => [...current, ripple]);\n      window.setTimeout(\n        () => setRipples((current) => current.filter((r) => r.id !== ripple.id)),\n        duration\n      );\n    }\n    onClick?.(event);\n  };\n\n  return (\n    <button\n      {...props}\n      data-slot=\"ripple-button\"\n      onClick={handleClick}\n      className={cn(\n        \"relative inline-flex h-11 cursor-pointer items-center justify-center overflow-hidden rounded-xl border bg-background px-6 text-sm font-medium transition-colors hover:bg-muted focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50\",\n        className\n      )}\n    >\n      <span className=\"relative z-10\">{children}</span>\n      <span aria-hidden className=\"pointer-events-none absolute inset-0\">\n        {ripples.map((ripple) => (\n          <span\n            key={ripple.id}\n            className=\"absolute rounded-full opacity-30 motion-safe:animate-ripple\"\n            style={{\n              left: ripple.x,\n              top: ripple.y,\n              width: ripple.size,\n              height: ripple.size,\n              background: rippleColor,\n              animationDuration: `${duration}ms`,\n            }}\n          />\n        ))}\n      </span>\n    </button>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/velora/ripple-button.tsx"
    }
  ],
  "cssVars": {
    "theme": {
      "animate-ripple": "ripple 600ms ease-out forwards"
    }
  },
  "css": {
    "@keyframes ripple": {
      "from": {
        "transform": "scale(0)",
        "opacity": "0.35"
      },
      "to": {
        "transform": "scale(1)",
        "opacity": "0"
      }
    }
  },
  "type": "registry:ui"
}