mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-03-07 06:29:30 -07:00
1 line
5.7 KiB
JSON
1 line
5.7 KiB
JSON
{"name":"TrueFocus","title":"TrueFocus","description":"Applies dynamic blur / clarity based over a series of words in order.","type":"registry:component","add":"when-added","files":[{"type":"registry:component","role":"file","content":"<script setup lang=\"ts\">\nimport { motion } from 'motion-v';\nimport { computed, nextTick, onMounted, onUnmounted, ref, watch, useTemplateRef } from 'vue';\n\ninterface TrueFocusProps {\n sentence?: string;\n manualMode?: boolean;\n blurAmount?: number;\n borderColor?: string;\n glowColor?: string;\n animationDuration?: number;\n pauseBetweenAnimations?: number;\n}\n\nconst props = withDefaults(defineProps<TrueFocusProps>(), {\n sentence: 'True Focus',\n manualMode: false,\n blurAmount: 5,\n borderColor: 'green',\n glowColor: 'rgba(0, 255, 0, 0.6)',\n animationDuration: 0.5,\n pauseBetweenAnimations: 1\n});\n\nconst words = computed(() => props.sentence.split(' '));\nconst currentIndex = ref(0);\nconst lastActiveIndex = ref<number | null>(null);\nconst containerRef = useTemplateRef<HTMLDivElement>('containerRef');\nconst wordRefs = ref<HTMLSpanElement[]>([]);\nconst focusRect = ref({ x: 0, y: 0, width: 0, height: 0 });\n\nlet interval: ReturnType<typeof setInterval> | null = null;\n\nwatch(\n [currentIndex, () => words.value.length],\n async () => {\n if (currentIndex.value === null || currentIndex.value === -1) return;\n if (!wordRefs.value[currentIndex.value] || !containerRef.value) return;\n\n await nextTick();\n\n const parentRect = containerRef.value.getBoundingClientRect();\n const activeRect = wordRefs.value[currentIndex.value].getBoundingClientRect();\n\n focusRect.value = {\n x: activeRect.left - parentRect.left,\n y: activeRect.top - parentRect.top,\n width: activeRect.width,\n height: activeRect.height\n };\n },\n { immediate: true }\n);\n\nconst handleMouseEnter = (index: number) => {\n if (props.manualMode) {\n lastActiveIndex.value = index;\n currentIndex.value = index;\n }\n};\n\nconst handleMouseLeave = () => {\n if (props.manualMode) {\n currentIndex.value = lastActiveIndex.value || 0;\n }\n};\n\nconst setWordRef = (el: HTMLSpanElement | null, index: number) => {\n if (el) {\n wordRefs.value[index] = el;\n }\n};\n\nonMounted(async () => {\n await nextTick();\n\n if (wordRefs.value[0] && containerRef.value) {\n const parentRect = containerRef.value.getBoundingClientRect();\n const activeRect = wordRefs.value[0].getBoundingClientRect();\n\n focusRect.value = {\n x: activeRect.left - parentRect.left,\n y: activeRect.top - parentRect.top,\n width: activeRect.width,\n height: activeRect.height\n };\n }\n\n watch(\n [() => props.manualMode, () => props.animationDuration, () => props.pauseBetweenAnimations, () => words.value],\n () => {\n if (interval) {\n clearInterval(interval);\n interval = null;\n }\n\n if (!props.manualMode) {\n interval = setInterval(\n () => {\n currentIndex.value = (currentIndex.value + 1) % words.value.length;\n },\n (props.animationDuration + props.pauseBetweenAnimations) * 1000\n );\n }\n },\n { immediate: true }\n );\n});\n\nonUnmounted(() => {\n if (interval) {\n clearInterval(interval);\n }\n});\n</script>\n\n<template>\n <div class=\"relative flex flex-wrap justify-center items-center gap-[1em]\" ref=\"containerRef\">\n <span\n v-for=\"(word, index) in words\"\n :key=\"index\"\n :ref=\"el => setWordRef(el as HTMLSpanElement, index)\"\n class=\"relative font-black text-7xl transition-[filter,color] duration-300 ease-in-out cursor-pointer\"\n :style=\"{\n filter: index === currentIndex ? 'blur(0px)' : `blur(${blurAmount}px)`,\n '--border-color': borderColor,\n '--glow-color': glowColor,\n transition: `filter ${animationDuration}s ease`\n }\"\n @mouseenter=\"handleMouseEnter(index)\"\n @mouseleave=\"handleMouseLeave\"\n >\n {{ word }}\n </span>\n\n <motion.div\n class=\"top-0 left-0 box-content absolute border-none pointer-events-none\"\n :animate=\"{\n x: focusRect.x,\n y: focusRect.y,\n width: focusRect.width,\n height: focusRect.height,\n opacity: currentIndex >= 0 ? 1 : 0\n }\"\n :transition=\"{\n duration: animationDuration\n }\"\n :style=\"{\n '--border-color': borderColor,\n '--glow-color': glowColor\n }\"\n >\n <spanx\n class=\"top-[-10px] left-[-10px] absolute filter-[drop-shadow(0_0_4px_var(--border-color,#fff))] border-[3px] border-(--border-color,#fff) border-r-0 border-b-0 rounded-[3px] w-4 h-4 transition-none\"\n ></spanx>\n\n <span\n class=\"top-[-10px] right-[-10px] absolute filter-[drop-shadow(0_0_4px_var(--border-color,#fff))] border-[3px] border-(--border-color,#fff) border-b-0 border-l-0 rounded-[3px] w-4 h-4 transition-none\"\n ></span>\n\n <span\n class=\"bottom-[-10px] left-[-10px] absolute filter-[drop-shadow(0_0_4px_var(--border-color,#fff))] border-[3px] border-(--border-color,#fff) border-t-0 border-r-0 rounded-[3px] w-4 h-4 transition-none\"\n ></span>\n\n <span\n class=\"right-[-10px] bottom-[-10px] absolute filter-[drop-shadow(0_0_4px_var(--border-color,#fff))] border-[3px] border-(--border-color,#fff) border-t-0 border-l-0 rounded-[3px] w-4 h-4 transition-none\"\n ></span>\n </motion.div>\n </div>\n</template>\n","path":"TrueFocus/TrueFocus.vue","_imports_":[],"registryDependencies":[],"dependencies":[],"devDependencies":[]}],"registryDependencies":[],"dependencies":[{"ecosystem":"js","name":"motion-v","version":"^1.5.0"}],"devDependencies":[],"categories":["TextAnimations"]} |