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

1 line
6.8 KiB
JSON

{"name":"AnimatedList","title":"AnimatedList","description":"List items enter with staggered motion variants for polished reveals.","type":"registry:component","add":"when-added","files":[{"type":"registry:component","role":"file","content":"<template>\n <div ref=\"containerRef\" :class=\"`relative w-[500px] ${className}`.trim()\">\n <div\n ref=\"listRef\"\n :class=\"`max-h-[400px] overflow-y-auto p-4 ${\n displayScrollbar\n ? '[&::-webkit-scrollbar]:w-[8px] [&::-webkit-scrollbar-track]:bg-[#0b0b0b] [&::-webkit-scrollbar-thumb]:bg-[#222] [&::-webkit-scrollbar-thumb]:rounded-[4px]'\n : 'scrollbar-hide'\n }`\"\n :style=\"{\n scrollbarWidth: displayScrollbar ? 'thin' : 'none',\n scrollbarColor: '#222 #0b0b0b'\n }\"\n @scroll=\"handleScroll\"\n >\n <Motion\n v-for=\"(item, index) in items\"\n :key=\"index\"\n tag=\"div\"\n :data-index=\"index\"\n class=\"mb-4 cursor-pointer\"\n :initial=\"{ scale: 0.7, opacity: 0 }\"\n :animate=\"getItemInView(index) ? { scale: 1, opacity: 1 } : { scale: 0.7, opacity: 0 }\"\n :transition=\"{ duration: 0.2, delay: 0.1 }\"\n @mouseenter=\"() => setSelectedIndex(index)\"\n @click=\"\n () => {\n setSelectedIndex(index);\n emit('itemSelected', item, index);\n }\n \"\n >\n <div :class=\"`p-4 bg-[#111] rounded-lg ${selectedIndex === index ? 'bg-[#222]' : ''} ${itemClassName}`\">\n <p class=\"text-white m-0\">{{ item }}</p>\n </div>\n </Motion>\n </div>\n <div\n v-if=\"showGradients\"\n class=\"absolute top-0 left-0 right-0 h-[50px] bg-gradient-to-b from-[#0b0b0b] to-transparent pointer-events-none transition-opacity duration-300 ease\"\n :style=\"{ opacity: topGradientOpacity }\"\n />\n <div\n v-if=\"showGradients\"\n class=\"absolute bottom-0 left-0 right-0 h-[100px] bg-gradient-to-t from-[#0b0b0b] to-transparent pointer-events-none transition-opacity duration-300 ease\"\n :style=\"{ opacity: bottomGradientOpacity }\"\n />\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { ref, onMounted, onUnmounted, watch, useTemplateRef } from 'vue';\nimport { Motion } from 'motion-v';\n\ninterface AnimatedListProps {\n items?: string[];\n showGradients?: boolean;\n enableArrowNavigation?: boolean;\n className?: string;\n itemClassName?: string;\n displayScrollbar?: boolean;\n initialSelectedIndex?: number;\n}\n\nconst props = withDefaults(defineProps<AnimatedListProps>(), {\n items: () => [\n 'Item 1',\n 'Item 2',\n 'Item 3',\n 'Item 4',\n 'Item 5',\n 'Item 6',\n 'Item 7',\n 'Item 8',\n 'Item 9',\n 'Item 10',\n 'Item 11',\n 'Item 12',\n 'Item 13',\n 'Item 14',\n 'Item 15'\n ],\n showGradients: true,\n enableArrowNavigation: true,\n className: '',\n itemClassName: '',\n displayScrollbar: true,\n initialSelectedIndex: -1\n});\n\nconst emit = defineEmits<{\n itemSelected: [item: string, index: number];\n}>();\n\nconst containerRef = useTemplateRef<HTMLDivElement>('containerRef');\nconst listRef = useTemplateRef<HTMLDivElement>('listRef');\nconst selectedIndex = ref(props.initialSelectedIndex);\nconst keyboardNav = ref(false);\nconst topGradientOpacity = ref(0);\nconst bottomGradientOpacity = ref(1);\nconst itemsInView = ref<boolean[]>([]);\n\nconst setSelectedIndex = (index: number) => {\n selectedIndex.value = index;\n};\n\nconst getItemInView = (index: number) => {\n return itemsInView.value[index] ?? false;\n};\n\nconst handleScroll = (e: Event) => {\n const target = e.target as HTMLDivElement;\n const { scrollTop, scrollHeight, clientHeight } = target;\n topGradientOpacity.value = Math.min(scrollTop / 50, 1);\n const bottomDistance = scrollHeight - (scrollTop + clientHeight);\n bottomGradientOpacity.value = scrollHeight <= clientHeight ? 0 : Math.min(bottomDistance / 50, 1);\n\n updateItemsInView();\n};\n\nconst updateItemsInView = () => {\n if (!listRef.value) return;\n\n const container = listRef.value;\n const containerRect = container.getBoundingClientRect();\n\n itemsInView.value = props.items.map((_, index) => {\n const item = container.querySelector(`[data-index=\"${index}\"]`) as HTMLElement;\n if (!item) return false;\n\n const itemRect = item.getBoundingClientRect();\n const viewHeight = containerRect.height;\n const itemTop = itemRect.top - containerRect.top;\n const itemBottom = itemTop + itemRect.height;\n\n return itemTop < viewHeight && itemBottom > 0;\n });\n};\n\nconst handleKeyDown = (e: KeyboardEvent) => {\n if (e.key === 'ArrowDown' || (e.key === 'Tab' && !e.shiftKey)) {\n e.preventDefault();\n keyboardNav.value = true;\n setSelectedIndex(Math.min(selectedIndex.value + 1, props.items.length - 1));\n } else if (e.key === 'ArrowUp' || (e.key === 'Tab' && e.shiftKey)) {\n e.preventDefault();\n keyboardNav.value = true;\n setSelectedIndex(Math.max(selectedIndex.value - 1, 0));\n } else if (e.key === 'Enter') {\n if (selectedIndex.value >= 0 && selectedIndex.value < props.items.length) {\n e.preventDefault();\n emit('itemSelected', props.items[selectedIndex.value], selectedIndex.value);\n }\n }\n};\n\nwatch([selectedIndex, keyboardNav], () => {\n if (!keyboardNav.value || selectedIndex.value < 0 || !listRef.value) return;\n const container = listRef.value;\n const selectedItem = container.querySelector(`[data-index=\"${selectedIndex.value}\"]`) as HTMLElement | null;\n if (selectedItem) {\n const extraMargin = 50;\n const containerScrollTop = container.scrollTop;\n const containerHeight = container.clientHeight;\n const itemTop = selectedItem.offsetTop;\n const itemBottom = itemTop + selectedItem.offsetHeight;\n if (itemTop < containerScrollTop + extraMargin) {\n container.scrollTo({ top: itemTop - extraMargin, behavior: 'smooth' });\n } else if (itemBottom > containerScrollTop + containerHeight - extraMargin) {\n container.scrollTo({\n top: itemBottom - containerHeight + extraMargin,\n behavior: 'smooth'\n });\n }\n }\n keyboardNav.value = false;\n});\n\nonMounted(() => {\n if (props.enableArrowNavigation) {\n window.addEventListener('keydown', handleKeyDown);\n }\n\n itemsInView.value = new Array(props.items.length).fill(true);\n setTimeout(updateItemsInView, 100);\n});\n\nonUnmounted(() => {\n if (props.enableArrowNavigation) {\n window.removeEventListener('keydown', handleKeyDown);\n }\n});\n</script>\n","path":"AnimatedList/AnimatedList.vue","_imports_":[],"registryDependencies":[],"dependencies":[],"devDependencies":[]}],"registryDependencies":[],"dependencies":[{"ecosystem":"js","name":"motion-v","version":"^1.5.0"}],"devDependencies":[],"categories":["Components"]}