mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-03-07 06:29:30 -07:00
1 line
7.7 KiB
JSON
1 line
7.7 KiB
JSON
{"name":"InfiniteScroll","title":"InfiniteScroll","description":"Infinite scrolling container auto-loads content near viewport end.","type":"registry:component","add":"when-added","files":[{"type":"registry:component","role":"file","content":"<template>\n <div class=\"w-full\">\n <div\n class=\"relative flex justify-center items-center w-full overflow-hidden infinite-scroll-wrapper\"\n ref=\"wrapperRef\"\n :style=\"{\n maxHeight: maxHeight,\n overscrollBehavior: 'none'\n }\"\n >\n <div\n class=\"flex flex-col px-4 infinite-scroll-container cursor-grab\"\n ref=\"containerRef\"\n :style=\"{\n transform: getTiltTransform(),\n width: width,\n overscrollBehavior: 'contain',\n transformOrigin: 'center center',\n transformStyle: 'preserve-3d'\n }\"\n >\n <div\n v-for=\"(item, index) in items\"\n :key=\"index\"\n class=\"box-border relative flex justify-center items-center p-4 border-2 border-white rounded-2xl font-semibold text-xl text-center infinite-scroll-item select-none\"\n :style=\"{\n height: itemMinHeight + 'px',\n marginTop: negativeMargin\n }\"\n >\n <component :is=\"item.content\" v-if=\"typeof item.content === 'object'\" />\n\n <template v-else>{{ item.content }}</template>\n </div>\n </div>\n </div>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { gsap } from 'gsap';\nimport { Observer } from 'gsap/all';\nimport { onMounted, onUnmounted, useTemplateRef, watch } from 'vue';\n\ngsap.registerPlugin(Observer);\n\ninterface InfiniteScrollItem {\n content: string | object;\n}\n\ninterface Props {\n width?: string;\n maxHeight?: string;\n negativeMargin?: string;\n items?: InfiniteScrollItem[];\n itemMinHeight?: number;\n isTilted?: boolean;\n tiltDirection?: 'left' | 'right';\n autoplay?: boolean;\n autoplaySpeed?: number;\n autoplayDirection?: 'down' | 'up';\n pauseOnHover?: boolean;\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n width: '30rem',\n maxHeight: '100%',\n negativeMargin: '-0.5em',\n items: () => [],\n itemMinHeight: 150,\n isTilted: false,\n tiltDirection: 'left',\n autoplay: false,\n autoplaySpeed: 0.5,\n autoplayDirection: 'down',\n pauseOnHover: false\n});\n\nconst wrapperRef = useTemplateRef<HTMLDivElement>('wrapperRef');\nconst containerRef = useTemplateRef<HTMLDivElement>('containerRef');\nlet observer: Observer | null = null;\nlet rafId: number | null = null;\nlet velocity = 0;\nlet stopTicker: (() => void) | null = null;\nlet startTicker: (() => void) | null = null;\n\nconst getTiltTransform = (): string => {\n if (!props.isTilted) return 'none';\n return props.tiltDirection === 'left'\n ? 'rotateX(20deg) rotateZ(-20deg) skewX(20deg)'\n : 'rotateX(20deg) rotateZ(20deg) skewX(-20deg)';\n};\n\nconst initializeScroll = () => {\n const container = containerRef.value;\n if (!container) return;\n if (props.items.length === 0) return;\n\n const divItems = gsap.utils.toArray<HTMLDivElement>(container.children);\n if (!divItems.length) return;\n\n const firstItem = divItems[0];\n const itemStyle = getComputedStyle(firstItem);\n const itemHeight = firstItem.offsetHeight;\n const itemMarginTop = parseFloat(itemStyle.marginTop) || 0;\n const totalItemHeight = itemHeight + itemMarginTop;\n const totalHeight = itemHeight * props.items.length + itemMarginTop * (props.items.length - 1);\n\n const wrapFn = gsap.utils.wrap(-totalHeight, totalHeight);\n\n divItems.forEach((child, i) => {\n const y = i * totalItemHeight;\n gsap.set(child, { y });\n });\n\n observer = Observer.create({\n target: container,\n type: 'wheel,touch,pointer',\n preventDefault: true,\n onPress: ({ target }) => {\n (target as HTMLElement).style.cursor = 'grabbing';\n },\n onRelease: ({ target }) => {\n (target as HTMLElement).style.cursor = 'grab';\n if (Math.abs(velocity) > 0.1) {\n const momentum = velocity * 0.8;\n divItems.forEach(child => {\n gsap.to(child, {\n duration: 1.5,\n ease: 'power2.out',\n y: `+=${momentum}`,\n modifiers: {\n y: gsap.utils.unitize(wrapFn)\n }\n });\n });\n }\n velocity = 0;\n },\n onChange: ({ deltaY, isDragging, event }) => {\n const d = event.type === 'wheel' ? -deltaY : deltaY;\n const distance = isDragging ? d * 5 : d * 1.5;\n\n velocity = distance * 0.5;\n\n divItems.forEach(child => {\n gsap.to(child, {\n duration: isDragging ? 0.3 : 1.2,\n ease: isDragging ? 'power1.out' : 'power3.out',\n y: `+=${distance}`,\n modifiers: {\n y: gsap.utils.unitize(wrapFn)\n }\n });\n });\n }\n });\n\n if (props.autoplay) {\n const directionFactor = props.autoplayDirection === 'down' ? 1 : -1;\n const speedPerFrame = props.autoplaySpeed * directionFactor;\n\n const tick = () => {\n divItems.forEach(child => {\n gsap.set(child, {\n y: `+=${speedPerFrame}`,\n modifiers: {\n y: gsap.utils.unitize(wrapFn)\n }\n });\n });\n rafId = requestAnimationFrame(tick);\n };\n\n rafId = requestAnimationFrame(tick);\n\n if (props.pauseOnHover) {\n stopTicker = () => rafId && cancelAnimationFrame(rafId);\n startTicker = () => {\n rafId = requestAnimationFrame(tick);\n };\n\n container.addEventListener('mouseenter', stopTicker);\n container.addEventListener('mouseleave', startTicker);\n }\n }\n};\n\nconst cleanup = () => {\n if (observer) {\n observer.kill();\n observer = null;\n }\n if (rafId) {\n cancelAnimationFrame(rafId);\n rafId = null;\n }\n\n velocity = 0;\n\n const container = containerRef.value;\n if (container && props.pauseOnHover && stopTicker && startTicker) {\n container.removeEventListener('mouseenter', stopTicker);\n container.removeEventListener('mouseleave', startTicker);\n }\n\n stopTicker = null;\n startTicker = null;\n};\n\nonMounted(() => {\n initializeScroll();\n});\n\nonUnmounted(() => {\n cleanup();\n});\n\nwatch(\n [\n () => props.items,\n () => props.autoplay,\n () => props.autoplaySpeed,\n () => props.autoplayDirection,\n () => props.pauseOnHover,\n () => props.isTilted,\n () => props.tiltDirection,\n () => props.negativeMargin\n ],\n () => {\n cleanup();\n setTimeout(() => {\n initializeScroll();\n }, 0);\n }\n);\n</script>\n\n<style scoped>\n.infinite-scroll-wrapper::before,\n.infinite-scroll-wrapper::after {\n content: '';\n position: absolute;\n background: linear-gradient(var(--dir, to bottom), #0b0b0b, transparent);\n height: 25%;\n width: 100%;\n z-index: 1;\n pointer-events: none;\n}\n\n.infinite-scroll-wrapper::before {\n top: 0;\n}\n\n.infinite-scroll-wrapper::after {\n --dir: to top;\n bottom: 0;\n}\n\n.infinite-scroll-container {\n backface-visibility: hidden;\n -webkit-backface-visibility: hidden;\n -moz-backface-visibility: hidden;\n -ms-backface-visibility: hidden;\n}\n\n.infinite-scroll-item {\n --accent-color: #ffffff;\n border-color: var(--accent-color);\n backface-visibility: hidden;\n -webkit-backface-visibility: hidden;\n -moz-backface-visibility: hidden;\n -ms-backface-visibility: hidden;\n transform: translateZ(0);\n}\n</style>\n","path":"InfiniteScroll/InfiniteScroll.vue","_imports_":[],"registryDependencies":[],"dependencies":[],"devDependencies":[]}],"registryDependencies":[],"dependencies":[{"ecosystem":"js","name":"gsap","version":"^3.13.0"}],"devDependencies":[],"categories":["Components"]} |