{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ripple",
  "type": "registry:component",
  "title": "Ripple",
  "description": "Concentric rings behind your content with a soft pulse that travels outward ring by ring, like a sonar ping.",
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "https://ui.minidev.pro/r/button.json",
    "utils"
  ],
  "files": [
    {
      "path": "registry/premium/ripple.tsx",
      "type": "registry:ui",
      "target": "components/ui/ripple.tsx",
      "content": "\"use client\"\nimport * as React from \"react\"\nimport { useInView, useReducedMotion } from \"motion/react\"\nimport { Radio } from \"lucide-react\"\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/components/ui/button\"\n\n/**\n * Concentric rings behind your content with a soft pulse that travels outward ring by ring, like a sonar ping.\n * Clicking or tapping the panel sends an extra ripple out from the pointer.\n * The rings sit behind the content, pause off screen and hold still for\n * reduced motion.\n */\ntype RippleProps = {\n  /** Number of rings. */\n  count?: number\n  /** Gap between neighbouring rings in pixels. */\n  spacing?: number\n  /** Diameter of the innermost ring in pixels. */\n  size?: number\n  /** Centre of the rings, in percent of the panel width and height. */\n  origin?: { x: number; y: number }\n  /** Seconds for one pulse to travel out and settle. */\n  duration?: number\n  /** Send a ripple out from the pointer on click or tap. */\n  interactive?: boolean\n  className?: string\n  children?: React.ReactNode\n}\n\ntype Burst = { id: number; x: number; y: number }\n\nfunction Ripple({\n  count = 7,\n  spacing = 44,\n  size = 128,\n  origin = { x: 50, y: 50 },\n  duration = 4,\n  interactive = true,\n  className,\n  children,\n}: RippleProps) {\n  const reduce = useReducedMotion()\n  const ref = React.useRef<HTMLDivElement>(null)\n  const inView = useInView(ref, { margin: \"80px\" })\n  const [bursts, setBursts] = React.useState<Burst[]>([])\n  const nextId = React.useRef(0)\n  const rings = Math.max(1, Math.round(count))\n  // The pulse reaches the outer ring at 45% of the cycle, then the rings rest.\n  const step = (duration * 0.45) / rings\n\n  const emit = (e: React.PointerEvent<HTMLDivElement>) => {\n    if (!interactive || reduce || e.button !== 0) return\n    const r = e.currentTarget.getBoundingClientRect()\n    const id = nextId.current++\n    setBursts((b) => [...b.slice(-5), { id, x: e.clientX - r.left, y: e.clientY - r.top }])\n  }\n\n  return (\n    <div\n      ref={ref}\n      data-slot=\"ripple\"\n      onPointerDown={emit}\n      className={cn(\"relative isolate h-[360px] w-full overflow-hidden rounded-2xl border border-border bg-bg\", className)}\n    >\n      <div aria-hidden className=\"pointer-events-none absolute inset-0 z-0\">\n        {Array.from({ length: rings }, (_, i) => {\n          const d = size + i * spacing * 2\n          const rest = Math.max(0.12, 0.9 - (i / rings) * 0.8)\n          return (\n            <span\n              key={i}\n              data-slot=\"ripple-ring\"\n              className=\"absolute block rounded-full border\"\n              style={{\n                left: `${origin.x}%`,\n                top: `${origin.y}%`,\n                width: d,\n                height: d,\n                borderColor: \"var(--accent-line)\",\n                background: i === 0 ? \"var(--accent-soft)\" : undefined,\n                [\"--ripple-o\" as string]: rest,\n                opacity: rest,\n                transform: \"translate(-50%, -50%)\",\n                animation: reduce ? undefined : `ripple-ping ${duration}s cubic-bezier(0.2, 0, 0, 1) ${(i * step).toFixed(3)}s infinite`,\n                animationPlayState: inView ? \"running\" : \"paused\",\n              }}\n            />\n          )\n        })}\n        {bursts.map((b) => (\n          <span\n            key={b.id}\n            data-slot=\"ripple-burst\"\n            onAnimationEnd={() => setBursts((all) => all.filter((x) => x.id !== b.id))}\n            className=\"absolute block -translate-x-1/2 -translate-y-1/2 rounded-full border\"\n            style={{\n              left: b.x,\n              top: b.y,\n              borderColor: \"var(--accent)\",\n              background: \"var(--accent-soft)\",\n              animation: \"ripple-burst 1.1s cubic-bezier(0.2, 0, 0, 1) forwards\",\n            }}\n          />\n        ))}\n      </div>\n      <div className=\"relative z-10 h-full\">\n        {children ?? (\n          <div className=\"flex h-full flex-col items-center justify-center gap-4 px-6 text-center\">\n            <span className=\"inline-flex items-center gap-1.5 rounded-full border border-accent-line bg-accent-soft px-2.5 py-0.5 text-xs font-medium text-accent-fg\">\n              <Radio className=\"size-3.5\" aria-hidden />\n              Live bank sync\n            </span>\n            <p className=\"max-w-[16ch] text-3xl font-medium tracking-[-0.035em] text-balance text-fg\">\n              Lumen hears every payment\n            </p>\n            <Button>Connect a bank</Button>\n          </div>\n        )}\n      </div>\n      <style>{`@keyframes ripple-ping{0%{transform:translate(-50%,-50%) scale(1);opacity:var(--ripple-o)}10%{transform:translate(-50%,-50%) scale(1.035);opacity:calc(var(--ripple-o) + 0.45)}32%,100%{transform:translate(-50%,-50%) scale(1);opacity:var(--ripple-o)}}@keyframes ripple-burst{from{width:8px;height:8px;opacity:0.9}to{width:440px;height:440px;opacity:0}}`}</style>\n    </div>\n  )\n}\n\nexport { Ripple }\nexport type { RippleProps }\n"
    }
  ]
}
