Added <MagicBento /> component

This commit is contained in:
Utkarsh-Singhal-26
2025-07-20 16:08:41 +05:30
parent 0e49709fe3
commit aa5be187eb
6 changed files with 1090 additions and 2 deletions

View File

@@ -1,5 +1,5 @@
// Highlighted sidebar items
export const NEW = ['Target Cursor', 'Ripple Grid'];
export const NEW = ['Target Cursor', 'Ripple Grid', 'Magic Bento'];
export const UPDATED = [];
// Used for main sidebar navigation
@@ -59,6 +59,7 @@ export const CATEGORIES = [
subcategories: [
'Animated List',
'Masonry',
'Magic Bento',
'Profile Card',
'Dock',
'Gooey Nav',

View File

@@ -47,6 +47,7 @@ const textAnimations = {
const components = {
'animated-list': () => import('../demo/Components/AnimatedListDemo.vue'),
'masonry': () => import('../demo/Components/MasonryDemo.vue'),
'magic-bento': () => import('../demo/Components/MagicBentoDemo.vue'),
'profile-card': () => import('../demo/Components/ProfileCardDemo.vue'),
'dock': () => import('../demo/Components/DockDemo.vue'),
'gooey-nav': () => import('../demo/Components/GooeyNavDemo.vue'),

View File

@@ -0,0 +1,26 @@
import code from '@content/Components/MagicBento/MagicBento.vue?raw';
import type { CodeObject } from '../../../types/code';
export const magicBento: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Components/MagicBento`,
installation: `npm install gsap`,
usage: `<template>
<MagicBento
:text-auto-hide="true"
:enable-stars="true"
:enable-spotlight="true"
:enable-border-glow="true"
:enable-tilt="true"
:enable-magnetism="true"
:click-effect="true"
:spotlight-radius="300"
:particle-count="12"
glow-color="132, 0, 255"
/>
</template>
<script setup lang="ts">
import MagicBento from "./MagicBento.vue";
</script>`,
code
};

View File

@@ -0,0 +1,918 @@
<script setup lang="ts">
import { gsap } from 'gsap';
import { computed, defineComponent, nextTick, onMounted, onUnmounted, ref, watch, type PropType } from 'vue';
interface BentoCardProps {
color?: string;
title?: string;
description?: string;
label?: string;
textAutoHide?: boolean;
disableAnimations?: boolean;
}
interface BentoProps {
textAutoHide?: boolean;
enableStars?: boolean;
enableSpotlight?: boolean;
enableBorderGlow?: boolean;
disableAnimations?: boolean;
spotlightRadius?: number;
particleCount?: number;
enableTilt?: boolean;
glowColor?: string;
clickEffect?: boolean;
enableMagnetism?: boolean;
}
const DEFAULT_PARTICLE_COUNT = 12;
const DEFAULT_SPOTLIGHT_RADIUS = 300;
const DEFAULT_GLOW_COLOR = '0, 200, 83';
const MOBILE_BREAKPOINT = 768;
const cardData: BentoCardProps[] = [
{
color: '#000C00',
title: 'Analytics',
description: 'Track user behavior',
label: 'Insights'
},
{
color: '#000C00',
title: 'Dashboard',
description: 'Centralized data view',
label: 'Overview'
},
{
color: '#000C00',
title: 'Collaboration',
description: 'Work together seamlessly',
label: 'Teamwork'
},
{
color: '#000C00',
title: 'Automation',
description: 'Streamline workflows',
label: 'Efficiency'
},
{
color: '#000C00',
title: 'Integration',
description: 'Connect favorite tools',
label: 'Connectivity'
},
{
color: '#000C00',
title: 'Security',
description: 'Enterprise-grade protection',
label: 'Protection'
}
];
const createParticleElement = (x: number, y: number, color: string = DEFAULT_GLOW_COLOR): HTMLDivElement => {
const el = document.createElement('div');
el.className = 'particle';
el.style.cssText = `
position: absolute;
width: 4px;
height: 4px;
border-radius: 50%;
background: rgba(${color}, 1);
box-shadow: 0 0 6px rgba(${color}, 0.6);
pointer-events: none;
z-index: 100;
left: ${x}px;
top: ${y}px;
`;
return el;
};
const calculateSpotlightValues = (radius: number) => ({
proximity: radius * 0.5,
fadeDistance: radius * 0.75
});
const updateCardGlowProperties = (card: HTMLElement, mouseX: number, mouseY: number, glow: number, radius: number) => {
const rect = card.getBoundingClientRect();
const relativeX = ((mouseX - rect.left) / rect.width) * 100;
const relativeY = ((mouseY - rect.top) / rect.height) * 100;
card.style.setProperty('--glow-x', `${relativeX}%`);
card.style.setProperty('--glow-y', `${relativeY}%`);
card.style.setProperty('--glow-intensity', glow.toString());
card.style.setProperty('--glow-radius', `${radius}px`);
};
const ParticleCard = defineComponent({
name: 'ParticleCard',
props: {
disableAnimations: { type: Boolean, default: false },
particleCount: { type: Number, default: DEFAULT_PARTICLE_COUNT },
glowColor: { type: String, default: DEFAULT_GLOW_COLOR },
enableTilt: { type: Boolean, default: true },
clickEffect: { type: Boolean, default: false },
enableMagnetism: { type: Boolean, default: false }
},
setup(props) {
const cardRef = ref<HTMLDivElement | null>(null);
const particlesRef = ref<HTMLDivElement[]>([]);
const timeoutsRef = ref<number[]>([]);
const isHoveredRef = ref(false);
const memoizedParticles = ref<HTMLDivElement[]>([]);
const particlesInitialized = ref(false);
const magnetismAnimationRef = ref<gsap.core.Tween | null>(null);
const initializeParticles = () => {
if (particlesInitialized.value || !cardRef.value) return;
const { width, height } = cardRef.value.getBoundingClientRect();
memoizedParticles.value = Array.from({ length: props.particleCount }, () =>
createParticleElement(Math.random() * width, Math.random() * height, props.glowColor)
);
particlesInitialized.value = true;
};
const clearAllParticles = () => {
timeoutsRef.value.forEach(clearTimeout);
timeoutsRef.value = [];
magnetismAnimationRef.value?.kill();
particlesRef.value.forEach(particle => {
gsap.to(particle, {
scale: 0,
opacity: 0,
duration: 0.3,
ease: 'back.in(1.7)',
onComplete: () => {
particle.parentNode?.removeChild(particle);
}
});
});
particlesRef.value = [];
};
const animateParticles = () => {
if (!cardRef.value || !isHoveredRef.value) return;
if (!particlesInitialized.value) {
initializeParticles();
}
memoizedParticles.value.forEach((particle, index) => {
const timeoutId = setTimeout(() => {
if (!isHoveredRef.value || !cardRef.value) return;
const clone = particle.cloneNode(true) as HTMLDivElement;
cardRef.value.appendChild(clone);
particlesRef.value.push(clone);
gsap.fromTo(clone, { scale: 0, opacity: 0 }, { scale: 1, opacity: 1, duration: 0.3, ease: 'back.out(1.7)' });
gsap.to(clone, {
x: (Math.random() - 0.5) * 100,
y: (Math.random() - 0.5) * 100,
rotation: Math.random() * 360,
duration: 2 + Math.random() * 2,
ease: 'none',
repeat: -1,
yoyo: true
});
gsap.to(clone, {
opacity: 0.3,
duration: 1.5,
ease: 'power2.inOut',
repeat: -1,
yoyo: true
});
}, index * 100);
timeoutsRef.value.push(timeoutId);
});
};
let cleanupEventListeners: (() => void) | null = null;
const setupEventListeners = () => {
if (props.disableAnimations || !cardRef.value) return;
const element = cardRef.value;
const handleMouseEnter = () => {
isHoveredRef.value = true;
animateParticles();
gsap.to(element, { y: -2, duration: 0.3, ease: 'power2.out' });
if (props.enableTilt) {
gsap.to(element, {
rotateX: 5,
rotateY: 5,
duration: 0.3,
ease: 'power2.out',
transformPerspective: 1000
});
}
};
const handleMouseLeave = () => {
isHoveredRef.value = false;
clearAllParticles();
if (props.enableTilt) {
gsap.to(element, {
rotateX: 0,
rotateY: 0,
y: 0,
duration: 0.3,
ease: 'power2.out'
});
}
if (props.enableMagnetism) {
gsap.to(element, {
x: 0,
y: 0,
duration: 0.3,
ease: 'power2.out'
});
}
};
const handleMouseMove = (e: MouseEvent) => {
if (!props.enableTilt && !props.enableMagnetism) return;
const rect = element.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const centerX = rect.width / 2;
const centerY = rect.height / 2;
if (props.enableTilt) {
const rotateX = ((y - centerY) / centerY) * -10;
const rotateY = ((x - centerX) / centerX) * 10;
gsap.to(element, {
rotateX,
rotateY,
duration: 0.1,
ease: 'power2.out',
transformPerspective: 1000
});
}
if (props.enableMagnetism) {
const magnetX = (x - centerX) * 0.05;
const magnetY = (y - centerY) * 0.05;
magnetismAnimationRef.value = gsap.to(element, {
x: magnetX,
y: magnetY,
duration: 0.3,
ease: 'power2.out'
});
}
};
const handleClick = (e: MouseEvent) => {
if (!props.clickEffect) return;
const rect = element.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const maxDistance = Math.max(
Math.hypot(x, y),
Math.hypot(x - rect.width, y),
Math.hypot(x, y - rect.height),
Math.hypot(x - rect.width, y - rect.height)
);
const ripple = document.createElement('div');
ripple.style.cssText = `
position: absolute;
width: ${maxDistance * 2}px;
height: ${maxDistance * 2}px;
border-radius: 50%;
background: radial-gradient(circle, rgba(${props.glowColor}, 0.4) 0%, rgba(${props.glowColor}, 0.2) 30%, transparent 70%);
left: ${x - maxDistance}px;
top: ${y - maxDistance}px;
pointer-events: none;
z-index: 1000;
`;
element.appendChild(ripple);
gsap.fromTo(
ripple,
{
scale: 0,
opacity: 1
},
{
scale: 1,
opacity: 0,
duration: 0.8,
ease: 'power2.out',
onComplete: () => ripple.remove()
}
);
};
element.addEventListener('mouseenter', handleMouseEnter);
element.addEventListener('mouseleave', handleMouseLeave);
element.addEventListener('mousemove', handleMouseMove);
element.addEventListener('click', handleClick);
cleanupEventListeners = () => {
isHoveredRef.value = false;
element.removeEventListener('mouseenter', handleMouseEnter);
element.removeEventListener('mouseleave', handleMouseLeave);
element.removeEventListener('mousemove', handleMouseMove);
element.removeEventListener('click', handleClick);
clearAllParticles();
};
};
onMounted(() => {
nextTick(() => {
setupEventListeners();
});
});
onUnmounted(() => {
cleanupEventListeners?.();
});
watch(
() => [
animateParticles,
clearAllParticles,
props.disableAnimations,
props.enableTilt,
props.enableMagnetism,
props.clickEffect,
props.glowColor
],
() => {
cleanupEventListeners?.();
setupEventListeners();
},
{ deep: true }
);
return {
cardRef
};
},
template: `
<div
ref="cardRef"
class="relative overflow-hidden"
:style="{ position: 'relative', overflow: 'hidden' }"
>
<slot />
</div>
`
});
const GlobalSpotlight = defineComponent({
name: 'GlobalSpotlight',
props: {
gridRef: { type: Object as PropType<HTMLDivElement | null>, required: true },
disableAnimations: { type: Boolean, default: false },
enabled: { type: Boolean, default: true },
spotlightRadius: { type: Number, default: DEFAULT_SPOTLIGHT_RADIUS },
glowColor: { type: String, default: DEFAULT_GLOW_COLOR }
},
setup(props) {
const spotlightRef = ref<HTMLDivElement | null>(null);
const isInsideSection = ref(false);
let cleanupEventListeners: (() => void) | null = null;
const setupEventListeners = () => {
if (props.disableAnimations || !props.gridRef || !props.enabled) return;
const spotlight = document.createElement('div');
spotlight.className = 'global-spotlight';
spotlight.style.cssText = `
position: fixed;
width: 800px;
height: 800px;
border-radius: 50%;
pointer-events: none;
background: radial-gradient(circle,
rgba(${props.glowColor}, 0.15) 0%,
rgba(${props.glowColor}, 0.08) 15%,
rgba(${props.glowColor}, 0.04) 25%,
rgba(${props.glowColor}, 0.02) 40%,
rgba(${props.glowColor}, 0.01) 65%,
transparent 70%
);
z-index: 200;
opacity: 0;
transform: translate(-50%, -50%);
mix-blend-mode: screen;
`;
document.body.appendChild(spotlight);
spotlightRef.value = spotlight;
const handleMouseMove = (e: MouseEvent) => {
if (!spotlightRef.value || !props.gridRef) return;
const section = props.gridRef.closest('.bento-section');
const rect = section?.getBoundingClientRect();
const mouseInside =
rect &&
e.clientX >= rect.left &&
e.clientX <= rect.right &&
e.clientY >= rect.top &&
e.clientY <= rect.bottom;
isInsideSection.value = mouseInside || false;
const cards = props.gridRef.querySelectorAll('.card') as NodeListOf<HTMLDivElement>;
if (!mouseInside) {
gsap.to(spotlightRef.value, {
opacity: 0,
duration: 0.3,
ease: 'power2.out'
});
cards.forEach(card => {
card.style.setProperty('--glow-intensity', '0');
});
return;
}
const { proximity, fadeDistance } = calculateSpotlightValues(props.spotlightRadius);
let minDistance = Infinity;
cards.forEach(card => {
const cardRect = card.getBoundingClientRect();
const centerX = cardRect.left + cardRect.width / 2;
const centerY = cardRect.top + cardRect.height / 2;
const distance =
Math.hypot(e.clientX - centerX, e.clientY - centerY) - Math.max(cardRect.width, cardRect.height) / 2;
const effectiveDistance = Math.max(0, distance);
minDistance = Math.min(minDistance, effectiveDistance);
let glowIntensity = 0;
if (effectiveDistance <= proximity) {
glowIntensity = 1;
} else if (effectiveDistance <= fadeDistance) {
glowIntensity = (fadeDistance - effectiveDistance) / (fadeDistance - proximity);
}
updateCardGlowProperties(card, e.clientX, e.clientY, glowIntensity, props.spotlightRadius);
});
gsap.to(spotlightRef.value, {
left: e.clientX,
top: e.clientY,
duration: 0.1,
ease: 'power2.out'
});
const targetOpacity =
minDistance <= proximity
? 0.8
: minDistance <= fadeDistance
? ((fadeDistance - minDistance) / (fadeDistance - proximity)) * 0.8
: 0;
gsap.to(spotlightRef.value, {
opacity: targetOpacity,
duration: targetOpacity > 0 ? 0.2 : 0.5,
ease: 'power2.out'
});
};
const handleMouseLeave = () => {
isInsideSection.value = false;
(props.gridRef?.querySelectorAll('.card') as NodeListOf<HTMLDivElement>).forEach(card => {
card.style.setProperty('--glow-intensity', '0');
});
if (spotlightRef.value) {
gsap.to(spotlightRef.value, {
opacity: 0,
duration: 0.3,
ease: 'power2.out'
});
}
};
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseleave', handleMouseLeave);
cleanupEventListeners = () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseleave', handleMouseLeave);
spotlightRef.value?.parentNode?.removeChild(spotlightRef.value);
};
};
onMounted(() => {
nextTick(() => {
setupEventListeners();
});
});
onUnmounted(() => {
cleanupEventListeners?.();
cleanupEventListeners = null;
});
watch(
() => props,
() => {
if (props.gridRef) {
cleanupEventListeners?.();
setupEventListeners();
}
},
{ deep: true }
);
},
template: `<div></div>`
});
const BentoCardGrid = defineComponent({
name: 'BentoCardGrid',
props: {
gridRef: { type: Object }
},
template: `
<div
class="relative gap-2 grid p-3 select-none bento-section"
:style="{ fontSize: 'clamp(1rem, 0.9rem + 0.5vw, 1.5rem)' }"
:ref="gridRef"
>
<slot />
</div>
`
});
const useMobileDetection = () => {
const isMobile = ref(false);
const checkMobile = () => {
isMobile.value = window.innerWidth <= MOBILE_BREAKPOINT;
};
onMounted(() => {
checkMobile();
window.addEventListener('resize', checkMobile);
onUnmounted(() => {
window.removeEventListener('resize', checkMobile);
});
});
return isMobile;
};
const props = withDefaults(defineProps<BentoProps>(), {
textAutoHide: true,
enableStars: true,
enableSpotlight: true,
enableBorderGlow: true,
disableAnimations: false,
spotlightRadius: DEFAULT_SPOTLIGHT_RADIUS,
particleCount: DEFAULT_PARTICLE_COUNT,
enableTilt: false,
glowColor: DEFAULT_GLOW_COLOR,
clickEffect: true,
enableMagnetism: true
});
const gridRef = ref<HTMLDivElement | null>(null);
const isMobile = useMobileDetection();
const cardElements = ref<HTMLDivElement[]>([]);
const shouldDisableAnimations = computed(() => props.disableAnimations || isMobile.value);
const baseClassName = computed(() => {
return `card flex flex-col justify-between relative aspect-[4/3] min-h-[200px] w-full max-w-full p-5 rounded-[20px] border border-solid font-light overflow-hidden transition-[box-shadow] duration-300 ease-in-out hover:shadow-[0_8px_25px_rgba(0,0,0,0.15)] ${
props.enableBorderGlow ? 'card--border-glow' : ''
}`;
});
const getCardStyle = (card: BentoCardProps) => ({
backgroundColor: card.color || 'var(--background-dark)',
borderColor: 'var(--border-color)',
color: 'var(--white)',
'--glow-x': '50%',
'--glow-y': '50%',
'--glow-intensity': '0',
'--glow-radius': '200px'
});
const setupCardRef = (el: HTMLDivElement | null, index: number) => {
if (!el) return;
cardElements.value[index] = el;
const handleMouseEnter = () => {
if (shouldDisableAnimations.value) return;
gsap.to(el, { y: -2, duration: 0.3, ease: 'power2.out' });
};
const handleMouseMove = (e: MouseEvent) => {
if (shouldDisableAnimations.value) return;
const rect = el.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const centerX = rect.width / 2;
const centerY = rect.height / 2;
if (props.enableTilt) {
const rotateX = ((y - centerY) / centerY) * -10;
const rotateY = ((x - centerX) / centerX) * 10;
gsap.to(el, {
rotateX,
rotateY,
duration: 0.1,
ease: 'power2.out',
transformPerspective: 1000
});
}
if (props.enableMagnetism) {
const magnetX = (x - centerX) * 0.05;
const magnetY = (y - centerY) * 0.05;
gsap.to(el, {
x: magnetX,
y: magnetY,
duration: 0.3,
ease: 'power2.out'
});
}
};
const handleMouseLeave = () => {
if (shouldDisableAnimations.value) return;
if (props.enableTilt) {
gsap.to(el, {
rotateX: 0,
rotateY: 0,
duration: 0.3,
ease: 'power2.out'
});
}
if (props.enableMagnetism) {
gsap.to(el, {
x: 0,
y: 0,
duration: 0.3,
ease: 'power2.out'
});
} else {
gsap.to(el, { y: 0, duration: 0.3, ease: 'power2.out' });
}
};
const handleClick = (e: MouseEvent) => {
if (!props.clickEffect || shouldDisableAnimations.value) return;
const rect = el.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const maxDistance = Math.max(
Math.hypot(x, y),
Math.hypot(x - rect.width, y),
Math.hypot(x, y - rect.height),
Math.hypot(x - rect.width, y - rect.height)
);
const ripple = document.createElement('div');
ripple.style.cssText = `
position: absolute;
width: ${maxDistance * 2}px;
height: ${maxDistance * 2}px;
border-radius: 50%;
background: radial-gradient(circle, rgba(${props.glowColor}, 0.4) 0%, rgba(${props.glowColor}, 0.2) 30%, transparent 70%);
left: ${x - maxDistance}px;
top: ${y - maxDistance}px;
pointer-events: none;
z-index: 1000;
`;
el.appendChild(ripple);
gsap.fromTo(
ripple,
{
scale: 0,
opacity: 1
},
{
scale: 1,
opacity: 0,
duration: 0.8,
ease: 'power2.out',
onComplete: () => ripple.remove()
}
);
};
el.addEventListener('mouseenter', handleMouseEnter);
el.addEventListener('mousemove', handleMouseMove);
el.addEventListener('mouseleave', handleMouseLeave);
el.addEventListener('click', handleClick);
};
</script>
<template>
<GlobalSpotlight
v-if="enableSpotlight"
:grid-ref="gridRef"
:disable-animations="shouldDisableAnimations"
:enabled="enableSpotlight"
:spotlight-radius="spotlightRadius"
:glow-color="glowColor"
/>
<BentoCardGrid :grid-ref="(el: HTMLDivElement) => (gridRef = el)">
<div class="gap-2 grid card-responsive">
<template v-for="(card, index) in cardData" :key="index">
<ParticleCard
v-if="enableStars"
:class="baseClassName"
:style="getCardStyle(card)"
:disable-animations="shouldDisableAnimations"
:particle-count="particleCount"
:glow-color="glowColor"
:enable-tilt="enableTilt"
:click-effect="clickEffect"
:enable-magnetism="enableMagnetism"
>
<div class="relative flex justify-between gap-3 text-white card__header">
<span class="text-base card__label">{{ card.label }}</span>
</div>
<div class="relative flex flex-col text-white card__content">
<h3 :class="`card__title font-normal text-base m-0 mb-1 ${textAutoHide ? 'text-clamp-1' : ''}`">
{{ card.title }}
</h3>
<p :class="`card__description text-xs leading-5 opacity-90 ${textAutoHide ? 'text-clamp-2' : ''}`">
{{ card.description }}
</p>
</div>
</ParticleCard>
<div
v-else
:class="baseClassName"
:style="getCardStyle(card)"
:ref="el => setupCardRef(el as HTMLDivElement, index)"
>
<div class="relative flex justify-between gap-3 text-white card__header">
<span class="text-base card__label">{{ card.label }}</span>
</div>
<div class="relative flex flex-col text-white card__content">
<h3 :class="`card__title font-normal text-base m-0 mb-1 ${textAutoHide ? 'text-clamp-1' : ''}`">
{{ card.title }}
</h3>
<p :class="`card__description text-xs leading-5 opacity-90 ${textAutoHide ? 'text-clamp-2' : ''}`">
{{ card.description }}
</p>
</div>
</div>
</template>
</div>
</BentoCardGrid>
</template>
<style>
.bento-section {
--glow-x: 50%;
--glow-y: 50%;
--glow-intensity: 0;
--glow-radius: 200px;
--glow-color: v-bind(glowColor);
--border-color: #333;
--background-dark: #060010;
--white: hsl(0, 0%, 100%);
}
.card-responsive {
grid-template-columns: 1fr;
width: 90%;
margin: 0 auto;
padding: 0.5rem;
}
@media (min-width: 600px) {
.card-responsive {
grid-template-columns: repeat(2, 1fr);
}
}
@media (min-width: 1024px) {
.card-responsive {
grid-template-columns: repeat(4, 1fr);
}
.card-responsive .card:nth-child(3) {
grid-column: span 2;
grid-row: span 2;
}
.card-responsive .card:nth-child(4) {
grid-column: 1 / span 2;
grid-row: 2 / span 2;
}
.card-responsive .card:nth-child(6) {
grid-column: 4;
grid-row: 3;
}
}
.card--border-glow::after {
content: '';
position: absolute;
inset: 0;
padding: 6px;
background: radial-gradient(
var(--glow-radius) circle at var(--glow-x) var(--glow-y),
rgba(v-bind(glowColor), calc(var(--glow-intensity) * 0.8)) 0%,
rgba(v-bind(glowColor), calc(var(--glow-intensity) * 0.4)) 30%,
transparent 60%
);
border-radius: inherit;
mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
mask-composite: subtract;
-webkit-mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
pointer-events: none;
transition: opacity 0.3s ease;
z-index: 1;
}
.card--border-glow:hover::after {
opacity: 1;
}
.card--border-glow:hover {
box-shadow:
0 4px 20px rgba(46, 24, 78, 0.4),
0 0 30px rgba(v-bind(glowColor), 0.2);
}
.particle::before {
content: '';
position: absolute;
top: -2px;
left: -2px;
right: -2px;
bottom: -2px;
background: rgba(v-bind(glowColor), 0.2);
border-radius: 50%;
z-index: -1;
}
.particle-container:hover {
box-shadow:
0 4px 20px rgba(46, 24, 78, 0.2),
0 0 30px rgba(v-bind(glowColor), 0.2);
}
.text-clamp-1 {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
line-clamp: 1;
overflow: hidden;
text-overflow: ellipsis;
}
.text-clamp-2 {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
line-clamp: 2;
overflow: hidden;
text-overflow: ellipsis;
}
@media (max-width: 599px) {
.card-responsive {
grid-template-columns: 1fr;
width: 90%;
margin: 0 auto;
padding: 0.5rem;
}
.card-responsive .card {
width: 100%;
min-height: 180px;
}
}
</style>

View File

@@ -0,0 +1,141 @@
<template>
<TabbedLayout>
<template #preview>
<div class="relative h-auto overflow-hidden demo-container">
<MagicBento
:enable-stars="enableStars"
:enable-spotlight="enableSpotlight"
:disable-animations="disableAnimations"
:spotlight-radius="spotlightRadius"
:enable-tilt="enableTilt"
:click-effect="clickEffect"
:enable-magnetism="enableMagnetism"
/>
</div>
<Customize>
<PreviewSlider title="Spotlight Radius" v-model="spotlightRadius" :min="50" :max="800" :step="10" />
<PreviewSwitch title="Stars Effect" v-model="enableStars" />
<PreviewSwitch title="Spotlight Effect" v-model="enableSpotlight" />
<PreviewSwitch title="Tilt Effect" v-model="enableTilt" />
<PreviewSwitch title="Click Effect" v-model="clickEffect" />
<PreviewSwitch title="Magnetism" v-model="enableMagnetism" />
<PreviewSwitch title="Disable All Animations" v-model="disableAnimations" />
</Customize>
<PropTable :data="propData" />
<Dependencies :dependency-list="['gsap']" />
</template>
<template #code>
<CodeExample :code-object="magicBento" />
</template>
<template #cli>
<CliInstallation :command="magicBento.cli" />
</template>
</TabbedLayout>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import CliInstallation from '../../components/code/CliInstallation.vue';
import CodeExample from '../../components/code/CodeExample.vue';
import Dependencies from '../../components/code/Dependencies.vue';
import Customize from '../../components/common/Customize.vue';
import PreviewSlider from '../../components/common/PreviewSlider.vue';
import PreviewSwitch from '../../components/common/PreviewSwitch.vue';
import PropTable from '../../components/common/PropTable.vue';
import TabbedLayout from '../../components/common/TabbedLayout.vue';
import { magicBento } from '../../constants/code/Components/magicBentoCode';
import MagicBento from '../../content/Components/MagicBento/MagicBento.vue';
const enableStars = ref(true);
const enableSpotlight = ref(true);
const disableAnimations = ref(false);
const spotlightRadius = ref(400);
const enableTilt = ref(false);
const clickEffect = ref(true);
const enableMagnetism = ref(false);
const propData = [
{
name: 'textAutoHide',
type: 'boolean',
default: 'true',
description: 'Whether text content should auto-hide on hover'
},
{
name: 'enableStars',
type: 'boolean',
default: 'true',
description: 'Enable particle star animation effect'
},
{
name: 'enableSpotlight',
type: 'boolean',
default: 'true',
description: 'Enable spotlight cursor following effect'
},
{
name: 'enableBorderGlow',
type: 'boolean',
default: 'true',
description: 'Enable border glow effect that follows cursor'
},
{
name: 'disableAnimations',
type: 'boolean',
default: 'false',
description: 'Disable all animations (automatically enabled on mobile)'
},
{
name: 'spotlightRadius',
type: 'number',
default: '300',
description: 'Radius of the spotlight effect in pixels'
},
{
name: 'particleCount',
type: 'number',
default: '12',
description: 'Number of particles in the star animation'
},
{
name: 'enableTilt',
type: 'boolean',
default: 'false',
description: 'Enable 3D tilt effect on card hover'
},
{
name: 'glowColor',
type: 'string',
default: '"77, 254, 6"',
description: 'RGB color values for glow effects (without rgba wrapper)'
},
{
name: 'clickEffect',
type: 'boolean',
default: 'true',
description: 'Enable ripple effect on card click'
},
{
name: 'enableMagnetism',
type: 'boolean',
default: 'true',
description: 'Enable subtle card attraction to cursor'
}
];
</script>
<style scoped>
.demo-container {
padding: 0 8px;
}
</style>

View File

@@ -12,7 +12,8 @@ export default defineConfig({
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
'@content': path.resolve(__dirname, 'src/content')
'@content': path.resolve(__dirname, 'src/content'),
vue: 'vue/dist/vue.esm-bundler.js'
}
}
});