mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-03-07 14:39:30 -07:00
1 line
4.8 KiB
JSON
1 line
4.8 KiB
JSON
{"name":"Counter","title":"Counter","description":"Flexible animated counter supporting increments + easing.","type":"registry:component","add":"when-added","files":[{"type":"registry:component","role":"file","content":"<template>\n <div class=\"relative inline-block\" :style=\"containerStyle\">\n <div class=\"flex overflow-hidden\" :style=\"counterStyles\">\n <div v-for=\"place in places\" :key=\"place\" class=\"relative w-[1ch] tabular-nums\" :style=\"digitStyles\">\n <Motion\n v-for=\"digit in 10\"\n :key=\"digit - 1\"\n tag=\"span\"\n class=\"absolute top-0 left-0 w-full h-full flex items-center justify-center\"\n :animate=\"{ y: getDigitPosition(place, digit - 1) }\"\n >\n {{ digit - 1 }}\n </Motion>\n </div>\n </div>\n <div class=\"pointer-events-none absolute inset-0\">\n <div class=\"absolute top-0 w-full\" :style=\"topGradientStyle ?? topGradientStyles\" />\n <div class=\"absolute bottom-0 w-full\" :style=\"bottomGradientStyle ?? bottomGradientStyles\" />\n </div>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, ref, watch } from 'vue';\nimport { Motion } from 'motion-v';\nimport type { CSSProperties } from 'vue';\n\ninterface CounterProps {\n value: number;\n fontSize?: number;\n padding?: number;\n places?: number[];\n gap?: number;\n borderRadius?: number;\n horizontalPadding?: number;\n textColor?: string;\n fontWeight?: string | number;\n containerStyle?: CSSProperties;\n counterStyle?: CSSProperties;\n digitStyle?: CSSProperties;\n gradientHeight?: number;\n gradientFrom?: string;\n gradientTo?: string;\n topGradientStyle?: CSSProperties;\n bottomGradientStyle?: CSSProperties;\n}\n\nconst props = withDefaults(defineProps<CounterProps>(), {\n fontSize: 100,\n padding: 0,\n places: () => [100, 10, 1],\n gap: 8,\n borderRadius: 4,\n horizontalPadding: 8,\n textColor: 'white',\n fontWeight: 'bold',\n containerStyle: () => ({}),\n counterStyle: () => ({}),\n digitStyle: () => ({}),\n gradientHeight: 16,\n gradientFrom: 'black',\n gradientTo: 'transparent',\n topGradientStyle: undefined,\n bottomGradientStyle: undefined\n});\n\nconst digitHeight = computed(() => props.fontSize + props.padding);\n\nconst counterStyles = computed(() => ({\n fontSize: `${props.fontSize}px`,\n gap: `${props.gap}px`,\n borderRadius: `${props.borderRadius}px`,\n paddingLeft: `${props.horizontalPadding}px`,\n paddingRight: `${props.horizontalPadding}px`,\n color: props.textColor,\n fontWeight: props.fontWeight,\n ...props.counterStyle\n}));\n\nconst digitStyles = computed(() => ({\n height: `${digitHeight.value}px`,\n ...props.digitStyle\n}));\n\nconst topGradientStyles = computed(\n (): CSSProperties => ({\n height: `${props.gradientHeight}px`,\n background: `linear-gradient(to bottom, ${props.gradientFrom}, ${props.gradientTo})`\n })\n);\n\nconst bottomGradientStyles = computed(\n (): CSSProperties => ({\n height: `${props.gradientHeight}px`,\n background: `linear-gradient(to top, ${props.gradientFrom}, ${props.gradientTo})`\n })\n);\n\nconst springValues = ref<Record<number, number>>({});\n\nconst initializeSpringValues = () => {\n props.places.forEach(place => {\n springValues.value[place] = Math.floor(props.value / place);\n });\n};\n\ninitializeSpringValues();\n\nwatch(\n () => props.value,\n (newValue, oldValue) => {\n if (newValue === oldValue) return;\n\n props.places.forEach(place => {\n const newRoundedValue = Math.floor(newValue / place);\n const oldRoundedValue = springValues.value[place];\n\n if (newRoundedValue !== oldRoundedValue) {\n springValues.value[place] = newRoundedValue;\n }\n });\n },\n { immediate: true }\n);\n\nwatch(\n () => digitHeight.value,\n () => {\n positionCache.clear();\n }\n);\n\nconst positionCache = new Map<string, number>();\n\nconst getDigitPosition = (place: number, digit: number): number => {\n const springValue = springValues.value[place] || 0;\n const cacheKey = `${place}-${digit}-${springValue}`;\n\n if (positionCache.has(cacheKey)) {\n return positionCache.get(cacheKey)!;\n }\n\n const placeValue = springValue % 10;\n const offset = (10 + digit - placeValue) % 10;\n let position = offset * digitHeight.value;\n\n if (offset > 5) {\n position -= 10 * digitHeight.value;\n }\n\n if (positionCache.size > 200) {\n const firstKey = positionCache.keys().next().value;\n if (typeof firstKey === 'string') {\n positionCache.delete(firstKey);\n }\n }\n\n positionCache.set(cacheKey, position);\n return position;\n};\n</script>\n","path":"Counter/Counter.vue","_imports_":[],"registryDependencies":[],"dependencies":[],"devDependencies":[]}],"registryDependencies":[],"dependencies":[{"ecosystem":"js","name":"motion-v","version":"^1.5.0"}],"devDependencies":[],"categories":["Components"]} |