Files
vue-bits/public/r/Carousel.json
David Haz e621971723 jsrepo v3
2025-12-15 23:50:24 +02:00

1 line
9.1 KiB
JSON

{"name":"Carousel","title":"Carousel","description":"Responsive carousel with touch gestures, looping and transitions.","type":"registry:component","add":"when-added","files":[{"type":"registry:component","role":"file","content":"<template>\n <div\n ref=\"containerRef\"\n :class=\"[\n 'relative overflow-hidden p-4',\n round ? 'rounded-full border border-[#333]' : 'rounded-[24px] border border-[#333]'\n ]\"\n :style=\"{\n width: `${baseWidth}px`,\n ...(round && { height: `${baseWidth}px` })\n }\"\n >\n <Motion\n tag=\"div\"\n class=\"flex\"\n drag=\"x\"\n :dragConstraints=\"dragConstraints\"\n :style=\"{\n width: itemWidth + 'px',\n gap: `${GAP}px`,\n perspective: 1000,\n perspectiveOrigin: `${currentIndex * trackItemOffset + itemWidth / 2}px 50%`,\n x: motionX\n }\"\n @dragEnd=\"handleDragEnd\"\n :animate=\"{ x: -(currentIndex * trackItemOffset) }\"\n :transition=\"effectiveTransition\"\n @animationComplete=\"handleAnimationComplete\"\n >\n <Motion\n v-for=\"(item, index) in carouselItems\"\n :key=\"index\"\n tag=\"div\"\n :class=\"[\n 'relative shrink-0 flex flex-col overflow-hidden cursor-grab active:cursor-grabbing',\n round\n ? 'items-center justify-center text-center bg-[#111] border border-[#333] rounded-full'\n : 'items-start justify-between bg-[#111] border border-[#333] rounded-[12px]'\n ]\"\n :style=\"{\n width: itemWidth + 'px',\n height: round ? itemWidth + 'px' : '100%',\n rotateY: getRotateY(index),\n ...(round && { borderRadius: '50%' })\n }\"\n :transition=\"effectiveTransition\"\n >\n <div :class=\"round ? 'p-0 m-0' : 'mb-4 p-5'\">\n <span class=\"flex h-[28px] w-[28px] items-center justify-center rounded-full bg-[#0b0b0b]\">\n <i :class=\"item.icon\" class=\"text-white text-base\"></i>\n </span>\n </div>\n\n <div class=\"p-5\">\n <div class=\"mb-1 font-black text-lg text-white\">{{ item.title }}</div>\n\n <p class=\"text-sm text-white\">{{ item.description }}</p>\n </div>\n </Motion>\n </Motion>\n\n <div :class=\"['flex w-full justify-center', round ? 'absolute z-20 bottom-12 left-1/2 -translate-x-1/2' : '']\">\n <div class=\"mt-4 flex w-[150px] justify-between px-8\">\n <Motion\n v-for=\"(_, index) in items\"\n :key=\"index\"\n tag=\"div\"\n :class=\"[\n 'h-2 w-2 rounded-full cursor-pointer transition-colors duration-150',\n currentIndex % items.length === index\n ? round\n ? 'bg-white'\n : 'bg-[#333333]'\n : round\n ? 'bg-[#555]'\n : 'bg-[rgba(51,51,51,0.4)]'\n ]\"\n :animate=\"{\n scale: currentIndex % items.length === index ? 1.2 : 1\n }\"\n @click=\"() => setCurrentIndex(index)\"\n :transition=\"{ duration: 0.15 }\"\n />\n </div>\n </div>\n </div>\n</template>\n\n<script lang=\"ts\">\nexport interface CarouselItem {\n title: string;\n description: string;\n id: number;\n icon: string;\n}\n\nexport interface CarouselProps {\n items?: CarouselItem[];\n baseWidth?: number;\n autoplay?: boolean;\n autoplayDelay?: number;\n pauseOnHover?: boolean;\n loop?: boolean;\n round?: boolean;\n}\n\nexport const DEFAULT_ITEMS: CarouselItem[] = [\n {\n title: 'Text Animations',\n description: 'Cool text animations for your projects.',\n id: 1,\n icon: 'pi pi-file'\n },\n {\n title: 'Animations',\n description: 'Smooth animations for your projects.',\n id: 2,\n icon: 'pi pi-circle'\n },\n {\n title: 'Components',\n description: 'Reusable components for your projects.',\n id: 3,\n icon: 'pi pi-objects-column'\n },\n {\n title: 'Backgrounds',\n description: 'Beautiful backgrounds and patterns for your projects.',\n id: 4,\n icon: 'pi pi-table'\n },\n {\n title: 'Common UI',\n description: 'Common UI components are coming soon!',\n id: 5,\n icon: 'pi pi-code'\n }\n];\n</script>\n\n<script setup lang=\"ts\">\nimport { ref, computed, onMounted, onUnmounted, watch, useTemplateRef } from 'vue';\nimport { Motion, useMotionValue, useTransform } from 'motion-v';\n\nconst DRAG_BUFFER = 0;\nconst VELOCITY_THRESHOLD = 500;\nconst GAP = 16;\nconst SPRING_OPTIONS = { type: 'spring' as const, stiffness: 300, damping: 30 };\n\nconst props = withDefaults(defineProps<CarouselProps>(), {\n items: () => DEFAULT_ITEMS,\n baseWidth: 300,\n autoplay: false,\n autoplayDelay: 3000,\n pauseOnHover: false,\n loop: false,\n round: false\n});\n\nconst containerPadding = 16;\nconst itemWidth = computed(() => props.baseWidth - containerPadding * 2);\nconst trackItemOffset = computed(() => itemWidth.value + GAP);\n\nconst carouselItems = computed(() => (props.loop ? [...props.items, props.items[0]] : props.items));\nconst currentIndex = ref<number>(0);\nconst motionX = useMotionValue(0);\nconst isHovered = ref<boolean>(false);\nconst isResetting = ref<boolean>(false);\n\nconst containerRef = useTemplateRef<HTMLDivElement>('containerRef');\nlet autoplayTimer: number | null = null;\n\nconst dragConstraints = computed(() => {\n return props.loop\n ? {}\n : {\n left: -trackItemOffset.value * (carouselItems.value.length - 1),\n right: 0\n };\n});\n\nconst effectiveTransition = computed(() => (isResetting.value ? { duration: 0 } : SPRING_OPTIONS));\n\nconst maxItems = Math.max(props.items.length + 1, 10);\nconst rotateYTransforms = Array.from({ length: maxItems }, (_, index) => {\n const range = computed(() => [\n -(index + 1) * trackItemOffset.value,\n -index * trackItemOffset.value,\n -(index - 1) * trackItemOffset.value\n ]);\n const outputRange = [90, 0, -90];\n return useTransform(motionX, range, outputRange, { clamp: false });\n});\n\nconst getRotateY = (index: number) => {\n return rotateYTransforms[index] || rotateYTransforms[0];\n};\n\nconst setCurrentIndex = (index: number) => {\n currentIndex.value = index;\n};\n\nconst handleAnimationComplete = () => {\n if (props.loop && currentIndex.value === carouselItems.value.length - 1) {\n isResetting.value = true;\n motionX.set(0);\n currentIndex.value = 0;\n setTimeout(() => {\n isResetting.value = false;\n }, 50);\n }\n};\n\ninterface DragInfo {\n offset: { x: number; y: number };\n velocity: { x: number; y: number };\n}\n\nconst handleDragEnd = (event: Event, info: DragInfo) => {\n const offset = info.offset.x;\n const velocity = info.velocity.x;\n\n if (offset < -DRAG_BUFFER || velocity < -VELOCITY_THRESHOLD) {\n if (props.loop && currentIndex.value === props.items.length - 1) {\n currentIndex.value = currentIndex.value + 1;\n } else {\n currentIndex.value = Math.min(currentIndex.value + 1, carouselItems.value.length - 1);\n }\n } else if (offset > DRAG_BUFFER || velocity > VELOCITY_THRESHOLD) {\n if (props.loop && currentIndex.value === 0) {\n currentIndex.value = props.items.length - 1;\n } else {\n currentIndex.value = Math.max(currentIndex.value - 1, 0);\n }\n }\n};\n\nconst startAutoplay = () => {\n if (props.autoplay && (!props.pauseOnHover || !isHovered.value)) {\n autoplayTimer = window.setInterval(() => {\n currentIndex.value = (() => {\n const prev = currentIndex.value;\n if (prev === props.items.length - 1 && props.loop) {\n return prev + 1;\n }\n if (prev === carouselItems.value.length - 1) {\n return props.loop ? 0 : prev;\n }\n return prev + 1;\n })();\n }, props.autoplayDelay);\n }\n};\n\nconst stopAutoplay = () => {\n if (autoplayTimer) {\n clearInterval(autoplayTimer);\n autoplayTimer = null;\n }\n};\n\nconst handleMouseEnter = () => {\n isHovered.value = true;\n if (props.pauseOnHover) {\n stopAutoplay();\n }\n};\n\nconst handleMouseLeave = () => {\n isHovered.value = false;\n if (props.pauseOnHover) {\n startAutoplay();\n }\n};\n\nwatch(\n [\n () => props.autoplay,\n () => props.autoplayDelay,\n isHovered,\n () => props.loop,\n () => props.items.length,\n () => carouselItems.value.length,\n () => props.pauseOnHover\n ],\n () => {\n stopAutoplay();\n startAutoplay();\n }\n);\n\nonMounted(() => {\n if (props.pauseOnHover && containerRef.value) {\n containerRef.value.addEventListener('mouseenter', handleMouseEnter);\n containerRef.value.addEventListener('mouseleave', handleMouseLeave);\n }\n startAutoplay();\n});\n\nonUnmounted(() => {\n if (containerRef.value) {\n containerRef.value.removeEventListener('mouseenter', handleMouseEnter);\n containerRef.value.removeEventListener('mouseleave', handleMouseLeave);\n }\n stopAutoplay();\n});\n</script>\n","path":"Carousel/Carousel.vue","_imports_":[],"registryDependencies":[],"dependencies":[],"devDependencies":[]}],"registryDependencies":[],"dependencies":[{"ecosystem":"js","name":"motion-v","version":"^1.5.0"}],"devDependencies":[],"categories":["Components"]}