{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "button-particles", "title": "Button Particles", "author": "designbycode", "dependencies": [ "react", "clsx", "tailwind-merge", "@radix-ui/react-slot", "class-variance-authority" ], "registryDependencies": [], "files": [ { "path": "resources/js/registry/new-york/components/ui/buttons/button-particles.tsx", "content": "import { Slot } from '@radix-ui/react-slot';\nimport type { VariantProps } from 'class-variance-authority';\nimport { cva } from 'class-variance-authority';\nimport * as React from 'react';\nimport { useEffect, useRef } from 'react';\nimport { cn } from '@/lib/utils';\n\nconst buttonVariants = cva(\n \"relative isolate inline-flex items-center justify-center gap-2 overflow-visible rounded-md text-sm font-medium whitespace-nowrap transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n {\n variants: {\n variant: {\n default:\n 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90',\n destructive:\n 'bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40',\n outline:\n 'border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground',\n secondary:\n 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',\n ghost: 'hover:bg-accent hover:text-accent-foreground',\n link: 'text-primary underline-offset-4 hover:underline',\n },\n size: {\n default: 'h-9 px-4 py-2 has-[>svg]:px-3',\n sm: 'h-8 rounded-md px-3 has-[>svg]:px-2.5',\n lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',\n icon: 'size-9',\n },\n },\n defaultVariants: {\n variant: 'default',\n size: 'default',\n },\n },\n);\n\n// --- Particle style injection ---\n// One unique keyframe per particle slot so each gets its own randomised\n// endpoint baked in at style-injection time. CSS custom properties\n// (--pdx, --pdy, --pdur) are set on the element at click time.\n\nconst DEFAULT_PARTICLE_COUNT = 20;\nconst PARTICLE_STYLE_ID = 'button-particle-styles-v2';\n\nfunction injectParticleStyles(maxIndex: number) {\n if (typeof document === 'undefined') {\n return;\n }\n\n let style = document.getElementById(PARTICLE_STYLE_ID);\n let existingCss = '';\n let existingMax = 0;\n\n if (style) {\n existingCss = style.textContent || '';\n existingMax = parseInt(style.dataset.maxIndex || '0', 10);\n\n if (existingMax >= maxIndex) {\n return;\n }\n } else {\n style = document.createElement('style');\n style.id = PARTICLE_STYLE_ID;\n }\n\n style.dataset.maxIndex = String(maxIndex);\n\n let css = existingCss;\n\n if (!css) {\n css = `\n .bp-particle {\n position: absolute;\n border-radius: 50%;\n pointer-events: none;\n z-index: 99999;\n will-change: transform, opacity;\n }\n `;\n }\n\n for (let i = existingMax + 1; i <= maxIndex; i++) {\n css += `\n @keyframes particle-burst-${i} {\n 0% { transform: translate(-50%, -50%) translate(0px, 0px) scale(1); opacity: 1; }\n 60% { opacity: 0.9; }\n 100% { transform: translate(-50%, -50%) translate(var(--pdx), var(--pdy)) scale(0); opacity: 0; }\n }\n .bp-particle[data-particle=\"burst\"][data-idx=\"${i}\"] {\n animation: particle-burst-${i} var(--pdur) cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards;\n }\n\n @keyframes sparkle-burst-${i} {\n 0% { transform: translate(-50%, -50%) translate(0px, 0px) rotate(0deg) scale(1); opacity: 1; }\n 60% { opacity: 0.9; }\n 100% { transform: translate(-50%, -50%) translate(var(--pdx), var(--pdy)) rotate(180deg) scale(0.3); opacity: 0; }\n }\n .bp-particle[data-particle=\"sparkle\"][data-idx=\"${i}\"] {\n animation: sparkle-burst-${i} var(--pdur) cubic-bezier(0.2, 0.8, 0.2, 1) forwards;\n }\n\n @keyframes confetti-spray-${i} {\n 0% { transform: translate(-50%, -50%) translate(0px, 0px) rotate(0deg) scale(1); opacity: 1; }\n 60% { opacity: 0.9; }\n 100% { transform: translate(-50%, -50%) translate(var(--pdx), var(--pdy)) rotate(360deg) scale(0.5); opacity: 0; }\n }\n .bp-particle[data-particle=\"confetti\"][data-idx=\"${i}\"] {\n animation: confetti-spray-${i} var(--pdur) cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards;\n }\n\n @keyframes vburst-${i} {\n 0% { transform: translate(-50%, -50%) translate(0px, 0px) scale(1); opacity: 1; }\n 60% { opacity: 0.9; }\n 100% { transform: translate(-50%, -50%) translate(var(--pdx), var(--pdy)) scale(0); opacity: 0; }\n }\n .bp-particle[data-particle=\"vburst\"][data-idx=\"${i}\"] {\n animation: vburst-${i} var(--pdur) cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards;\n }\n\n @keyframes hburst-${i} {\n 0% { transform: translate(-50%, -50%) translate(0px, 0px) scale(1); opacity: 1; }\n 60% { opacity: 0.9; }\n 100% { transform: translate(-50%, -50%) translate(var(--pdx), var(--pdy)) scale(0); opacity: 0; }\n }\n .bp-particle[data-particle=\"hburst\"][data-idx=\"${i}\"] {\n animation: hburst-${i} var(--pdur) cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards;\n }\n\n @keyframes spiral-burst-${i} {\n 0% { transform: translate(-50%, -50%) translate(0px, 0px) rotate(0deg) scale(1); opacity: 1; }\n 60% { opacity: 0.9; }\n 100% { transform: translate(-50%, -50%) translate(var(--pdx), var(--pdy)) rotate(720deg) scale(0); opacity: 0; }\n }\n .bp-particle[data-particle=\"spiral\"][data-idx=\"${i}\"] {\n animation: spiral-burst-${i} var(--pdur) cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards;\n }\n `;\n }\n\n style.textContent = css;\n document.head.appendChild(style);\n}\n\ninjectParticleStyles(DEFAULT_PARTICLE_COUNT);\n\n// --- Colour palette ---\nconst DEFAULT_COLORS = [\n '#ff0083', // hot pink\n '#ff6b6b', // coral red\n '#ffd93d', // golden yellow\n '#6bcb77', // mint green\n '#4d96ff', // vivid blue\n '#c77dff', // violet\n '#ff9f1c', // amber orange\n '#00f5d4', // cyan\n];\n\nexport type ParticleType =\n | 'burst'\n | 'sparkle'\n | 'confetti'\n | 'vburst'\n | 'hburst'\n | 'spiral';\n\nfunction ButtonParticles({\n className,\n variant,\n size,\n particle = 'burst',\n particles = DEFAULT_PARTICLE_COUNT,\n colors = DEFAULT_COLORS,\n children,\n asChild = false,\n ...props\n}: React.ComponentProps<'button'> &\n VariantProps & {\n asChild?: boolean;\n particle?: ParticleType;\n particles?: number;\n colors?: string[];\n }) {\n const Comp = asChild ? Slot : 'button';\n const ref = useRef(null);\n\n useEffect(() => {\n injectParticleStyles(particles);\n }, [particles]);\n\n const createParticle = (\n buttonEl: HTMLElement,\n originX: number,\n originY: number,\n index: number,\n ) => {\n if (!buttonEl) {\n return;\n }\n\n const el = document.createElement('div');\n el.classList.add('bp-particle');\n el.dataset.idx = String(index);\n el.dataset.particle = particle;\n\n const color = colors[Math.floor(Math.random() * colors.length)];\n // Odd index → stroke only (mirrors the SCSS nth-of-type(odd) rule)\n const isStroke = index % 2 === 1;\n\n switch (particle) {\n case 'burst': {\n const angle = Math.random() * 2 * Math.PI;\n const dist = 70 + Math.random() * 90;\n const sz = 14 + Math.random() * 14;\n el.style.width = `${sz}px`;\n el.style.height = `${sz}px`;\n\n if (isStroke) {\n el.style.backgroundColor = 'transparent';\n el.style.border = `3px solid ${color}`;\n } else {\n el.style.backgroundColor = color;\n el.style.border = 'none';\n }\n\n const dur = 550 + Math.random() * 400;\n el.style.setProperty('--pdx', `${Math.cos(angle) * dist}px`);\n el.style.setProperty('--pdy', `${Math.sin(angle) * dist}px`);\n el.style.setProperty('--pdur', `${dur}ms`);\n el.style.left = `${originX - sz / 2}px`;\n el.style.top = `${originY - sz / 2}px`;\n buttonEl.appendChild(el);\n setTimeout(() => el.remove(), dur + 50);\n break;\n }\n case 'sparkle': {\n const dx = (Math.random() - 0.5) * 80;\n const dy = -(60 + Math.random() * 100);\n const sz = 8 + Math.random() * 12;\n el.style.width = `${sz}px`;\n el.style.height = `${sz}px`;\n\n if (isStroke) {\n el.style.backgroundColor = 'transparent';\n el.style.border = `2px solid ${color}`;\n } else {\n el.style.backgroundColor = color;\n el.style.border = 'none';\n }\n\n const dur = 600 + Math.random() * 400;\n el.style.setProperty('--pdx', `${dx}px`);\n el.style.setProperty('--pdy', `${dy}px`);\n el.style.setProperty('--pdur', `${dur}ms`);\n el.style.left = `${originX - sz / 2}px`;\n el.style.top = `${originY - sz / 2}px`;\n buttonEl.appendChild(el);\n setTimeout(() => el.remove(), dur + 50);\n break;\n }\n case 'confetti': {\n const spreadAngle = (Math.random() - 0.5) * Math.PI * 0.8;\n const angle = -Math.PI / 2 + spreadAngle;\n const dist = 80 + Math.random() * 120;\n const dx = Math.cos(angle) * dist;\n const dy = Math.sin(angle) * dist - 40;\n const sz = 10 + Math.random() * 8;\n el.style.width = `${sz}px`;\n el.style.height = `${sz * 0.5}px`;\n el.style.borderRadius = '2px';\n el.style.backgroundColor = color;\n el.style.border = 'none';\n const dur = 700 + Math.random() * 400;\n el.style.setProperty('--pdx', `${dx}px`);\n el.style.setProperty('--pdy', `${dy}px`);\n el.style.setProperty('--pdur', `${dur}ms`);\n el.style.left = `${originX - sz / 2}px`;\n el.style.top = `${originY - sz / 4}px`;\n buttonEl.appendChild(el);\n setTimeout(() => el.remove(), dur + 50);\n break;\n }\n case 'vburst': {\n const isUp = index % 2 === 0;\n const dist = 80 + Math.random() * 100;\n const sz = 12 + Math.random() * 12;\n el.style.width = `${sz}px`;\n el.style.height = `${sz}px`;\n el.style.backgroundColor = color;\n el.style.border = 'none';\n const dur = 550 + Math.random() * 400;\n el.style.setProperty(\n '--pdx',\n `${(Math.random() - 0.5) * 20}px`,\n );\n el.style.setProperty('--pdy', `${isUp ? -dist : dist}px`);\n el.style.setProperty('--pdur', `${dur}ms`);\n el.style.left = `${originX - sz / 2}px`;\n el.style.top = `${originY - sz / 2}px`;\n buttonEl.appendChild(el);\n setTimeout(() => el.remove(), dur + 50);\n break;\n }\n case 'hburst': {\n const isRight = index % 2 === 0;\n const dist = 80 + Math.random() * 100;\n const sz = 12 + Math.random() * 12;\n el.style.width = `${sz}px`;\n el.style.height = `${sz}px`;\n el.style.backgroundColor = color;\n el.style.border = 'none';\n const dur = 550 + Math.random() * 400;\n el.style.setProperty('--pdx', `${isRight ? dist : -dist}px`);\n el.style.setProperty(\n '--pdy',\n `${(Math.random() - 0.5) * 40}px`,\n );\n el.style.setProperty('--pdur', `${dur}ms`);\n el.style.left = `${originX - sz / 2}px`;\n el.style.top = `${originY - sz / 2}px`;\n buttonEl.appendChild(el);\n setTimeout(() => el.remove(), dur + 50);\n break;\n }\n case 'spiral': {\n const baseAngle = Math.random() * 2 * Math.PI;\n const dist = 60 + Math.random() * 100;\n const sz = 8 + Math.random() * 8;\n el.style.width = `${sz}px`;\n el.style.height = `${sz}px`;\n el.style.backgroundColor = color;\n el.style.border = 'none';\n const dur = 600 + Math.random() * 400;\n el.style.setProperty(\n '--pdx',\n `${Math.cos(baseAngle) * dist}px`,\n );\n el.style.setProperty(\n '--pdy',\n `${Math.sin(baseAngle) * dist}px`,\n );\n el.style.setProperty('--pdur', `${dur}ms`);\n el.style.left = `${originX - sz / 2}px`;\n el.style.top = `${originY - sz / 2}px`;\n buttonEl.appendChild(el);\n setTimeout(() => el.remove(), dur + 50);\n break;\n }\n }\n };\n\n const handleClick = (e: React.MouseEvent) => {\n if (!ref.current) {\n return;\n }\n\n props.onClick?.(e);\n\n if (e.defaultPrevented) {\n return;\n }\n\n const cx = ref.current.offsetWidth / 2;\n const cy = ref.current.offsetHeight / 2;\n const buttonEl = ref.current;\n\n for (let i = 1; i <= particles; i++) {\n setTimeout(() => createParticle(buttonEl, cx, cy, i), i * 12);\n }\n };\n\n return (\n \n {children}\n \n );\n}\n\nexport { ButtonParticles, buttonVariants };\n", "type": "registry:ui" } ], "meta": { "category": "components", "version": "1.0.0" }, "type": "registry:ui" }