Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 30 additions & 9 deletions apps/www/content/docs/components/pulsating-button.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
Expand All @@ -70,15 +83,23 @@ import { PulsatingButton } from "@/components/ui/pulsating-button"
<PulsatingButton>Pulsating Button</PulsatingButton>
```

## Examples

### Ripple Variant

<ComponentPreview name="pulsating-button-demo-2" />

## 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)
112 changes: 106 additions & 6 deletions apps/www/public/llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLButtonElement> {
pulseColor?: string
duration?: string
distance?: string
variant?: "pulse" | "ripple"
}

export const PulsatingButton = React.forwardRef<
Expand All @@ -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<HTMLButtonElement>(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 (
<button
ref={ref}
ref={innerRef}
className={cn(
"bg-primary text-primary-foreground relative flex cursor-pointer items-center justify-center rounded-lg px-4 py-2 text-center",
className
)}
style={
{
"--pulse-color": pulseColor,
...(pulseColor && { "--pulse-color": pulseColor }),
"--duration": duration,
"--distance": distance,
} as React.CSSProperties
}
{...props}
>
<div className="relative z-10">{children}</div>
<div className="absolute top-1/2 left-1/2 size-full -translate-x-1/2 -translate-y-1/2 animate-pulse rounded-lg bg-inherit" />
<span className="relative z-10">{children}</span>
<span
aria-hidden="true"
className={cn(
"pointer-events-none absolute inset-0 rounded-[inherit] bg-inherit",
variant === "pulse" ? "animate-pulse" : "animate-pulse-ripple"
)}
/>
</button>
)
}
Expand All @@ -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 (
<PulsatingButton
variant="ripple"
distance="10px"
className="bg-blue-400 text-white"
>
Join Affiliate Program
</PulsatingButton>
)
}



===== COMPONENT: rainbow-button =====
Title: Rainbow Button
Expand Down
1 change: 1 addition & 0 deletions apps/www/public/llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 17 additions & 0 deletions apps/www/public/r/pulsating-button-demo-2.json
Original file line number Diff line number Diff line change
@@ -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 <PulsatingButton\n variant=\"ripple\"\n distance=\"10px\"\n className=\"bg-blue-400 text-white\"\n >\n Join Affiliate Program\n </PulsatingButton>\n )\n}\n",
"type": "registry:example"
}
]
}
17 changes: 13 additions & 4 deletions apps/www/public/r/pulsating-button.json
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLButtonElement> {\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 <button\n ref={ref}\n className={cn(\n \"bg-primary text-primary-foreground relative flex cursor-pointer items-center justify-center rounded-lg px-4 py-2 text-center\",\n className\n )}\n style={\n {\n \"--pulse-color\": pulseColor,\n \"--duration\": duration,\n } as React.CSSProperties\n }\n {...props}\n >\n <div className=\"relative z-10\">{children}</div>\n <div className=\"absolute top-1/2 left-1/2 size-full -translate-x-1/2 -translate-y-1/2 animate-pulse rounded-lg bg-inherit\" />\n </button>\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<HTMLButtonElement> {\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<HTMLButtonElement>(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 <button\n ref={innerRef}\n className={cn(\n \"bg-primary text-primary-foreground relative flex cursor-pointer items-center justify-center rounded-lg px-4 py-2 text-center\",\n className\n )}\n style={\n {\n ...(pulseColor && { \"--pulse-color\": pulseColor }),\n \"--duration\": duration,\n \"--distance\": distance,\n } as React.CSSProperties\n }\n {...props}\n >\n <span className=\"relative z-10\">{children}</span>\n <span\n aria-hidden=\"true\"\n className={cn(\n \"pointer-events-none absolute inset-0 rounded-[inherit] bg-inherit\",\n variant === \"pulse\" ? \"animate-pulse\" : \"animate-pulse-ripple\"\n )}\n />\n </button>\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)"
}
}
}
Expand Down
30 changes: 27 additions & 3 deletions apps/www/public/r/registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
}
}
}
Expand Down Expand Up @@ -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",
Expand Down
30 changes: 27 additions & 3 deletions apps/www/public/registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
}
}
}
Expand Down Expand Up @@ -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",
Expand Down
Loading
Loading