coders-club / src /components /ui /hover-button.tsx
kumar-aditya's picture
Upload 108 files
a7b8df9 verified
import * as React from "react"
import { cn } from "@/lib/utils"
interface HoverButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
children: React.ReactNode
}
const HoverButton = React.forwardRef<HTMLButtonElement, HoverButtonProps>(
({ className, children, ...props }, ref) => {
const buttonRef = React.useRef<HTMLButtonElement>(null)
const [isListening, setIsListening] = React.useState(false)
const [circles, setCircles] = React.useState<Array<{
id: number
x: number
y: number
color: string
fadeState: "in" | "out" | null
}>>([])
const lastAddedRef = React.useRef(0)
const createCircle = React.useCallback((x: number, y: number) => {
const buttonWidth = buttonRef.current?.offsetWidth || 0
const xPos = x / buttonWidth
const color = `linear-gradient(to right, var(--circle-start) ${xPos * 100}%, var(--circle-end) ${
xPos * 100
}%)`
setCircles((prev) => [
...prev,
{ id: Date.now(), x, y, color, fadeState: null },
])
}, [])
const handlePointerMove = React.useCallback(
(event: React.PointerEvent<HTMLButtonElement>) => {
if (!isListening) return
const currentTime = Date.now()
if (currentTime - lastAddedRef.current > 100) {
lastAddedRef.current = currentTime
const rect = event.currentTarget.getBoundingClientRect()
const x = event.clientX - rect.left
const y = event.clientY - rect.top
createCircle(x, y)
}
},
[isListening, createCircle]
)
const handlePointerEnter = React.useCallback(() => {
setIsListening(true)
}, [])
const handlePointerLeave = React.useCallback(() => {
setIsListening(false)
}, [])
React.useEffect(() => {
circles.forEach((circle) => {
if (!circle.fadeState) {
setTimeout(() => {
setCircles((prev) =>
prev.map((c) =>
c.id === circle.id ? { ...c, fadeState: "in" } : c
)
)
}, 0)
setTimeout(() => {
setCircles((prev) =>
prev.map((c) =>
c.id === circle.id ? { ...c, fadeState: "out" } : c
)
)
}, 1000)
setTimeout(() => {
setCircles((prev) => prev.filter((c) => c.id !== circle.id))
}, 2200)
}
})
}, [circles])
return (
<button
ref={buttonRef}
className={cn(
"relative isolate px-4 sm:px-6 lg:px-8 py-2.5 sm:py-3 rounded-2xl sm:rounded-3xl",
"text-foreground font-medium text-sm sm:text-base leading-6",
"backdrop-blur-lg bg-[rgba(43,55,80,0.1)]",
"cursor-pointer overflow-hidden touch-target",
"min-h-[44px] min-w-[44px]",
"before:content-[''] before:absolute before:inset-0",
"before:rounded-[inherit] before:pointer-events-none",
"before:z-[1]",
"before:shadow-[inset_0_0_0_1px_rgba(170,202,255,0.2),inset_0_0_16px_0_rgba(170,202,255,0.1),inset_0_-3px_12px_0_rgba(170,202,255,0.15),0_1px_3px_0_rgba(0,0,0,0.50),0_4px_12px_0_rgba(0,0,0,0.45)]",
"before:mix-blend-multiply before:transition-transform before:duration-300",
"active:before:scale-[0.975] hover:scale-105 transition-all duration-200",
className
)}
onPointerMove={handlePointerMove}
onPointerEnter={handlePointerEnter}
onPointerLeave={handlePointerLeave}
{...props}
style={{
"--circle-start": "var(--tw-gradient-from, #a0d9f8)",
"--circle-end": "var(--tw-gradient-to, #3a5bbf)",
}}
>
{circles.map(({ id, x, y, color, fadeState }) => (
<div
key={id}
className={cn(
"absolute w-2 h-2 sm:w-3 sm:h-3 -translate-x-1/2 -translate-y-1/2 rounded-full",
"blur-sm sm:blur-lg pointer-events-none z-[-1] transition-opacity duration-300",
fadeState === "in" && "opacity-60 sm:opacity-75",
fadeState === "out" && "opacity-0 duration-1000",
!fadeState && "opacity-0"
)}
style={{
left: x,
top: y,
background: color,
}}
/>
))}
{children}
</button>
)
}
)
HoverButton.displayName = "HoverButton"
export { HoverButton }