mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-03-07 22:49:31 -07:00
1 line
5.9 KiB
JSON
1 line
5.9 KiB
JSON
{"name":"BounceCards","title":"BounceCards","description":"Cards bounce that bounce in on mount.","type":"registry:component","add":"when-added","files":[{"type":"registry:component","role":"file","content":"<template>\n <div\n :class=\"['relative flex items-center justify-center', className]\"\n :style=\"{\n width: typeof containerWidth === 'number' ? `${containerWidth}px` : containerWidth,\n height: typeof containerHeight === 'number' ? `${containerHeight}px` : containerHeight\n }\"\n >\n <div\n v-for=\"(src, idx) in images\"\n :key=\"idx\"\n ref=\"cardRefs\"\n class=\"absolute w-[200px] aspect-square border-[5px] border-white rounded-[25px] overflow-hidden shadow-[0_4px_10px_rgba(0,0,0,0.2)] bg-[#0b0b0b] opacity-0\"\n :style=\"{ transform: transformStyles[idx] ?? 'none' }\"\n @mouseenter=\"() => pushSiblings(idx)\"\n @mouseleave=\"resetSiblings\"\n >\n <div v-if=\"!imageLoaded[idx]\" class=\"absolute inset-0 z-[1] bg-[#0b0b0b] overflow-hidden shimmer-container\"></div>\n\n <img\n class=\"absolute inset-0 w-full h-full object-cover z-[2] transition-opacity duration-700 ease-out\"\n :src=\"src\"\n :alt=\"`card-${idx}`\"\n :style=\"{ opacity: imageLoaded[idx] ? 1 : 0 }\"\n @load=\"() => onImageLoad(idx)\"\n @error=\"() => onImageError(idx)\"\n />\n </div>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { onMounted, onUnmounted, ref, watch, nextTick } from 'vue';\nimport { gsap } from 'gsap';\n\nexport interface BounceCardsProps {\n className?: string;\n images?: string[];\n containerWidth?: number | string;\n containerHeight?: number | string;\n animationDelay?: number;\n animationStagger?: number;\n easeType?: string;\n transformStyles?: string[];\n enableHover?: boolean;\n}\n\nconst props = withDefaults(defineProps<BounceCardsProps>(), {\n className: '',\n images: () => [],\n containerWidth: 400,\n containerHeight: 400,\n animationDelay: 0.5,\n animationStagger: 0.06,\n easeType: 'elastic.out(1, 0.8)',\n transformStyles: () => [\n 'rotate(10deg) translate(-170px)',\n 'rotate(5deg) translate(-85px)',\n 'rotate(-3deg)',\n 'rotate(-10deg) translate(85px)',\n 'rotate(2deg) translate(170px)'\n ],\n enableHover: true\n});\n\nconst imageLoaded = ref(new Array(props.images.length).fill(false));\nconst cardRefs = ref<HTMLElement[]>([]);\n\nconst getNoRotationTransform = (transformStr: string): string => {\n const hasRotate = /rotate\\([\\s\\S]*?\\)/.test(transformStr);\n if (hasRotate) {\n return transformStr.replace(/rotate\\([\\s\\S]*?\\)/, 'rotate(0deg)');\n } else if (transformStr === 'none') {\n return 'rotate(0deg)';\n } else {\n return `${transformStr} rotate(0deg)`;\n }\n};\n\nconst getPushedTransform = (baseTransform: string, offsetX: number): string => {\n const translateRegex = /translate\\(([-0-9.]+)px\\)/;\n const match = baseTransform.match(translateRegex);\n if (match) {\n const currentX = parseFloat(match[1]);\n const newX = currentX + offsetX;\n return baseTransform.replace(translateRegex, `translate(${newX}px)`);\n } else {\n return baseTransform === 'none' ? `translate(${offsetX}px)` : `${baseTransform} translate(${offsetX}px)`;\n }\n};\n\nconst pushSiblings = (hoveredIdx: number) => {\n if (!props.enableHover) return;\n\n props.images.forEach((_, i) => {\n gsap.killTweensOf(cardRefs.value[i]);\n\n const baseTransform = props.transformStyles[i] || 'none';\n\n if (i === hoveredIdx) {\n const noRotationTransform = getNoRotationTransform(baseTransform);\n gsap.to(cardRefs.value[i], {\n transform: noRotationTransform,\n duration: 0.4,\n ease: 'back.out(1.4)',\n overwrite: 'auto'\n });\n } else {\n const offsetX = i < hoveredIdx ? -160 : 160;\n const pushedTransform = getPushedTransform(baseTransform, offsetX);\n const distance = Math.abs(hoveredIdx - i);\n const delay = distance * 0.05;\n\n gsap.to(cardRefs.value[i], {\n transform: pushedTransform,\n duration: 0.4,\n ease: 'back.out(1.4)',\n delay,\n overwrite: 'auto'\n });\n }\n });\n};\n\nconst resetSiblings = () => {\n if (!props.enableHover) return;\n\n props.images.forEach((_, i) => {\n gsap.killTweensOf(cardRefs.value[i]);\n const baseTransform = props.transformStyles[i] || 'none';\n gsap.to(cardRefs.value[i], {\n transform: baseTransform,\n duration: 0.4,\n ease: 'back.out(1.4)',\n overwrite: 'auto'\n });\n });\n};\n\nconst onImageLoad = (idx: number) => {\n imageLoaded.value[idx] = true;\n};\n\nconst onImageError = (idx: number) => {\n imageLoaded.value[idx] = true;\n};\n\nconst playEntranceAnimation = () => {\n gsap.killTweensOf(cardRefs.value);\n gsap.set(cardRefs.value, { opacity: 0, scale: 0 });\n\n gsap.fromTo(\n cardRefs.value,\n { scale: 0, opacity: 0 },\n {\n scale: 1,\n opacity: 1,\n stagger: props.animationStagger,\n ease: props.easeType,\n delay: props.animationDelay\n }\n );\n};\n\nonMounted(playEntranceAnimation);\nwatch(\n () => props.images,\n async () => {\n await nextTick();\n gsap.set(cardRefs.value, { opacity: 0, scale: 0 });\n playEntranceAnimation();\n }\n);\n\nonUnmounted(() => {\n gsap.killTweensOf(cardRefs.value);\n});\n</script>\n\n<style scoped>\n.shimmer-container {\n background: linear-gradient(110deg, transparent 40%, rgba(255, 255, 255, 0.1) 50%, transparent 60%);\n background-size: 600% 600%;\n background-position: -600% 0;\n animation: shimmer-sweep 6s infinite;\n}\n\n@keyframes shimmer-sweep {\n 0% {\n background-position: -600% 0;\n }\n 100% {\n background-position: 200% 0;\n }\n}\n</style>\n","path":"BounceCards/BounceCards.vue","_imports_":[],"registryDependencies":[],"dependencies":[],"devDependencies":[]}],"registryDependencies":[],"dependencies":[{"ecosystem":"js","name":"gsap","version":"^3.13.0"}],"devDependencies":[],"categories":["Components"]} |