diff options
| author | Max Nanis | 2026-02-18 20:42:03 -0500 |
|---|---|---|
| committer | Max Nanis | 2026-02-18 20:42:03 -0500 |
| commit | 3eaa56f0306ead818f64c3d99fc6d230d9b970a4 (patch) | |
| tree | 9fecc2f1456e6321572e0e65f57106916df173e2 /jb-ui/src/components/ui/magic-card.tsx | |
| download | amt-jb-3eaa56f0306ead818f64c3d99fc6d230d9b970a4.tar.gz amt-jb-3eaa56f0306ead818f64c3d99fc6d230d9b970a4.zip | |
HERE WE GO, HERE WE GO, HERE WE GO
Diffstat (limited to 'jb-ui/src/components/ui/magic-card.tsx')
| -rw-r--r-- | jb-ui/src/components/ui/magic-card.tsx | 101 |
1 files changed, 101 insertions, 0 deletions
diff --git a/jb-ui/src/components/ui/magic-card.tsx b/jb-ui/src/components/ui/magic-card.tsx new file mode 100644 index 0000000..e6503d8 --- /dev/null +++ b/jb-ui/src/components/ui/magic-card.tsx @@ -0,0 +1,101 @@ +import React, { useCallback, useEffect } from "react" +import { motion, useMotionTemplate, useMotionValue } from "motion/react" + +import { cn } from "@/lib/utils" + +interface MagicCardProps { + children?: React.ReactNode + className?: string + gradientSize?: number + gradientColor?: string + gradientOpacity?: number + gradientFrom?: string + gradientTo?: string +} + +export function MagicCard({ + children, + className, + gradientSize = 200, + gradientColor = "#262626", + gradientOpacity = 0.8, + gradientFrom = "#9E7AFF", + gradientTo = "#FE8BBB", +}: MagicCardProps) { + const mouseX = useMotionValue(-gradientSize) + const mouseY = useMotionValue(-gradientSize) + const reset = useCallback(() => { + mouseX.set(-gradientSize) + mouseY.set(-gradientSize) + }, [gradientSize, mouseX, mouseY]) + + const handlePointerMove = useCallback( + (e: React.PointerEvent<HTMLDivElement>) => { + const rect = e.currentTarget.getBoundingClientRect() + mouseX.set(e.clientX - rect.left) + mouseY.set(e.clientY - rect.top) + }, + [mouseX, mouseY] + ) + + useEffect(() => { + reset() + }, [reset]) + + useEffect(() => { + const handleGlobalPointerOut = (e: PointerEvent) => { + if (!e.relatedTarget) { + reset() + } + } + + const handleVisibility = () => { + if (document.visibilityState !== "visible") { + reset() + } + } + + window.addEventListener("pointerout", handleGlobalPointerOut) + window.addEventListener("blur", reset) + document.addEventListener("visibilitychange", handleVisibility) + + return () => { + window.removeEventListener("pointerout", handleGlobalPointerOut) + window.removeEventListener("blur", reset) + document.removeEventListener("visibilitychange", handleVisibility) + } + }, [reset]) + + return ( + <div + className={cn("group relative rounded-[inherit]", className)} + onPointerMove={handlePointerMove} + onPointerLeave={reset} + onPointerEnter={reset} + > + <motion.div + className="bg-border pointer-events-none absolute inset-0 rounded-[inherit] duration-300 group-hover:opacity-100" + style={{ + background: useMotionTemplate` + radial-gradient(${gradientSize}px circle at ${mouseX}px ${mouseY}px, + ${gradientFrom}, + ${gradientTo}, + var(--border) 100% + ) + `, + }} + /> + <div className="bg-background absolute inset-px rounded-[inherit]" /> + <motion.div + className="pointer-events-none absolute inset-px rounded-[inherit] opacity-0 transition-opacity duration-300 group-hover:opacity-100" + style={{ + background: useMotionTemplate` + radial-gradient(${gradientSize}px circle at ${mouseX}px ${mouseY}px, ${gradientColor}, transparent 100%) + `, + opacity: gradientOpacity, + }} + /> + <div className="relative">{children}</div> + </div> + ) +} |
