Menu

Button Collection

A collection of premium button variants including Shiny, Neumorphic, Magnetic, and Pulse effects.

Preview

Live interactive preview.

Installation

Add this component to your project using the CLI:

terminal
npx -y vui-registry-cli-v1@latest add button-stack

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-icon.tsx

'use client'
import React, { useState } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import { ArrowRight, Check, LoaderCircle } from 'lucide-react'

export default function BtnIcon() {
  const [status, setStatus] = useState<'idle' | 'loading' | 'done'>('idle')

  const handleClick = () => {
    if (status !== 'idle') return
    setStatus('loading')
    setTimeout(() => setStatus('done'), 1200)
    setTimeout(() => setStatus('idle'), 2600)
  }

  return (
    <motion.button
      onClick={handleClick}
      whileHover={{ scale: 1.02 }}
      whileTap={{ scale: 0.97 }}
      className="group relative flex min-w-[200px] items-center justify-between gap-4 overflow-hidden rounded-[18px] bg-foreground px-5 py-3 text-sm font-semibold text-background"
    >
      <div className="absolute inset-0 bg-[linear-gradient(120deg,transparent_0%,rgba(255,255,255,0.16)_50%,transparent_100%)] opacity-0 transition-opacity duration-500 group-hover:opacity-100" />
      <AnimatePresence mode="wait" initial={false}>
        <motion.span
          key={status}
          initial={{ opacity: 0, y: 8 }}
          animate={{ opacity: 1, y: 0 }}
          exit={{ opacity: 0, y: -8 }}
          className="relative z-10"
        >
          {status === 'idle' ? 'Continue flow' : status === 'loading' ? 'Routing request' : 'Delivered'}
        </motion.span>
      </AnimatePresence>
      <span className="relative z-10 flex h-8 w-8 items-center justify-center rounded-[12px] bg-background/10">
        <AnimatePresence mode="wait" initial={false}>
          {status === 'idle' && (
            <motion.span key="arrow" initial={{ opacity: 0, scale: 0.8 }} animate={{ opacity: 1, scale: 1 }} exit={{ opacity: 0, scale: 0.8 }}>
              <ArrowRight size={18} />
            </motion.span>
          )}
          {status === 'loading' && (
            <motion.span key="spinner" initial={{ opacity: 0, scale: 0.8 }} animate={{ opacity: 1, scale: 1 }} exit={{ opacity: 0, scale: 0.8 }}>
              <LoaderCircle size={18} className="animate-spin" />
            </motion.span>
          )}
          {status === 'done' && (
            <motion.span key="check" initial={{ opacity: 0, scale: 0.8 }} animate={{ opacity: 1, scale: 1 }} exit={{ opacity: 0, scale: 0.8 }}>
              <Check size={18} />
            </motion.span>
          )}
        </AnimatePresence>
      </span>
    </motion.button>
  )
}

Source Code

btn-minimal.tsx

'use client'
import React, { useState } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import { Check, LoaderCircle } from 'lucide-react'

export default function BtnMinimal() {
  const [status, setStatus] = useState<'idle' | 'loading' | 'success'>('idle')

  const handleClick = () => {
    if (status !== 'idle') return
    setStatus('loading')
    setTimeout(() => setStatus('success'), 1100)
    setTimeout(() => setStatus('idle'), 2600)
  }

  return (
    <motion.button
      onClick={handleClick}
      whileHover={{ y: -1 }}
      whileTap={{ scale: 0.98 }}
      className="relative min-w-[180px] overflow-hidden rounded-[16px] border border-border bg-background px-6 py-3 text-sm font-semibold text-foreground shadow-[0_1px_0_rgba(255,255,255,0.25)_inset]"
    >
      <motion.div
        className="absolute inset-x-0 bottom-0 h-px bg-foreground/20"
        animate={{ scaleX: status === 'loading' ? [0.2, 0.85, 0.55] : 1, opacity: status === 'success' ? 0 : 1 }}
        transition={{ duration: status === 'loading' ? 1.1 : 0.3, ease: 'easeInOut' }}
      />
      <AnimatePresence mode="wait" initial={false}>
        {status === 'idle' && (
          <motion.span
            key="idle"
            initial={{ opacity: 0, y: 8 }}
            animate={{ opacity: 1, y: 0 }}
            exit={{ opacity: 0, y: -8 }}
            className="flex items-center justify-center gap-2"
          >
            Deploy Draft
            <span className="text-xs text-muted-foreground">Instant</span>
          </motion.span>
        )}
        {status === 'loading' && (
          <motion.span
            key="loading"
            initial={{ opacity: 0, y: 8 }}
            animate={{ opacity: 1, y: 0 }}
            exit={{ opacity: 0, y: -8 }}
            className="flex items-center justify-center gap-2 text-muted-foreground"
          >
            <LoaderCircle className="h-4 w-4 animate-spin" />
            Syncing...
          </motion.span>
        )}
        {status === 'success' && (
          <motion.span
            key="success"
            initial={{ opacity: 0, y: 8 }}
            animate={{ opacity: 1, y: 0 }}
            exit={{ opacity: 0, y: -8 }}
            className="flex items-center justify-center gap-2 text-emerald-500"
          >
            <Check className="h-4 w-4" />
            Live now
          </motion.span>
        )}
      </AnimatePresence>
    </motion.button>
  )
}

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-minimal.tsx

'use client'
import React, { useState } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import { Bookmark, Check } from 'lucide-react'

export default function BtnOutlineMinimal() {
  const [saved, setSaved] = useState(false)

  const handleClick = () => {
    setSaved(true)
    setTimeout(() => setSaved(false), 1800)
  }

  return (
    <motion.button
      onClick={handleClick}
      whileHover={{ y: -1 }}
      whileTap={{ scale: 0.985 }}
      className="relative min-w-[190px] rounded-[16px] border border-foreground/20 bg-background px-6 py-3 text-sm font-semibold text-foreground transition-colors duration-300 hover:border-foreground/50"
    >
      <div className="flex items-center justify-center gap-3">
        <motion.span
          animate={{ rotate: saved ? -8 : 0, scale: saved ? 1.05 : 1 }}
          transition={{ type: 'spring', stiffness: 420, damping: 24 }}
          className="inline-flex"
        >
          {saved ? <Check className="h-4 w-4 text-emerald-500" /> : <Bookmark className="h-4 w-4" />}
        </motion.span>
        <span>{saved ? 'Saved for review' : 'Save checkpoint'}</span>
      </div>
      <AnimatePresence>
        {saved && (
          <motion.span
            initial={{ opacity: 0, y: 8 }}
            animate={{ opacity: 1, y: 0 }}
            exit={{ opacity: 0, y: 8 }}
            className="absolute left-1/2 top-[calc(100%+10px)] -translate-x-1/2 whitespace-nowrap rounded-xl border border-emerald-500/20 bg-emerald-500/10 px-3 py-1 text-[10px] font-medium tracking-wide text-emerald-500"
          >
            Added to shortlist
          </motion.span>
        )}
      </AnimatePresence>
    </motion.button>
  )
}

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>
   )
 }

Source Code

button-stack.tsx
'use client'

import React, { useState } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import BtnSolid from './btn-solid'
import BtnGhost from './btn-ghost'
import BtnNeon from './btn-neon'
import BtnMinimal from './btn-minimal'
import BtnOutlineMinimal from './btn-outline-minimal'
import BtnIcon from './btn-icon'

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: 'Ghost', Component: BtnGhost, cmd: 'npx vui-registry-cli-v1 add btn-ghost' },
    { name: 'Neon', Component: BtnNeon, cmd: 'npx vui-registry-cli-v1 add btn-neon' },
    { name: 'Minimal', Component: BtnMinimal, cmd: 'npx vui-registry-cli-v1 add btn-minimal' },
    { name: 'Outline Minimal', Component: BtnOutlineMinimal, cmd: 'npx vui-registry-cli-v1 add btn-outline-minimal' },
    { name: 'Icon', Component: BtnIcon, cmd: 'npx vui-registry-cli-v1 add btn-icon' },
  ]
  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>
  )
}

Dependencies

  • framer-motion: latest
  • lucide-react: latest
  • clsx: latest
  • tailwind-merge: latest

Props

Component property reference.

NameTypeDefaultDescription
variant'shiny' | 'neumorphic' | 'magnetic' | 'pulse''shiny'Visual style of the button.
size'sm' | 'md' | 'lg''md'Physical size of the button.
onClick() => voidundefinedCallback function on click.
childrenReactNodeundefinedButton 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.