{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "buttons-ripple",
  "title": "Ripple buttons",
  "description": "Styled buttons: the Ripple set. 5 decorative buttons variations (RippleButton, EchoButton, SonarButton, PingButton, FlashButton) on the ai2 token system, powered by framer-motion, reduced-motion aware and sized sm to xl. Part of the free styled layer.",
  "dependencies": [
    "motion@^12.42.2"
  ],
  "registryDependencies": [
    "@ai2/tokens"
  ],
  "files": [
    {
      "path": "registry/ai2/styled/buttons-ripple.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { AnimatePresence, motion, useReducedMotion, type HTMLMotionProps } from \"motion/react\"\n\nimport { cn } from \"@/lib/utils\"\n\n/* Ripple button family: 5 variations on a wave or ring spreading from the click. All are sized, use framer-motion, take colour ONLY from tokens (the wave colour is primary-foreground -> visible over the button in both themes) and have no wave under reduced-motion. */\n\nexport type StyledSize = \"sm\" | \"md\" | \"lg\" | \"xl\"\n\nconst sizes: Record<StyledSize, string> = {\n  sm: \"h-8 gap-1.5 rounded-md px-3 text-sm\",\n  md: \"h-9 gap-2 rounded-lg px-4 text-sm\",\n  lg: \"h-10 gap-2 rounded-lg px-5 text-sm\",\n  xl: \"h-12 gap-2.5 rounded-xl px-6 text-base\",\n}\n\nconst base =\n  \"relative inline-flex shrink-0 select-none items-center justify-center overflow-hidden bg-primary font-medium whitespace-nowrap text-primary-foreground outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 [&_i]:text-base [&_i]:leading-none\"\n\ntype Click = { id: number; x: number; y: number }\n\nfunction useClicks(reduce: boolean, ttl: number) {\n  const [clicks, setClicks] = React.useState<Click[]>([])\n  const idRef = React.useRef(0)\n  const spawn = (e: React.PointerEvent<HTMLButtonElement>) => {\n    if (reduce) return\n    const rect = e.currentTarget.getBoundingClientRect()\n    const id = idRef.current++\n    setClicks((prev) => [...prev, { id, x: e.clientX - rect.left, y: e.clientY - rect.top }])\n    window.setTimeout(() => setClicks((prev) => prev.filter((c) => c.id !== id)), ttl)\n  }\n  return { clicks, spawn }\n}\n\ntype Props = React.ComponentProps<\"button\"> & { size?: StyledSize }\n\n/* Ripple: tiklama noktasindan buyuyup sonen dolu daire (Material). */\nexport function RippleButton({ className, size = \"md\", children, onPointerDown, ...props }: Props) {\n  const reduce = useReducedMotion()\n  const { clicks, spawn } = useClicks(!!reduce, 620)\n  return (\n    <button\n      data-slot=\"styled-button\"\n      onPointerDown={(e) => {\n        spawn(e)\n        onPointerDown?.(e)\n      }}\n      className={cn(base, sizes[size], className)}\n      {...props}\n    >\n      <AnimatePresence>\n        {clicks.map((c) => (\n          <motion.span\n            key={c.id}\n            aria-hidden=\"true\"\n            className=\"pointer-events-none absolute rounded-full bg-primary-foreground/40\"\n            style={{ left: c.x, top: c.y }}\n            initial={{ width: 0, height: 0, x: 0, y: 0, opacity: 0.5 }}\n            animate={{ width: 260, height: 260, x: -130, y: -130, opacity: 0 }}\n            transition={{ duration: 0.62, ease: \"easeOut\" }}\n          />\n        ))}\n      </AnimatePresence>\n      <span className=\"relative flex items-center gap-2\">{children}</span>\n    </button>\n  )\n}\n\n/* Echo: a single thin ring spreading from the click (not filled). */\nexport function EchoButton({ className, size = \"md\", children, onPointerDown, ...props }: Props) {\n  const reduce = useReducedMotion()\n  const { clicks, spawn } = useClicks(!!reduce, 640)\n  return (\n    <button\n      data-slot=\"styled-button\"\n      onPointerDown={(e) => {\n        spawn(e)\n        onPointerDown?.(e)\n      }}\n      className={cn(base, sizes[size], className)}\n      {...props}\n    >\n      <AnimatePresence>\n        {clicks.map((c) => (\n          <motion.span\n            key={c.id}\n            aria-hidden=\"true\"\n            className=\"pointer-events-none absolute rounded-full border-2 border-primary-foreground/60\"\n            style={{ left: c.x, top: c.y }}\n            initial={{ width: 0, height: 0, x: 0, y: 0, opacity: 0.7 }}\n            animate={{ width: 220, height: 220, x: -110, y: -110, opacity: 0 }}\n            transition={{ duration: 0.64, ease: \"easeOut\" }}\n          />\n        ))}\n      </AnimatePresence>\n      <span className=\"relative flex items-center gap-2\">{children}</span>\n    </button>\n  )\n}\n\n/* Sonar: tiklamada ust uste 3 halka kademeli yayilir. */\nexport function SonarButton({ className, size = \"md\", children, onPointerDown, ...props }: Props) {\n  const reduce = useReducedMotion()\n  const { clicks, spawn } = useClicks(!!reduce, 900)\n  return (\n    <button\n      data-slot=\"styled-button\"\n      onPointerDown={(e) => {\n        spawn(e)\n        onPointerDown?.(e)\n      }}\n      className={cn(base, sizes[size], className)}\n      {...props}\n    >\n      <AnimatePresence>\n        {clicks.map((c) => (\n          <span key={c.id} aria-hidden=\"true\" className=\"pointer-events-none absolute\" style={{ left: c.x, top: c.y }}>\n            {[0, 0.12, 0.24].map((delay) => (\n              <motion.span\n                key={delay}\n                className=\"absolute rounded-full border border-primary-foreground/50\"\n                initial={{ width: 0, height: 0, x: 0, y: 0, opacity: 0.6 }}\n                animate={{ width: 200, height: 200, x: -100, y: -100, opacity: 0 }}\n                transition={{ duration: 0.8, ease: \"easeOut\", delay }}\n              />\n            ))}\n          </span>\n        ))}\n      </AnimatePresence>\n      <span className=\"relative flex items-center gap-2\">{children}</span>\n    </button>\n  )\n}\n\n/* Ping: a token ring repeating continuously around the button (to draw attention). */\nexport function PingButton({\n  className,\n  size = \"md\",\n  children,\n  ...props\n}: HTMLMotionProps<\"button\"> & { size?: StyledSize }) {\n  const reduce = useReducedMotion()\n  return (\n    <span className=\"relative inline-flex\">\n      {!reduce && (\n        <motion.span\n          aria-hidden=\"true\"\n          className=\"pointer-events-none absolute inset-0 rounded-lg ring-2 ring-primary\"\n          animate={{ scale: [1, 1.25], opacity: [0.6, 0] }}\n          transition={{ duration: 1.6, ease: \"easeOut\", repeat: Infinity }}\n        />\n      )}\n      <motion.button\n        data-slot=\"styled-button\"\n        whileTap={reduce ? undefined : { scale: 0.97 }}\n        transition={{ type: \"spring\", stiffness: 400, damping: 20 }}\n        className={cn(base, sizes[size], \"overflow-visible\", className)}\n        {...props}\n      >\n        <span className=\"relative flex items-center gap-2\">{children as React.ReactNode}</span>\n      </motion.button>\n    </span>\n  )\n}\n\n/* Flash: tiklamada buton kisa sure parlar (foreground overlay). */\nexport function FlashButton({ className, size = \"md\", children, onPointerDown, ...props }: Props) {\n  const reduce = useReducedMotion()\n  const { clicks, spawn } = useClicks(!!reduce, 360)\n  return (\n    <button\n      data-slot=\"styled-button\"\n      onPointerDown={(e) => {\n        spawn(e)\n        onPointerDown?.(e)\n      }}\n      className={cn(base, sizes[size], className)}\n      {...props}\n    >\n      <AnimatePresence>\n        {clicks.map((c) => (\n          <motion.span\n            key={c.id}\n            aria-hidden=\"true\"\n            className=\"pointer-events-none absolute inset-0 bg-primary-foreground/30\"\n            initial={{ opacity: 0.5 }}\n            animate={{ opacity: 0 }}\n            transition={{ duration: 0.34, ease: \"easeOut\" }}\n          />\n        ))}\n      </AnimatePresence>\n      <span className=\"relative flex items-center gap-2\">{children}</span>\n    </button>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/ui/buttons-ripple.tsx"
    }
  ],
  "categories": [
    "styled",
    "buttons"
  ],
  "type": "registry:component"
}