mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-03-07 06:29:30 -07:00
1 line
7.6 KiB
JSON
1 line
7.6 KiB
JSON
{"name":"RotatingText","title":"RotatingText","description":"Cycles through multiple phrases with 3D rotate / flip transitions.","type":"registry:component","add":"when-added","files":[{"type":"registry:component","role":"file","content":"<script setup lang=\"ts\">\nimport { AnimatePresence, Motion, type Target, type Transition, type VariantLabels } from 'motion-v';\nimport { computed, onMounted, onUnmounted, ref, watch } from 'vue';\n\ntype StaggerFrom = 'first' | 'last' | 'center' | 'random' | number;\ntype SplitBy = 'characters' | 'words' | 'lines';\n\ninterface WordElement {\n characters: string[];\n needsSpace: boolean;\n}\n\ninterface RotatingTextProps {\n texts: string[];\n transition?: Transition;\n initial?: boolean | Target | VariantLabels;\n animate?: Target | VariantLabels;\n exit?: Target | VariantLabels;\n animatePresenceMode?: 'sync' | 'wait';\n animatePresenceInitial?: boolean;\n rotationInterval?: number;\n staggerDuration?: number;\n staggerFrom?: StaggerFrom;\n loop?: boolean;\n auto?: boolean;\n splitBy?: SplitBy;\n onNext?: (index: number) => void;\n mainClassName?: string;\n splitLevelClassName?: string;\n elementLevelClassName?: string;\n}\n\nconst cn = (...classes: (string | undefined | null | boolean)[]): string => {\n return classes.filter(Boolean).join(' ');\n};\n\nconst props = withDefaults(defineProps<RotatingTextProps>(), {\n transition: () =>\n ({\n type: 'spring',\n damping: 25,\n stiffness: 300\n }) as Transition,\n initial: () => ({ y: '100%', opacity: 0 }) as Target,\n animate: () => ({ y: 0, opacity: 1 }) as Target,\n exit: () => ({ y: '-120%', opacity: 0 }) as Target,\n animatePresenceMode: 'wait',\n animatePresenceInitial: false,\n rotationInterval: 2000,\n staggerDuration: 0,\n staggerFrom: 'first',\n loop: true,\n auto: true,\n splitBy: 'characters'\n});\n\nconst currentTextIndex = ref(0);\nlet intervalId: ReturnType<typeof setInterval> | null = null;\n\nconst splitIntoCharacters = (text: string): string[] => {\n if (typeof Intl !== 'undefined' && 'Segmenter' in Intl) {\n const IntlWithSegmenter = Intl as typeof Intl & {\n Segmenter: new (\n locales?: string | string[],\n options?: { granularity: 'grapheme' | 'word' | 'sentence' }\n ) => {\n segment: (text: string) => Iterable<{ segment: string }>;\n };\n };\n const segmenter = new IntlWithSegmenter.Segmenter('en', { granularity: 'grapheme' });\n return [...segmenter.segment(text)].map(({ segment }) => segment);\n }\n\n return [...text];\n};\nconst elements = computed((): WordElement[] => {\n const currentText = props.texts[currentTextIndex.value];\n\n switch (props.splitBy) {\n case 'characters': {\n const words = currentText.split(' ');\n return words.map((word, i) => ({\n characters: splitIntoCharacters(word),\n needsSpace: i !== words.length - 1\n }));\n }\n case 'words': {\n const words = currentText.split(' ');\n return words.map((word, i) => ({\n characters: [word],\n needsSpace: i !== words.length - 1\n }));\n }\n case 'lines': {\n const lines = currentText.split('\\n');\n return lines.map((line, i) => ({\n characters: [line],\n needsSpace: i !== lines.length - 1\n }));\n }\n default: {\n const parts = currentText.split(props.splitBy!);\n return parts.map((part, i) => ({\n characters: [part],\n needsSpace: i !== parts.length - 1\n }));\n }\n }\n});\n\nconst getStaggerDelay = (index: number, totalChars: number): number => {\n const { staggerDuration, staggerFrom } = props;\n\n switch (staggerFrom) {\n case 'first':\n return index * staggerDuration;\n case 'last':\n return (totalChars - 1 - index) * staggerDuration;\n case 'center': {\n const center = Math.floor(totalChars / 2);\n return Math.abs(center - index) * staggerDuration;\n }\n case 'random': {\n const randomIndex = Math.floor(Math.random() * totalChars);\n return Math.abs(randomIndex - index) * staggerDuration;\n }\n default:\n return Math.abs((staggerFrom as number) - index) * staggerDuration;\n }\n};\n\nconst handleIndexChange = (newIndex: number): void => {\n currentTextIndex.value = newIndex;\n props.onNext?.(newIndex);\n};\n\nconst next = (): void => {\n const isAtEnd = currentTextIndex.value === props.texts.length - 1;\n const nextIndex = isAtEnd ? (props.loop ? 0 : currentTextIndex.value) : currentTextIndex.value + 1;\n\n if (nextIndex !== currentTextIndex.value) {\n handleIndexChange(nextIndex);\n }\n};\n\nconst previous = (): void => {\n const isAtStart = currentTextIndex.value === 0;\n const prevIndex = isAtStart\n ? props.loop\n ? props.texts.length - 1\n : currentTextIndex.value\n : currentTextIndex.value - 1;\n\n if (prevIndex !== currentTextIndex.value) {\n handleIndexChange(prevIndex);\n }\n};\n\nconst jumpTo = (index: number): void => {\n const validIndex = Math.max(0, Math.min(index, props.texts.length - 1));\n if (validIndex !== currentTextIndex.value) {\n handleIndexChange(validIndex);\n }\n};\n\nconst reset = (): void => {\n if (currentTextIndex.value !== 0) {\n handleIndexChange(0);\n }\n};\n\nconst cleanupInterval = (): void => {\n if (intervalId) {\n clearInterval(intervalId);\n intervalId = null;\n }\n};\n\nconst startInterval = (): void => {\n if (props.auto) {\n intervalId = setInterval(next, props.rotationInterval);\n }\n};\n\ndefineExpose({\n next,\n previous,\n jumpTo,\n reset\n});\n\nwatch(\n () => [props.auto, props.rotationInterval] as const,\n () => {\n cleanupInterval();\n startInterval();\n }\n);\n\nonMounted(() => {\n startInterval();\n});\n\nonUnmounted(() => {\n cleanupInterval();\n});\n</script>\n\n<template>\n <Motion\n tag=\"span\"\n :class=\"cn('flex flex-wrap whitespace-pre-wrap relative', mainClassName)\"\n v-bind=\"$attrs\"\n :transition=\"transition\"\n layout\n >\n <span class=\"sr-only\">\n {{ texts[currentTextIndex] }}\n </span>\n\n <AnimatePresence :mode=\"animatePresenceMode\" :initial=\"animatePresenceInitial\">\n <Motion\n :key=\"currentTextIndex\"\n tag=\"span\"\n :class=\"cn(splitBy === 'lines' ? 'flex flex-col w-full' : 'flex flex-wrap whitespace-pre-wrap relative')\"\n aria-hidden=\"true\"\n layout\n >\n <span v-for=\"(wordObj, wordIndex) in elements\" :key=\"wordIndex\" :class=\"cn('inline-flex', splitLevelClassName)\">\n <Motion\n v-for=\"(char, charIndex) in wordObj.characters\"\n :key=\"charIndex\"\n tag=\"span\"\n :initial=\"initial\"\n :animate=\"animate\"\n :exit=\"exit\"\n :transition=\"{\n ...transition,\n delay: getStaggerDelay(\n elements.slice(0, wordIndex).reduce((sum, word) => sum + word.characters.length, 0) + charIndex,\n elements.reduce((sum, word) => sum + word.characters.length, 0)\n )\n }\"\n :class=\"cn('inline-block', elementLevelClassName)\"\n >\n {{ char }}\n </Motion>\n <span v-if=\"wordObj.needsSpace\" class=\"whitespace-pre\"></span>\n </span>\n </Motion>\n </AnimatePresence>\n </Motion>\n</template>\n","path":"RotatingText/RotatingText.vue","_imports_":[],"registryDependencies":[],"dependencies":[],"devDependencies":[]}],"registryDependencies":[],"dependencies":[{"ecosystem":"js","name":"motion-v","version":"^1.5.0"}],"devDependencies":[],"categories":["TextAnimations"]} |