kmcom-nuxt-layers 1.6.39 → 1.6.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/layers/core/app/composables/useErrorLog.ts +6 -11
  2. package/layers/core/app/composables/useLoading.ts +15 -46
  3. package/layers/core/app/composables/useScrollGuard.ts +115 -129
  4. package/layers/forms/app/components/Form/Field.vue +2 -2
  5. package/layers/forms/app/composables/useFormSchema.ts +1 -3
  6. package/layers/forms/app/config/fields.ts +12 -6
  7. package/layers/forms/app/types/fields.ts +2 -20
  8. package/layers/forms/nuxt.config.ts +0 -10
  9. package/layers/motion/app/components/Motion/CountUp.vue +39 -0
  10. package/layers/motion/app/components/Motion/Cursor.vue +101 -0
  11. package/layers/motion/app/components/Motion/Magnetic.vue +22 -0
  12. package/layers/motion/app/components/Motion/Marquee.vue +130 -130
  13. package/layers/motion/app/components/Motion/MarqueeText.vue +147 -0
  14. package/layers/motion/app/components/Motion/Tilt.vue +22 -0
  15. package/layers/motion/app/components/Motion/VelocityEffect.vue +33 -71
  16. package/layers/motion/app/composables/useCountUp.ts +61 -0
  17. package/layers/motion/app/composables/useCursorFollower.ts +25 -0
  18. package/layers/motion/app/composables/useMagneticElement.ts +56 -0
  19. package/layers/motion/app/composables/useMarqueeCopies.ts +50 -0
  20. package/layers/motion/app/composables/useMarqueeVelocity.ts +44 -0
  21. package/layers/motion/app/composables/useSmoothScroll.ts +11 -0
  22. package/layers/motion/app/composables/useTiltEffect.ts +58 -0
  23. package/layers/motion/app/plugins/locomotive-scroll.client.ts +1 -1
  24. package/layers/motion/app/types/app-config.d.ts +11 -2
  25. package/layers/motion/tsconfig.json +1 -0
  26. package/layers/routing/app/middleware/02.governance.global.ts +7 -18
  27. package/layers/routing/app/types/routing.ts +10 -0
  28. package/layers/routing/app/utils/resolveRoute.ts +31 -0
  29. package/layers/shader/package.json +2 -1
  30. package/layers/theme/app/composables/useAccentColor.ts +1 -12
  31. package/layers/theme/app/composables/useThemeContrast.ts +0 -11
  32. package/layers/theme/app/composables/useThemeMotion.ts +0 -11
  33. package/layers/theme/app/composables/useThemeTransparency.ts +0 -14
  34. package/layers/theme/app/plugins/theme.client.ts +20 -3
  35. package/layers/theme/app/types/app-config.d.ts +2 -2
  36. package/layers/ui/app/components/Base/Modal.vue +0 -1
  37. package/layers/ui/app/composables/color.ts +1 -32
  38. package/layers/ui/app/utils/createModal.ts +11 -2
  39. package/package.json +28 -26
  40. package/layers/content/app/.DS_Store +0 -0
  41. package/layers/content/app/pages/blog/[slug].vue +0 -19
  42. package/layers/content/app/pages/blog/index.vue +0 -22
  43. package/layers/core/app/.DS_Store +0 -0
  44. package/layers/core/app/assets/.DS_Store +0 -0
  45. package/layers/forms/app/.DS_Store +0 -0
  46. package/layers/layout/app/.DS_Store +0 -0
  47. package/layers/motion/app/.DS_Store +0 -0
  48. package/layers/shader/app/.DS_Store +0 -0
  49. package/layers/theme/app/.DS_Store +0 -0
  50. package/layers/ui/app/.DS_Store +0 -0
@@ -0,0 +1,22 @@
1
+ <script setup lang="ts">
2
+ const {
3
+ strength = 0.3,
4
+ radius = 100,
5
+ damping = 30,
6
+ stiffness = 300,
7
+ } = defineProps<{
8
+ strength?: number
9
+ radius?: number
10
+ damping?: number
11
+ stiffness?: number
12
+ }>()
13
+
14
+ const el = ref<HTMLElement | null>(null)
15
+ const { isActive } = useMagneticElement(el, { strength, radius, damping, stiffness })
16
+ </script>
17
+
18
+ <template>
19
+ <div ref="el" class="will-change-transform">
20
+ <slot :is-active="isActive" />
21
+ </div>
22
+ </template>
@@ -1,142 +1,142 @@
1
1
  <script setup lang="ts">
2
- const props = withDefaults(
3
- defineProps<{
4
- /**
5
- * Base animation speed in pixels per second
6
- */
7
- speed?: number
8
- /**
9
- * Direction of scroll
10
- */
11
- direction?: 'left' | 'right'
12
- /**
13
- * Pause animation on hover
14
- */
15
- pauseOnHover?: boolean
16
- /**
17
- * Gap between repeated items
18
- */
19
- gap?: string
20
- /**
21
- * Enable velocity-based speed (scroll velocity affects marquee speed)
22
- */
23
- velocityBased?: boolean
24
- /**
25
- * How much scroll velocity affects speed (0-1)
26
- * 0 = no effect, 1 = velocity fully controls speed
27
- */
28
- velocitySensitivity?: number
29
- /**
30
- * Reverse direction based on scroll direction
31
- */
32
- velocityDirection?: boolean
33
- /**
34
- * Minimum speed multiplier when using velocity
35
- */
36
- minSpeed?: number
37
- /**
38
- * Maximum speed multiplier when using velocity
39
- */
40
- maxSpeed?: number
41
- }>(),
42
- {
43
- speed: 50,
44
- direction: 'left',
45
- pauseOnHover: true,
46
- gap: '2rem',
47
- velocityBased: false,
48
- velocitySensitivity: 0.5,
49
- velocityDirection: false,
50
- minSpeed: 0.2,
51
- maxSpeed: 5,
52
- }
53
- )
54
-
55
- const { gsap } = useGsap()
56
- const { velocity: scrollVelocity, direction: scrollDirection } = useSmoothScroll()
57
-
58
- const containerRef = ref<HTMLElement | null>(null)
59
- const contentRef = ref<HTMLElement | null>(null)
60
- const tweenRef = ref<gsap.core.Tween | null>(null)
61
-
62
- const isPaused = ref(false)
63
- const currentTimeScale = ref(1)
64
-
65
- onMounted(() => {
66
- if (!containerRef.value || !contentRef.value) return
67
-
68
- const content = contentRef.value
69
- const contentWidth = content.offsetWidth
70
-
71
- // Set up the infinite scroll animation
72
- tweenRef.value = gsap.to(content, {
73
- x: props.direction === 'left' ? -contentWidth / 2 : contentWidth / 2,
74
- duration: contentWidth / props.speed,
75
- ease: 'none',
76
- repeat: -1,
77
- modifiers: {
78
- x: gsap.utils.unitize((x) => {
79
- const mod = contentWidth / 2
80
- return props.direction === 'left' ? parseFloat(x) % mod : Math.abs(parseFloat(x) % mod)
81
- }),
82
- },
2
+ const props = withDefaults(
3
+ defineProps<{
4
+ /**
5
+ * Base animation speed in pixels per second
6
+ */
7
+ speed?: number
8
+ /**
9
+ * Direction of scroll
10
+ */
11
+ direction?: 'left' | 'right'
12
+ /**
13
+ * Pause animation on hover
14
+ */
15
+ pauseOnHover?: boolean
16
+ /**
17
+ * Gap between repeated items
18
+ */
19
+ gap?: string
20
+ /**
21
+ * Enable velocity-based speed (scroll velocity affects marquee speed)
22
+ */
23
+ velocityBased?: boolean
24
+ /**
25
+ * How much scroll velocity affects speed (0-1)
26
+ * 0 = no effect, 1 = velocity fully controls speed
27
+ */
28
+ velocitySensitivity?: number
29
+ /**
30
+ * Reverse direction based on scroll direction
31
+ */
32
+ velocityDirection?: boolean
33
+ /**
34
+ * Minimum speed multiplier when using velocity
35
+ */
36
+ minSpeed?: number
37
+ /**
38
+ * Maximum speed multiplier when using velocity
39
+ */
40
+ maxSpeed?: number
41
+ }>(),
42
+ {
43
+ speed: 50,
44
+ direction: 'left',
45
+ pauseOnHover: true,
46
+ gap: '2rem',
47
+ velocityBased: false,
48
+ velocitySensitivity: 0.5,
49
+ velocityDirection: false,
50
+ minSpeed: 0.2,
51
+ maxSpeed: 5,
52
+ }
53
+ )
54
+
55
+ const { gsap } = useGsap()
56
+ const { velocity: scrollVelocity, direction: scrollDirection } = useSmoothScroll()
57
+
58
+ const containerRef = ref<HTMLElement | null>(null)
59
+ const contentRef = ref<HTMLElement | null>(null)
60
+ const tweenRef = ref<gsap.core.Tween | null>(null)
61
+
62
+ const isPaused = ref(false)
63
+ const currentTimeScale = ref(1)
64
+
65
+ onMounted(() => {
66
+ if (!containerRef.value || !contentRef.value) return
67
+
68
+ const content = contentRef.value
69
+ const contentWidth = content.offsetWidth
70
+
71
+ // Set up the infinite scroll animation
72
+ tweenRef.value = gsap.to(content, {
73
+ x: props.direction === 'left' ? -contentWidth / 2 : contentWidth / 2,
74
+ duration: contentWidth / props.speed,
75
+ ease: 'none',
76
+ repeat: -1,
77
+ modifiers: {
78
+ x: gsap.utils.unitize((x) => {
79
+ const mod = contentWidth / 2
80
+ return props.direction === 'left' ? parseFloat(x) % mod : Math.abs(parseFloat(x) % mod)
81
+ }),
82
+ },
83
+ })
83
84
  })
84
- })
85
-
86
- // Watch velocity changes for velocity-based animation
87
- watch(
88
- [scrollVelocity, scrollDirection],
89
- ([vel, dir]) => {
90
- if (!props.velocityBased || !tweenRef.value || isPaused.value) return
91
-
92
- // Calculate velocity multiplier
93
- const absVelocity = Math.abs(vel)
94
- const velocityEffect = absVelocity * props.velocitySensitivity * 0.02
95
85
 
96
- // Base multiplier from velocity magnitude
97
- let multiplier = 1 + velocityEffect
86
+ // Watch velocity changes for velocity-based animation
87
+ watch(
88
+ [scrollVelocity, scrollDirection],
89
+ ([vel, dir]) => {
90
+ if (!props.velocityBased || !tweenRef.value || isPaused.value) return
91
+
92
+ // Calculate velocity multiplier
93
+ const absVelocity = Math.abs(vel)
94
+ const velocityEffect = absVelocity * props.velocitySensitivity * 0.02
95
+
96
+ // Base multiplier from velocity magnitude
97
+ let multiplier = 1 + velocityEffect
98
+
99
+ // Clamp to min/max
100
+ multiplier = Math.max(props.minSpeed, Math.min(props.maxSpeed, multiplier))
101
+
102
+ // Optionally reverse direction based on scroll direction
103
+ if (props.velocityDirection && dir !== 0) {
104
+ const baseDirection = props.direction === 'left' ? 1 : -1
105
+ multiplier *= dir * baseDirection
106
+ }
107
+
108
+ // Smoothly interpolate to target
109
+ gsap.to(currentTimeScale, {
110
+ value: multiplier,
111
+ duration: 0.3,
112
+ ease: 'power2.out',
113
+ onUpdate: () => {
114
+ if (tweenRef.value) {
115
+ tweenRef.value.timeScale(currentTimeScale.value)
116
+ }
117
+ },
118
+ })
119
+ },
120
+ { immediate: false }
121
+ )
98
122
 
99
- // Clamp to min/max
100
- multiplier = Math.max(props.minSpeed, Math.min(props.maxSpeed, multiplier))
123
+ onUnmounted(() => {
124
+ tweenRef.value?.kill()
125
+ })
101
126
 
102
- // Optionally reverse direction based on scroll direction
103
- if (props.velocityDirection && dir !== 0) {
104
- const baseDirection = props.direction === 'left' ? 1 : -1
105
- multiplier *= dir * baseDirection
127
+ function handleMouseEnter() {
128
+ if (props.pauseOnHover && tweenRef.value) {
129
+ gsap.to(tweenRef.value, { timeScale: 0, duration: 0.5 })
130
+ isPaused.value = true
106
131
  }
107
-
108
- // Smoothly interpolate to target
109
- gsap.to(currentTimeScale, {
110
- value: multiplier,
111
- duration: 0.3,
112
- ease: 'power2.out',
113
- onUpdate: () => {
114
- if (tweenRef.value) {
115
- tweenRef.value.timeScale(currentTimeScale.value)
116
- }
117
- },
118
- })
119
- },
120
- { immediate: false }
121
- )
122
-
123
- onUnmounted(() => {
124
- tweenRef.value?.kill()
125
- })
126
-
127
- function handleMouseEnter() {
128
- if (props.pauseOnHover && tweenRef.value) {
129
- gsap.to(tweenRef.value, { timeScale: 0, duration: 0.5 })
130
- isPaused.value = true
131
132
  }
132
- }
133
133
 
134
- function handleMouseLeave() {
135
- if (props.pauseOnHover && tweenRef.value) {
136
- gsap.to(tweenRef.value, { timeScale: currentTimeScale.value, duration: 0.5 })
137
- isPaused.value = false
134
+ function handleMouseLeave() {
135
+ if (props.pauseOnHover && tweenRef.value) {
136
+ gsap.to(tweenRef.value, { timeScale: currentTimeScale.value, duration: 0.5 })
137
+ isPaused.value = false
138
+ }
138
139
  }
139
- }
140
140
  </script>
141
141
 
142
142
  <template>
@@ -0,0 +1,147 @@
1
+ <script setup lang="ts">
2
+ import type { ScrollTrigger as GSAPScrollTrigger } from 'gsap/ScrollTrigger'
3
+
4
+ interface VelocityMapping {
5
+ input: [number, number]
6
+ output: [number, number]
7
+ }
8
+
9
+ const {
10
+ texts = [],
11
+ velocity = 100,
12
+ className = '',
13
+ damping = 50,
14
+ stiffness = 400,
15
+ velocityMapping = { input: [0, 1000] as [number, number], output: [0, 5] as [number, number] },
16
+ pauseOnHover = false,
17
+ parallaxClassName = '',
18
+ scrollerClassName = '',
19
+ parallaxStyle = {},
20
+ scrollerStyle = {},
21
+ } = defineProps<{
22
+ texts?: string[]
23
+ velocity?: number
24
+ className?: string
25
+ damping?: number
26
+ stiffness?: number
27
+ velocityMapping?: VelocityMapping
28
+ pauseOnHover?: boolean
29
+ parallaxClassName?: string
30
+ scrollerClassName?: string
31
+ parallaxStyle?: Record<string, string | number>
32
+ scrollerStyle?: Record<string, string | number>
33
+ }>()
34
+
35
+ const { gsap, ScrollTrigger } = useGsap()
36
+ const { velocityFactor } = useMarqueeVelocity({ damping, stiffness, velocityMapping })
37
+
38
+ const containerRef = ref<HTMLElement[]>([])
39
+ const copyRefs = ref<HTMLSpanElement[]>([])
40
+
41
+ const { copyWidths, calculatedCopies } = useMarqueeCopies(
42
+ containerRef,
43
+ copyRefs,
44
+ computed(() => texts.length)
45
+ )
46
+
47
+ const baseX = ref<number[]>([])
48
+ const directionFactors = ref<number[]>([])
49
+ const isPaused = ref(false)
50
+
51
+ let scrollTriggerInstance: GSAPScrollTrigger | null = null
52
+ let animLastTime = 0
53
+
54
+ const wrap = (min: number, max: number, v: number): number => {
55
+ const range = max - min
56
+ if (range === 0) return min
57
+ return ((((v - min) % range) + range) % range) + min
58
+ }
59
+
60
+ const scrollTransforms = computed(() =>
61
+ texts.map((_, index) => {
62
+ const singleWidth = copyWidths.value[index]
63
+ if (!singleWidth) return '0px'
64
+ return `${wrap(-singleWidth, 0, baseX.value[index] ?? 0)}px`
65
+ })
66
+ )
67
+
68
+ const setCopyRef = (el: Element | ComponentPublicInstance | null, index: number) => {
69
+ if (el instanceof HTMLSpanElement) copyRefs.value[index] = el
70
+ }
71
+
72
+ function animate() {
73
+ if (isPaused.value) return
74
+
75
+ const now = performance.now()
76
+ const delta = animLastTime ? now - animLastTime : 16
77
+ animLastTime = now
78
+
79
+ texts.forEach((_, index) => {
80
+ const baseVelocity = index % 2 !== 0 ? -velocity : velocity
81
+ let moveBy = (directionFactors.value[index] ?? 1) * baseVelocity * (delta / 1000)
82
+
83
+ if (velocityFactor.value < 0) {
84
+ directionFactors.value[index] = -1
85
+ } else if (velocityFactor.value > 0) {
86
+ directionFactors.value[index] = 1
87
+ }
88
+
89
+ moveBy += (directionFactors.value[index] ?? 1) * moveBy * velocityFactor.value
90
+ baseX.value[index] = (baseX.value[index] ?? 0) + moveBy
91
+ })
92
+ }
93
+
94
+ onMounted(async () => {
95
+ await nextTick()
96
+
97
+ baseX.value = new Array(texts.length).fill(0)
98
+ directionFactors.value = new Array(texts.length).fill(1)
99
+ animLastTime = performance.now()
100
+
101
+ const firstContainer = containerRef.value[0]
102
+ if (firstContainer) {
103
+ scrollTriggerInstance = ScrollTrigger.create({
104
+ trigger: firstContainer,
105
+ start: 'top bottom',
106
+ end: 'bottom top',
107
+ })
108
+ }
109
+
110
+ gsap.ticker.add(animate)
111
+ })
112
+
113
+ onUnmounted(() => {
114
+ gsap.ticker.remove(animate)
115
+ scrollTriggerInstance?.kill()
116
+ })
117
+ </script>
118
+
119
+ <template>
120
+ <section
121
+ class="w-full overflow-x-hidden"
122
+ @mouseenter="isPaused = pauseOnHover"
123
+ @mouseleave="isPaused = false"
124
+ >
125
+ <div
126
+ v-for="(text, index) in texts"
127
+ :key="index"
128
+ ref="containerRef"
129
+ :class="`${parallaxClassName} relative w-full overflow-x-hidden overflow-y-visible`"
130
+ :style="parallaxStyle"
131
+ >
132
+ <div
133
+ :class="`${scrollerClassName} font-styles-logo flex py-2 text-center font-sans text-4xl font-medium tracking-[-0.02em] whitespace-nowrap text-neutral drop-shadow md:text-[5rem] md:leading-[5rem]`"
134
+ :style="{ transform: `translateX(${scrollTransforms[index] ?? '0px'})`, ...scrollerStyle }"
135
+ >
136
+ <span
137
+ v-for="spanIndex in calculatedCopies[index] ?? 15"
138
+ :key="spanIndex"
139
+ :ref="spanIndex === 1 ? (el) => setCopyRef(el, index) : undefined"
140
+ :class="`shrink-0 ${className} text-primary-500`"
141
+ >
142
+ {{ text }}&nbsp;
143
+ </span>
144
+ </div>
145
+ </div>
146
+ </section>
147
+ </template>
@@ -0,0 +1,22 @@
1
+ <script setup lang="ts">
2
+ const {
3
+ maxTilt = 15,
4
+ perspective = 800,
5
+ damping = 40,
6
+ stiffness = 250,
7
+ } = defineProps<{
8
+ maxTilt?: number
9
+ perspective?: number
10
+ damping?: number
11
+ stiffness?: number
12
+ }>()
13
+
14
+ const el = ref<HTMLElement | null>(null)
15
+ useTiltEffect(el, { maxTilt, perspective, damping, stiffness })
16
+ </script>
17
+
18
+ <template>
19
+ <div ref="el" class="will-change-transform" style="transform-style: preserve-3d">
20
+ <slot />
21
+ </div>
22
+ </template>
@@ -9,78 +9,43 @@ type EffectType =
9
9
  | 'translateY'
10
10
  | 'hueRotate'
11
11
 
12
- const props = withDefaults(
13
- defineProps<{
14
- /**
15
- * Type of velocity effect to apply
16
- */
17
- effect: EffectType
18
- /**
19
- * Intensity multiplier (default varies by effect type)
20
- */
21
- intensity?: number
22
- /**
23
- * Whether to use smoothed velocity (recommended)
24
- */
25
- smooth?: boolean
26
- /**
27
- * Lerp factor for smoothing (0.1 = very smooth, 0.3 = responsive)
28
- */
29
- smoothFactor?: number
30
- /**
31
- * Tag to render as
32
- */
33
- as?: string
34
- }>(),
35
- {
36
- smooth: true,
37
- smoothFactor: 0.15,
38
- as: 'div',
39
- }
40
- )
41
-
12
+ const {
13
+ effect,
14
+ intensity = undefined,
15
+ damping = 15,
16
+ stiffness = 150,
17
+ as = 'div',
18
+ } = defineProps<{
19
+ effect: EffectType
20
+ intensity?: number
21
+ damping?: number
22
+ stiffness?: number
23
+ as?: string
24
+ }>()
25
+
26
+ const { gsap } = useGsap()
42
27
  const { velocity } = useSmoothScroll()
43
28
 
44
- // Smoothed velocity for stable animations
45
29
  const smoothVelocity = ref(0)
46
30
  const smoothAbsVelocity = ref(0)
47
- let animationFrame: number | null = null
48
31
 
49
- function lerp(start: number, end: number, factor: number): number {
50
- return start + (end - start) * factor
51
- }
32
+ function tick() {
33
+ const d = damping / 1000
34
+ const s = stiffness / 1000
52
35
 
53
- function updateSmoothedValues() {
54
- smoothVelocity.value = lerp(smoothVelocity.value, velocity.value, props.smoothFactor)
55
- smoothAbsVelocity.value = lerp(
56
- smoothAbsVelocity.value,
57
- Math.abs(velocity.value),
58
- props.smoothFactor
59
- )
36
+ smoothVelocity.value += (velocity.value - smoothVelocity.value) * s
37
+ smoothVelocity.value *= 1 - d
38
+
39
+ smoothAbsVelocity.value += (Math.abs(velocity.value) - smoothAbsVelocity.value) * s
40
+ smoothAbsVelocity.value *= 1 - d
60
41
 
61
42
  if (Math.abs(smoothVelocity.value) < 0.01) smoothVelocity.value = 0
62
43
  if (smoothAbsVelocity.value < 0.01) smoothAbsVelocity.value = 0
63
-
64
- animationFrame = requestAnimationFrame(updateSmoothedValues)
65
44
  }
66
45
 
67
- onMounted(() => {
68
- if (props.smooth) {
69
- animationFrame = requestAnimationFrame(updateSmoothedValues)
70
- }
71
- })
72
-
73
- onUnmounted(() => {
74
- if (animationFrame) cancelAnimationFrame(animationFrame)
75
- })
76
-
77
- // Get the active velocity value
78
- const activeVelocity = computed(() => (props.smooth ? smoothVelocity.value : velocity.value))
79
- const activeAbsVelocity = computed(() =>
80
- props.smooth ? smoothAbsVelocity.value : Math.abs(velocity.value)
81
- )
46
+ onMounted(() => gsap.ticker.add(tick))
47
+ onUnmounted(() => gsap.ticker.remove(tick))
82
48
 
83
- // Default intensities per effect type
84
49
  const defaultIntensities: Record<EffectType, number> = {
85
50
  skew: 3,
86
51
  scale: 0.12,
@@ -92,15 +57,14 @@ const defaultIntensities: Record<EffectType, number> = {
92
57
  hueRotate: 45,
93
58
  }
94
59
 
95
- const intensity = computed(() => props.intensity ?? defaultIntensities[props.effect])
60
+ const activeIntensity = computed(() => intensity ?? defaultIntensities[effect])
96
61
 
97
- // Compute the style based on effect type
98
62
  const effectStyle = computed(() => {
99
- const vel = activeVelocity.value
100
- const absVel = activeAbsVelocity.value
101
- const int = intensity.value
63
+ const vel = smoothVelocity.value
64
+ const absVel = smoothAbsVelocity.value
65
+ const int = activeIntensity.value
102
66
 
103
- switch (props.effect) {
67
+ switch (effect) {
104
68
  case 'skew':
105
69
  return { transform: `skewX(${Math.max(-15, Math.min(15, vel * int))}deg)` }
106
70
  case 'scale':
@@ -122,13 +86,11 @@ const effectStyle = computed(() => {
122
86
  }
123
87
  })
124
88
 
125
- // Classes for performance optimization
126
89
  const effectClasses = computed(() => {
127
- const classes = []
128
- if (['skew', 'scale', 'rotate', 'translateY'].includes(props.effect)) {
129
- classes.push('will-change-transform')
90
+ if (['skew', 'scale', 'rotate', 'translateY'].includes(effect)) {
91
+ return ['will-change-transform']
130
92
  }
131
- return classes
93
+ return []
132
94
  })
133
95
  </script>
134
96
 
@@ -0,0 +1,61 @@
1
+ import type { MaybeRefOrGetter } from 'vue'
2
+ import type { ScrollTrigger as GSAPScrollTrigger } from 'gsap/ScrollTrigger'
3
+
4
+ interface UseCountUpOptions {
5
+ to: number
6
+ from?: number
7
+ duration?: number
8
+ ease?: string
9
+ format?: (n: number) => string
10
+ once?: boolean
11
+ }
12
+
13
+ export function useCountUp(elementRef: MaybeRefOrGetter<HTMLElement | null>, opts: UseCountUpOptions) {
14
+ const { gsap, ScrollTrigger } = useGsap()
15
+
16
+ const displayValue = ref(String(opts.from ?? 0))
17
+ const isComplete = ref(false)
18
+
19
+ const format = opts.format ?? ((n: number) => n.toLocaleString())
20
+ const counter = { value: opts.from ?? 0 }
21
+
22
+ let scrollTriggerInstance: GSAPScrollTrigger | null = null
23
+ let tween: gsap.core.Tween | null = null
24
+
25
+ function startCount() {
26
+ tween = gsap.to(counter, {
27
+ value: opts.to,
28
+ duration: opts.duration ?? 2,
29
+ ease: opts.ease ?? 'power2.out',
30
+ onUpdate: () => {
31
+ displayValue.value = format(Math.round(counter.value))
32
+ },
33
+ onComplete: () => {
34
+ isComplete.value = true
35
+ displayValue.value = format(opts.to)
36
+ },
37
+ })
38
+ }
39
+
40
+ onMounted(async () => {
41
+ await nextTick()
42
+ displayValue.value = format(opts.from ?? 0)
43
+
44
+ const el = toValue(elementRef)
45
+ if (!el) return
46
+
47
+ scrollTriggerInstance = ScrollTrigger.create({
48
+ trigger: el,
49
+ start: 'top 80%',
50
+ once: opts.once ?? true,
51
+ onEnter: startCount,
52
+ })
53
+ })
54
+
55
+ onUnmounted(() => {
56
+ tween?.kill()
57
+ scrollTriggerInstance?.kill()
58
+ })
59
+
60
+ return { displayValue, isComplete }
61
+ }