Card Spotlight
November 2024
Cover
A spotlight on album cover, because the safety of your data is our priority.
Gradient Cover
A spotlight on album cover, because the safety of your data is our priority.
TSX
import { useEffect, useRef, useState } from "react";
interface AlbumProps {
className?: string;
}
const AlbumIcon = (props: AlbumProps) => {
return (
<svg className={props.className} xmlns="http://www.w3.org/2000/svg" width="1.2em" height="1.2em" viewBox="0 0 24 24"><path fill="currentColor" d="M17.29 11.969a1.33 1.33 0 0 1-1.322 1.337a1.33 1.33 0 0 1-1.323-1.337a1.33 1.33 0 0 1 1.323-1.338c.73 0 1.323.599 1.323 1.338" /><path fill="currentColor" fillRule="evenodd" d="M18.132 7.408c-.849-.12-1.942-.12-3.305-.12H9.173c-1.363 0-2.456 0-3.305.12c-.877.125-1.608.393-2.152 1.02c-.543.628-.71 1.397-.716 2.293c-.006.866.139 1.962.319 3.329l.365 2.771c.141 1.069.255 1.933.432 2.61c.185.704.457 1.289.968 1.741s1.12.648 1.834.74C7.605 22 8.468 22 9.533 22h4.934c1.065 0 1.928 0 2.615-.088c.715-.092 1.323-.288 1.834-.74s.783-1.037.968-1.741c.177-.677.291-1.542.432-2.61l.365-2.771c.18-1.367.325-2.463.319-3.33c-.007-.895-.172-1.664-.716-2.291c-.544-.628-1.275-.896-2.152-1.021M6.052 8.732c-.726.104-1.094.292-1.34.578c-.248.285-.384.678-.39 1.42c-.005.762.126 1.765.315 3.195l.05.38l.371-.273c.96-.702 2.376-.668 3.288.095l3.384 2.833c.32.268.871.318 1.269.084l.235-.138c1.125-.662 2.634-.592 3.672.19l1.832 1.38c.09-.496.171-1.105.273-1.876l.352-2.675c.189-1.43.32-2.433.314-3.195c-.005-.742-.141-1.135-.388-1.42c-.247-.286-.615-.474-1.342-.578c-.745-.106-1.745-.107-3.172-.107h-5.55c-1.427 0-2.427.001-3.172.107" clipRule="evenodd" /><path fill="currentColor" d="M8.859 2h6.282c.21 0 .37 0 .51.015a2.62 2.62 0 0 1 2.159 1.672H6.19a2.62 2.62 0 0 1 2.159-1.672c.14-.015.3-.015.51-.015M6.88 4.5c-1.252 0-2.278.84-2.62 1.954l-.021.07c.358-.12.73-.2 1.108-.253c.973-.139 2.202-.139 3.629-.139h6.203c1.427 0 2.656 0 3.628.139c.378.053.75.132 1.11.253l-.021-.07C19.553 5.34 18.527 4.5 17.276 4.5z" /></svg>
)
}
interface CardSpotlightProps {
title?: string;
description?: string;
gradient?: boolean;
}
const CardSpotlight = ({ gradient = false, title = "", description = "" }: CardSpotlightProps) => {
const divRef = useRef<HTMLDivElement>(null);
const [isFocused, setIsFocused] = useState(false);
const [position, setPosition] = useState({ x: 0, y: 0 });
const [opacity, setOpacity] = useState(0);
const [hue, setHue] = useState(0);
const updateColor = () => {
setHue((prev) => (prev + 1) % 360);
};
useEffect(() => {
if (!gradient) return;
const interval = setInterval(updateColor, 100);
return () => clearInterval(interval);
}, []);
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
if (!divRef.current || isFocused) return;
const div = divRef.current;
const rect = div.getBoundingClientRect();
setPosition({ x: e.clientX - rect.left, y: e.clientY - rect.top });
};
const handleFocus = () => {
setIsFocused(true);
setOpacity(1);
};
const handleBlur = () => {
setIsFocused(false);
setOpacity(0);
};
const handleMouseEnter = () => {
setOpacity(1);
};
const handleMouseLeave = () => {
setOpacity(0);
};
return (
<div
ref={divRef}
onMouseMove={handleMouseMove}
onFocus={handleFocus}
onBlur={handleBlur}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
className="relative max-w-xs rounded-3xl border border-neutral-800 bg-[#101010] p-8 overflow-hidden"
>
<div
className="pointer-events-none absolute -inset-px opacity-0 transition-all duration-500 ease-in-out"
style={{
opacity,
background: gradient
? `radial-gradient(600px circle at ${position.x}px ${position.y}px, hsl(${hue}, 100%, 50%, .15), transparent 40%)`
: `radial-gradient(600px circle at ${position.x}px ${position.y}px, rgba(255,255,255,.15), transparent 40%)`,
}}
/>
<div className="mb-4">
<AlbumIcon className="h-8 w-8 text-neutral-400" />
</div>
<h3 className="mb-2 font-medium tracking-tight text-neutral-100">
{title}
</h3>
<p className="text-sm text-neutral-400">
{description}
</p>
</div>
);
};
export default CardSpotlight;