Installation
Add this component to your project using the CLI:
terminal
npx vui-registry-cli-v1 add button-stackSource Code
button-stack.tsx
'use client'
import React, { useState } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import BtnSolid from './btn-solid'
import BtnOutline from './btn-outline'
import BtnGhost from './btn-ghost'
import BtnGradient from './btn-gradient'
import BtnNeon from './btn-neon'
import BtnRipple from './btn-ripple'
export default function ButtonStack() {
const [copied, setCopied] = useState<string | null>(null)
function copy(name: string, cmd: string) {
navigator.clipboard.writeText(cmd)
setCopied(name)
setTimeout(() => setCopied(null), 1500)
}
const variants = [
{ name: 'Solid', Component: BtnSolid, cmd: 'npx vui-registry-cli-v1 add btn-solid' },
{ name: 'Outline', Component: BtnOutline, cmd: 'npx vui-registry-cli-v1 add btn-outline' },
{ name: 'Ghost', Component: BtnGhost, cmd: 'npx vui-registry-cli-v1 add btn-ghost' },
{ name: 'Gradient', Component: BtnGradient, cmd: 'npx vui-registry-cli-v1 add btn-gradient' },
{ name: 'Neon', Component: BtnNeon, cmd: 'npx vui-registry-cli-v1 add btn-neon' },
{ name: 'Ripple', Component: BtnRipple, cmd: 'npx vui-registry-cli-v1 add btn-ripple' },
]
return (
<div className="w-full grid gap-6 md:grid-cols-2 p-4">
{variants.map((v) => (
<div key={v.name} className="rounded-2xl p-[2px] border border-border bg-card/50 backdrop-blur-sm overflow-hidden">
<div className="rounded-xl border border-border bg-background/60">
<div className="flex items-center justify-between px-4 py-3 border-b border-border/50 bg-muted/10 relative">
<div className="text-xs font-medium text-muted-foreground uppercase tracking-wider">{v.name}</div>
<button
onClick={() => copy(v.name, v.cmd)}
className="text-[10px] font-mono border-b border-border absolute top-2 right-3"
>
<AnimatePresence mode="wait">
{copied === v.name ? (
<motion.span key="copied" initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} className="text-emerald-500">
Copied
</motion.span>
) : (
<motion.span key="copy" initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}>
CLI
</motion.span>
)}
</AnimatePresence>
</button>
</div>
<div className="p-6 min-h-[160px] grid place-items-center">
<v.Component />
</div>
</div>
</div>
))}
</div>
)
}
Source Code
btn-ghost.tsx
'use client'
import { motion, AnimatePresence, useMotionTemplate, useMotionValue, useSpring } from 'framer-motion'
import React, { useState, useRef } from 'react'
export default function BtnGhost() {
const [status, setStatus] = useState<'idle' | 'loading' | 'success'>('idle')
const btnRef = useRef<HTMLButtonElement>(null)
// 1. Magnetic Mouse Physics
const mouseX = useMotionValue(0)
const mouseY = useMotionValue(0)
const springConfig = { stiffness: 150, damping: 15, mass: 0.6 }
const x = useSpring(mouseX, springConfig)
const y = useSpring(mouseY, springConfig)
// 2. Spotlight Effect (The "Ray-cast")
const spotlightX = useMotionValue(0)
const spotlightY = useMotionValue(0)
const background = useMotionTemplate`radial-gradient(650px circle at ${spotlightX} px ${spotlightY}px, rgba(0,0,0,0.06), transparent 80%)`
const darkBackground = useMotionTemplate`radial-gradient(650px circle at ${spotlightX}px ${spotlightY}px, rgba(255,255,255,0.12), transparent 80%)`
const handleMouseMove = (e: React.MouseEvent<HTMLButtonElement>) => {
if (!btnRef.current) return
const rect = btnRef.current.getBoundingClientRect()
// Calculate magnetic pull
const centerX = rect.left + rect.width / 2
const centerY = rect.top + rect.height / 2
mouseX.set((e.clientX - centerX) * 0.2)
mouseY.set((e.clientY - centerY) * 0.4)
// Calculate spotlight position
spotlightX.set(e.clientX - rect.left)
spotlightY.set(e.clientY - rect.top)
}
const handleMouseLeave = () => {
mouseX.set(0)
mouseY.set(0)
}
const handleClick = () => {
if (status !== 'idle') return
setStatus('loading')
setTimeout(() => setStatus('success'), 1800)
setTimeout(() => setStatus('idle'), 4000)
}
return (
<motion.button
ref={btnRef}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
onClick={handleClick}
style={{ x, y }}
whileTap={{ scale: 0.96, transition: { duration: 0.1 } }}
className="relative group px-10 py-4 rounded-2xl overflow-hidden
bg-black/[0.02] dark:bg-white/[0.03] backdrop-blur-xl
border-[1px] border-black/10 dark:border-white/10
shadow-[0_1px_2px_rgba(0,0,0,0.05)]
transition-colors duration-500"
>
{/* Dynamic Spotlight Layer */}
<motion.div
className="absolute inset-0 pointer-events-none opacity-0 group-hover:opacity-100 transition-opacity duration-500 dark:hidden"
style={{ background }}
/>
<motion.div
className="absolute inset-0 pointer-events-none opacity-0 group-hover:opacity-100 transition-opacity duration-500 hidden dark:block"
style={{ background: darkBackground }}
/>
{/* Content Morphing */}
<div className="relative z-10 flex items-center justify-center gap-3 min-w-[120px]">
<AnimatePresence mode="wait">
{status === 'idle' && (
<motion.div
key="idle"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
className="flex items-center gap-2"
>
<span className="text-black/80 dark:text-white/90 font-semibold tracking-tight">
Confirm Order
</span>
<motion.span
animate={{ x: [0, 3, 0] }}
transition={{ repeat: Infinity, duration: 2 }}
className="text-black/40 dark:text-white/40"
>
→
</motion.span>
</motion.div>
)}
{status === 'loading' && (
<motion.div
key="loading"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 1.1 }}
className="flex items-center gap-3"
>
<svg className="animate-spin h-5 w-5 text-black/60 dark:text-white/60" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
</svg>
<span className="text-black/50 dark:text-white/50 font-medium">Verifying</span>
</motion.div>
)}
{status === 'success' && (
<motion.div
key="success"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
className="flex items-center gap-2 text-emerald-600 dark:text-emerald-400"
>
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={3}>
<motion.path
initial={{ pathLength: 0 }}
animate={{ pathLength: 1 }}
transition={{ duration: 0.4 }}
strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7"
/>
</svg>
<span className="font-bold">Success</span>
</motion.div>
)}
</AnimatePresence>
</div>
{/* Subtle Bottom Glow on Hover */}
<motion.div
className="absolute bottom-0 left-1/2 -translate-x-1/2 w-1/2 h-[1px] bg-gradient-to-r from-transparent via-black/20 dark:via-white/30 to-transparent opacity-0 group-hover:opacity-100 transition-opacity"
/>
</motion.button>
)
}Source Code
btn-gradient.tsx
'use client'
import { motion, AnimatePresence } from 'framer-motion'
import React, { useState } from 'react'
import confetti from 'canvas-confetti'
export default function BtnGradient() {
const [isClicked, setIsClicked] = useState(false)
const handleAction = () => {
if (isClicked) return
setIsClicked(true)
// Premium haptic confetti
confetti({
particleCount: 30,
spread: 40,
origin: { y: 0.7 },
colors: ['#22d3ee', '#ffffff'],
disableForReducedMotion: true
})
setTimeout(() => setIsClicked(false), 2000)
}
return (
<div className="flex items-center justify-center p-4">
<motion.button
onClick={handleAction}
whileHover="hover"
whileTap="tap"
className="relative px-12 py-4 rounded-2xl overflow-hidden isolation-auto
bg-white dark:bg-slate-950
border border-slate-200 dark:border-slate-800
shadow-[0_4px_15px_rgba(0,0,0,0.05)]
hover:shadow-[0_15px_30px_rgba(34,211,238,0.2)]
transition-all duration-500 group"
>
{/* 1. INTERNAL LIQUID GLOW (Cyan Mixture) */}
<motion.div
variants={{
hover: { opacity: 1, scale: 1.2 },
initial: { opacity: 0, scale: 1 }
}}
className="absolute inset-0 z-0 pointer-events-none"
style={{
background: 'radial-gradient(circle at center, rgba(34,211,238,0.15) 0%, transparent 70%)',
}}
/>
{/* 2. THE PREMIUM BEAM (Ultra-slow & Elegant) */}
<motion.div
animate={{ x: ['-200%', '200%'] }}
transition={{ duration: 7, repeat: Infinity, ease: "linear" }}
className="absolute inset-0 z-0 w-40 bg-gradient-to-r from-transparent via-cyan-400/10 dark:via-cyan-400/20 to-transparent skew-x-[45deg] pointer-events-none"
/>
{/* 3. CLICK ANIMATION (The "Surge") */}
<AnimatePresence>
{isClicked && (
<motion.span
initial={{ opacity: 1, scale: 0 }}
animate={{ opacity: 0, scale: 2.5 }}
className="absolute inset-0 z-10 bg-cyan-400/30 rounded-full pointer-events-none"
transition={{ duration: 0.6, ease: "easeOut" }}
/>
)}
</AnimatePresence>
{/* 4. CONTENT LAYERING */}
<div className="relative z-20 flex items-center gap-4">
<motion.span
className="text-slate-700 dark:text-cyan-50 font-bold tracking-[0.2em] text-[12px] uppercase"
animate={isClicked ? { opacity: 0.5, y: 1 } : { opacity: 1, y: 0 }}
>
{isClicked ? 'Processing' : 'Get Started'}
</motion.span>
{/* Animated Icon */}
<div className="relative w-2 h-2">
<motion.div
animate={isClicked ? { scale: [1, 2, 1], rotate: 180 } : {}}
className="w-full h-full bg-cyan-500 rounded-full shadow-[0_0_10px_#22d3ee]"
/>
<motion.div
animate={{ scale: [1, 1.5, 1], opacity: [0.5, 0, 0.5] }}
transition={{ repeat: Infinity, duration: 2 }}
className="absolute inset-0 bg-cyan-400 rounded-full blur-[2px]"
/>
</div>
</div>
{/* 5. BOTTOM REFRACTION (The "Jewelry" Edge) */}
<div className="absolute bottom-0 left-0 right-0 h-[2px] bg-gradient-to-r from-transparent via-cyan-400/40 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
</motion.button>
</div>
)
}Source Code
btn-neon.tsx
'use client'
import { motion, AnimatePresence } from 'framer-motion'
import React, { useState } from 'react'
export default function BtnNeon() {
const [isClicked, setIsClicked] = useState(false)
const handleAction = () => {
if (isClicked) return
setIsClicked(true)
setTimeout(() => setIsClicked(false), 2000)
}
return (
<div className="relative flex items-center justify-center py-12">
<motion.button
onClick={handleAction}
whileHover="hover"
whileTap={{ scale: 0.98 }}
initial="initial"
className="relative px-14 py-5 rounded-2xl overflow-hidden bg-[#050505]
border border-white/[0.05] shadow-2xl transition-all duration-700 group"
>
{/* 1. THE LUXURY BEAM: Slowed to 12s for a "Calm" Premium feel */}
<div className="absolute inset-0 z-10 p-[1.2px] rounded-2xl overflow-hidden pointer-events-none">
<motion.div
animate={{ rotate: 360 }}
transition={{
duration: 12, // Ultra-slow rotation for premium feel
repeat: Infinity,
ease: "linear"
}}
style={{ originX: '50%', originY: '50%' }}
className="absolute inset-[-400%] opacity-0 group-hover:opacity-100 transition-opacity duration-1000"
>
<div className="w-full h-full bg-[conic-gradient(from_0deg,transparent_0deg,transparent_300deg,#22d3ee_340deg,transparent_360deg)]" />
</motion.div>
</div>
{/* 2. THE SHIELD: Deep Matte Black core */}
<div className="absolute inset-[1.2px] bg-[#050505] rounded-[15px] z-10 transition-colors duration-700" />
{/* 3. INTERNAL AMBIENCE: Soft "Breathing" Cyan Light */}
<motion.div
variants={{
hover: { opacity: 0.15 },
initial: { opacity: 0 }
}}
className="absolute inset-0 z-0 bg-[radial-gradient(circle_at_center,#22d3ee_0%,transparent_70%)] transition-opacity duration-1000"
/>
{/* 4. CLICK RESPONSE: The "Energy Surge" ripple */}
<AnimatePresence>
{isClicked && (
<motion.span
initial={{ opacity: 1, scale: 0 }}
animate={{ opacity: 0, scale: 3 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.8, ease: "easeOut" }}
className="absolute inset-0 z-20 bg-cyan-400/20 rounded-full pointer-events-none"
/>
)}
</AnimatePresence>
{/* 5. CONTENT: Boutique Typography */}
<div className="relative z-30 flex items-center gap-8">
<div className="flex flex-col items-start gap-1">
<span className="text-[8px] font-black uppercase tracking-[0.5em] text-cyan-500/50">
VELOCITY ui
</span>
<span className="text-[14px] font-semibold text-white/80 tracking-widest group-hover:text-white transition-colors duration-500">
{isClicked ? 'SYSTEM ACTIVE' : 'INITIALIZE'}
</span>
</div>
{/* Precision Haptic Icon */}
<div className="relative flex items-center justify-center">
<div className="w-1.5 h-1.5 rounded-full bg-cyan-500 shadow-[0_0_15px_#22d3ee] z-10" />
<motion.div
animate={{ scale: [1, 2, 1], opacity: [0.2, 0, 0.2] }}
transition={{ repeat: Infinity, duration: 4 }}
className="absolute w-4 h-4 rounded-full border border-cyan-400"
/>
</div>
</div>
{/* 6. EXTERNAL BLOOM: Subtle floor glow */}
<div className="absolute -inset-10 bg-cyan-500/[0.03] blur-[100px] opacity-0 group-hover:opacity-100 transition-opacity duration-1000 -z-10" />
</motion.button>
</div>
)
}Source Code
btn-outline.tsx
'use client'
import { motion } from 'framer-motion'
import React from 'react'
export default function BtnLayout() {
return (
<div className="py-12 flex justify-center bg-transparent">
<motion.button
whileHover="hover"
whileTap={{ scale: 0.98 }}
className="relative px-8 py-3 rounded-full bg-white dark:bg-[#0a0a0a] group
border border-slate-200 dark:border-zinc-800
transition-all duration-500 ease-out isolation-auto"
>
{/* 1. THE "NO-LEAK" INNER TRACK */}
<div className="absolute inset-[1px] rounded-full overflow-hidden pointer-events-none">
{/* Subtle Shine - Contained strictly inside */}
<motion.div
variants={{
hover: { x: ['-100%', '100%'] },
initial: { x: '-100%' }
}}
transition={{ duration: 1.5, ease: "easeInOut" }}
className="absolute inset-0 bg-gradient-to-r from-transparent via-cyan-500/10 to-transparent"
/>
</div>
{/* 2. CONTENT STRUCTURE */}
<div className="relative z-10 flex items-center gap-5">
{/* Status Badge - Compact & Clean */}
<div className="flex items-center gap-2 px-2.5 py-1 rounded-full bg-slate-50 dark:bg-zinc-900 border border-slate-100 dark:border-zinc-800">
<div className="w-1 h-1 rounded-full bg-cyan-500 shadow-[0_0_5px_rgba(6,182,212,0.5)]" />
<span className="text-[8px] font-black tracking-[0.2em] text-slate-500 uppercase">
Active
</span>
</div>
{/* Typography */}
<span className="text-sm font-bold text-slate-800 dark:text-zinc-200 tracking-tight">
Execute Layout
</span>
{/* 3. PRECISION ICON */}
<div className="flex items-center justify-center transition-transform duration-500 group-hover:translate-x-1">
<svg
width="16" height="16" viewBox="0 0 24 24" fill="none"
stroke="currentColor" strokeWidth="2.5"
className="text-cyan-500"
>
<path d="M5 12h14m-7-7l7 7-7 7" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</div>
</div>
{/* 4. CONTROLLED GLOW - No messy leaking, just a soft lift */}
<div className="absolute inset-0 rounded-full shadow-[0_0_20px_rgba(6,182,212,0)] group-hover:shadow-[0_0_25px_rgba(6,182,212,0.1)] transition-shadow duration-500 -z-10" />
</motion.button>
</div>
)
}Source Code
btn-ripple.tsx
'use client'
import React, { useState } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
export default function BtnRipple() {
const [ripples, setRipples] = useState<{ id: number; x: number; y: number }[]>([])
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
const rect = e.currentTarget.getBoundingClientRect()
const x = e.clientX - rect.left
const y = e.clientY - rect.top
// Using a more reliable ID
const newRipple = { id: Math.random(), x, y }
setRipples((prev) => [...prev, newRipple])
}
return (
<div className="py-12 flex justify-center bg-transparent">
<motion.button
onMouseDown={handleClick}
whileHover="hover"
whileTap={{ scale: 0.98 }}
className="relative px-10 py-4 rounded-full overflow-hidden
bg-white dark:bg-[#0c0c0c]
border border-slate-200/80 dark:border-white/[0.08]
shadow-[0_4px_20px_-4px_rgba(0,0,0,0.05)]
dark:shadow-[0_10px_30px_-10px_rgba(0,0,0,0.5),inset_0_1px_1px_rgba(255,255,255,0.03)]
transition-all duration-500 group isolation-auto"
>
{/* 1. THE POLISHED RIPPLE (Dual-Layer Refraction) */}
<div className="absolute inset-0 pointer-events-none z-0">
<AnimatePresence>
{ripples.map((ripple) => (
<React.Fragment key={ripple.id}>
{/* Core Ripple (Sharp) */}
<motion.span
initial={{ scale: 0, opacity: 0.6 }}
animate={{ scale: 10, opacity: 0 }}
exit={{ opacity: 0 }}
onAnimationComplete={() => setRipples(prev => prev.filter(r => r.id !== ripple.id))}
transition={{ duration: 0.7, ease: [0.16, 1, 0.3, 1] }}
className="absolute bg-cyan-400/20 rounded-full"
style={{ top: ripple.y, left: ripple.x, width: 20, height: 20, transform: 'translate(-50%, -50%)' }}
/>
{/* Bloom Ripple (Soft) */}
<motion.span
initial={{ scale: 0, opacity: 0.3 }}
animate={{ scale: 15, opacity: 0 }}
transition={{ duration: 1, ease: "easeOut" }}
className="absolute bg-cyan-300/10 rounded-full blur-xl"
style={{ top: ripple.y, left: ripple.x, width: 30, height: 30, transform: 'translate(-50%, -50%)' }}
/>
</React.Fragment>
))}
</AnimatePresence>
</div>
{/* 2. SATIN SHEEN (Moves slightly on hover) */}
<motion.div
variants={{ hover: { opacity: 1, x: '20%' }, initial: { opacity: 0, x: '-20%' } }}
className="absolute inset-0 bg-gradient-to-r from-transparent via-cyan-400/[0.03] dark:via-white/[0.02] to-transparent pointer-events-none transition-opacity duration-700"
/>
{/* 3. CONTENT (Premium Spacing) */}
<div className="relative z-10 flex items-center gap-6">
<div className="flex flex-col items-start gap-0.5">
<span className="text-[8px] font-black uppercase tracking-[0.4em] text-slate-400 dark:text-zinc-500 group-hover:text-cyan-500/60 transition-colors">
Velocity UI
</span>
<span className="text-sm font-bold text-slate-800 dark:text-zinc-100 tracking-tight">
Continue Journey
</span>
</div>
{/* Action Hub */}
<div className="flex items-center justify-center w-7 h-7 rounded-full bg-slate-50 dark:bg-zinc-900 border border-slate-100 dark:border-white/[0.05] group-hover:border-cyan-500/30 transition-all duration-500">
<motion.svg
variants={{ hover: { x: 2 } }}
width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3"
className="text-slate-400 dark:text-zinc-500 group-hover:text-cyan-500 transition-colors"
>
<path d="M5 12h14m-7-7l7 7-7 7" strokeLinecap="round" strokeLinejoin="round" />
</motion.svg>
</div>
</div>
{/* 4. MASKED OVERLAY (Prevents sub-pixel jitter) */}
<div className="absolute inset-0 rounded-full border border-transparent group-hover:border-cyan-500/10 transition-colors duration-700 pointer-events-none" />
</motion.button>
</div>
)
}Source Code
btn-solid.tsx
'use client'
import { motion } from 'framer-motion'
export default function BtnSolid() {
return (
<motion.button
whileHover={{ scale: 1.04 }}
whileTap={{ scale: 0.97 }}
transition={{ type: 'spring', stiffness: 450, damping: 28 }}
className="px-5 py-2.5 rounded-xl bg-primary text-primary-foreground font-semibold shadow-md"
>
Continue
</motion.button>
)
}
Dependencies
framer-motion: latestlucide-react: latestclsx: latesttailwind-merge: latest
Props
Component property reference.
| Name | Type | Default | Description |
|---|---|---|---|
| variant | 'shiny' | 'neumorphic' | 'magnetic' | 'pulse' | 'shiny' | Visual style of the button. |
| size | 'sm' | 'md' | 'lg' | 'md' | Physical size of the button. |
| onClick | () => void | undefined | Callback function on click. |
| children | ReactNode | undefined | Button content. |
Context Worth Keeping In Orbit
Most components here are inspired by outstanding libraries and creators in the ecosystem. I don’t claim to be the original author — this is my space for learning, rebuilding, and understanding great work at a deeper level.
I’m still a student of the craft, constantly studying the best and translating what I learn through my own perspective. Every piece reflects curiosity, respect for the community, and small creative touches that feel true to me.
I’ve done my best to credit inspirations properly. If anything is missing or inaccurate, I truly appreciate a message so it can be corrected with care.

