Files
vue-bits/public/r/CountUp.json
2026-01-21 16:08:55 +05:30

1 line
4.3 KiB
JSON

{"name":"CountUp","title":"CountUp","description":"Animated number counter supporting formatting and decimals.","type":"registry:component","add":"when-added","files":[{"type":"registry:component","role":"file","content":"<template>\n <span ref=\"elementRef\" :class=\"className\" />\n</template>\n\n<script setup lang=\"ts\">\nimport { ref, onMounted, onUnmounted, watch, computed, useTemplateRef } from 'vue';\n\ninterface Props {\n to: number;\n from?: number;\n direction?: 'up' | 'down';\n delay?: number;\n duration?: number;\n className?: string;\n startWhen?: boolean;\n separator?: string;\n onStart?: () => void;\n onEnd?: () => void;\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n from: 0,\n direction: 'up',\n delay: 0,\n duration: 2,\n className: '',\n startWhen: true,\n separator: ''\n});\n\nconst elementRef = useTemplateRef<HTMLSpanElement>('elementRef');\nconst currentValue = ref(props.direction === 'down' ? props.to : props.from);\nconst isInView = ref(false);\nconst animationId = ref<number | null>(null);\nconst hasStarted = ref(false);\n\nlet intersectionObserver: IntersectionObserver | null = null;\n\nconst damping = computed(() => 20 + 40 * (1 / props.duration));\nconst stiffness = computed(() => 100 * (1 / props.duration));\n\nlet velocity = 0;\nlet startTime = 0;\n\nconst formatNumber = (value: number) => {\n const options = {\n useGrouping: !!props.separator,\n minimumFractionDigits: 0,\n maximumFractionDigits: 0\n };\n\n const formattedNumber = Intl.NumberFormat('en-US', options).format(Number(value.toFixed(0)));\n\n return props.separator ? formattedNumber.replace(/,/g, props.separator) : formattedNumber;\n};\n\nconst updateDisplay = () => {\n if (elementRef.value) {\n elementRef.value.textContent = formatNumber(currentValue.value);\n }\n};\n\nconst springAnimation = (timestamp: number) => {\n if (!startTime) startTime = timestamp;\n\n const target = props.direction === 'down' ? props.from : props.to;\n const current = currentValue.value;\n\n const displacement = target - current;\n const springForce = displacement * stiffness.value;\n const dampingForce = velocity * damping.value;\n const acceleration = springForce - dampingForce;\n\n velocity += acceleration * 0.016; // Assuming 60fps\n currentValue.value += velocity * 0.016;\n\n updateDisplay();\n\n if (Math.abs(displacement) > 0.01 || Math.abs(velocity) > 0.01) {\n animationId.value = requestAnimationFrame(springAnimation);\n } else {\n currentValue.value = target;\n updateDisplay();\n animationId.value = null;\n\n if (props.onEnd) {\n props.onEnd();\n }\n }\n};\n\nconst startAnimation = () => {\n if (hasStarted.value || !isInView.value || !props.startWhen) return;\n\n hasStarted.value = true;\n\n if (props.onStart) {\n props.onStart();\n }\n\n setTimeout(() => {\n startTime = 0;\n velocity = 0;\n animationId.value = requestAnimationFrame(springAnimation);\n }, props.delay * 1000);\n};\n\nconst setupIntersectionObserver = () => {\n if (!elementRef.value) return;\n\n intersectionObserver = new IntersectionObserver(\n ([entry]) => {\n if (entry.isIntersecting && !isInView.value) {\n isInView.value = true;\n startAnimation();\n }\n },\n {\n threshold: 0,\n rootMargin: '0px'\n }\n );\n\n intersectionObserver.observe(elementRef.value);\n};\n\nconst cleanup = () => {\n if (animationId.value) {\n cancelAnimationFrame(animationId.value);\n animationId.value = null;\n }\n\n if (intersectionObserver) {\n intersectionObserver.disconnect();\n intersectionObserver = null;\n }\n};\n\nwatch(\n [() => props.from, () => props.to, () => props.direction],\n () => {\n currentValue.value = props.direction === 'down' ? props.to : props.from;\n updateDisplay();\n hasStarted.value = false;\n },\n { immediate: true }\n);\n\nwatch(\n () => props.startWhen,\n () => {\n if (props.startWhen && isInView.value && !hasStarted.value) {\n startAnimation();\n }\n }\n);\n\nonMounted(() => {\n updateDisplay();\n setupIntersectionObserver();\n});\n\nonUnmounted(() => {\n cleanup();\n});\n</script>\n","path":"CountUp/CountUp.vue","_imports_":[],"registryDependencies":[],"dependencies":[],"devDependencies":[]}],"registryDependencies":[],"dependencies":[],"devDependencies":[],"categories":["TextAnimations"]}