mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-03-07 22:49:31 -07:00
1 line
7.4 KiB
JSON
1 line
7.4 KiB
JSON
{"name":"CardSwap","title":"CardSwap","description":"Cards animate position swapping with smooth layout transitions.","type":"registry:component","add":"when-added","files":[{"type":"registry:component","role":"file","content":"<template>\n <div\n ref=\"containerRef\"\n class=\"card-swap-container absolute bottom-0 right-0 transform translate-x-[5%] translate-y-[20%] origin-bottom-right perspective-[900px] overflow-visible max-[768px]:translate-x-[25%] max-[768px]:translate-y-[25%] max-[768px]:scale-[0.75] max-[480px]:translate-x-[25%] max-[480px]:translate-y-[25%] max-[480px]:scale-[0.55]\"\n :style=\"{\n width: typeof width === 'number' ? `${width}px` : width,\n height: typeof height === 'number' ? `${height}px` : height\n }\"\n >\n <div\n v-for=\"(_, index) in 3\"\n :key=\"index\"\n ref=\"cardRefs\"\n class=\"card-swap-card absolute top-1/2 left-1/2 rounded-xl border border-white bg-black [transform-style:preserve-3d] [will-change:transform] [backface-visibility:hidden]\"\n :style=\"{\n width: typeof width === 'number' ? `${width}px` : width,\n height: typeof height === 'number' ? `${height}px` : height\n }\"\n @click=\"handleCardClick(index)\"\n >\n <slot :name=\"`card-${index}`\" :index=\"index\" />\n </div>\n </div>\n</template>\n\n<script lang=\"ts\">\nimport gsap from 'gsap';\n\nexport interface CardSwapProps {\n width?: number | string;\n height?: number | string;\n cardDistance?: number;\n verticalDistance?: number;\n delay?: number;\n pauseOnHover?: boolean;\n onCardClick?: (idx: number) => void;\n skewAmount?: number;\n easing?: 'linear' | 'elastic';\n}\n\ninterface Slot {\n x: number;\n y: number;\n z: number;\n zIndex: number;\n}\n\nconst makeSlot = (i: number, distX: number, distY: number, total: number): Slot => ({\n x: i * distX,\n y: -i * distY,\n z: -i * distX * 1.5,\n zIndex: total - i\n});\n\nconst placeNow = (el: HTMLElement, slot: Slot, skew: number) => {\n gsap.set(el, {\n x: slot.x,\n y: slot.y,\n z: slot.z,\n xPercent: -50,\n yPercent: -50,\n skewY: skew,\n transformOrigin: 'center center',\n zIndex: slot.zIndex,\n force3D: true\n });\n};\n\nexport { makeSlot, placeNow };\n</script>\n\n<script setup lang=\"ts\">\nimport { ref, onMounted, onUnmounted, watch, nextTick, computed, useTemplateRef } from 'vue';\n\nconst props = withDefaults(defineProps<CardSwapProps>(), {\n width: 500,\n height: 400,\n cardDistance: 60,\n verticalDistance: 70,\n delay: 5000,\n pauseOnHover: false,\n skewAmount: 6,\n easing: 'elastic'\n});\n\nconst emit = defineEmits<{\n 'card-click': [index: number];\n}>();\n\nconst containerRef = useTemplateRef<HTMLDivElement>('containerRef');\nconst cardRefs = ref<HTMLElement[]>([]);\nconst order = ref<number[]>([0, 1, 2]);\nconst tlRef = ref<gsap.core.Timeline | null>(null);\nconst intervalRef = ref<number>();\n\nconst handleCardClick = (index: number) => {\n emit('card-click', index);\n props.onCardClick?.(index);\n};\n\nconst config = computed(() => {\n return props.easing === 'elastic'\n ? {\n ease: 'elastic.out(0.6,0.9)',\n durDrop: 2,\n durMove: 2,\n durReturn: 2,\n promoteOverlap: 0.9,\n returnDelay: 0.05\n }\n : {\n ease: 'power1.inOut',\n durDrop: 0.8,\n durMove: 0.8,\n durReturn: 0.8,\n promoteOverlap: 0.45,\n returnDelay: 0.2\n };\n});\n\nconst initializeCards = () => {\n if (!cardRefs.value.length) return;\n\n const total = cardRefs.value.length;\n\n cardRefs.value.forEach((el, i) => {\n if (el) {\n placeNow(el, makeSlot(i, props.cardDistance, props.verticalDistance, total), props.skewAmount);\n }\n });\n};\n\nconst updateCardPositions = () => {\n if (!cardRefs.value.length) return;\n\n const total = cardRefs.value.length;\n\n cardRefs.value.forEach((el, i) => {\n if (el) {\n const slot = makeSlot(i, props.cardDistance, props.verticalDistance, total);\n gsap.set(el, {\n x: slot.x,\n y: slot.y,\n z: slot.z,\n skewY: props.skewAmount\n });\n }\n });\n};\n\nconst swap = () => {\n if (order.value.length < 2) return;\n\n const [front, ...rest] = order.value;\n const elFront = cardRefs.value[front];\n if (!elFront) return;\n\n const tl = gsap.timeline();\n tlRef.value = tl;\n\n tl.to(elFront, {\n y: '+=500',\n duration: config.value.durDrop,\n ease: config.value.ease\n });\n\n tl.addLabel('promote', `-=${config.value.durDrop * config.value.promoteOverlap}`);\n rest.forEach((idx, i) => {\n const el = cardRefs.value[idx];\n if (!el) return;\n\n const slot = makeSlot(i, props.cardDistance, props.verticalDistance, cardRefs.value.length);\n tl.set(el, { zIndex: slot.zIndex }, 'promote');\n tl.to(\n el,\n {\n x: slot.x,\n y: slot.y,\n z: slot.z,\n duration: config.value.durMove,\n ease: config.value.ease\n },\n `promote+=${i * 0.15}`\n );\n });\n\n const backSlot = makeSlot(\n cardRefs.value.length - 1,\n props.cardDistance,\n props.verticalDistance,\n cardRefs.value.length\n );\n tl.addLabel('return', `promote+=${config.value.durMove * config.value.returnDelay}`);\n tl.call(\n () => {\n gsap.set(elFront, { zIndex: backSlot.zIndex });\n },\n undefined,\n 'return'\n );\n tl.set(elFront, { x: backSlot.x, z: backSlot.z }, 'return');\n tl.to(\n elFront,\n {\n y: backSlot.y,\n duration: config.value.durReturn,\n ease: config.value.ease\n },\n 'return'\n );\n\n tl.call(() => {\n order.value = [...rest, front];\n });\n};\n\nconst startAnimation = () => {\n stopAnimation();\n swap();\n intervalRef.value = window.setInterval(swap, props.delay);\n};\n\nconst stopAnimation = () => {\n tlRef.value?.kill();\n if (intervalRef.value) {\n clearInterval(intervalRef.value);\n }\n};\n\nconst resumeAnimation = () => {\n tlRef.value?.play();\n intervalRef.value = window.setInterval(swap, props.delay);\n};\n\nconst setupHoverListeners = () => {\n if (props.pauseOnHover && containerRef.value) {\n containerRef.value.addEventListener('mouseenter', stopAnimation);\n containerRef.value.addEventListener('mouseleave', resumeAnimation);\n }\n};\n\nconst removeHoverListeners = () => {\n if (containerRef.value) {\n containerRef.value.removeEventListener('mouseenter', stopAnimation);\n containerRef.value.removeEventListener('mouseleave', resumeAnimation);\n }\n};\n\nwatch(\n () => [props.cardDistance, props.verticalDistance, props.skewAmount],\n () => {\n updateCardPositions();\n }\n);\n\nwatch(\n () => props.delay,\n () => {\n if (intervalRef.value) {\n clearInterval(intervalRef.value);\n intervalRef.value = window.setInterval(swap, props.delay);\n }\n }\n);\n\nwatch(\n () => props.pauseOnHover,\n () => {\n removeHoverListeners();\n setupHoverListeners();\n }\n);\n\nwatch(\n () => props.easing,\n () => {}\n);\n\nonMounted(() => {\n nextTick(() => {\n initializeCards();\n startAnimation();\n setupHoverListeners();\n });\n});\n\nonUnmounted(() => {\n stopAnimation();\n removeHoverListeners();\n});\n</script>\n","path":"CardSwap/CardSwap.vue","_imports_":[],"registryDependencies":[],"dependencies":[],"devDependencies":[]}],"registryDependencies":[],"dependencies":[{"ecosystem":"js","name":"gsap","version":"^3.13.0"}],"devDependencies":[],"categories":["Components"]} |