mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-03-07 06:29:30 -07:00
1 line
5.5 KiB
JSON
1 line
5.5 KiB
JSON
{"name":"TextType","title":"TextType","description":"Typewriter effect with blinking cursor and adjustable typing cadence.","type":"registry:component","add":"when-added","files":[{"type":"registry:component","role":"file","content":"<script setup lang=\"ts\">\nimport { ref, onMounted, onBeforeUnmount, watch, computed, useTemplateRef } from 'vue';\nimport { gsap } from 'gsap';\n\ninterface TextTypeProps {\n className?: string;\n showCursor?: boolean;\n hideCursorWhileTyping?: boolean;\n cursorCharacter?: string;\n cursorBlinkDuration?: number;\n cursorClassName?: string;\n text: string | string[];\n as?: string;\n typingSpeed?: number;\n initialDelay?: number;\n pauseDuration?: number;\n deletingSpeed?: number;\n loop?: boolean;\n textColors?: string[];\n variableSpeed?: { min: number; max: number };\n onSentenceComplete?: (sentence: string, index: number) => void;\n startOnVisible?: boolean;\n reverseMode?: boolean;\n}\n\nconst props = withDefaults(defineProps<TextTypeProps>(), {\n as: 'div',\n typingSpeed: 50,\n initialDelay: 0,\n pauseDuration: 2000,\n deletingSpeed: 30,\n loop: true,\n className: '',\n showCursor: true,\n hideCursorWhileTyping: false,\n cursorCharacter: '|',\n cursorBlinkDuration: 0.5,\n textColors: () => [],\n startOnVisible: false,\n reverseMode: false\n});\n\nconst displayedText = ref('');\nconst currentCharIndex = ref(0);\nconst isDeleting = ref(false);\nconst currentTextIndex = ref(0);\nconst isVisible = ref(!props.startOnVisible);\nconst cursorRef = useTemplateRef('cursorRef');\nconst containerRef = useTemplateRef('containerRef');\n\nconst textArray = computed(() => (Array.isArray(props.text) ? props.text : [props.text]));\n\nconst getRandomSpeed = () => {\n if (!props.variableSpeed) return props.typingSpeed;\n const { min, max } = props.variableSpeed;\n return Math.random() * (max - min) + min;\n};\n\nconst getCurrentTextColor = () => {\n if (!props.textColors.length) return '#ffffff';\n return props.textColors[currentTextIndex.value % props.textColors.length];\n};\n\nlet timeout: ReturnType<typeof setTimeout> | null = null;\n\nconst clearTimeoutIfNeeded = () => {\n if (timeout) clearTimeout(timeout);\n};\n\nconst executeTypingAnimation = () => {\n const currentText = textArray.value[currentTextIndex.value];\n const processedText = props.reverseMode ? currentText.split('').reverse().join('') : currentText;\n\n if (isDeleting.value) {\n if (displayedText.value === '') {\n isDeleting.value = false;\n if (currentTextIndex.value === textArray.value.length - 1 && !props.loop) return;\n\n props.onSentenceComplete?.(textArray.value[currentTextIndex.value], currentTextIndex.value);\n\n currentTextIndex.value = (currentTextIndex.value + 1) % textArray.value.length;\n currentCharIndex.value = 0;\n timeout = setTimeout(() => {}, props.pauseDuration);\n } else {\n timeout = setTimeout(() => {\n displayedText.value = displayedText.value.slice(0, -1);\n }, props.deletingSpeed);\n }\n } else {\n if (currentCharIndex.value < processedText.length) {\n timeout = setTimeout(\n () => {\n displayedText.value += processedText[currentCharIndex.value];\n currentCharIndex.value += 1;\n },\n props.variableSpeed ? getRandomSpeed() : props.typingSpeed\n );\n } else if (textArray.value.length > 1) {\n timeout = setTimeout(() => {\n isDeleting.value = true;\n }, props.pauseDuration);\n }\n }\n};\n\nwatch(\n [displayedText, currentCharIndex, isDeleting, isVisible],\n () => {\n if (!isVisible.value) return;\n clearTimeoutIfNeeded();\n\n if (currentCharIndex.value === 0 && !isDeleting.value && displayedText.value === '') {\n timeout = setTimeout(() => {\n executeTypingAnimation();\n }, props.initialDelay);\n } else {\n executeTypingAnimation();\n }\n },\n { immediate: true }\n);\n\nonMounted(() => {\n if (props.showCursor && cursorRef.value) {\n gsap.set(cursorRef.value, { opacity: 1 });\n gsap.to(cursorRef.value, {\n opacity: 0,\n duration: props.cursorBlinkDuration,\n repeat: -1,\n yoyo: true,\n ease: 'power2.inOut'\n });\n }\n\n if (props.startOnVisible && containerRef.value) {\n const observer = new IntersectionObserver(\n entries => {\n entries.forEach(entry => {\n if (entry.isIntersecting) isVisible.value = true;\n });\n },\n { threshold: 0.1 }\n );\n if (containerRef.value instanceof Element) {\n observer.observe(containerRef.value);\n }\n onBeforeUnmount(() => observer.disconnect());\n }\n});\n\nonBeforeUnmount(() => {\n clearTimeoutIfNeeded();\n});\n</script>\n\n<template>\n <component\n :is=\"as\"\n ref=\"containerRef\"\n :class=\"`inline-block whitespace-pre-wrap tracking-tight ${className}`\"\n v-bind=\"$attrs\"\n >\n <span class=\"inline\" :style=\"{ color: getCurrentTextColor() }\">\n {{ displayedText }}\n </span>\n <span\n v-if=\"showCursor\"\n ref=\"cursorRef\"\n :class=\"`ml-1 inline-block opacity-100 ${\n hideCursorWhileTyping && (currentCharIndex < textArray[currentTextIndex].length || isDeleting) ? 'hidden' : ''\n } ${cursorClassName}`\"\n >\n {{ cursorCharacter }}\n </span>\n </component>\n</template>\n","path":"TextType/TextType.vue","_imports_":[],"registryDependencies":[],"dependencies":[],"devDependencies":[]}],"registryDependencies":[],"dependencies":[{"ecosystem":"js","name":"gsap","version":"^3.13.0"}],"devDependencies":[],"categories":["TextAnimations"]} |