mirror of
https://github.com/hanzoai/shadcn.git
synced 2026-10-30 18:22:59 +00:00
347 lines
11 KiB
TypeScript
347 lines
11 KiB
TypeScript
"use client"
|
|
|
|
import * as React from "react"
|
|
import { cva, type VariantProps } from "class-variance-authority"
|
|
|
|
import { cn } from "@/lib/utils"
|
|
|
|
const button3DVariants = cva(
|
|
[
|
|
"relative inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium",
|
|
"transition-all duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
"disabled:pointer-events-none disabled:opacity-50 cursor-pointer select-none",
|
|
"transform-gpu will-change-transform active:scale-95",
|
|
"before:absolute before:inset-0 before:rounded-md before:transition-all before:duration-150",
|
|
"after:absolute after:inset-0 after:rounded-md after:transition-all after:duration-150",
|
|
],
|
|
{
|
|
variants: {
|
|
variant: {
|
|
default: [
|
|
"bg-primary text-primary-foreground shadow-md hover:bg-primary/90",
|
|
"before:bg-gradient-to-t before:from-primary/80 before:to-primary before:translate-y-1 before:shadow-lg",
|
|
"hover:before:translate-y-0.5 active:before:translate-y-0",
|
|
"after:bg-gradient-to-b after:from-white/20 after:to-transparent after:opacity-0",
|
|
"hover:after:opacity-100",
|
|
],
|
|
destructive: [
|
|
"bg-destructive text-destructive-foreground shadow-md hover:bg-destructive/90",
|
|
"before:bg-gradient-to-t before:from-destructive/80 before:to-destructive before:translate-y-1 before:shadow-lg",
|
|
"hover:before:translate-y-0.5 active:before:translate-y-0",
|
|
"after:bg-gradient-to-b after:from-white/20 after:to-transparent after:opacity-0",
|
|
"hover:after:opacity-100",
|
|
],
|
|
outline: [
|
|
"border border-input bg-background text-foreground shadow-sm hover:bg-accent hover:text-accent-foreground",
|
|
"before:bg-gradient-to-t before:from-muted/50 before:to-background before:translate-y-1 before:border before:border-input",
|
|
"hover:before:translate-y-0.5 active:before:translate-y-0",
|
|
"after:bg-gradient-to-b after:from-white/10 after:to-transparent after:opacity-0",
|
|
"hover:after:opacity-100",
|
|
],
|
|
secondary: [
|
|
"bg-secondary text-secondary-foreground shadow-md hover:bg-secondary/80",
|
|
"before:bg-gradient-to-t before:from-secondary/80 before:to-secondary before:translate-y-1 before:shadow-lg",
|
|
"hover:before:translate-y-0.5 active:before:translate-y-0",
|
|
"after:bg-gradient-to-b after:from-white/20 after:to-transparent after:opacity-0",
|
|
"hover:after:opacity-100",
|
|
],
|
|
ghost: [
|
|
"text-foreground hover:bg-accent hover:text-accent-foreground",
|
|
"before:bg-gradient-to-t before:from-accent/30 before:to-accent/10 before:translate-y-1 before:opacity-0",
|
|
"hover:before:opacity-100 hover:before:translate-y-0.5 active:before:translate-y-0",
|
|
"after:bg-gradient-to-b after:from-white/10 after:to-transparent after:opacity-0",
|
|
"hover:after:opacity-50",
|
|
],
|
|
link: [
|
|
"text-primary underline-offset-4 hover:underline",
|
|
"before:hidden after:hidden",
|
|
],
|
|
neon: [
|
|
"bg-gradient-to-r from-purple-500 to-pink-500 text-white shadow-lg",
|
|
"before:bg-gradient-to-t before:from-purple-600 before:to-pink-600 before:translate-y-1 before:shadow-lg",
|
|
"hover:before:translate-y-0.5 active:before:translate-y-0",
|
|
"after:bg-gradient-to-b after:from-white/30 after:to-transparent after:opacity-0",
|
|
"hover:after:opacity-100",
|
|
"hover:shadow-purple-500/25 hover:shadow-2xl",
|
|
],
|
|
glass: [
|
|
"bg-white/10 backdrop-blur-md border border-white/20 text-foreground shadow-lg",
|
|
"before:bg-gradient-to-t before:from-white/5 before:to-white/20 before:translate-y-1 before:backdrop-blur-md before:border before:border-white/20",
|
|
"hover:before:translate-y-0.5 active:before:translate-y-0",
|
|
"after:bg-gradient-to-b after:from-white/30 after:to-transparent after:opacity-0",
|
|
"hover:after:opacity-100",
|
|
],
|
|
gradient: [
|
|
"bg-gradient-to-r from-blue-500 via-purple-500 to-pink-500 text-white shadow-lg",
|
|
"before:bg-gradient-to-t before:from-blue-600 before:via-purple-600 before:to-pink-600 before:translate-y-1 before:shadow-lg",
|
|
"hover:before:translate-y-0.5 active:before:translate-y-0",
|
|
"after:bg-gradient-to-b after:from-white/20 after:to-transparent after:opacity-0",
|
|
"hover:after:opacity-100",
|
|
"hover:bg-gradient-to-r hover:from-blue-400 hover:via-purple-400 hover:to-pink-400",
|
|
],
|
|
},
|
|
size: {
|
|
default: "h-10 px-4 py-2",
|
|
sm: "h-9 rounded-md px-3",
|
|
lg: "h-11 rounded-md px-8",
|
|
xl: "h-12 rounded-lg px-10 text-base",
|
|
icon: "h-10 w-10",
|
|
},
|
|
depth: {
|
|
none: "before:translate-y-0",
|
|
sm: "before:translate-y-0.5 hover:before:translate-y-0 active:before:translate-y-0",
|
|
default:
|
|
"before:translate-y-1 hover:before:translate-y-0.5 active:before:translate-y-0",
|
|
lg: "before:translate-y-1.5 hover:before:translate-y-0.5 active:before:translate-y-0",
|
|
xl: "before:translate-y-2 hover:before:translate-y-1 active:before:translate-y-0",
|
|
},
|
|
},
|
|
defaultVariants: {
|
|
variant: "default",
|
|
size: "default",
|
|
depth: "default",
|
|
},
|
|
}
|
|
)
|
|
|
|
interface Button3DProps
|
|
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
VariantProps<typeof button3DVariants> {
|
|
asChild?: boolean
|
|
tiltOnHover?: boolean
|
|
glowIntensity?: number
|
|
shadowColor?: string
|
|
pressAnimation?: boolean
|
|
}
|
|
|
|
const Button3D = React.forwardRef<HTMLButtonElement, Button3DProps>(
|
|
(
|
|
{
|
|
className,
|
|
variant,
|
|
size,
|
|
depth,
|
|
tiltOnHover = false,
|
|
glowIntensity = 0,
|
|
shadowColor,
|
|
pressAnimation = true,
|
|
children,
|
|
onMouseEnter,
|
|
onMouseLeave,
|
|
onMouseMove,
|
|
onMouseDown,
|
|
onMouseUp,
|
|
style,
|
|
...props
|
|
},
|
|
ref
|
|
) => {
|
|
const buttonRef = React.useRef<HTMLButtonElement>(null)
|
|
const [isHovered, setIsHovered] = React.useState(false)
|
|
const [isPressed, setIsPressed] = React.useState(false)
|
|
const [tiltTransform, setTiltTransform] = React.useState({ x: 0, y: 0 })
|
|
|
|
const updateTilt = React.useCallback(
|
|
(e: React.MouseEvent<HTMLButtonElement>) => {
|
|
if (!tiltOnHover || !buttonRef.current) return
|
|
|
|
const rect = buttonRef.current.getBoundingClientRect()
|
|
const centerX = rect.left + rect.width / 2
|
|
const centerY = rect.top + rect.height / 2
|
|
|
|
const deltaX = (e.clientX - centerX) / (rect.width / 2)
|
|
const deltaY = (e.clientY - centerY) / (rect.height / 2)
|
|
|
|
setTiltTransform({
|
|
x: deltaY * -10, // Inverted for more natural feel
|
|
y: deltaX * 10,
|
|
})
|
|
},
|
|
[tiltOnHover]
|
|
)
|
|
|
|
const resetTilt = React.useCallback(() => {
|
|
setTiltTransform({ x: 0, y: 0 })
|
|
}, [])
|
|
|
|
const handleMouseEnter = React.useCallback(
|
|
(e: React.MouseEvent<HTMLButtonElement>) => {
|
|
setIsHovered(true)
|
|
onMouseEnter?.(e)
|
|
},
|
|
[onMouseEnter]
|
|
)
|
|
|
|
const handleMouseLeave = React.useCallback(
|
|
(e: React.MouseEvent<HTMLButtonElement>) => {
|
|
setIsHovered(false)
|
|
resetTilt()
|
|
onMouseLeave?.(e)
|
|
},
|
|
[onMouseLeave, resetTilt]
|
|
)
|
|
|
|
const handleMouseMove = React.useCallback(
|
|
(e: React.MouseEvent<HTMLButtonElement>) => {
|
|
if (tiltOnHover) {
|
|
updateTilt(e)
|
|
}
|
|
onMouseMove?.(e)
|
|
},
|
|
[onMouseMove, tiltOnHover, updateTilt]
|
|
)
|
|
|
|
const handleMouseDown = React.useCallback(
|
|
(e: React.MouseEvent<HTMLButtonElement>) => {
|
|
if (pressAnimation) {
|
|
setIsPressed(true)
|
|
}
|
|
onMouseDown?.(e)
|
|
},
|
|
[onMouseDown, pressAnimation]
|
|
)
|
|
|
|
const handleMouseUp = React.useCallback(
|
|
(e: React.MouseEvent<HTMLButtonElement>) => {
|
|
setIsPressed(false)
|
|
onMouseUp?.(e)
|
|
},
|
|
[onMouseUp]
|
|
)
|
|
|
|
React.useImperativeHandle(ref, () => buttonRef.current!)
|
|
|
|
const buttonStyle: React.CSSProperties = {
|
|
...style,
|
|
transform: tiltOnHover
|
|
? `perspective(1000px) rotateX(${tiltTransform.x}deg) rotateY(${tiltTransform.y}deg) ${isPressed ? "translateY(1px)" : ""}`
|
|
: isPressed
|
|
? "translateY(1px)"
|
|
: undefined,
|
|
filter:
|
|
glowIntensity > 0
|
|
? `drop-shadow(0 0 ${glowIntensity * 4}px ${shadowColor || "currentColor"})`
|
|
: undefined,
|
|
transition: "all 150ms cubic-bezier(0.4, 0, 0.2, 1)",
|
|
}
|
|
|
|
return (
|
|
<button
|
|
ref={buttonRef}
|
|
className={cn(button3DVariants({ variant, size, depth }), className)}
|
|
style={buttonStyle}
|
|
onMouseEnter={handleMouseEnter}
|
|
onMouseLeave={handleMouseLeave}
|
|
onMouseMove={handleMouseMove}
|
|
onMouseDown={handleMouseDown}
|
|
onMouseUp={handleMouseUp}
|
|
{...props}
|
|
>
|
|
<span className="relative z-10 flex items-center justify-center gap-2">
|
|
{children}
|
|
</span>
|
|
</button>
|
|
)
|
|
}
|
|
)
|
|
Button3D.displayName = "Button3D"
|
|
|
|
// Preset button variants for common use cases
|
|
const Button3DPreset = {
|
|
Hero: React.forwardRef<
|
|
HTMLButtonElement,
|
|
Omit<Button3DProps, "variant" | "size" | "depth">
|
|
>((props, ref) => (
|
|
<Button3D
|
|
ref={ref}
|
|
variant="gradient"
|
|
size="xl"
|
|
depth="lg"
|
|
tiltOnHover
|
|
glowIntensity={2}
|
|
{...props}
|
|
/>
|
|
)),
|
|
|
|
CTA: React.forwardRef<
|
|
HTMLButtonElement,
|
|
Omit<Button3DProps, "variant" | "size" | "depth">
|
|
>((props, ref) => (
|
|
<Button3D
|
|
ref={ref}
|
|
variant="neon"
|
|
size="lg"
|
|
depth="default"
|
|
tiltOnHover
|
|
glowIntensity={1}
|
|
{...props}
|
|
/>
|
|
)),
|
|
|
|
Glass: React.forwardRef<
|
|
HTMLButtonElement,
|
|
Omit<Button3DProps, "variant" | "size" | "depth">
|
|
>((props, ref) => (
|
|
<Button3D
|
|
ref={ref}
|
|
variant="glass"
|
|
size="default"
|
|
depth="sm"
|
|
tiltOnHover
|
|
{...props}
|
|
/>
|
|
)),
|
|
|
|
Minimal: React.forwardRef<
|
|
HTMLButtonElement,
|
|
Omit<Button3DProps, "variant" | "size" | "depth">
|
|
>((props, ref) => (
|
|
<Button3D ref={ref} variant="ghost" size="default" depth="sm" {...props} />
|
|
)),
|
|
|
|
Danger: React.forwardRef<
|
|
HTMLButtonElement,
|
|
Omit<Button3DProps, "variant" | "size" | "depth">
|
|
>((props, ref) => (
|
|
<Button3D
|
|
ref={ref}
|
|
variant="destructive"
|
|
size="default"
|
|
depth="default"
|
|
shadowColor="rgb(239 68 68)"
|
|
{...props}
|
|
/>
|
|
)),
|
|
}
|
|
|
|
// Assign display names
|
|
Button3DPreset.Hero.displayName = "Button3DPreset.Hero"
|
|
Button3DPreset.CTA.displayName = "Button3DPreset.CTA"
|
|
Button3DPreset.Glass.displayName = "Button3DPreset.Glass"
|
|
Button3DPreset.Minimal.displayName = "Button3DPreset.Minimal"
|
|
Button3DPreset.Danger.displayName = "Button3DPreset.Danger"
|
|
|
|
// Loading button with 3D spinner
|
|
const Button3DLoading = React.forwardRef<
|
|
HTMLButtonElement,
|
|
Button3DProps & { loading?: boolean }
|
|
>(({ loading = false, children, disabled, ...props }, ref) => {
|
|
return (
|
|
<Button3D ref={ref} disabled={disabled || loading} {...props}>
|
|
{loading && (
|
|
<div className="animate-spin rounded-full h-4 w-4 border-2 border-current border-t-transparent" />
|
|
)}
|
|
{!loading && children}
|
|
</Button3D>
|
|
)
|
|
})
|
|
Button3DLoading.displayName = "Button3DLoading"
|
|
|
|
export {
|
|
Button3D,
|
|
Button3DPreset,
|
|
Button3DLoading,
|
|
button3DVariants,
|
|
type Button3DProps,
|
|
}
|