Preview
Live interactive preview.
Installation
Add this component to your project using the CLI:
terminal
npx -y vui-registry-cli-v1@latest add premium-buttonSource Code
premium-button.tsx
'use client'
import React, { useRef, useState } from 'react'
import { motion, useMotionValue, useSpring, useTransform, AnimatePresence, useMotionTemplate } from 'framer-motion'
import { cn } from '@/lib/utils'
import confetti from 'canvas-confetti'
export function PremiumSubmitButton({
className,
children,
...props
}: React.ComponentPropsWithoutRef<typeof motion.button>) {
const [status, setStatus] = useState<'idle' | 'loading' | 'success'>('idle')
const ref = useRef<HTMLButtonElement>(null)
// 3D Tilt Values
const x = useMotionValue(0)
const y = useMotionValue(0)
const xSpring = useSpring(x, { stiffness: 200, damping: 25, mass: 0.5 })
const ySpring = useSpring(y, { stiffness: 200, damping: 25, mass: 0.5 })
const rotateX = useTransform(ySpring, [-0.5, 0.5], ['4deg', '-4deg'])
const rotateY = useTransform(xSpring, [-0.5, 0.5], ['-4deg', '4deg'])
// Spotlight Coordinates
const mouseX = useMotionValue(0)
const mouseY = useMotionValue(0)
// FIX: Hook initialized at the top level to prevent render crashes
const spotlightBackground = useMotionTemplate`radial-gradient(80px circle at ${mouseX}px ${mouseY}px, rgba(255,255,255,0.15), transparent 100%)`
const handleMouseMove = (e: React.MouseEvent<HTMLButtonElement>) => {
if (!ref.current || status !== 'idle') return
const rect = ref.current.getBoundingClientRect()
// Tilt logic
x.set((e.clientX - rect.left) / rect.width - 0.5)
y.set((e.clientY - rect.top) / rect.height - 0.5)
// Spotlight logic
mouseX.set(e.clientX - rect.left)
mouseY.set(e.clientY - rect.top)
}
const handleMouseLeave = () => {
x.set(0)
y.set(0)
}
const handleAction = async () => {
if (status !== 'idle') return
setStatus('loading')
await new Promise((r) => setTimeout(r, 1500))
setStatus('success')
// Refined, monochromatic confetti burst
const rect = ref.current?.getBoundingClientRect()
const xPos = rect ? (rect.left + rect.width / 2) / window.innerWidth : 0.5
const yPos = rect ? (rect.top + rect.height / 2) / window.innerHeight : 0.5
confetti({
particleCount: 50,
spread: 60,
startVelocity: 30,
scalar: 0.6,
ticks: 80,
origin: { x: xPos, y: yPos },
colors: ['#ffffff', '#a3a3a3', '#525252'],
disableForReducedMotion: true,
})
setTimeout(() => setStatus('idle'), 2500)
}
return (
<div style={{ perspective: '1000px' }} className="flex items-center justify-center p-4">
<motion.button
ref={ref}
layout
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
onClick={handleAction}
style={{ rotateX, rotateY, transformStyle: 'preserve-3d' }}
whileTap={{ scale: 0.98, rotateX: 0, rotateY: 0 }}
transition={{ type: 'spring', bounce: 0, duration: 0.4 }}
className={cn(
'group relative inline-flex items-center justify-center',
'min-w-[140px] px-6 py-2.5 rounded-xl',
'bg-foreground text-background font-medium text-sm',
'shadow-[0_4px_12px_rgba(0,0,0,0.1)] dark:shadow-[0_4px_20px_rgba(255,255,255,0.05)]',
'border border-transparent ring-1 ring-inset ring-background/10',
'overflow-hidden transition-all duration-300',
status === 'success' && 'bg-emerald-500 text-white ring-emerald-400/50 shadow-emerald-500/20',
className
)}
{...props}
>
{/* Dynamic Hover Spotlight */}
{status === 'idle' && (
<motion.div
className="absolute inset-0 z-0 opacity-0 group-hover:opacity-100 transition-opacity duration-500 pointer-events-none"
style={{ background: spotlightBackground }}
/>
)}
{/* Content Layer */}
<div className="relative z-10 flex items-center justify-center" style={{ transform: 'translateZ(10px)' }}>
<AnimatePresence mode="wait" initial={false}>
{status === 'idle' && (
<motion.span
key="idle"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ type: "spring", bounce: 0, duration: 0.3 }}
className="flex items-center gap-2 tracking-wide"
>
{children || 'Submit'}
</motion.span>
)}
{status === 'loading' && (
<motion.span
key="loading"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.8 }}
transition={{ type: "spring", bounce: 0, duration: 0.3 }}
className="flex items-center justify-center"
>
<svg className="animate-spin h-4 w-4 text-background/70" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="3"></circle>
<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"></path>
</svg>
</motion.span>
)}
{status === 'success' && (
<motion.span
key="success"
initial={{ opacity: 0, scale: 0.5 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ type: "spring", bounce: 0.4, duration: 0.4 }}
className="flex items-center justify-center text-white"
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<motion.path
initial={{ pathLength: 0 }}
animate={{ pathLength: 1 }}
transition={{ duration: 0.4, ease: "easeOut" }}
d="M5 13l4 4L19 7"
/>
</svg>
</motion.span>
)}
</AnimatePresence>
</div>
</motion.button>
</div>
)
}Dependencies
framer-motion: latestcanvas-confetti: latest
Props
Component property reference.
| Name | Type | Default | Description |
|---|---|---|---|
| className | string | - | Custom class names for the button. |
| motionProps | React.ComponentPropsWithoutRef<typeof motion.button> | - | Pass any Framer Motion button props (onClick, whileTap, etc.). |
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.