(function (global){
"use strict";
const autoplayPaths={
circle: (t)=> ({
x: 50 + 35 * Math.cos(t * Math.PI * 2),
y: 50 + 35 * Math.sin(t * Math.PI * 2),
}),
horizontal: (t)=> ({
x: 50 + 45 * Math.sin(t * Math.PI * 2),
y: 50,
}),
vertical: (t)=> ({
x: 50,
y: 50 + 45 * Math.sin(t * Math.PI * 2),
}),
diagonal: (t)=> ({
x: 50 + 45 * Math.sin(t * Math.PI * 2),
y: 50 + 45 * Math.sin(t * Math.PI * 2),
}),
figure8: (t)=> ({
x: 50 + 35 * Math.sin(t * Math.PI * 2),
y: 50 + 25 * Math.sin(t * Math.PI * 4),
}),
};
const autoplaySpeeds={
slow: 6000,
normal: 3000,
fast: 1500,
};
const ColorUtils={
parseColor(color){
if(!color||typeof color!=="string") return null;
color=color.trim().toLowerCase();
if(color.startsWith("#")){
return this.hexToRgb(color);
}
if(color.startsWith("rgb")){
const match=color.match(/rgba?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/i,
);
if(match){
return {
r: parseInt(match[1], 10),
g: parseInt(match[2], 10),
b: parseInt(match[3], 10),
};}}
if(color.startsWith("hsl")){
const match=color.match(/hsla?\s*\(\s*([\d.]+)\s*,\s*([\d.]+)%\s*,\s*([\d.]+)%/i,
);
if(match){
return this.hslToRgb(parseFloat(match[1]),
parseFloat(match[2]),
parseFloat(match[3]),
);
}}
if(typeof document!=="undefined"){
const temp=document.createElement("div");
temp.style.color=color;
document.body.appendChild(temp);
const computed=getComputedStyle(temp).color;
document.body.removeChild(temp);
const match=computed.match(/rgba?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/i,
);
if(match){
return {
r: parseInt(match[1], 10),
g: parseInt(match[2], 10),
b: parseInt(match[3], 10),
};}}
return null;
},
hexToRgb(hex){
hex=hex.replace(/^#/, "");
if(hex.length===3||hex.length===4){
hex=hex
.split("")
.map((c)=> c + c)
.join("");
}
if(hex.length===8){
hex=hex.substring(0, 6);
}
if(hex.length!==6) return null;
const num=parseInt(hex, 16);
if(isNaN(num)) return null;
return {
r: (num >> 16) & 255,
g: (num >> 8) & 255,
b: num & 255,
};},
hslToRgb(h, s, l){
s /=100;
l /=100;
const c=(1 - Math.abs(2 * l - 1)) * s;
const x=c * (1 - Math.abs(((h / 60) % 2) - 1));
const m=l - c / 2;
let r=0,
g=0,
b=0;
if(h >=0&&h < 60){
r=c;
g=x;
b=0;
}else if(h >=60&&h < 120){
r=x;
g=c;
b=0;
}else if(h >=120&&h < 180){
r=0;
g=c;
b=x;
}else if(h >=180&&h < 240){
r=0;
g=x;
b=c;
}else if(h >=240&&h < 300){
r=x;
g=0;
b=c;
}else if(h >=300&&h < 360){
r=c;
g=0;
b=x;
}
return {
r: Math.round((r + m) * 255),
g: Math.round((g + m) * 255),
b: Math.round((b + m) * 255),
};},
rgbToHsl(r, g, b){
r /=255;
g /=255;
b /=255;
const max=Math.max(r, g, b);
const min=Math.min(r, g, b);
let h=0;
let s=0;
const l=(max + min) / 2;
if(max!==min){
const d=max - min;
s=l > 0.5 ? d / (2 - max - min):d / (max + min);
switch (max){
case r:
h=((g - b) / d + (g < b ? 6:0)) / 6;
break;
case g:
h=((b - r) / d + 2) / 6;
break;
case b:
h=((r - g) / d + 4) / 6;
break;
}}
return {
h: Math.round(h * 360),
s: Math.round(s * 100),
l: Math.round(l * 100),
};},
toHslString(color){
const rgb=this.parseColor(color);
if(!rgb) return null;
const hsl=this.rgbToHsl(rgb.r, rgb.g, rgb.b);
return `${hsl.h}, ${hsl.s}%, ${hsl.l}%`;
},
};
class SpotlightCard {
constructor(element, options={}){
this.element =
typeof element==="string" ? document.querySelector(element):element;
if(!this.element){
console.warn("SpotlightCard: Element not found");
return;
}
this.options={
color: null,
gradientColors: [],
size: "md",
intensity: "medium",
variant: "default",
mode: "hover",
autoplay: false,
autoplayType: "circle",
autoplaySpeed: "normal",
disabled: false,
cardBg: null,
cardBorder: null,
cardRadius: null,
onMouseEnter: null,
onMouseLeave: null,
onMouseMove: null,
...options,
};
this._rafId=null;
this._autoplayRafId=null;
this._autoplayStartTime=null;
this._isHovering=false;
this._handleMouseMove=this._handleMouseMove.bind(this);
this._handleMouseEnter=this._handleMouseEnter.bind(this);
this._handleMouseLeave=this._handleMouseLeave.bind(this);
this._animateAutoplay=this._animateAutoplay.bind(this);
this._init();
}
_init(){
this.element.classList.add("spotlight-card");
if(!this.options.cardRadius){
const computedRadius=getComputedStyle(this.element).borderRadius;
if(computedRadius&&computedRadius!=="0px"){
this.options.cardRadius=computedRadius;
}}
this._applyOptions();
this._addEventListeners();
if(this.options.autoplay){
this._startAutoplay();
}}
_applyColor(color){
if(color){
const hslString=ColorUtils.toHslString(color);
if(hslString){
this.element.style.setProperty("--spotlight-color", hslString);
}}else{
this.element.style.removeProperty("--spotlight-color");
}}
_applyGradientColors(colors){
if(!colors||colors.length===0){
for (let i=1; i <=10; i++){
this.element.style.removeProperty(`--spotlight-gradient-${i}`);
}
this.element.style.removeProperty("--spotlight-gradient-count");
return;
}
this.element.style.setProperty("--spotlight-gradient-count",
colors.length,
);
colors.forEach((color, index)=> {
const hslString=ColorUtils.toHslString(color);
if(hslString){
this.element.style.setProperty(`--spotlight-gradient-${index + 1}`,
hslString,
);
}});
for (let i=colors.length + 1; i <=10; i++){
this.element.style.removeProperty(`--spotlight-gradient-${i}`);
}
this._buildGradient(colors);
}
_buildGradient(colors){
if(!colors||colors.length===0) return;
const hslColors=colors
.map((c)=> ColorUtils.toHslString(c))
.filter(Boolean);
if(hslColors.length===0) return;
this.element.setAttribute("data-gradient-colors", hslColors.join("|"));
}
_applyCardStyles(){
const { cardBg, cardBorder, cardRadius }=this.options;
if(cardBg){
this.element.style.setProperty("--spotlight-card-bg", cardBg);
}else{
this.element.style.removeProperty("--spotlight-card-bg");
}
if(cardBorder){
this.element.style.setProperty("--spotlight-card-border", cardBorder);
}else{
this.element.style.removeProperty("--spotlight-card-border");
}
if(cardRadius){
this.element.style.setProperty("--spotlight-card-radius", cardRadius);
}else{
this.element.style.removeProperty("--spotlight-card-radius");
}}
_applyOptions(){
const {
color,
gradientColors,
size,
intensity,
variant,
mode,
autoplay,
autoplayType,
autoplaySpeed,
disabled,
}=this.options;
this.setSize(size);
this.setIntensity(intensity);
this.element.setAttribute("data-variant", variant);
this.element.setAttribute("data-mode", mode);
this.element.setAttribute("data-autoplay", autoplay);
this.element.setAttribute("data-autoplay-type", autoplayType);
this.element.setAttribute("data-autoplay-speed", autoplaySpeed);
this.element.setAttribute("data-disabled", disabled);
this._applyColor(color);
this._applyGradientColors(gradientColors);
this._applyCardStyles();
this.element.classList.toggle("spotlight-card--proximity",
mode==="proximity",
);
this.element.classList.toggle("spotlight-card--autoplay", autoplay);
this.element.classList.toggle("spotlight-card--border",
variant==="border",
);
this.element.classList.toggle("spotlight-card--gradient",
variant==="gradient",
);
this.element.classList.toggle("spotlight-card--gradient-border",
variant==="gradient-border",
);
}
_addEventListeners(){
this.element.addEventListener("mouseenter", this._handleMouseEnter);
this.element.addEventListener("mouseleave", this._handleMouseLeave);
this.element.addEventListener("mousemove", this._handleMouseMove);
if(this.options.mode==="proximity"){
document.addEventListener("mousemove", this._handleMouseMove);
}}
_removeEventListeners(){
document.removeEventListener("mousemove", this._handleMouseMove);
this.element.removeEventListener("mousemove", this._handleMouseMove);
this.element.removeEventListener("mouseenter", this._handleMouseEnter);
this.element.removeEventListener("mouseleave", this._handleMouseLeave);
if(this._rafId){
cancelAnimationFrame(this._rafId);
this._rafId=null;
}}
_handleMouseMove(e){
if(this.options.disabled) return;
if(this._rafId) cancelAnimationFrame(this._rafId);
this._rafId=requestAnimationFrame(()=> {
const rect=this.element.getBoundingClientRect();
const x=e.clientX - rect.left;
const y=e.clientY - rect.top;
this.element.style.setProperty("--mouse-x", `${x}px`);
this.element.style.setProperty("--mouse-y", `${y}px`);
if(typeof this.options.onMouseMove==="function"){
this.options.onMouseMove({ x, y, event: e });
}});
}
_handleMouseEnter(e){
if(this.options.disabled) return;
this._isHovering=true;
if(this.options.autoplay){
this._stopAutoplay();
}
if(typeof this.options.onMouseEnter==="function"){
this.options.onMouseEnter(e);
}}
_handleMouseLeave(e){
if(this.options.disabled) return;
this._isHovering=false;
if(this.options.autoplay){
this._startAutoplay();
}else{
this.element.style.setProperty("--mouse-x", "50%");
this.element.style.setProperty("--mouse-y", "50%");
}
if(typeof this.options.onMouseLeave==="function"){
this.options.onMouseLeave(e);
}}
_animateAutoplay(timestamp){
if(!this.options.autoplay||this._isHovering||this.options.disabled){
return;
}
if(!this._autoplayStartTime){
this._autoplayStartTime=timestamp;
}
let duration;
if(typeof this.options.autoplaySpeed==="number"){
duration=this.options.autoplaySpeed;
}else{
duration =
autoplaySpeeds[this.options.autoplaySpeed]||autoplaySpeeds.normal;
}
const elapsed=timestamp - this._autoplayStartTime;
const progress=(elapsed % duration) / duration;
const pathFn =
autoplayPaths[this.options.autoplayType]||autoplayPaths.circle;
const { x, y }=pathFn(progress);
const rect=this.element.getBoundingClientRect();
const pixelX=(x / 100) * rect.width;
const pixelY=(y / 100) * rect.height;
this.element.style.setProperty("--mouse-x", `${pixelX}px`);
this.element.style.setProperty("--mouse-y", `${pixelY}px`);
this._autoplayRafId=requestAnimationFrame(this._animateAutoplay);
}
_startAutoplay(){
if(this._autoplayRafId) return;
this._autoplayStartTime=null;
this._autoplayRafId=requestAnimationFrame(this._animateAutoplay);
}
_stopAutoplay(){
if(this._autoplayRafId){
cancelAnimationFrame(this._autoplayRafId);
this._autoplayRafId=null;
}}
setOptions(newOptions){
const modeChanged =
newOptions.mode&&newOptions.mode!==this.options.mode;
const autoplayChanged =
newOptions.autoplay!==undefined &&
newOptions.autoplay!==this.options.autoplay;
this.options={ ...this.options, ...newOptions };
this._applyOptions();
if(modeChanged){
this._removeEventListeners();
this._addEventListeners();
}
if(autoplayChanged){
if(this.options.autoplay){
this._startAutoplay();
}else{
this._stopAutoplay();
}}
}
setColor(color){
this.options.color=color;
this._applyColor(color);
}
setGradientColors(colors){
this.options.gradientColors=colors;
this._applyGradientColors(colors);
}
setCardBg(color){
this.options.cardBg=color;
if(color){
this.element.style.setProperty("--spotlight-card-bg", color);
}else{
this.element.style.removeProperty("--spotlight-card-bg");
}}
setCardBorder(color){
this.options.cardBorder=color;
if(color){
this.element.style.setProperty("--spotlight-card-border", color);
}else{
this.element.style.removeProperty("--spotlight-card-border");
}}
setCardRadius(radius){
this.options.cardRadius=radius;
if(radius){
this.element.style.setProperty("--spotlight-card-radius", radius);
}else{
this.element.style.removeProperty("--spotlight-card-radius");
}}
setSize(size){
this.options.size=size;
if(typeof size==="number" ||
(typeof size==="string"&&/^\d+/.test(size))
){
const pxValue =
typeof size==="number"
? `${size}px`
: size.endsWith("px")
? size
: `${size}px`;
this.element.style.setProperty("--spotlight-size", pxValue);
this.element.setAttribute("data-size", "custom");
this.element.setAttribute("data-size-value", pxValue);
}else{
this.element.style.removeProperty("--spotlight-size");
this.element.setAttribute("data-size", size);
this.element.removeAttribute("data-size-value");
}}
setIntensity(intensity){
this.options.intensity=intensity;
if(typeof intensity==="number" ||
(typeof intensity==="string"&&/^[\d.]+$/.test(intensity))
){
let value=parseFloat(intensity);
if(value > 1) value=value / 100;
value=Math.max(0, Math.min(1, value));
const spotlightOpacity=(value * 0.35).toFixed(3);
const borderOpacity=(value * 0.9).toFixed(3);
this.element.style.setProperty("--spotlight-opacity", spotlightOpacity);
this.element.style.setProperty("--spotlight-border-opacity",
borderOpacity,
);
this.element.setAttribute("data-intensity", "custom");
this.element.setAttribute("data-intensity-value", value);
}else{
this.element.style.removeProperty("--spotlight-opacity");
this.element.style.removeProperty("--spotlight-border-opacity");
this.element.setAttribute("data-intensity", intensity);
this.element.removeAttribute("data-intensity-value");
}}
setVariant(variant){
this.options.variant=variant;
this.element.setAttribute("data-variant", variant);
this.element.classList.toggle("spotlight-card--border",
variant==="border",
);
this.element.classList.toggle("spotlight-card--gradient",
variant==="gradient",
);
this.element.classList.toggle("spotlight-card--gradient-border",
variant==="gradient-border",
);
}
setAutoplay(enabled, type="circle", speed=3000){
this.options.autoplay=enabled;
this.options.autoplayType=type;
this.options.autoplaySpeed=speed;
this.element.setAttribute("data-autoplay", enabled);
this.element.setAttribute("data-autoplay-type", type);
this.element.setAttribute("data-autoplay-speed", speed);
this.element.classList.toggle("spotlight-card--autoplay", enabled);
if(enabled&&!this._isHovering){
this._startAutoplay();
}else{
this._stopAutoplay();
}}
enable(){
this.options.disabled=false;
this.element.setAttribute("data-disabled", "false");
if(this.options.autoplay){
this._startAutoplay();
}}
disable(){
this.options.disabled=true;
this.element.setAttribute("data-disabled", "true");
this._stopAutoplay();
}
toggle(){
if(this.options.disabled){
this.enable();
}else{
this.disable();
}
return this.options.disabled;
}
destroy(){
this._removeEventListeners();
this._stopAutoplay();
this.element.classList.remove("spotlight-card",
"spotlight-card--proximity",
"spotlight-card--autoplay",
"spotlight-card--border",
"spotlight-card--gradient",
"spotlight-card--gradient-border",
);
const attrs=[
"size",
"intensity",
"variant",
"mode",
"autoplay",
"autoplay-type",
"autoplay-speed",
"disabled",
"gradient-colors",
];
attrs.forEach((attr)=> this.element.removeAttribute(`data-${attr}`));
const propsToRemove=[
"--mouse-x",
"--mouse-y",
"--spotlight-color",
"--spotlight-card-bg",
"--spotlight-card-border",
"--spotlight-card-radius",
"--spotlight-gradient-count",
];
propsToRemove.forEach((prop)=> this.element.style.removeProperty(prop));
for (let i=1; i <=10; i++){
this.element.style.removeProperty(`--spotlight-gradient-${i}`);
}}
}
class SpotlightCardManager {
constructor(){
this.cards=new Map();
}
init(selector="[data-spotlight-card]", options={}){
const elements=document.querySelectorAll(selector);
const instances=[];
elements.forEach((element)=> {
if(this.cards.has(element)){
instances.push(this.cards.get(element));
return;
}
const speedAttr=element.getAttribute("data-spotlight-autoplay-speed");
const parsedSpeed=speedAttr ? parseInt(speedAttr, 10):NaN;
const dataOptions={
color: element.getAttribute("data-spotlight-color"),
size: element.getAttribute("data-spotlight-size"),
intensity: element.getAttribute("data-spotlight-intensity"),
variant: element.getAttribute("data-spotlight-variant"),
mode: element.getAttribute("data-spotlight-mode"),
autoplay: element.getAttribute("data-spotlight-autoplay")==="true",
autoplayType: element.getAttribute("data-spotlight-autoplay-type"),
autoplaySpeed: !isNaN(parsedSpeed) ? parsedSpeed:speedAttr,
disabled: element.getAttribute("data-spotlight-disabled")==="true",
cardBg: element.getAttribute("data-spotlight-card-bg"),
cardBorder: element.getAttribute("data-spotlight-card-border"),
cardRadius: element.getAttribute("data-spotlight-card-radius"),
};
const gradientColorsAttr=element.getAttribute("data-spotlight-gradient-colors",
);
if(gradientColorsAttr){
dataOptions.gradientColors=gradientColorsAttr
.split(",")
.map((c)=> c.trim())
.filter(Boolean);
}
Object.keys(dataOptions).forEach((key)=> {
if(dataOptions[key]===null||dataOptions[key]===undefined){
delete dataOptions[key];
}});
const mergedOptions={ ...options, ...dataOptions };
const instance=new SpotlightCard(element, mergedOptions);
this.cards.set(element, instance);
instances.push(instance);
});
return instances;
}
get(element){
return this.cards.get(element);
}
has(element){
return this.cards.has(element);
}
remove(element){
const instance=this.cards.get(element);
if(instance){
instance.destroy();
this.cards.delete(element);
}}
getAll(){
return Array.from(this.cards.values());
}
destroyAll(){
this.cards.forEach((instance)=> instance.destroy());
this.cards.clear();
}
setAllOptions(options){
this.cards.forEach((instance)=> instance.setOptions(options));
}}
function autoInit(){
const manager=new SpotlightCardManager();
manager.init();
document.querySelectorAll('[data-spotlight-children]').forEach((parent)=> {
const childSelector=parent.getAttribute('data-spotlight-children');
let configStr=parent.getAttribute('data-spotlight-config');
let config={};
try { config=JSON.parse(configStr||'{}'); } catch(e){}
const opts={};
if(config['data-spotlight-color']) opts.color=config['data-spotlight-color'];
if(config['data-spotlight-gradient-colors']){
opts.gradientColors=config['data-spotlight-gradient-colors'].split(',').map(s=> s.trim());
}
if(config['data-spotlight-size']) opts.size=config['data-spotlight-size'];
if(config['data-spotlight-intensity']) opts.intensity=config['data-spotlight-intensity'];
if(config['data-spotlight-variant']) opts.variant=config['data-spotlight-variant'];
if(config['data-spotlight-mode']) opts.mode=config['data-spotlight-mode'];
if(config['data-spotlight-autoplay']==='true'){
opts.autoplay=true;
if(config['data-spotlight-autoplay-type']) opts.autoplayType=config['data-spotlight-autoplay-type'];
if(config['data-spotlight-autoplay-speed']) opts.autoplaySpeed=parseInt(config['data-spotlight-autoplay-speed']);
}
const children=parent.querySelectorAll(childSelector);
children.forEach((child)=> {
if(child.style.position===''||child.style.position==='static'){
child.style.position='relative';
}
child.style.overflow='hidden';
const instance=new SpotlightCard(child, opts);
manager.cards.set(child, instance);
});
});
global.spotlightCardManager=manager;
}
global.SpotlightCard=SpotlightCard;
global.SpotlightCardManager=SpotlightCardManager;
global.SpotlightColorUtils=ColorUtils;
if(document.readyState==="loading"){
document.addEventListener("DOMContentLoaded", autoInit);
}else{
autoInit();
}})(typeof window!=="undefined" ? window:this);