diff --git a/apps/www/content/docs/components/pulsating-button.mdx b/apps/www/content/docs/components/pulsating-button.mdx index 6e216500f..7814dd482 100644 --- a/apps/www/content/docs/components/pulsating-button.mdx +++ b/apps/www/content/docs/components/pulsating-button.mdx @@ -41,14 +41,27 @@ Add the following animations to your global CSS file. ```css title="app/globals.css" showLineNumbers {2, 4-12} @theme inline { --animate-pulse: pulse var(--duration) ease-out infinite; + --animate-pulse-ripple: pulse-ripple var(--duration) + cubic-bezier(0.16, 1, 0.3, 1) infinite; @keyframes pulse { 0%, 100% { - box-shadow: 0 0 0 0 var(--pulse-color); + box-shadow: 0 0 0 0 var(--pulse-color, oklch(from var(--bg) l c h / 0.5)); } 50% { - box-shadow: 0 0 0 8px var(--pulse-color); + box-shadow: 0 0 0 var(--distance) + var(--pulse-color, oklch(from var(--bg) l c h / 0.5)); + } + } + + @keyframes pulse-ripple { + 0% { + box-shadow: 0 0 0 0 oklch(from var(--pulse-color, var(--bg)) l c h / 1); + } + 100% { + box-shadow: 0 0 0 var(--distance) + oklch(from var(--pulse-color, var(--bg)) l c h / 0); } } } @@ -70,15 +83,23 @@ import { PulsatingButton } from "@/components/ui/pulsating-button" Pulsating Button ``` +## Examples + +### Ripple Variant + + + ## Props -| Prop | Type | Default | Description | -| ------------ | ----------------- | ------- | -------------------------------------------------------- | -| `children` | `React.ReactNode` | `-` | The content of the button. | -| `className` | `string` | `-` | Additional class names for the button. | -| `pulseColor` | `string` | `-` | The rbg numbers only for the color of the pulsing waves. | -| `duration` | `string` | `-` | The time span of one pulse. | +| Prop | Type | Default | Description | +| ------------ | --------------------- | -------------------------------- | ------------------------------------------ | +| `children` | `React.ReactNode` | `-` | The content of the button. | +| `className` | `string` | `-` | Additional class names for the button. | +| `pulseColor` | `string` | `Derived from button background` | Any valid CSS color for the pulsing waves. | +| `duration` | `string` | `1.5s` | The duration of one pulse. | +| `distance` | `string` | `8px` | How far the pulse expands from the button. | +| `variant` | `"pulse" \| "ripple"` | `pulse` | The pulse animation style. | ## Credits -- Credit to [@shikhap04](https://github.com/shikhap04) +- Credit to [@shikhap04](https://github.com/shikhap04) & [@abdmjd1](https://github.com/abdmjd1) diff --git a/apps/www/public/llms-full.txt b/apps/www/public/llms-full.txt index 2a89c2f05..4c49bccda 100644 --- a/apps/www/public/llms-full.txt +++ b/apps/www/public/llms-full.txt @@ -12883,13 +12883,17 @@ Title: Pulsating Button Description: An animated pulsating button useful for capturing attention of users. --- file: magicui/pulsating-button.tsx --- -import React from "react" +"use client" + +import React, { useImperativeHandle, useLayoutEffect, useRef } from "react" import { cn } from "@/lib/utils" interface PulsatingButtonProps extends React.ButtonHTMLAttributes { pulseColor?: string duration?: string + distance?: string + variant?: "pulse" | "ripple" } export const PulsatingButton = React.forwardRef< @@ -12900,29 +12904,106 @@ export const PulsatingButton = React.forwardRef< { className, children, - pulseColor = "#808080", + pulseColor, duration = "1.5s", + distance = "8px", + variant = "pulse", ...props }, ref ) => { + const innerRef = useRef(null) + useImperativeHandle(ref, () => innerRef.current!) + + useLayoutEffect(() => { + const button = innerRef.current + if (!button) return + + if (pulseColor) { + button.style.removeProperty("--bg") + return + } + + let animationFrameId = 0 + let currentBg = "" + + const updateBg = () => { + animationFrameId = 0 + + const nextBg = getComputedStyle(button).backgroundColor + if (nextBg === currentBg) return + + currentBg = nextBg + button.style.setProperty("--bg", nextBg) + } + + const scheduleBgUpdate = () => { + if (animationFrameId) return + animationFrameId = window.requestAnimationFrame(updateBg) + } + + updateBg() + + const themeObserver = new MutationObserver(scheduleBgUpdate) + themeObserver.observe(document.documentElement, { + attributes: true, + attributeFilter: ["class"], + }) + + const buttonObserver = new MutationObserver(scheduleBgUpdate) + buttonObserver.observe(button, { + attributes: true, + }) + + const syncEvents = [ + "blur", + "focus", + "pointerenter", + "pointerleave", + ] as const + + for (const eventName of syncEvents) { + button.addEventListener(eventName, scheduleBgUpdate) + } + + return () => { + if (animationFrameId) { + window.cancelAnimationFrame(animationFrameId) + } + + themeObserver.disconnect() + buttonObserver.disconnect() + + for (const eventName of syncEvents) { + button.removeEventListener(eventName, scheduleBgUpdate) + } + } + }, [pulseColor]) + return ( ) } @@ -12942,6 +13023,25 @@ export default function PulsatingButtonDemo() { } +===== EXAMPLE: pulsating-button-demo-2 ===== +Title: Pulsating Button Demo 2 + +--- file: example/pulsating-button-demo-2.tsx --- +import { PulsatingButton } from "@/registry/magicui/pulsating-button" + +export default function PulsatingButtonDemo2() { + return ( + + Join Affiliate Program + + ) +} + + ===== COMPONENT: rainbow-button ===== Title: Rainbow Button diff --git a/apps/www/public/llms.txt b/apps/www/public/llms.txt index bbed3d35b..ffbd983a5 100644 --- a/apps/www/public/llms.txt +++ b/apps/www/public/llms.txt @@ -193,6 +193,7 @@ This file provides LLM-friendly entry points to documentation and examples. - [Cool Mode Demo](https://github.com/magicuidesign/magicui/blob/main/example/cool-mode-demo.tsx): Example usage - [Cool Mode Custom](https://github.com/magicuidesign/magicui/blob/main/example/cool-mode-custom.tsx): Example usage - [Pulsating Button Demo](https://github.com/magicuidesign/magicui/blob/main/example/pulsating-button-demo.tsx): Example usage +- [Pulsating Button Demo 2](https://github.com/magicuidesign/magicui/blob/main/example/pulsating-button-demo-2.tsx): Example usage - [Ripple Button Demo](https://github.com/magicuidesign/magicui/blob/main/example/ripple-button-demo.tsx): Example usage - [File Tree Demo](https://github.com/magicuidesign/magicui/blob/main/example/file-tree-demo.tsx): Example usage - [Blur Fade Demo](https://github.com/magicuidesign/magicui/blob/main/example/blur-fade-demo.tsx): Example usage diff --git a/apps/www/public/r/pulsating-button-demo-2.json b/apps/www/public/r/pulsating-button-demo-2.json new file mode 100644 index 000000000..02bb6f21a --- /dev/null +++ b/apps/www/public/r/pulsating-button-demo-2.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://ui.shadcn.com/schema/registry-item.json", + "name": "pulsating-button-demo-2", + "type": "registry:example", + "title": "Pulsating Button Demo 2", + "description": "Example showing an animated pulsating button with a ripple varaint.", + "registryDependencies": [ + "@magicui/pulsating-button" + ], + "files": [ + { + "path": "registry/example/pulsating-button-demo-2.tsx", + "content": "import { PulsatingButton } from \"@/registry/magicui/pulsating-button\"\n\nexport default function PulsatingButtonDemo2() {\n return (\n \n Join Affiliate Program\n \n )\n}\n", + "type": "registry:example" + } + ] +} \ No newline at end of file diff --git a/apps/www/public/r/pulsating-button.json b/apps/www/public/r/pulsating-button.json index 853f24b01..dff60452f 100644 --- a/apps/www/public/r/pulsating-button.json +++ b/apps/www/public/r/pulsating-button.json @@ -7,22 +7,31 @@ "files": [ { "path": "registry/magicui/pulsating-button.tsx", - "content": "import React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\ninterface PulsatingButtonProps extends React.ButtonHTMLAttributes {\n pulseColor?: string\n duration?: string\n}\n\nexport const PulsatingButton = React.forwardRef<\n HTMLButtonElement,\n PulsatingButtonProps\n>(\n (\n {\n className,\n children,\n pulseColor = \"#808080\",\n duration = \"1.5s\",\n ...props\n },\n ref\n ) => {\n return (\n \n
{children}
\n
\n \n )\n }\n)\n\nPulsatingButton.displayName = \"PulsatingButton\"\n", + "content": "\"use client\"\n\nimport React, { useImperativeHandle, useLayoutEffect, useRef } from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\ninterface PulsatingButtonProps extends React.ButtonHTMLAttributes {\n pulseColor?: string\n duration?: string\n distance?: string\n variant?: \"pulse\" | \"ripple\"\n}\n\nexport const PulsatingButton = React.forwardRef<\n HTMLButtonElement,\n PulsatingButtonProps\n>(\n (\n {\n className,\n children,\n pulseColor,\n duration = \"1.5s\",\n distance = \"8px\",\n variant = \"pulse\",\n ...props\n },\n ref\n ) => {\n const innerRef = useRef(null)\n useImperativeHandle(ref, () => innerRef.current!)\n\n useLayoutEffect(() => {\n const button = innerRef.current\n if (!button) return\n\n if (pulseColor) {\n button.style.removeProperty(\"--bg\")\n return\n }\n\n let animationFrameId = 0\n let currentBg = \"\"\n\n const updateBg = () => {\n animationFrameId = 0\n\n const nextBg = getComputedStyle(button).backgroundColor\n if (nextBg === currentBg) return\n\n currentBg = nextBg\n button.style.setProperty(\"--bg\", nextBg)\n }\n\n const scheduleBgUpdate = () => {\n if (animationFrameId) return\n animationFrameId = window.requestAnimationFrame(updateBg)\n }\n\n updateBg()\n\n const themeObserver = new MutationObserver(scheduleBgUpdate)\n themeObserver.observe(document.documentElement, {\n attributes: true,\n attributeFilter: [\"class\"],\n })\n\n const buttonObserver = new MutationObserver(scheduleBgUpdate)\n buttonObserver.observe(button, {\n attributes: true,\n })\n\n const syncEvents = [\n \"blur\",\n \"focus\",\n \"pointerenter\",\n \"pointerleave\",\n ] as const\n\n for (const eventName of syncEvents) {\n button.addEventListener(eventName, scheduleBgUpdate)\n }\n\n return () => {\n if (animationFrameId) {\n window.cancelAnimationFrame(animationFrameId)\n }\n\n themeObserver.disconnect()\n buttonObserver.disconnect()\n\n for (const eventName of syncEvents) {\n button.removeEventListener(eventName, scheduleBgUpdate)\n }\n }\n }, [pulseColor])\n\n return (\n \n {children}\n \n \n )\n }\n)\n\nPulsatingButton.displayName = \"PulsatingButton\"\n", "type": "registry:ui" } ], "cssVars": { "theme": { - "animate-pulse": "pulse var(--duration) ease-out infinite" + "animate-pulse": "pulse var(--duration) ease-out infinite", + "animate-pulse-ripple": "pulse-ripple var(--duration) cubic-bezier(0.16, 1, 0.3, 1) infinite" } }, "css": { "@keyframes pulse": { "0%, 100%": { - "boxShadow": "0 0 0 0 var(--pulse-color)" + "boxShadow": "0 0 0 0 var(--pulse-color, oklch(from var(--bg) l c h / 0.5))" }, "50%": { - "boxShadow": "0 0 0 8px var(--pulse-color)" + "boxShadow": "0 0 0 var(--distance) var(--pulse-color, oklch(from var(--bg) l c h / 0.5))" + } + }, + "@keyframes pulse-ripple": { + "0%": { + "boxShadow": "0 0 0 0 oklch(from var(--pulse-color, var(--bg)) l c h / 1)" + }, + "100%": { + "boxShadow": "0 0 0 var(--distance) oklch(from var(--pulse-color, var(--bg)) l c h / 0)" } } } diff --git a/apps/www/public/r/registry.json b/apps/www/public/r/registry.json index 2bbad9d0e..197f12ebf 100644 --- a/apps/www/public/r/registry.json +++ b/apps/www/public/r/registry.json @@ -1000,16 +1000,25 @@ ], "cssVars": { "theme": { - "animate-pulse": "pulse var(--duration) ease-out infinite" + "animate-pulse": "pulse var(--duration) ease-out infinite", + "animate-pulse-ripple": "pulse-ripple var(--duration) cubic-bezier(0.16, 1, 0.3, 1) infinite" } }, "css": { "@keyframes pulse": { "0%, 100%": { - "boxShadow": "0 0 0 0 var(--pulse-color)" + "boxShadow": "0 0 0 0 var(--pulse-color, oklch(from var(--bg) l c h / 0.5))" }, "50%": { - "boxShadow": "0 0 0 8px var(--pulse-color)" + "boxShadow": "0 0 0 var(--distance) var(--pulse-color, oklch(from var(--bg) l c h / 0.5))" + } + }, + "@keyframes pulse-ripple": { + "0%": { + "boxShadow": "0 0 0 0 oklch(from var(--pulse-color, var(--bg)) l c h / 1)" + }, + "100%": { + "boxShadow": "0 0 0 var(--distance) oklch(from var(--pulse-color, var(--bg)) l c h / 0)" } } } @@ -3020,6 +3029,21 @@ } ] }, + { + "name": "pulsating-button-demo-2", + "type": "registry:example", + "title": "Pulsating Button Demo 2", + "description": "Example showing an animated pulsating button with a ripple varaint.", + "registryDependencies": [ + "@magicui/pulsating-button" + ], + "files": [ + { + "path": "registry/example/pulsating-button-demo-2.tsx", + "type": "registry:example" + } + ] + }, { "name": "ripple-button-demo", "type": "registry:example", diff --git a/apps/www/public/registry.json b/apps/www/public/registry.json index 2bbad9d0e..197f12ebf 100644 --- a/apps/www/public/registry.json +++ b/apps/www/public/registry.json @@ -1000,16 +1000,25 @@ ], "cssVars": { "theme": { - "animate-pulse": "pulse var(--duration) ease-out infinite" + "animate-pulse": "pulse var(--duration) ease-out infinite", + "animate-pulse-ripple": "pulse-ripple var(--duration) cubic-bezier(0.16, 1, 0.3, 1) infinite" } }, "css": { "@keyframes pulse": { "0%, 100%": { - "boxShadow": "0 0 0 0 var(--pulse-color)" + "boxShadow": "0 0 0 0 var(--pulse-color, oklch(from var(--bg) l c h / 0.5))" }, "50%": { - "boxShadow": "0 0 0 8px var(--pulse-color)" + "boxShadow": "0 0 0 var(--distance) var(--pulse-color, oklch(from var(--bg) l c h / 0.5))" + } + }, + "@keyframes pulse-ripple": { + "0%": { + "boxShadow": "0 0 0 0 oklch(from var(--pulse-color, var(--bg)) l c h / 1)" + }, + "100%": { + "boxShadow": "0 0 0 var(--distance) oklch(from var(--pulse-color, var(--bg)) l c h / 0)" } } } @@ -3020,6 +3029,21 @@ } ] }, + { + "name": "pulsating-button-demo-2", + "type": "registry:example", + "title": "Pulsating Button Demo 2", + "description": "Example showing an animated pulsating button with a ripple varaint.", + "registryDependencies": [ + "@magicui/pulsating-button" + ], + "files": [ + { + "path": "registry/example/pulsating-button-demo-2.tsx", + "type": "registry:example" + } + ] + }, { "name": "ripple-button-demo", "type": "registry:example", diff --git a/apps/www/registry.json b/apps/www/registry.json index 2bbad9d0e..197f12ebf 100644 --- a/apps/www/registry.json +++ b/apps/www/registry.json @@ -1000,16 +1000,25 @@ ], "cssVars": { "theme": { - "animate-pulse": "pulse var(--duration) ease-out infinite" + "animate-pulse": "pulse var(--duration) ease-out infinite", + "animate-pulse-ripple": "pulse-ripple var(--duration) cubic-bezier(0.16, 1, 0.3, 1) infinite" } }, "css": { "@keyframes pulse": { "0%, 100%": { - "boxShadow": "0 0 0 0 var(--pulse-color)" + "boxShadow": "0 0 0 0 var(--pulse-color, oklch(from var(--bg) l c h / 0.5))" }, "50%": { - "boxShadow": "0 0 0 8px var(--pulse-color)" + "boxShadow": "0 0 0 var(--distance) var(--pulse-color, oklch(from var(--bg) l c h / 0.5))" + } + }, + "@keyframes pulse-ripple": { + "0%": { + "boxShadow": "0 0 0 0 oklch(from var(--pulse-color, var(--bg)) l c h / 1)" + }, + "100%": { + "boxShadow": "0 0 0 var(--distance) oklch(from var(--pulse-color, var(--bg)) l c h / 0)" } } } @@ -3020,6 +3029,21 @@ } ] }, + { + "name": "pulsating-button-demo-2", + "type": "registry:example", + "title": "Pulsating Button Demo 2", + "description": "Example showing an animated pulsating button with a ripple varaint.", + "registryDependencies": [ + "@magicui/pulsating-button" + ], + "files": [ + { + "path": "registry/example/pulsating-button-demo-2.tsx", + "type": "registry:example" + } + ] + }, { "name": "ripple-button-demo", "type": "registry:example", diff --git a/apps/www/registry/__index__.tsx b/apps/www/registry/__index__.tsx index 1719d025d..b3c0ec9ed 100644 --- a/apps/www/registry/__index__.tsx +++ b/apps/www/registry/__index__.tsx @@ -3143,6 +3143,23 @@ export const Index: Record = { }), meta: undefined, }, + "pulsating-button-demo-2": { + name: "pulsating-button-demo-2", + description: "Example showing an animated pulsating button with a ripple varaint.", + type: "registry:example", + registryDependencies: ["@magicui/pulsating-button"], + files: [{ + path: "registry/example/pulsating-button-demo-2.tsx", + type: "registry:example", + target: "" + }], + component: React.lazy(async () => { + const mod = await import("@/registry/example/pulsating-button-demo-2.tsx") + const exportName = Object.keys(mod).find(key => typeof mod[key] === 'function' || typeof mod[key] === 'object') ?? item.name + return { default: mod.default ?? mod[exportName] } + }), + meta: undefined, + }, "ripple-button-demo": { name: "ripple-button-demo", description: "Example showing an animated button with ripple effect.", diff --git a/apps/www/registry/example/pulsating-button-demo-2.tsx b/apps/www/registry/example/pulsating-button-demo-2.tsx new file mode 100644 index 000000000..edb87b5de --- /dev/null +++ b/apps/www/registry/example/pulsating-button-demo-2.tsx @@ -0,0 +1,13 @@ +import { PulsatingButton } from "@/registry/magicui/pulsating-button" + +export default function PulsatingButtonDemo2() { + return ( + + Join Affiliate Program + + ) +} diff --git a/apps/www/registry/magicui/pulsating-button.tsx b/apps/www/registry/magicui/pulsating-button.tsx index 7e1254883..e2ea6142b 100644 --- a/apps/www/registry/magicui/pulsating-button.tsx +++ b/apps/www/registry/magicui/pulsating-button.tsx @@ -1,10 +1,14 @@ -import React from "react" +"use client" + +import React, { useImperativeHandle, useLayoutEffect, useRef } from "react" import { cn } from "@/lib/utils" interface PulsatingButtonProps extends React.ButtonHTMLAttributes { pulseColor?: string duration?: string + distance?: string + variant?: "pulse" | "ripple" } export const PulsatingButton = React.forwardRef< @@ -15,29 +19,106 @@ export const PulsatingButton = React.forwardRef< { className, children, - pulseColor = "#808080", + pulseColor, duration = "1.5s", + distance = "8px", + variant = "pulse", ...props }, ref ) => { + const innerRef = useRef(null) + useImperativeHandle(ref, () => innerRef.current!) + + useLayoutEffect(() => { + const button = innerRef.current + if (!button) return + + if (pulseColor) { + button.style.removeProperty("--bg") + return + } + + let animationFrameId = 0 + let currentBg = "" + + const updateBg = () => { + animationFrameId = 0 + + const nextBg = getComputedStyle(button).backgroundColor + if (nextBg === currentBg) return + + currentBg = nextBg + button.style.setProperty("--bg", nextBg) + } + + const scheduleBgUpdate = () => { + if (animationFrameId) return + animationFrameId = window.requestAnimationFrame(updateBg) + } + + updateBg() + + const themeObserver = new MutationObserver(scheduleBgUpdate) + themeObserver.observe(document.documentElement, { + attributes: true, + attributeFilter: ["class"], + }) + + const buttonObserver = new MutationObserver(scheduleBgUpdate) + buttonObserver.observe(button, { + attributes: true, + }) + + const syncEvents = [ + "blur", + "focus", + "pointerenter", + "pointerleave", + ] as const + + for (const eventName of syncEvents) { + button.addEventListener(eventName, scheduleBgUpdate) + } + + return () => { + if (animationFrameId) { + window.cancelAnimationFrame(animationFrameId) + } + + themeObserver.disconnect() + buttonObserver.disconnect() + + for (const eventName of syncEvents) { + button.removeEventListener(eventName, scheduleBgUpdate) + } + } + }, [pulseColor]) + return ( ) } diff --git a/apps/www/registry/registry-examples.ts b/apps/www/registry/registry-examples.ts index 702f6aa65..18e913412 100644 --- a/apps/www/registry/registry-examples.ts +++ b/apps/www/registry/registry-examples.ts @@ -1520,6 +1520,20 @@ export const examples: Registry["items"] = [ }, ], }, + { + name: "pulsating-button-demo-2", + type: "registry:example", + title: "Pulsating Button Demo 2", + description: + "Example showing an animated pulsating button with a ripple varaint.", + registryDependencies: ["@magicui/pulsating-button"], + files: [ + { + path: "example/pulsating-button-demo-2.tsx", + type: "registry:example", + }, + ], + }, { name: "ripple-button-demo", type: "registry:example", diff --git a/apps/www/registry/registry-ui.ts b/apps/www/registry/registry-ui.ts index c9e0de4cd..140129cdd 100644 --- a/apps/www/registry/registry-ui.ts +++ b/apps/www/registry/registry-ui.ts @@ -952,12 +952,30 @@ export const ui: Registry["items"] = [ cssVars: { theme: { "animate-pulse": "pulse var(--duration) ease-out infinite", + "animate-pulse-ripple": + "pulse-ripple var(--duration) cubic-bezier(0.16, 1, 0.3, 1) infinite", }, }, css: { "@keyframes pulse": { - "0%, 100%": { boxShadow: "0 0 0 0 var(--pulse-color)" }, - "50%": { boxShadow: "0 0 0 8px var(--pulse-color)" }, + "0%, 100%": { + boxShadow: + "0 0 0 0 var(--pulse-color, oklch(from var(--bg) l c h / 0.5))", + }, + "50%": { + boxShadow: + "0 0 0 var(--distance) var(--pulse-color, oklch(from var(--bg) l c h / 0.5))", + }, + }, + "@keyframes pulse-ripple": { + "0%": { + boxShadow: + "0 0 0 0 oklch(from var(--pulse-color, var(--bg)) l c h / 1)", + }, + "100%": { + boxShadow: + "0 0 0 var(--distance) oklch(from var(--pulse-color, var(--bg)) l c h / 0)", + }, }, }, }, diff --git a/apps/www/styles/globals.css b/apps/www/styles/globals.css index e4d249b9f..cea7b8501 100644 --- a/apps/www/styles/globals.css +++ b/apps/www/styles/globals.css @@ -167,6 +167,7 @@ alternate; --animate-shine: shine var(--duration) infinite linear; --animate-pulse: pulse var(--duration) ease-out infinite; + --animate-pulse-ripple: pulse-ripple var(--duration) cubic-bezier(0.16, 1, 0.3, 1) infinite; --animate-rainbow: rainbow var(--speed, 2s) infinite linear; --animate-line-shadow: line-shadow 15s linear infinite; --animate-aurora: aurora 8s ease-in-out infinite alternate; @@ -366,10 +367,19 @@ @keyframes pulse { 0%, 100% { - box-shadow: 0 0 0 0 var(--pulse-color); + box-shadow: 0 0 0 0 var(--pulse-color, oklch(from var(--bg) l c h / 0.5)); } 50% { - box-shadow: 0 0 0 8px var(--pulse-color); + box-shadow: 0 0 0 var(--distance) var(--pulse-color, oklch(from var(--bg) l c h / 0.5)); + } + } + + @keyframes pulse-ripple { + 0% { + box-shadow: 0 0 0 0 oklch(from var(--pulse-color, var(--bg)) l c h / 1); + } + 100% { + box-shadow: 0 0 0 var(--distance) oklch(from var(--pulse-color, var(--bg)) l c h / 0); } }