reborn-ui 0.1.1

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 (127) hide show
  1. package/README.md +47 -0
  2. package/dist/index.js +871 -0
  3. package/dist/index.js.map +1 -0
  4. package/package.json +40 -0
  5. package/registry/.gitkeep +2 -0
  6. package/registry/components/animate-grid.json +18 -0
  7. package/registry/components/animated-beam.json +16 -0
  8. package/registry/components/animated-circular-progressbar.json +16 -0
  9. package/registry/components/animated-list.json +22 -0
  10. package/registry/components/animated-testimonials.json +18 -0
  11. package/registry/components/animated-tooltip.json +18 -0
  12. package/registry/components/apple-card-carousel.json +35 -0
  13. package/registry/components/aurora-background.json +16 -0
  14. package/registry/components/balance-slider.json +16 -0
  15. package/registry/components/bending-gallery.json +18 -0
  16. package/registry/components/bento-grid.json +24 -0
  17. package/registry/components/bg-black-hole.json +14 -0
  18. package/registry/components/bg-bubbles.json +18 -0
  19. package/registry/components/bg-falling-stars.json +16 -0
  20. package/registry/components/bg-neural.json +14 -0
  21. package/registry/components/bg-particle-whirlpool.json +18 -0
  22. package/registry/components/bg-silk.json +16 -0
  23. package/registry/components/bg-stars.json +18 -0
  24. package/registry/components/bg-stractium.json +16 -0
  25. package/registry/components/blur-reveal.json +18 -0
  26. package/registry/components/book.json +28 -0
  27. package/registry/components/border-beam.json +16 -0
  28. package/registry/components/box-reveal.json +18 -0
  29. package/registry/components/card-3d.json +24 -0
  30. package/registry/components/card-spotlight.json +16 -0
  31. package/registry/components/carousel-3d.json +15 -0
  32. package/registry/components/color-picker.json +26 -0
  33. package/registry/components/colourful-text.json +18 -0
  34. package/registry/components/compare.json +22 -0
  35. package/registry/components/confetti.json +22 -0
  36. package/registry/components/container-scroll.json +26 -0
  37. package/registry/components/container-text-flip.json +19 -0
  38. package/registry/components/cosmic-portal.json +18 -0
  39. package/registry/components/direction-aware-hover.json +16 -0
  40. package/registry/components/dock.json +32 -0
  41. package/registry/components/expandable-gallery.json +16 -0
  42. package/registry/components/file-tree.json +28 -0
  43. package/registry/components/file-upload.json +22 -0
  44. package/registry/components/flickering-grid.json +16 -0
  45. package/registry/components/flip-card.json +16 -0
  46. package/registry/components/flip-words.json +16 -0
  47. package/registry/components/fluid-cursor.json +16 -0
  48. package/registry/components/focus.json +16 -0
  49. package/registry/components/github-globe.json +23 -0
  50. package/registry/components/glare-card.json +18 -0
  51. package/registry/components/globe.json +19 -0
  52. package/registry/components/glow-border.json +16 -0
  53. package/registry/components/glowing-effect.json +18 -0
  54. package/registry/components/gradient-button.json +16 -0
  55. package/registry/components/halo-search.json +16 -0
  56. package/registry/components/hyper-text.json +19 -0
  57. package/registry/components/icon-cloud.json +16 -0
  58. package/registry/components/image-trail-cursor.json +22 -0
  59. package/registry/components/images-slider.json +18 -0
  60. package/registry/components/infinite-grid.json +47 -0
  61. package/registry/components/input.json +18 -0
  62. package/registry/components/interactive-grid-pattern.json +16 -0
  63. package/registry/components/interactive-hover-button.json +16 -0
  64. package/registry/components/iphone-mockup.json +16 -0
  65. package/registry/components/lamp-effect.json +16 -0
  66. package/registry/components/lens.json +18 -0
  67. package/registry/components/letter-pullup.json +18 -0
  68. package/registry/components/light-speed.json +31 -0
  69. package/registry/components/line-shadow-text.json +16 -0
  70. package/registry/components/link-preview.json +16 -0
  71. package/registry/components/liquid-background.json +18 -0
  72. package/registry/components/liquid-glass.json +16 -0
  73. package/registry/components/liquid-logo.json +24 -0
  74. package/registry/components/logo-cloud.json +24 -0
  75. package/registry/components/logo-origami.json +22 -0
  76. package/registry/components/marquee.json +20 -0
  77. package/registry/components/meteors.json +16 -0
  78. package/registry/components/morphing-tabs.json +16 -0
  79. package/registry/components/morphing-text.json +16 -0
  80. package/registry/components/multi-step-loader.json +16 -0
  81. package/registry/components/neon-border.json +16 -0
  82. package/registry/components/number-ticker.json +18 -0
  83. package/registry/components/orbit.json +16 -0
  84. package/registry/components/particle-image.json +24 -0
  85. package/registry/components/particles-bg.json +18 -0
  86. package/registry/components/pattern-background.json +18 -0
  87. package/registry/components/photo-gallery.json +16 -0
  88. package/registry/components/radiant-text.json +16 -0
  89. package/registry/components/rainbow-button.json +16 -0
  90. package/registry/components/ripple-button.json +16 -0
  91. package/registry/components/ripple.json +24 -0
  92. package/registry/components/safari-mockup.json +16 -0
  93. package/registry/components/scratch-to-reveal.json +18 -0
  94. package/registry/components/scroll-island.json +20 -0
  95. package/registry/components/shader-toy.json +22 -0
  96. package/registry/components/shimmer-button.json +16 -0
  97. package/registry/components/sleek-line-cursor.json +12 -0
  98. package/registry/components/smooth-cursor.json +23 -0
  99. package/registry/components/snowfall-bg.json +18 -0
  100. package/registry/components/sparkles-text.json +18 -0
  101. package/registry/components/sparkles.json +18 -0
  102. package/registry/components/spinning-text.json +18 -0
  103. package/registry/components/spline.json +23 -0
  104. package/registry/components/spring-calendar.json +22 -0
  105. package/registry/components/svg-mask.json +16 -0
  106. package/registry/components/tailed-cursor.json +14 -0
  107. package/registry/components/testimonial-slider.json +16 -0
  108. package/registry/components/tetris.json +19 -0
  109. package/registry/components/text-3d.json +16 -0
  110. package/registry/components/text-generate-effect.json +16 -0
  111. package/registry/components/text-glitch.json +12 -0
  112. package/registry/components/text-highlight.json +16 -0
  113. package/registry/components/text-hover-effect.json +16 -0
  114. package/registry/components/text-reveal-card.json +20 -0
  115. package/registry/components/text-reveal.json +18 -0
  116. package/registry/components/text-scroll-reveal.json +20 -0
  117. package/registry/components/timeline.json +18 -0
  118. package/registry/components/tracing-beam.json +19 -0
  119. package/registry/components/vanishing-input.json +18 -0
  120. package/registry/components/video-text.json +16 -0
  121. package/registry/components/vortex.json +19 -0
  122. package/registry/components/warp-background.json +22 -0
  123. package/registry/components/wavy-background.json +19 -0
  124. package/registry/components/world-map.json +19 -0
  125. package/registry/registry.json +2007 -0
  126. package/templates/composables/useMouseState.ts +21 -0
  127. package/templates/lib/utils.ts +13 -0
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "sparkles-text",
3
+ "dependencies": [
4
+ "motion-v"
5
+ ],
6
+ "files": [
7
+ {
8
+ "path": "index.ts",
9
+ "content": "export { default as SparklesText } from \"./SparklesText.vue\";\r\n"
10
+ },
11
+ {
12
+ "path": "SparklesText.vue",
13
+ "content": "<template>\r\n <div\r\n class=\"text-6xl font-bold\"\r\n :class=\"props.class\"\r\n >\r\n <span class=\"relative inline-block\">\r\n <template\r\n v-for=\"sparkle in sparkles\"\r\n :key=\"sparkle.id\"\r\n >\r\n <!-- Animated star SVG with fade, scale, and rotation effects -->\r\n <Motion\r\n :initial=\"{ opacity: 0, scale: 0, rotate: 75 }\"\r\n :animate=\"{\r\n opacity: [0, 1, 0],\r\n scale: [0, sparkle.scale, 0],\r\n rotate: [75, 120, 150],\r\n }\"\r\n :transition=\"{\r\n duration: 0.8,\r\n repeat: Infinity,\r\n delay: sparkle.delay,\r\n }\"\r\n as=\"svg\"\r\n class=\"pointer-events-none absolute z-20\"\r\n :style=\"{\r\n left: sparkle.x,\r\n top: sparkle.y,\r\n opacity: 0,\r\n }\"\r\n width=\"21\"\r\n height=\"21\"\r\n viewBox=\"0 0 21 21\"\r\n >\r\n <path\r\n d=\"M9.82531 0.843845C10.0553 0.215178 10.9446 0.215178 11.1746 0.843845L11.8618 2.72026C12.4006 4.19229 12.3916 6.39157 13.5 7.5C14.6084 8.60843 16.8077 8.59935 18.2797 9.13822L20.1561 9.82534C20.7858 10.0553 20.7858 10.9447 20.1561 11.1747L18.2797 11.8618C16.8077 12.4007 14.6084 12.3916 13.5 13.5C12.3916 14.6084 12.4006 16.8077 11.8618 18.2798L11.1746 20.1562C10.9446 20.7858 10.0553 20.7858 9.82531 20.1562L9.13819 18.2798C8.59932 16.8077 8.60843 14.6084 7.5 13.5C6.39157 12.3916 4.19225 12.4007 2.72023 11.8618L0.843814 11.1747C0.215148 10.9447 0.215148 10.0553 0.843814 9.82534L2.72023 9.13822C4.19225 8.59935 6.39157 8.60843 7.5 7.5C8.60843 6.39157 8.59932 4.19229 9.13819 2.72026L9.82531 0.843845Z\"\r\n :fill=\"sparkle.color\"\r\n />\r\n </Motion>\r\n </template>\r\n {{ text }}\r\n </span>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { Motion } from \"motion-v\";\r\nimport { ref, onMounted, onUnmounted } from \"vue\";\r\n\r\ninterface Sparkle {\r\n id: string;\r\n x: string;\r\n y: string;\r\n color: string;\r\n delay: number;\r\n scale: number;\r\n lifespan: number;\r\n}\r\n\r\ninterface Props {\r\n text: string;\r\n sparklesCount?: number;\r\n colors?: {\r\n first: string;\r\n second: string;\r\n };\r\n class?: string;\r\n}\r\n\r\nconst props = withDefaults(defineProps<Props>(), {\r\n sparklesCount: 10,\r\n colors: () => ({ first: \"#9E7AFF\", second: \"#FE8BBB\" }),\r\n});\r\n\r\nconst sparkles = ref<Sparkle[]>([]);\r\n\r\n// Generate a new sparkle with randomized properties\r\nfunction generateStar(): Sparkle {\r\n const starX = `${Math.random() * 100}%`;\r\n const starY = `${Math.random() * 100}%`;\r\n const color = Math.random() > 0.5 ? props.colors.first : props.colors.second;\r\n const delay = Math.random() * 2;\r\n const scale = Math.random() * 1 + 0.3;\r\n const lifespan = Math.random() * 10 + 5;\r\n const id = `${starX}-${starY}-${Date.now()}`;\r\n return { id, x: starX, y: starY, color, delay, scale, lifespan };\r\n}\r\n\r\n// Initialize sparkles array with random stars\r\nfunction initializeStars() {\r\n sparkles.value = Array.from({ length: props.sparklesCount }, generateStar);\r\n}\r\n\r\n// Update sparkles - regenerate dead ones and update lifespans\r\nfunction updateStars() {\r\n sparkles.value = sparkles.value.map((star) => {\r\n if (star.lifespan <= 0) {\r\n return generateStar();\r\n } else {\r\n return { ...star, lifespan: star.lifespan - 0.1 };\r\n }\r\n });\r\n}\r\n\r\nlet interval: number;\r\n\r\n// Start animation loop\r\nonMounted(() => {\r\n initializeStars();\r\n interval = window.setInterval(updateStars, 100);\r\n});\r\n\r\n// Cleanup on unmount\r\nonUnmounted(() => {\r\n if (interval) {\r\n clearInterval(interval);\r\n }\r\n});\r\n</script>\r\n"
14
+ }
15
+ ],
16
+ "fileCount": 2,
17
+ "contentHash": "b0019f0747ae8208ee4e991b4ea014960633854b"
18
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "sparkles",
3
+ "dependencies": [
4
+ "@vueuse/core"
5
+ ],
6
+ "files": [
7
+ {
8
+ "path": "index.ts",
9
+ "content": "export { default as Sparkles } from \"./Sparkles.vue\";\r\n"
10
+ },
11
+ {
12
+ "path": "Sparkles.vue",
13
+ "content": "<template>\r\n <div\r\n ref=\"containerRef\"\r\n class=\"relative size-full overflow-hidden will-change-transform\"\r\n :style=\"{ background }\"\r\n >\r\n <canvas\r\n ref=\"canvasRef\"\r\n class=\"absolute inset-0 size-full\"\r\n />\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { useRafFn, templateRef } from \"@vueuse/core\";\r\nimport { ref, onMounted, onBeforeUnmount } from \"vue\";\r\n\r\ninterface Props {\r\n background?: string;\r\n particleColor?: string;\r\n minSize?: number;\r\n maxSize?: number;\r\n speed?: number;\r\n particleDensity?: number;\r\n}\r\n\r\ninterface Particle {\r\n x: number;\r\n y: number;\r\n size: number;\r\n opacity: number;\r\n vx: number;\r\n vy: number;\r\n phase: number;\r\n phaseSpeed: number;\r\n}\r\n\r\nconst props = withDefaults(defineProps<Props>(), {\r\n background: \"#0d47a1\",\r\n particleColor: \"#ffffff\",\r\n minSize: 1,\r\n maxSize: 3,\r\n speed: 4,\r\n particleDensity: 120,\r\n});\r\n\r\nconst containerRef = templateRef<HTMLElement | null>(\"containerRef\");\r\nconst canvasRef = templateRef<HTMLCanvasElement | null>(\"canvasRef\");\r\nconst particles = ref<Particle[]>([]);\r\nconst ctx = ref<CanvasRenderingContext2D | null>(null);\r\n\r\n// Adjust canvas size on mount and resize\r\nfunction resizeCanvas() {\r\n if (!canvasRef.value || !containerRef.value) return;\r\n\r\n const dpr = window.devicePixelRatio || 1;\r\n const rect = containerRef.value.getBoundingClientRect();\r\n\r\n canvasRef.value.width = rect.width * dpr;\r\n canvasRef.value.height = rect.height * dpr;\r\n\r\n if (ctx.value) {\r\n ctx.value.scale(dpr, dpr);\r\n }\r\n}\r\n\r\nfunction generateParticles(): void {\r\n const newParticles: Particle[] = [];\r\n const count = props.particleDensity;\r\n\r\n for (let i = 0; i < count; i++) {\r\n const baseSpeed = 0.05;\r\n const speedVariance = Math.random() * 0.3 + 0.7;\r\n\r\n newParticles.push({\r\n x: Math.random() * 100,\r\n y: Math.random() * 100,\r\n size: Math.random() * (props.maxSize - props.minSize) + props.minSize,\r\n opacity: Math.random() * 0.5 + 0.3,\r\n vx: (Math.random() - 0.5) * baseSpeed * speedVariance * props.speed,\r\n vy: ((Math.random() - 0.5) * baseSpeed - baseSpeed * 0.3) * speedVariance * props.speed,\r\n phase: Math.random() * Math.PI * 2,\r\n phaseSpeed: 0.015,\r\n });\r\n }\r\n\r\n particles.value = newParticles;\r\n}\r\n\r\nfunction updateAndDrawParticles() {\r\n if (!ctx.value || !canvasRef.value) return;\r\n\r\n const canvas = canvasRef.value;\r\n ctx.value.clearRect(0, 0, canvas.width, canvas.height);\r\n\r\n particles.value = particles.value.map((particle) => {\r\n let newX = particle.x + particle.vx;\r\n let newY = particle.y + particle.vy;\r\n\r\n if (newX < -2) newX = 102;\r\n if (newX > 102) newX = -2;\r\n if (newY < -2) newY = 102;\r\n if (newY > 102) newY = -2;\r\n\r\n const newPhase = (particle.phase + particle.phaseSpeed) % (Math.PI * 2);\r\n const opacity = 0.3 + (Math.sin(newPhase) * 0.3 + 0.3);\r\n\r\n // Draw particle\r\n ctx.value!.beginPath();\r\n ctx.value!.arc(\r\n (newX * canvas.width) / 100,\r\n (newY * canvas.height) / 100,\r\n particle.size,\r\n 0,\r\n Math.PI * 2,\r\n );\r\n ctx.value!.fillStyle = `${props.particleColor}${Math.floor(opacity * 255)\r\n .toString(16)\r\n .padStart(2, \"0\")}`;\r\n ctx.value!.fill();\r\n\r\n return {\r\n ...particle,\r\n x: newX,\r\n y: newY,\r\n phase: newPhase,\r\n opacity,\r\n };\r\n });\r\n}\r\n\r\nconst { pause, resume } = useRafFn(updateAndDrawParticles, { immediate: false });\r\n\r\n// Handle window resize\r\nlet resizeObserver: ResizeObserver | undefined;\r\n\r\nonMounted(() => {\r\n if (!canvasRef.value) return;\r\n\r\n ctx.value = canvasRef.value.getContext(\"2d\");\r\n resizeCanvas();\r\n generateParticles();\r\n\r\n // Set up resize observer\r\n resizeObserver = new ResizeObserver(resizeCanvas);\r\n if (containerRef.value) {\r\n resizeObserver.observe(containerRef.value);\r\n }\r\n\r\n resume();\r\n});\r\n\r\nonBeforeUnmount(() => {\r\n pause();\r\n if (resizeObserver && containerRef.value) {\r\n resizeObserver.unobserve(containerRef.value);\r\n }\r\n});\r\n</script>\r\n"
14
+ }
15
+ ],
16
+ "fileCount": 2,
17
+ "contentHash": "f578fb3c9ee066ce5334e08754688e490c874c42"
18
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "spinning-text",
3
+ "dependencies": [
4
+ "motion-v"
5
+ ],
6
+ "files": [
7
+ {
8
+ "path": "index.ts",
9
+ "content": "export { default as SpinningText } from \"./SpinningText.vue\";\r\n"
10
+ },
11
+ {
12
+ "path": "SpinningText.vue",
13
+ "content": "<template>\r\n <Motion\r\n as=\"div\"\r\n :class=\"cn('relative', props.class)\"\r\n initial=\"hidden\"\r\n animate=\"visible\"\r\n :variants=\"containerVariants\"\r\n :transition=\"finalTransition\"\r\n >\r\n <span\r\n v-for=\"(letter, index) in letters\"\r\n :key=\"`${letter}-${index}`\"\r\n class=\"absolute left-1/2 top-1/2\"\r\n :variants=\"itemVariants\"\r\n :style=\"{\r\n '--index': index,\r\n '--total': letters.length,\r\n '--radius': radius,\r\n transform: `\r\n translate(-50%, -50%)\r\n rotate(calc(360deg / var(--total) * var(--index)))\r\n translateY(calc(var(--radius, 5) * -1ch))\r\n `,\r\n transformOrigin: 'center',\r\n }\"\r\n >\r\n {{ letter }}\r\n </span>\r\n </Motion>\r\n</template>\r\n\r\n<script lang=\"ts\" setup>\r\nimport { cn } from \"~/lib/utils\";\r\nimport { Motion } from \"motion-v\";\r\nimport type { Variant, Transition } from \"motion-v\";\r\n\r\nconst BASE_TRANSITION = {\r\n repeat: Infinity,\r\n ease: \"linear\",\r\n};\r\n\r\nconst BASE_ITEM_VARIANTS = {\r\n hidden: { opacity: 1 },\r\n visible: { opacity: 1 },\r\n};\r\n\r\ninterface CircularTextProps {\r\n text: string;\r\n duration?: number;\r\n class?: string;\r\n reverse?: boolean;\r\n radius?: number;\r\n transition?: Transition;\r\n variants?: {\r\n container?: Variant;\r\n item?: Variant;\r\n };\r\n}\r\n\r\nconst props = withDefaults(defineProps<CircularTextProps>(), {\r\n duration: 10,\r\n radius: 5,\r\n});\r\n\r\nconst letters = computed(() => {\r\n let letters = props.text.split(\"\");\r\n letters.push(\" \");\r\n return letters;\r\n});\r\nconst finalTransition = computed(() => ({\r\n ...BASE_TRANSITION,\r\n ...props.transition,\r\n duration: props.transition?.duration ?? props.duration,\r\n}));\r\n\r\nconst containerVariants = computed(() => ({\r\n visible: { rotate: props.reverse ? -360 : 360 },\r\n // ...props.variants?.container,\r\n}));\r\n\r\nconst itemVariants = computed(() => ({\r\n ...BASE_ITEM_VARIANTS,\r\n ...props?.variants?.item,\r\n}));\r\n</script>\r\n<style scoped></style>\r\n"
14
+ }
15
+ ],
16
+ "fileCount": 2,
17
+ "contentHash": "ffe482d2738e37b8d144698b9b5c87389e411557"
18
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "spline",
3
+ "dependencies": [
4
+ "@splinetool/runtime",
5
+ "@vueuse/core"
6
+ ],
7
+ "files": [
8
+ {
9
+ "path": "index.ts",
10
+ "content": "export { default as Spline } from \"./Spline.vue\";\r\nexport { default as ParentSize } from \"./ParentSize.vue\";\r\n"
11
+ },
12
+ {
13
+ "path": "ParentSize.vue",
14
+ "content": "<!-- ParentSize.vue -->\r\n<template>\r\n <div\r\n ref=\"target\"\r\n :style=\"mergedStyles\"\r\n :class=\"cn('w-full h-full', props.class)\"\r\n v-bind=\"attrsWithoutClassAndStyle\"\r\n >\r\n <slot />\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { ref, reactive, computed, useAttrs } from \"vue\";\r\nimport { useDebounceFn, useResizeObserver } from \"@vueuse/core\";\r\nimport { cn } from \"@/lib/utils\";\r\n\r\nconst props = defineProps({\r\n class: String,\r\n debounceTime: {\r\n type: Number,\r\n default: 300,\r\n },\r\n ignoreDimensions: {\r\n type: [Array, String],\r\n default: () => [],\r\n },\r\n parentSizeStyles: Object,\r\n enableDebounceLeadingCall: {\r\n type: Boolean,\r\n default: true,\r\n },\r\n});\r\n\r\nconst attrs = useAttrs();\r\nconst target = ref<HTMLElement | null>(null);\r\nconst state = reactive({\r\n width: 0,\r\n height: 0,\r\n top: 0,\r\n left: 0,\r\n});\r\n\r\nconst mergedStyles = computed(() => ({\r\n ...props.parentSizeStyles,\r\n ...(attrs.style as object),\r\n}));\r\n\r\nconst mergedClass = computed(() => [\"w-full h-full\", props.class]);\r\n\r\nconst attrsWithoutClassAndStyle = computed(() => {\r\n const { class: _, style: __, ...rest } = attrs;\r\n return rest;\r\n});\r\n\r\nconst normalizedIgnore = computed(() =>\r\n Array.isArray(props.ignoreDimensions) ? props.ignoreDimensions : [props.ignoreDimensions],\r\n);\r\n\r\nfunction updateDimensions(rect: DOMRectReadOnly) {\r\n const { width, height, top, left } = rect;\r\n const newState = { width, height, top, left };\r\n\r\n const hasChange = Object.keys(newState).some(\r\n (key) => state[key as keyof typeof state] !== newState[key as keyof typeof state],\r\n );\r\n\r\n if (!hasChange) return;\r\n\r\n const shouldUpdate = !Object.keys(newState).every((key) =>\r\n normalizedIgnore.value.includes(key as keyof typeof state),\r\n );\r\n\r\n if (shouldUpdate) {\r\n Object.assign(state, newState);\r\n }\r\n}\r\n\r\nconst debouncedUpdate = useDebounceFn(updateDimensions, props.debounceTime);\r\n\r\nuseResizeObserver(target, (entries) => {\r\n const entry = entries[0];\r\n if (entry) debouncedUpdate(entry.contentRect);\r\n});\r\n</script>\r\n"
15
+ },
16
+ {
17
+ "path": "Spline.vue",
18
+ "content": "<template>\r\n <ParentSize\r\n :parent-size-styles=\"parentSizeStyles\"\r\n :debounce-time=\"50\"\r\n v-bind=\"$attrs\"\r\n >\r\n <template #default>\r\n <canvas\r\n ref=\"canvasRef\"\r\n :style=\"canvasStyle\"\r\n />\r\n <slot v-if=\"isLoading\" />\r\n </template>\r\n </ParentSize>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\n/* eslint-disable @typescript-eslint/no-explicit-any */\r\n\r\nimport { ref, onMounted, onUnmounted, computed, watch, nextTick } from \"vue\";\r\nimport { Application, type SplineEventName } from \"@splinetool/runtime\";\r\nimport { useDebounceFn, useIntersectionObserver } from \"@vueuse/core\";\r\nimport ParentSize from \"./ParentSize.vue\";\r\n\r\nconst props = defineProps({\r\n scene: {\r\n type: String,\r\n required: true,\r\n },\r\n onLoad: Function,\r\n renderOnDemand: {\r\n type: Boolean,\r\n default: true,\r\n },\r\n style: Object,\r\n});\r\n\r\nconst emit = defineEmits([\r\n \"error\",\r\n \"spline-mouse-down\",\r\n \"spline-mouse-up\",\r\n \"spline-mouse-hover\",\r\n \"spline-key-down\",\r\n \"spline-key-up\",\r\n \"spline-start\",\r\n \"spline-look-at\",\r\n \"spline-follow\",\r\n \"spline-scroll\",\r\n]);\r\n\r\nconst canvasRef = ref<HTMLCanvasElement | null>(null);\r\nconst isLoading = ref(false);\r\nconst splineApp = ref<Application | null>(null);\r\nconst isVisible = ref(true);\r\n\r\n// eslint-disable-next-line func-style\r\nlet cleanup: () => void = () => {};\r\n\r\nconst parentSizeStyles = computed(() => ({\r\n overflow: \"hidden\",\r\n ...props.style,\r\n}));\r\n\r\nconst canvasStyle = computed(() => ({\r\n display: \"block\",\r\n width: \"100%\",\r\n height: \"100%\",\r\n}));\r\n\r\n// Use IntersectionObserver to detect when component is visible\r\nconst { stop: stopIntersectionObserver } = useIntersectionObserver(\r\n canvasRef,\r\n ([{ isIntersecting }]) => {\r\n isVisible.value = isIntersecting;\r\n if (isIntersecting && splineApp.value) {\r\n // When becoming visible again, force a resize\r\n nextTick(() => {\r\n if (canvasRef.value && splineApp.value) {\r\n splineApp.value.requestRender();\r\n splineApp.value.setSize(canvasRef.value.clientWidth, canvasRef.value.clientHeight);\r\n }\r\n });\r\n }\r\n },\r\n { threshold: 0.1 },\r\n);\r\n\r\nfunction eventHandler(name: SplineEventName, handler?: (e: any) => void) {\r\n if (!handler || !splineApp.value) return;\r\n const debouncedHandler = useDebounceFn(handler, 50, { maxWait: 100 });\r\n splineApp.value.addEventListener(name, debouncedHandler);\r\n return () => splineApp.value?.removeEventListener(name, debouncedHandler);\r\n}\r\n\r\nasync function initSpline() {\r\n if (!canvasRef.value) return;\r\n\r\n isLoading.value = true;\r\n\r\n try {\r\n // Clean up previous instance if exists\r\n if (splineApp.value) {\r\n splineApp.value.dispose();\r\n splineApp.value = null;\r\n }\r\n\r\n splineApp.value = new Application(canvasRef.value, {\r\n renderOnDemand: props.renderOnDemand,\r\n });\r\n\r\n await splineApp.value.load(props.scene);\r\n\r\n // Set up event listeners\r\n const cleanUpFns = [\r\n eventHandler(\"mouseDown\", (e: any) => emit(\"spline-mouse-down\", e)),\r\n eventHandler(\"mouseUp\", (e: any) => emit(\"spline-mouse-up\", e)),\r\n eventHandler(\"mouseHover\", (e: any) => emit(\"spline-mouse-hover\", e)),\r\n eventHandler(\"keyDown\", (e: any) => emit(\"spline-key-down\", e)),\r\n eventHandler(\"keyUp\", (e: any) => emit(\"spline-key-up\", e)),\r\n eventHandler(\"start\", (e: any) => emit(\"spline-start\", e)),\r\n eventHandler(\"lookAt\", (e: any) => emit(\"spline-look-at\", e)),\r\n eventHandler(\"follow\", (e: any) => emit(\"spline-follow\", e)),\r\n eventHandler(\"scroll\", (e: any) => emit(\"spline-scroll\", e)),\r\n ].filter(Boolean);\r\n\r\n isLoading.value = false;\r\n props.onLoad?.(splineApp.value);\r\n\r\n return () => {\r\n cleanUpFns.forEach((fn) => fn?.());\r\n };\r\n } catch (err) {\r\n console.error(\"Spline initialization error:\", err);\r\n emit(\"error\", err);\r\n isLoading.value = false;\r\n return () => {};\r\n }\r\n}\r\n\r\nasync function initialize() {\r\n cleanup();\r\n cleanup = (await initSpline()) ?? (() => {});\r\n}\r\n\r\nonMounted(async () => {\r\n await initialize();\r\n\r\n // Reinitialize when becoming visible again\r\n watch(isVisible, (visible) => {\r\n if (visible) {\r\n initialize();\r\n }\r\n });\r\n});\r\n\r\nonUnmounted(() => {\r\n stopIntersectionObserver();\r\n if (splineApp.value) {\r\n splineApp.value.dispose();\r\n splineApp.value = null;\r\n }\r\n});\r\n</script>\r\n"
19
+ }
20
+ ],
21
+ "fileCount": 3,
22
+ "contentHash": "9c20894c397981e2af84dbec7037b92631650daf"
23
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "spring-calendar",
3
+ "dependencies": [
4
+ "motion-v"
5
+ ],
6
+ "files": [
7
+ {
8
+ "path": "index.ts",
9
+ "content": "export { default as SpringCalendar } from \"./SpringCalendar.vue\";\r\nexport { default as TextMorph } from \"./TextMorph.vue\";\r\n"
10
+ },
11
+ {
12
+ "path": "SpringCalendar.vue",
13
+ "content": "<template>\r\n <MotionConfig :transition=\"{ duration: 0.7, type: 'spring', bounce: 0.5 }\">\r\n <Motion\r\n layout\r\n as=\"div\"\r\n class=\"flex w-full max-w-lg flex-col gap-6 overflow-hidden rounded-3xl border bg-muted/50 p-8\"\r\n :animate=\"{\r\n height: calendarData[activeIndex].events ? 'auto' : 'fit',\r\n }\"\r\n >\r\n <TextMorph\r\n :text=\"calendarData[activeIndex].day\"\r\n class=\"w-fit font-bold\"\r\n :morph-time=\"0.5\"\r\n :cool-down-time=\"0.1\"\r\n />\r\n\r\n <Motion\r\n v-if=\"calendarData[activeIndex].events\"\r\n :key=\"'event-container' + Math.random()\"\r\n layout\r\n as=\"div\"\r\n class=\"flex flex-col gap-4\"\r\n :initial=\"{ x: 10, opacity: 0 }\"\r\n :animate=\"{ x: 0, opacity: 1 }\"\r\n >\r\n <Motion\r\n as=\"div\"\r\n class=\"flex items-center gap-2\"\r\n layout\r\n :initial=\"{ x: 10, opacity: 0 }\"\r\n :animate=\"{ x: 0, opacity: 1 }\"\r\n >\r\n <Icon name=\"lucide:calendar\" />\r\n <span class=\"font-medium\">Upcoming Events</span>\r\n </Motion>\r\n\r\n <div class=\"flex flex-wrap gap-4\">\r\n <Motion\r\n v-for=\"event in calendarData[activeIndex].events\"\r\n :key=\"event.title + event.time + Math.random()\"\r\n as=\"div\"\r\n layout\r\n class=\"w-full max-w-44 rounded-lg border p-3\"\r\n :initial=\"{ x: 10, opacity: 0 }\"\r\n :animate=\"{ x: 0, opacity: 1 }\"\r\n >\r\n <p class=\"text-sm font-medium\">{{ event.title }}</p>\r\n <p class=\"text-xs text-muted-foreground\">{{ event.day }}, {{ event.time }}</p>\r\n </Motion>\r\n </div>\r\n </Motion>\r\n\r\n <div class=\"flex flex-wrap gap-3\">\r\n <Motion\r\n v-for=\"(day, index) in calendarData\"\r\n :key=\"day.date + '-' + index\"\r\n as=\"button\"\r\n layout\r\n class=\"flex flex-col rounded-2xl border border-border p-3 text-center opacity-100 duration-200 hover:bg-muted-foreground/10\"\r\n :class=\"activeIndex === index ? 'bg-muted-foreground/5' : ''\"\r\n :while-hover=\"{ scale: 1.1 }\"\r\n :while-press=\"{ scale: 0.8 }\"\r\n :transition=\"{ duration: 0.01 }\"\r\n @click=\"setActive(index)\"\r\n >\r\n <span class=\"text-xs font-medium uppercase\">{{ day.month }}</span>\r\n <span class=\"font-semibold\">{{ day.date }}</span>\r\n <span class=\"text-xs font-medium uppercase text-primary duration-200\">\r\n {{ day.day }}\r\n </span>\r\n </Motion>\r\n </div>\r\n </Motion>\r\n </MotionConfig>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { ref } from \"vue\";\r\nimport { Motion, MotionConfig } from \"motion-v\";\r\n\r\ninterface CalendarEvent {\r\n title: string;\r\n day: string;\r\n time: string;\r\n}\r\n\r\ninterface CalendarDay {\r\n month: string;\r\n date: number;\r\n day: string;\r\n events?: CalendarEvent[];\r\n}\r\n\r\nconst props = defineProps<{\r\n calendarData: CalendarDay[];\r\n initialIndex?: number;\r\n}>();\r\n\r\nconst emit = defineEmits<{\r\n (e: \"update:activeIndex\", value: number): void;\r\n}>();\r\n\r\nconst activeIndex = ref(props.initialIndex ?? 0);\r\n\r\nfunction setActive(index: number) {\r\n activeIndex.value = index;\r\n emit(\"update:activeIndex\", index);\r\n}\r\n</script>\r\n"
14
+ },
15
+ {
16
+ "path": "TextMorph.vue",
17
+ "content": "<template>\r\n <div\r\n :class=\"\r\n cn(\r\n 'relative h-16 w-full max-w-screen-md text-center font-sans text-[40pt] font-bold leading-none [filter:url(#threshold)_blur(0.6px)]',\r\n props.class,\r\n )\r\n \"\r\n >\r\n <span\r\n ref=\"text1Ref\"\r\n :class=\"cn(TEXT_CLASSES)\"\r\n />\r\n <span\r\n ref=\"text2Ref\"\r\n :class=\"cn(TEXT_CLASSES)\"\r\n />\r\n\r\n <svg\r\n id=\"filters\"\r\n class=\"fixed size-0\"\r\n preserveAspectRatio=\"xMidYMid slice\"\r\n >\r\n <defs>\r\n <filter id=\"threshold\">\r\n <feColorMatrix\r\n in=\"SourceGraphic\"\r\n type=\"matrix\"\r\n values=\"1 0 0 0 0\r\n 0 1 0 0 0\r\n 0 0 1 0 0\r\n 0 0 0 255 -140\"\r\n />\r\n </filter>\r\n </defs>\r\n </svg>\r\n </div>\r\n</template>\r\n\r\n<script lang=\"ts\" setup>\r\nimport { ref, watch, onMounted, onUnmounted } from \"vue\";\r\nimport { cn } from \"@/lib/utils\";\r\n\r\nconst TEXT_CLASSES = \"absolute inset-x-0 top-0 m-auto inline-block w-full\";\r\n\r\ninterface Props {\r\n class?: string;\r\n text: string;\r\n morphTime?: number;\r\n coolDownTime?: number;\r\n}\r\nconst props = withDefaults(defineProps<Props>(), {\r\n morphTime: 1.5,\r\n coolDownTime: 0.5,\r\n});\r\n\r\nconst text1Ref = ref<HTMLSpanElement>();\r\nconst text2Ref = ref<HTMLSpanElement>();\r\n\r\nconst previousText = ref(props.text);\r\nconst currentText = ref(props.text);\r\nconst morph = ref(0);\r\nconst coolDown = ref(0);\r\nconst time = ref(new Date());\r\n\r\nconst isAnimating = ref(false);\r\n\r\nlet animationFrameId: number = 0;\r\n\r\nfunction setStyles(fraction: number) {\r\n if (!text1Ref.value || !text2Ref.value) return;\r\n\r\n text2Ref.value.textContent = currentText.value;\r\n text1Ref.value.textContent = previousText.value;\r\n\r\n text2Ref.value.style.filter = `blur(${Math.min(8 / fraction - 8, 100)}px)`;\r\n text2Ref.value.style.opacity = `${Math.pow(fraction, 0.4) * 100}%`;\r\n\r\n const invertedFraction = 1 - fraction;\r\n text1Ref.value.style.filter = `blur(${Math.min(8 / invertedFraction - 8, 100)}px)`;\r\n text1Ref.value.style.opacity = `${Math.pow(invertedFraction, 0.4) * 100}%`;\r\n}\r\n\r\nfunction doMorph() {\r\n morph.value -= coolDown.value;\r\n coolDown.value = 0;\r\n\r\n let fraction = morph.value / props.morphTime;\r\n\r\n if (fraction > 1) {\r\n coolDown.value = props.coolDownTime;\r\n fraction = 1;\r\n }\r\n\r\n setStyles(fraction);\r\n\r\n if (fraction === 1) {\r\n previousText.value = currentText.value;\r\n }\r\n}\r\n\r\nfunction doCoolDown() {\r\n morph.value = 0;\r\n\r\n if (text1Ref.value && text2Ref.value) {\r\n text2Ref.value.style.filter = \"none\";\r\n text2Ref.value.style.opacity = \"100%\";\r\n text1Ref.value.style.filter = \"none\";\r\n text1Ref.value.style.opacity = \"0%\";\r\n }\r\n}\r\n\r\nfunction animate() {\r\n animationFrameId = requestAnimationFrame(animate);\r\n\r\n const newTime = new Date();\r\n const dt = (newTime.getTime() - time.value.getTime()) / 1000;\r\n time.value = newTime;\r\n\r\n coolDown.value -= dt;\r\n\r\n if (coolDown.value <= 0) {\r\n doMorph();\r\n\r\n // Stop if morphing is done\r\n if (coolDown.value > 0) {\r\n cancelAnimationFrame(animationFrameId);\r\n isAnimating.value = false;\r\n }\r\n } else {\r\n doCoolDown();\r\n }\r\n}\r\n\r\nwatch(\r\n () => props.text,\r\n (newText) => {\r\n if (newText !== currentText.value) {\r\n previousText.value = currentText.value;\r\n currentText.value = newText;\r\n morph.value = 0;\r\n coolDown.value = 0;\r\n time.value = new Date();\r\n\r\n if (!isAnimating.value) {\r\n isAnimating.value = true;\r\n animate();\r\n }\r\n }\r\n },\r\n);\r\n\r\nonMounted(() => {\r\n animate();\r\n});\r\n\r\nonUnmounted(() => {\r\n cancelAnimationFrame(animationFrameId);\r\n});\r\n</script>\r\n"
18
+ }
19
+ ],
20
+ "fileCount": 3,
21
+ "contentHash": "ce66b0415c6f34668c42786da9a832187902f40d"
22
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "svg-mask",
3
+ "dependencies": [],
4
+ "files": [
5
+ {
6
+ "path": "index.ts",
7
+ "content": "export { default as SvgMask } from \"./SVGMask.vue\";\r\n"
8
+ },
9
+ {
10
+ "path": "SVGMask.vue",
11
+ "content": "<template>\r\n <div\r\n ref=\"svgMaskContainerRef\"\r\n :class=\"cn('h-screen relative', isHovered ? 'bg-slate-900' : 'bg-white', props.class)\"\r\n @mousemove=\"updateMousePosition\"\r\n >\r\n <div\r\n :style=\"{\r\n maskSize: `${maskSize}px`,\r\n maskPosition: `${mousePosition.x ? mousePosition.x - maskSize / 2 : 0}px ${\r\n mousePosition.y ? mousePosition.y - maskSize / 2 : 0\r\n }px`,\r\n transition: 'mask-size 0.2s ease-in-out',\r\n }\"\r\n class=\"absolute flex size-full items-center justify-center bg-black text-6xl text-white bg-grid-white/[0.2] [mask-image:url(https://cdn.inspira-ui.com/images/mask.svg)] [mask-repeat:no-repeat] [mask-size:40px]\"\r\n >\r\n <div class=\"absolute inset-0 z-0 size-full bg-black opacity-50\"></div>\r\n <div\r\n class=\"relative z-20 mx-auto max-w-4xl text-center text-4xl font-bold text-white\"\r\n :onmouseenter=\"() => (isHovered = true)\"\r\n :onmouseleave=\"() => (isHovered = false)\"\r\n >\r\n <slot name=\"base\"></slot>\r\n </div>\r\n </div>\r\n\r\n <div class=\"flex size-full items-center justify-center text-white\">\r\n <slot name=\"reveal\"></slot>\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script lang=\"ts\" setup>\r\nimport { ref, computed, type HTMLAttributes } from \"vue\";\r\nimport { cn } from \"@/lib/utils\";\r\n\r\ninterface Props {\r\n class?: HTMLAttributes[\"class\"];\r\n size?: number;\r\n revealSize?: number;\r\n}\r\n\r\nconst props = withDefaults(defineProps<Props>(), {\r\n size: 10,\r\n revealSize: 600,\r\n});\r\n\r\nconst isHovered = ref(false);\r\nconst svgMaskContainerRef = ref<HTMLDivElement>();\r\nconst mousePosition = ref<{ x: null | number; y: null | number }>({ x: null, y: null });\r\n\r\nconst maskSize = computed(() => {\r\n return isHovered.value ? props.revealSize : props.size;\r\n});\r\n\r\nfunction updateMousePosition(event: MouseEvent) {\r\n if (!svgMaskContainerRef.value) return;\r\n\r\n const rect = svgMaskContainerRef.value.getBoundingClientRect();\r\n mousePosition.value = { x: event.clientX - rect.left, y: event.clientY - rect.top };\r\n}\r\n</script>\r\n"
12
+ }
13
+ ],
14
+ "fileCount": 2,
15
+ "contentHash": "ef2a64f6b95b1de3edd3f4b9dd3cb1aead7257ce"
16
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "tailed-cursor",
3
+ "dependencies": [
4
+ "ogl"
5
+ ],
6
+ "files": [
7
+ {
8
+ "path": "TailedCursor.vue",
9
+ "content": "<template>\r\n <div\r\n ref=\"containerRef\"\r\n class=\"relative w-full h-full\"\r\n />\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { Color, Polyline, Renderer, Transform, Vec3 } from \"ogl\";\r\nimport { onBeforeUnmount, onMounted, ref, watch } from \"vue\";\r\n\r\ninterface RibbonsProps {\r\n colors?: string[];\r\n baseSpring?: number;\r\n baseFriction?: number;\r\n baseThickness?: number;\r\n offsetFactor?: number;\r\n maxAge?: number;\r\n pointCount?: number;\r\n speedMultiplier?: number;\r\n enableFade?: boolean;\r\n enableShaderEffect?: boolean;\r\n effectAmplitude?: number;\r\n backgroundColor?: number[];\r\n}\r\n\r\nconst props = withDefaults(defineProps<RibbonsProps>(), {\r\n colors: () => [\"#ff9346\", \"#7cff67\", \"#ffee51\", \"#00d8ff\"],\r\n baseSpring: 0.03,\r\n baseFriction: 0.9,\r\n baseThickness: 30,\r\n offsetFactor: 0.05,\r\n maxAge: 500,\r\n pointCount: 50,\r\n speedMultiplier: 0.6,\r\n enableFade: false,\r\n enableShaderEffect: false,\r\n effectAmplitude: 2,\r\n backgroundColor: () => [0, 0, 0, 0],\r\n});\r\n\r\nconst containerRef = ref<HTMLDivElement | null>(null);\r\n\r\nonMounted(() => {\r\n const container = containerRef.value;\r\n if (!container) return;\r\n\r\n // Create a renderer with an alpha-enabled context.\r\n const renderer = new Renderer({ dpr: window.devicePixelRatio || 2, alpha: true });\r\n const gl = renderer.gl;\r\n if (Array.isArray(props.backgroundColor) && props.backgroundColor.length === 4) {\r\n gl.clearColor(\r\n props.backgroundColor[0],\r\n props.backgroundColor[1],\r\n props.backgroundColor[2],\r\n props.backgroundColor[3],\r\n );\r\n } else {\r\n gl.clearColor(0, 0, 0, 0);\r\n }\r\n\r\n gl.canvas.style.position = \"absolute\";\r\n gl.canvas.style.top = \"0\";\r\n gl.canvas.style.left = \"0\";\r\n gl.canvas.style.width = \"100%\";\r\n gl.canvas.style.height = \"100%\";\r\n container.appendChild(gl.canvas);\r\n\r\n const scene = new Transform();\r\n const lines: {\r\n spring: number;\r\n friction: number;\r\n mouseVelocity: Vec3;\r\n mouseOffset: Vec3;\r\n points: Vec3[];\r\n polyline: Polyline;\r\n }[] = [];\r\n\r\n const vertex = `\r\n precision highp float;\r\n \r\n attribute vec3 position;\r\n attribute vec3 next;\r\n attribute vec3 prev;\r\n attribute vec2 uv;\r\n attribute float side;\r\n \r\n uniform vec2 uResolution;\r\n uniform float uDPR;\r\n uniform float uThickness;\r\n uniform float uTime;\r\n uniform float uEnableShaderEffect;\r\n uniform float uEffectAmplitude;\r\n \r\n varying vec2 vUV;\r\n \r\n vec4 getPosition() {\r\n vec4 current = vec4(position, 1.0);\r\n vec2 aspect = vec2(uResolution.x / uResolution.y, 1.0);\r\n vec2 nextScreen = next.xy * aspect;\r\n vec2 prevScreen = prev.xy * aspect;\r\n vec2 tangent = normalize(nextScreen - prevScreen);\r\n vec2 normal = vec2(-tangent.y, tangent.x);\r\n normal /= aspect;\r\n normal *= mix(1.0, 0.1, pow(abs(uv.y - 0.5) * 2.0, 2.0));\r\n float dist = length(nextScreen - prevScreen);\r\n normal *= smoothstep(0.0, 0.02, dist);\r\n float pixelWidthRatio = 1.0 / (uResolution.y / uDPR);\r\n float pixelWidth = current.w * pixelWidthRatio;\r\n normal *= pixelWidth * uThickness;\r\n current.xy -= normal * side;\r\n if(uEnableShaderEffect > 0.5) {\r\n current.xy += normal * sin(uTime + current.x * 10.0) * uEffectAmplitude;\r\n }\r\n return current;\r\n }\r\n \r\n void main() {\r\n // Pass the original uv to the fragment shader.\r\n vUV = uv;\r\n gl_Position = getPosition();\r\n }\r\n `;\r\n\r\n // Fragment shader uses vUV.y (progress along the ribbon) for fade.\r\n const fragment = `\r\n precision highp float;\r\n uniform vec3 uColor;\r\n uniform float uOpacity;\r\n uniform float uEnableFade;\r\n varying vec2 vUV;\r\n void main() {\r\n float fadeFactor = 1.0;\r\n if(uEnableFade > 0.5) {\r\n fadeFactor = 1.0 - smoothstep(0.0, 1.0, vUV.y);\r\n }\r\n gl_FragColor = vec4(uColor, uOpacity * fadeFactor);\r\n }\r\n `;\r\n\r\n function resize() {\r\n if (!container) return;\r\n const width = container.clientWidth;\r\n const height = container.clientHeight;\r\n renderer.setSize(width, height);\r\n lines.forEach((line) => line.polyline.resize());\r\n }\r\n window.addEventListener(\"resize\", resize);\r\n\r\n const center = (props.colors.length - 1) / 2;\r\n props.colors.forEach((color, index) => {\r\n const spring = props.baseSpring + (Math.random() - 0.5) * 0.05;\r\n const friction = props.baseFriction + (Math.random() - 0.5) * 0.05;\r\n const thickness = props.baseThickness + (Math.random() - 0.5) * 3;\r\n const mouseOffset = new Vec3(\r\n (index - center) * props.offsetFactor + (Math.random() - 0.5) * 0.01,\r\n (Math.random() - 0.5) * 0.1,\r\n 0,\r\n );\r\n\r\n const line = {\r\n spring,\r\n friction,\r\n mouseVelocity: new Vec3(),\r\n mouseOffset,\r\n points: [] as Vec3[],\r\n polyline: {} as Polyline,\r\n };\r\n\r\n const count = props.pointCount;\r\n const points: Vec3[] = [];\r\n for (let i = 0; i < count; i++) {\r\n points.push(new Vec3());\r\n }\r\n line.points = points;\r\n\r\n line.polyline = new Polyline(gl, {\r\n points,\r\n vertex,\r\n fragment,\r\n uniforms: {\r\n uColor: { value: new Color(color) },\r\n uThickness: { value: thickness },\r\n uOpacity: { value: 1.0 },\r\n uTime: { value: 0.0 },\r\n uEnableShaderEffect: { value: props.enableShaderEffect ? 1.0 : 0.0 },\r\n uEffectAmplitude: { value: props.effectAmplitude },\r\n uEnableFade: { value: props.enableFade ? 1.0 : 0.0 },\r\n },\r\n });\r\n line.polyline.mesh.setParent(scene);\r\n lines.push(line);\r\n });\r\n\r\n resize();\r\n\r\n const mouse = new Vec3();\r\n function updateMouse(e: MouseEvent | TouchEvent) {\r\n let x: number, y: number;\r\n if (!container) return;\r\n const rect = container.getBoundingClientRect();\r\n if (\"changedTouches\" in e && e.changedTouches.length) {\r\n x = e.changedTouches[0].clientX - rect.left;\r\n y = e.changedTouches[0].clientY - rect.top;\r\n } else if (e instanceof MouseEvent) {\r\n x = e.clientX - rect.left;\r\n y = e.clientY - rect.top;\r\n } else {\r\n x = 0;\r\n y = 0;\r\n }\r\n const width = container.clientWidth;\r\n const height = container.clientHeight;\r\n mouse.set((x / width) * 2 - 1, (y / height) * -2 + 1, 0);\r\n }\r\n container.addEventListener(\"mousemove\", updateMouse);\r\n container.addEventListener(\"touchstart\", updateMouse);\r\n container.addEventListener(\"touchmove\", updateMouse);\r\n\r\n const tmp = new Vec3();\r\n let frameId: number;\r\n let lastTime = performance.now();\r\n function update() {\r\n frameId = requestAnimationFrame(update);\r\n const currentTime = performance.now();\r\n const dt = currentTime - lastTime;\r\n lastTime = currentTime;\r\n\r\n lines.forEach((line) => {\r\n tmp.copy(mouse).add(line.mouseOffset).sub(line.points[0]).multiply(line.spring);\r\n line.mouseVelocity.add(tmp).multiply(line.friction);\r\n line.points[0].add(line.mouseVelocity);\r\n\r\n for (let i = 1; i < line.points.length; i++) {\r\n if (isFinite(props.maxAge) && props.maxAge > 0) {\r\n const segmentDelay = props.maxAge / (line.points.length - 1);\r\n const alpha = Math.min(1, (dt * props.speedMultiplier) / segmentDelay);\r\n line.points[i].lerp(line.points[i - 1], alpha);\r\n } else {\r\n line.points[i].lerp(line.points[i - 1], 0.9);\r\n }\r\n }\r\n if (line.polyline.mesh.program.uniforms.uTime) {\r\n line.polyline.mesh.program.uniforms.uTime.value = currentTime * 0.001;\r\n }\r\n line.polyline.updateGeometry();\r\n });\r\n\r\n renderer.render({ scene });\r\n }\r\n update();\r\n\r\n // Watch for prop changes and update component accordingly\r\n watch(\r\n () => props.enableShaderEffect,\r\n (newValue) => {\r\n lines.forEach((line) => {\r\n if (line.polyline.mesh.program.uniforms.uEnableShaderEffect) {\r\n line.polyline.mesh.program.uniforms.uEnableShaderEffect.value = newValue ? 1.0 : 0.0;\r\n }\r\n });\r\n },\r\n );\r\n\r\n watch(\r\n () => props.enableFade,\r\n (newValue) => {\r\n lines.forEach((line) => {\r\n if (line.polyline.mesh.program.uniforms.uEnableFade) {\r\n line.polyline.mesh.program.uniforms.uEnableFade.value = newValue ? 1.0 : 0.0;\r\n }\r\n });\r\n },\r\n );\r\n\r\n watch(\r\n () => props.effectAmplitude,\r\n (newValue) => {\r\n lines.forEach((line) => {\r\n if (line.polyline.mesh.program.uniforms.uEffectAmplitude) {\r\n line.polyline.mesh.program.uniforms.uEffectAmplitude.value = newValue;\r\n }\r\n });\r\n },\r\n );\r\n\r\n onBeforeUnmount(() => {\r\n window.removeEventListener(\"resize\", resize);\r\n if (container) {\r\n container.removeEventListener(\"mousemove\", updateMouse);\r\n container.removeEventListener(\"touchstart\", updateMouse);\r\n container.removeEventListener(\"touchmove\", updateMouse);\r\n }\r\n cancelAnimationFrame(frameId);\r\n if (gl.canvas && container && gl.canvas.parentNode === container) {\r\n container.removeChild(gl.canvas);\r\n }\r\n });\r\n});\r\n</script>\r\n"
10
+ }
11
+ ],
12
+ "fileCount": 1,
13
+ "contentHash": "5171c40b6463da27b2576a30b75e3a921ebf4c35"
14
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "testimonial-slider",
3
+ "dependencies": [],
4
+ "files": [
5
+ {
6
+ "path": "index.ts",
7
+ "content": "export { default as TestimonialSlider } from \"./TestimonialSlider.vue\";\r\n"
8
+ },
9
+ {
10
+ "path": "TestimonialSlider.vue",
11
+ "content": "<template>\r\n <div class=\"mx-auto w-full max-w-3xl text-center\">\r\n <!-- Testimonial image -->\r\n <div class=\"relative h-32\">\r\n <div\r\n class=\"pointer-events-none absolute left-1/2 top-0 size-[480px] -translate-x-1/2 before:absolute before:inset-0 before:-z-10 before:rounded-full before:bg-gradient-to-b before:from-zinc-500/25 before:via-zinc-500/5 before:via-25% before:to-zinc-500/0 before:to-75%\"\r\n >\r\n <div\r\n class=\"h-32 [mask-image:_linear-gradient(0deg,transparent,theme(colors.white)_20%,theme(colors.white))]\"\r\n >\r\n <transition-group name=\"testimonial-image\">\r\n <div\r\n v-for=\"(testimonial, index) in testimonials\"\r\n v-show=\"active === index\"\r\n :key=\"`image-${index}`\"\r\n class=\"absolute inset-0 -z-10 flex h-full flex-col\"\r\n >\r\n <NuxtImg\r\n class=\"relative left-1/2 top-11 -translate-x-1/2 rounded-full\"\r\n :src=\"testimonial.img\"\r\n width=\"60\"\r\n height=\"60\"\r\n :alt=\"testimonial.name\"\r\n />\r\n </div>\r\n </transition-group>\r\n </div>\r\n </div>\r\n </div>\r\n <!-- Text -->\r\n <div class=\"mb-8 transition-all delay-300 duration-150 ease-in-out\">\r\n <div\r\n ref=\"testimonialsRef\"\r\n class=\"relative flex flex-col\"\r\n >\r\n <transition-group name=\"testimonial-text\">\r\n <div\r\n v-for=\"(testimonial, index) in testimonials\"\r\n v-show=\"active === index\"\r\n :key=\"`text-${index}`\"\r\n class=\"w-full\"\r\n >\r\n <div\r\n class=\"text-2xl font-bold text-zinc-900 before:content-['\\201C'] after:content-['\\201D'] dark:text-zinc-100\"\r\n >\r\n {{ testimonial.quote }}\r\n </div>\r\n </div>\r\n </transition-group>\r\n </div>\r\n </div>\r\n <div class=\"mt-4 flex w-full items-center justify-between gap-4 pt-12 md:pt-0\">\r\n <button\r\n class=\"group/button flex size-7 items-center justify-center rounded-full bg-gray-100 dark:bg-neutral-800\"\r\n @click=\"handlePrev\"\r\n >\r\n <Icon\r\n name=\"lucide:arrow-left\"\r\n class=\"size-5 text-black transition-transform duration-300 group-hover/button:rotate-12 dark:text-neutral-400\"\r\n />\r\n </button>\r\n\r\n <!-- Name and Org -->\r\n <div class=\"flex flex-col items-center gap-1\">\r\n <span class=\"text-base italic\">{{ testimonials.at(active)?.name }}</span>\r\n <span class=\"text-sm italic\">{{ testimonials.at(active)?.role }}</span>\r\n </div>\r\n\r\n <button\r\n class=\"group/button flex size-7 items-center justify-center rounded-full bg-gray-100 dark:bg-neutral-800\"\r\n @click=\"handleNext\"\r\n >\r\n <Icon\r\n name=\"lucide:arrow-right\"\r\n class=\"size-5 text-black transition-transform duration-300 group-hover/button:-rotate-12 dark:text-neutral-400\"\r\n />\r\n </button>\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script lang=\"ts\" setup>\r\nimport { ref, onMounted, onUnmounted } from \"vue\";\r\n\r\ninterface Testimonial {\r\n img: string;\r\n quote: string;\r\n name: string;\r\n role: string;\r\n}\r\n\r\nconst props = withDefaults(\r\n defineProps<{\r\n testimonials?: Testimonial[];\r\n autoRotate?: boolean;\r\n duration?: number;\r\n }>(),\r\n {\r\n autoRotate: true,\r\n duration: 5,\r\n testimonials: () => [],\r\n },\r\n);\r\n\r\nconst active = ref<number>(0);\r\nconst autorotate = ref(props.autoRotate);\r\nconst testimonialsRef = ref<HTMLElement | null>(null);\r\nlet intervalId: number | null = null;\r\n\r\nfunction heightFix() {\r\n if (testimonialsRef.value && testimonialsRef.value.parentElement) {\r\n testimonialsRef.value.parentElement.style.height = `${testimonialsRef.value.clientHeight}px`;\r\n }\r\n}\r\n\r\nfunction setActiveIndex(index: number) {\r\n active.value = index;\r\n autorotate.value = false;\r\n resetAutorotate();\r\n}\r\n\r\nfunction startAutorotate() {\r\n intervalId = window.setInterval(() => {\r\n active.value = active.value + 1 === props.testimonials.length ? 0 : active.value + 1;\r\n heightFix();\r\n }, props.duration * 1000);\r\n}\r\n\r\nfunction resetAutorotate() {\r\n if (intervalId) {\r\n clearInterval(intervalId);\r\n }\r\n if (autorotate.value) {\r\n startAutorotate();\r\n }\r\n}\r\n\r\nfunction handleNext() {\r\n setActiveIndex((active.value + 1) % props.testimonials.length);\r\n}\r\n\r\nfunction handlePrev() {\r\n setActiveIndex((active.value - 1 + props.testimonials.length) % props.testimonials.length);\r\n}\r\n\r\nonMounted(() => {\r\n heightFix();\r\n if (autorotate.value) {\r\n startAutorotate();\r\n }\r\n});\r\n\r\nonUnmounted(() => {\r\n if (intervalId) {\r\n clearInterval(intervalId);\r\n }\r\n});\r\n</script>\r\n\r\n<style>\r\n/* Testimonial image transitions */\r\n.testimonial-image-enter-active {\r\n transition: all 700ms cubic-bezier(0.68, -0.3, 0.32, 1);\r\n}\r\n.testimonial-image-leave-active {\r\n transition: all 700ms cubic-bezier(0.68, -0.3, 0.32, 1);\r\n}\r\n.testimonial-image-enter-from {\r\n opacity: 0;\r\n transform: rotate(-60deg);\r\n}\r\n.testimonial-image-enter-to {\r\n opacity: 1;\r\n transform: rotate(0deg);\r\n}\r\n.testimonial-image-leave-from {\r\n opacity: 1;\r\n transform: rotate(0deg);\r\n}\r\n.testimonial-image-leave-to {\r\n opacity: 0;\r\n transform: rotate(60deg);\r\n}\r\n\r\n/* Testimonial text transitions */\r\n.testimonial-text-enter-active {\r\n transition: all 500ms ease-in-out 200ms;\r\n}\r\n.testimonial-text-leave-active {\r\n transition: all 300ms ease-out 300ms;\r\n position: absolute;\r\n}\r\n.testimonial-text-enter-from {\r\n opacity: 0;\r\n transform: translateX(-1rem);\r\n}\r\n.testimonial-text-enter-to {\r\n opacity: 1;\r\n transform: translateX(0);\r\n}\r\n.testimonial-text-leave-from {\r\n opacity: 1;\r\n transform: translateX(0);\r\n}\r\n.testimonial-text-leave-to {\r\n opacity: 0;\r\n transform: translateX(1rem);\r\n}\r\n</style>\r\n"
12
+ }
13
+ ],
14
+ "fileCount": 2,
15
+ "contentHash": "4f1f1584cfef0ff0fac97595e157732b1cad35ed"
16
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "tetris",
3
+ "dependencies": [
4
+ "@vueuse/core",
5
+ "theme-colors"
6
+ ],
7
+ "files": [
8
+ {
9
+ "path": "index.ts",
10
+ "content": "export { default as Tetris } from \"./Tetris.vue\";\r\n"
11
+ },
12
+ {
13
+ "path": "Tetris.vue",
14
+ "content": "<template>\r\n <Transition\r\n appear\r\n name=\"fade\"\r\n >\r\n <div\r\n :style=\"{\r\n '--cell-size': `${width / cols}px`,\r\n '--grid-rows': rows - 1,\r\n }\"\r\n :class=\"cn('relative w-full', props.class)\"\r\n >\r\n <div\r\n ref=\"el\"\r\n class=\"absolute inset-0 grid justify-center -space-y-px\"\r\n :style=\"{ gridTemplateRows: `repeat(var(--grid-rows), var(--cell-size))` }\"\r\n >\r\n <div\r\n v-for=\"(row, rowIndex) in grid\"\r\n :key=\"rowIndex\"\r\n class=\"grid flex-1 grid-flow-col -space-x-px\"\r\n :style=\"{ gridTemplateColumns: `repeat(${cols}, var(--cell-size))` }\"\r\n >\r\n <div\r\n v-for=\"(cell, cellIndex) in row\"\r\n :key=\"cellIndex\"\r\n :style=\"{\r\n '--border-light': theme[100],\r\n '--border-dark': theme[900],\r\n '--square-light': theme[500],\r\n '--square-hover-light': theme[400],\r\n '--square-dark': theme[700],\r\n '--square-hover-dark': theme[600],\r\n }\"\r\n class=\"relative border border-[color:var(--border-light)] dark:border-[color:var(--border-dark)]\"\r\n >\r\n <div\r\n class=\"absolute inset-0 bg-[color:var(--square-light)] opacity-0 transition-opacity duration-1000 will-change-[opacity] hover:bg-[color:var(--square-hover-light)] dark:bg-[color:var(--square-dark)] dark:hover:bg-[color:var(--square-hover-dark)]\"\r\n :class=\"[cell && 'cursor-pointer opacity-60']\"\r\n @click=\"cell && removeCell(rowIndex, cellIndex)\"\r\n />\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n </Transition>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { useElementSize } from \"@vueuse/core\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport { getColors } from \"theme-colors\";\r\nimport { ref, onMounted, onUnmounted, watch } from \"vue\";\r\n\r\ninterface Props {\r\n class?: string;\r\n squareColor: string;\r\n base?: number;\r\n}\r\n\r\nconst props = withDefaults(defineProps<Props>(), {\r\n base: 10,\r\n});\r\n\r\nconst theme = getColors(props.squareColor);\r\n\r\nconst el = ref(null);\r\nconst grid = ref<(boolean | null)[][]>([]);\r\nconst rows = ref(0);\r\nconst cols = ref(0);\r\n\r\nconst { width, height } = useElementSize(el);\r\n\r\nfunction createGrid() {\r\n grid.value = [];\r\n\r\n for (let i = 0; i < rows.value; i++) {\r\n grid.value.push(new Array(cols.value).fill(null));\r\n }\r\n}\r\n\r\nfunction createNewCell() {\r\n const x = Math.floor(Math.random() * cols.value);\r\n\r\n grid.value[0][x] = true;\r\n}\r\n\r\nfunction moveCellsDown() {\r\n for (let row = rows.value - 1; row >= 0; row--) {\r\n for (let col = 0; col < cols.value; col++) {\r\n const cell = grid.value[row][col];\r\n const nextCell = Array.isArray(grid.value[row + 1]) ? grid.value[row + 1][col] : cell;\r\n if (cell !== null && nextCell === null) {\r\n grid.value[row + 1][col] = grid.value[row][col];\r\n grid.value[row][col] = null;\r\n }\r\n }\r\n }\r\n\r\n setTimeout(() => {\r\n const isFilled = grid.value[rows.value - 1].every((cell) => cell !== null);\r\n if (Array.isArray(grid.value[rows.value]) && isFilled) {\r\n for (let col = 0; col < cols.value; col++) {\r\n grid.value[rows.value][col] = null;\r\n }\r\n }\r\n }, 500);\r\n}\r\n\r\nfunction clearColumn() {\r\n const isFilled = grid.value[rows.value - 1].every((cell) => cell === true);\r\n if (!isFilled) return;\r\n\r\n for (let col = 0; col < cols.value; col++) {\r\n grid.value[rows.value - 1][col] = null;\r\n }\r\n}\r\n\r\nfunction removeCell(row: number, col: number) {\r\n grid.value[row][col] = null;\r\n}\r\n\r\nfunction calcGrid() {\r\n const cell = width.value / props.base;\r\n\r\n rows.value = Math.floor(height.value / cell);\r\n cols.value = Math.floor(width.value / cell);\r\n\r\n createGrid();\r\n}\r\n\r\nwatch(width, calcGrid);\r\n\r\n// eslint-disable-next-line no-undef\r\nlet intervalId: NodeJS.Timeout | undefined;\r\n// eslint-disable-next-line no-undef\r\nlet timeoutId: NodeJS.Timeout | undefined;\r\n\r\nonMounted(() => {\r\n timeoutId = setTimeout(calcGrid, 50);\r\n\r\n intervalId = setInterval(() => {\r\n clearColumn();\r\n moveCellsDown();\r\n createNewCell();\r\n }, 1000);\r\n});\r\n\r\nonUnmounted(() => {\r\n clearInterval(intervalId);\r\n clearTimeout(timeoutId);\r\n});\r\n</script>\r\n\r\n<style scoped>\r\n.fade-enter-active,\r\n.fade-leave-active {\r\n transition: opacity 0.5s ease;\r\n}\r\n\r\n.fade-enter-from,\r\n.fade-leave-to {\r\n opacity: 0;\r\n}\r\n</style>\r\n"
15
+ }
16
+ ],
17
+ "fileCount": 2,
18
+ "contentHash": "ea89df0ce83e5e35b878027a45972a5df146fafc"
19
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "text-3d",
3
+ "dependencies": [],
4
+ "files": [
5
+ {
6
+ "path": "index.ts",
7
+ "content": "export { default as Text3d } from \"./Text3d.vue\";\r\n"
8
+ },
9
+ {
10
+ "path": "Text3d.vue",
11
+ "content": "<template>\r\n <div\r\n :class=\"\r\n cn('text-3d flex items-center justify-center', animate ? 'animate-text-3d' : '', props.class)\r\n \"\r\n >\r\n <slot></slot>\r\n </div>\r\n</template>\r\n\r\n<script lang=\"ts\" setup>\r\nimport { computed, type HTMLAttributes } from \"vue\";\r\nimport { cn } from \"@/lib/utils\";\r\n\r\ninterface Props {\r\n textColor?: string;\r\n letterSpacing?: number;\r\n strokeColor?: string;\r\n shadowColor?: string;\r\n strokeSize?: number;\r\n shadow1Size?: number;\r\n shadow2Size?: number;\r\n class?: HTMLAttributes[\"class\"];\r\n animate?: boolean;\r\n animationDuration?: number;\r\n}\r\n\r\nconst props = withDefaults(defineProps<Props>(), {\r\n textColor: \"white\",\r\n letterSpacing: -0.1,\r\n strokeColor: \"black\",\r\n shadowColor: \"yellow\",\r\n strokeSize: 20,\r\n shadow1Size: 7,\r\n shadow2Size: 10,\r\n animate: true,\r\n animationDuration: 1500,\r\n});\r\n\r\nconst letterSpacingInCh = computed(() => {\r\n return `${props.letterSpacing}ch`;\r\n});\r\n\r\nconst strokeSizeInPx = computed(() => {\r\n return `${props.strokeSize}px`;\r\n});\r\n\r\nconst shadow1SizeInPx = computed(() => {\r\n return `${props.shadow1Size}px`;\r\n});\r\n\r\nconst shadow2SizeInPx = computed(() => {\r\n return `${props.shadow2Size}px`;\r\n});\r\n\r\nconst animationDurationInMs = computed(() => {\r\n return `${props.animationDuration}ms`;\r\n});\r\n</script>\r\n\r\n<style scoped>\r\n.text-3d {\r\n paint-order: stroke fill;\r\n letter-spacing: v-bind(letterSpacingInCh);\r\n -webkit-text-stroke: v-bind(strokeSizeInPx) v-bind(strokeColor);\r\n text-shadow:\r\n v-bind(shadow1SizeInPx) v-bind(shadow1SizeInPx) 0px v-bind(strokeColor),\r\n v-bind(shadow2SizeInPx) v-bind(shadow2SizeInPx) 0px v-bind(shadowColor);\r\n color: v-bind(textColor);\r\n}\r\n\r\n.animate-text-3d {\r\n animation: wiggle v-bind(animationDurationInMs) ease-in-out infinite alternate;\r\n animation-timing-function: ease-in-out;\r\n transform-origin: center;\r\n}\r\n\r\n@keyframes wiggle {\r\n 0% {\r\n transform: rotate(0deg);\r\n }\r\n 12% {\r\n transform: rotate(5deg);\r\n }\r\n 25% {\r\n transform: rotate(-5deg);\r\n }\r\n 38% {\r\n transform: rotate(3deg);\r\n }\r\n 50% {\r\n transform: rotate(0deg);\r\n }\r\n 100% {\r\n transform: rotate(0deg);\r\n }\r\n}\r\n</style>\r\n"
12
+ }
13
+ ],
14
+ "fileCount": 2,
15
+ "contentHash": "00947c81493e935a9edc96dfa271aaba8ec40795"
16
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "text-generate-effect",
3
+ "dependencies": [],
4
+ "files": [
5
+ {
6
+ "path": "index.ts",
7
+ "content": "export { default as TextGenerateEffect } from \"./TextGenerateEffect.vue\";\r\n"
8
+ },
9
+ {
10
+ "path": "TextGenerateEffect.vue",
11
+ "content": "<template>\r\n <div :class=\"cn('leading-snug tracking-wide', props.class)\">\r\n <div ref=\"scope\">\r\n <span\r\n v-for=\"(word, idx) in wordsArray\"\r\n :key=\"word + idx\"\r\n class=\"inline-block\"\r\n :style=\"spanStyle\"\r\n >\r\n {{ word }}&nbsp;\r\n </span>\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { computed, type HTMLAttributes, onMounted, ref } from \"vue\";\r\n\r\nimport { cn } from \"@/lib/utils\";\r\n\r\nconst props = withDefaults(\r\n defineProps<{\r\n words: string;\r\n filter?: boolean;\r\n duration?: number;\r\n delay?: number;\r\n class: HTMLAttributes[\"class\"];\r\n }>(),\r\n { duration: 0.7, delay: 0, filter: true },\r\n);\r\n\r\nconst scope = ref(null);\r\nconst wordsArray = computed(() => props.words.split(\" \"));\r\n\r\nconst spanStyle = computed(() => ({\r\n opacity: 0,\r\n filter: props.filter ? \"blur(10px)\" : \"none\",\r\n transition: `opacity ${props.duration}s, filter ${props.duration}s`,\r\n}));\r\n\r\nonMounted(() => {\r\n if (scope.value) {\r\n const spans = (scope.value as HTMLElement).querySelectorAll(\"span\");\r\n\r\n setTimeout(() => {\r\n spans.forEach((span: HTMLElement, index: number) => {\r\n setTimeout(() => {\r\n span.style.opacity = \"1\";\r\n span.style.filter = props.filter ? \"blur(0px)\" : \"none\";\r\n }, index * 200);\r\n });\r\n }, props.delay);\r\n }\r\n});\r\n</script>\r\n"
12
+ }
13
+ ],
14
+ "fileCount": 2,
15
+ "contentHash": "abdd4143f1d595254878c29913d35fe94c68628f"
16
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "text-glitch",
3
+ "dependencies": [],
4
+ "files": [
5
+ {
6
+ "path": "TextGlitch.vue",
7
+ "content": "<template>\r\n <div\r\n class=\"glitch\"\r\n :class=\"[hoverClass, props.class]\"\r\n :data-text=\"text\"\r\n >\r\n {{ text }}\r\n </div>\r\n</template>\r\n\r\n<script lang=\"ts\" setup>\r\nimport type { HTMLAttributes } from \"vue\";\r\n\r\ninterface Props {\r\n text?: string;\r\n speed?: number;\r\n enableShadows?: boolean;\r\n enableOnHover?: boolean;\r\n class?: HTMLAttributes[\"class\"];\r\n}\r\n\r\nconst {\r\n speed = 0.5,\r\n enableShadows = true,\r\n enableOnHover = false,\r\n text = \"\",\r\n ...props\r\n} = defineProps<Props>();\r\nconst afterDuration = computed(() => `${speed * 3}s`);\r\nconst beforeDuration = computed(() => `${speed * 2}s`);\r\nconst afterShadow = computed(() => (enableShadows ? \"-5px 0 red\" : \"none\"));\r\nconst beforeShadow = computed(() => (enableShadows ? \"5px 0 cyan\" : \"none\"));\r\n\r\nconst hoverClass = computed(() => (enableOnHover ? \"enable-on-hover\" : \"\"));\r\n</script>\r\n\r\n<style scoped>\r\n.glitch {\r\n color: #fff;\r\n font-size: clamp(2rem, 10vw, 6rem);\r\n white-space: nowrap;\r\n font-weight: 900;\r\n position: relative;\r\n margin: 0 auto;\r\n user-select: none;\r\n cursor: pointer;\r\n}\r\n\r\n.glitch::after,\r\n.glitch::before {\r\n content: attr(data-text);\r\n position: absolute;\r\n top: 0;\r\n color: #fff;\r\n background-color: #060606;\r\n overflow: hidden;\r\n clip-path: inset(0 0 0 0);\r\n}\r\n\r\n.glitch:not(.enable-on-hover)::after {\r\n left: 10px;\r\n text-shadow: v-bind(afterShadow);\r\n animation: animate-glitch v-bind(afterDuration) infinite linear alternate-reverse;\r\n}\r\n.glitch:not(.enable-on-hover)::before {\r\n left: -10px;\r\n text-shadow: v-bind(beforeShadow);\r\n animation: animate-glitch v-bind(beforeDuration) infinite linear alternate-reverse;\r\n}\r\n\r\n.glitch.enable-on-hover::after,\r\n.glitch.enable-on-hover::before {\r\n content: \"\";\r\n opacity: 0;\r\n animation: none;\r\n}\r\n\r\n.glitch.enable-on-hover:hover::after {\r\n content: attr(data-text);\r\n opacity: 1;\r\n left: 10px;\r\n text-shadow: v-bind(afterShadow);\r\n animation: animate-glitch v-bind(afterDuration) infinite linear alternate-reverse;\r\n}\r\n.glitch.enable-on-hover:hover::before {\r\n content: attr(data-text);\r\n opacity: 1;\r\n left: -10px;\r\n text-shadow: v-bind(beforeShadow);\r\n animation: animate-glitch v-bind(beforeDuration) infinite linear alternate-reverse;\r\n}\r\n\r\n@keyframes animate-glitch {\r\n 0% {\r\n clip-path: inset(20% 0 50% 0);\r\n }\r\n 5% {\r\n clip-path: inset(10% 0 60% 0);\r\n }\r\n 10% {\r\n clip-path: inset(15% 0 55% 0);\r\n }\r\n 15% {\r\n clip-path: inset(25% 0 35% 0);\r\n }\r\n 20% {\r\n clip-path: inset(30% 0 40% 0);\r\n }\r\n 25% {\r\n clip-path: inset(40% 0 20% 0);\r\n }\r\n 30% {\r\n clip-path: inset(10% 0 60% 0);\r\n }\r\n 35% {\r\n clip-path: inset(15% 0 55% 0);\r\n }\r\n 40% {\r\n clip-path: inset(25% 0 35% 0);\r\n }\r\n 45% {\r\n clip-path: inset(30% 0 40% 0);\r\n }\r\n 50% {\r\n clip-path: inset(20% 0 50% 0);\r\n }\r\n 55% {\r\n clip-path: inset(10% 0 60% 0);\r\n }\r\n 60% {\r\n clip-path: inset(15% 0 55% 0);\r\n }\r\n 65% {\r\n clip-path: inset(25% 0 35% 0);\r\n }\r\n 70% {\r\n clip-path: inset(30% 0 40% 0);\r\n }\r\n 75% {\r\n clip-path: inset(40% 0 20% 0);\r\n }\r\n 80% {\r\n clip-path: inset(20% 0 50% 0);\r\n }\r\n 85% {\r\n clip-path: inset(10% 0 60% 0);\r\n }\r\n 90% {\r\n clip-path: inset(15% 0 55% 0);\r\n }\r\n 95% {\r\n clip-path: inset(25% 0 35% 0);\r\n }\r\n 100% {\r\n clip-path: inset(30% 0 40% 0);\r\n }\r\n}\r\n</style>\r\n"
8
+ }
9
+ ],
10
+ "fileCount": 1,
11
+ "contentHash": "b96acdb4df3fa8beb8b2ec1dd742c4a421f0e5b1"
12
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "text-highlight",
3
+ "dependencies": [],
4
+ "files": [
5
+ {
6
+ "path": "index.ts",
7
+ "content": "export { default as TextHighlight } from \"./TextHighlight.vue\";\r\n"
8
+ },
9
+ {
10
+ "path": "TextHighlight.vue",
11
+ "content": "<template>\r\n <span :class=\"cn('inline-block px-1 pb-1', props.class)\"><slot /></span>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { computed, type HTMLAttributes } from \"vue\";\r\nimport { cn } from \"@/lib/utils\";\r\n\r\ninterface Props {\r\n delay?: number;\r\n duration?: number;\r\n class?: HTMLAttributes[\"class\"];\r\n textEndColor?: string;\r\n}\r\n\r\nconst props = withDefaults(defineProps<Props>(), {\r\n delay: 0,\r\n duration: 2000,\r\n endColor: \"inherit\",\r\n});\r\n\r\nconst delayMs = computed(() => `${props.delay}ms`);\r\nconst durationMs = computed(() => `${props.duration}ms`);\r\n</script>\r\n\r\n<style scoped>\r\n@keyframes background-expand {\r\n 0% {\r\n background-size: 0% 100%;\r\n }\r\n 100% {\r\n background-size: 100% 100%;\r\n }\r\n}\r\n\r\n@keyframes text-color-change {\r\n 0% {\r\n color: inherit;\r\n }\r\n 100% {\r\n color: v-bind(textEndColor);\r\n }\r\n}\r\n\r\nspan {\r\n background-size: 0% 100%;\r\n background-repeat: no-repeat;\r\n background-position: left center;\r\n animation:\r\n background-expand v-bind(durationMs) ease-in-out v-bind(delayMs) forwards,\r\n text-color-change v-bind(durationMs) ease-in-out v-bind(delayMs) forwards;\r\n}\r\n</style>\r\n"
12
+ }
13
+ ],
14
+ "fileCount": 2,
15
+ "contentHash": "d5711c025d39981f7dc59a59fcaab3ce47313dd6"
16
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "text-hover-effect",
3
+ "dependencies": [],
4
+ "files": [
5
+ {
6
+ "path": "index.ts",
7
+ "content": "export { default as TextHoverEffect } from \"./TextHoverEffect.vue\";\r\n"
8
+ },
9
+ {
10
+ "path": "TextHoverEffect.vue",
11
+ "content": "<template>\r\n <svg\r\n ref=\"svgRef\"\r\n width=\"100%\"\r\n height=\"100%\"\r\n viewBox=\"0 0 300 100\"\r\n xmlns=\"http://www.w3.org/2000/svg\"\r\n class=\"select-none\"\r\n @mouseenter=\"handleMouseEnter\"\r\n @mouseleave=\"handleMouseLeave\"\r\n @mousemove=\"handleMouseMove\"\r\n @touchstart=\"handleTouchStart\"\r\n @touchmove=\"handleTouchMove\"\r\n @touchend=\"handleTouchEnd\"\r\n >\r\n <defs>\r\n <linearGradient\r\n id=\"textGradient\"\r\n gradientUnits=\"userSpaceOnUse\"\r\n cx=\"50%\"\r\n cy=\"50%\"\r\n r=\"25%\"\r\n >\r\n <stop\r\n v-if=\"hovered\"\r\n offset=\"0%\"\r\n stop-color=\"var(--yellow-500)\"\r\n />\r\n <stop\r\n v-if=\"hovered\"\r\n offset=\"25%\"\r\n stop-color=\"var(--red-500)\"\r\n />\r\n <stop\r\n v-if=\"hovered\"\r\n offset=\"50%\"\r\n stop-color=\"var(--blue-500)\"\r\n />\r\n <stop\r\n v-if=\"hovered\"\r\n offset=\"75%\"\r\n stop-color=\"var(--cyan-500)\"\r\n />\r\n <stop\r\n v-if=\"hovered\"\r\n offset=\"100%\"\r\n stop-color=\"var(--violet-500)\"\r\n />\r\n </linearGradient>\r\n\r\n <!-- Radial Gradient -->\r\n <radialGradient\r\n id=\"revealMask\"\r\n gradientUnits=\"userSpaceOnUse\"\r\n r=\"20%\"\r\n :cx=\"maskPosition.cx\"\r\n :cy=\"maskPosition.cy\"\r\n :style=\"{\r\n transition: `cx ${transitionDuration}ms ease-out, cy ${transitionDuration}ms ease-out`,\r\n }\"\r\n >\r\n <stop\r\n offset=\"0%\"\r\n stop-color=\"white\"\r\n />\r\n <stop\r\n offset=\"100%\"\r\n stop-color=\"black\"\r\n />\r\n </radialGradient>\r\n\r\n <mask id=\"textMask\">\r\n <rect\r\n x=\"0\"\r\n y=\"0\"\r\n width=\"100%\"\r\n height=\"100%\"\r\n fill=\"url(#revealMask)\"\r\n />\r\n </mask>\r\n </defs>\r\n\r\n <text\r\n x=\"50%\"\r\n y=\"50%\"\r\n text-anchor=\"middle\"\r\n dominant-baseline=\"middle\"\r\n :stroke-width=\"strokeWidth\"\r\n :style=\"{ opacity: hovered ? opacity : 0 }\"\r\n class=\"fill-transparent stroke-neutral-200 font-[helvetica] text-7xl font-bold dark:stroke-neutral-800\"\r\n >\r\n {{ text }}\r\n </text>\r\n\r\n <!-- Animated Text Stroke -->\r\n <text\r\n x=\"50%\"\r\n y=\"50%\"\r\n text-anchor=\"middle\"\r\n dominant-baseline=\"middle\"\r\n :stroke-width=\"strokeWidth\"\r\n :style=\"strokeStyle\"\r\n class=\"fill-transparent stroke-neutral-200 font-[helvetica] text-7xl font-bold dark:stroke-neutral-800\"\r\n >\r\n {{ text }}\r\n </text>\r\n\r\n <text\r\n x=\"50%\"\r\n y=\"50%\"\r\n text-anchor=\"middle\"\r\n dominant-baseline=\"middle\"\r\n stroke=\"url(#textGradient)\"\r\n :stroke-width=\"strokeWidth\"\r\n mask=\"url(#textMask)\"\r\n class=\"fill-transparent font-[helvetica] text-7xl font-bold\"\r\n >\r\n {{ text }}\r\n </text>\r\n </svg>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { ref, reactive, computed } from \"vue\";\r\n\r\ninterface Props {\r\n strokeWidth?: number;\r\n text: string;\r\n duration?: number;\r\n opacity?: number;\r\n}\r\n\r\nconst props = withDefaults(defineProps<Props>(), {\r\n strokeWidth: 0.75,\r\n duration: 200,\r\n opacity: 0.75,\r\n});\r\n\r\nconst svgRef = ref<SVGSVGElement | null>(null);\r\nconst cursor = reactive({ x: 0, y: 0 });\r\nconst hovered = ref(false);\r\n\r\n// Set transition duration for smoother animation\r\nconst transitionDuration = props.duration ? props.duration * 1000 : 200;\r\n\r\n// Reactive gradient position\r\nconst maskPosition = computed(() => {\r\n if (svgRef.value) {\r\n const svgRect = svgRef.value.getBoundingClientRect();\r\n const cxPercentage = ((cursor.x - svgRect.left) / svgRect.width) * 100;\r\n const cyPercentage = ((cursor.y - svgRect.top) / svgRect.height) * 100;\r\n return { cx: `${cxPercentage}%`, cy: `${cyPercentage}%` };\r\n }\r\n return { cx: \"50%\", cy: \"50%\" }; // Default position\r\n});\r\n\r\n// Reactive style for stroke animation\r\nconst strokeStyle = computed(() => ({\r\n strokeDashoffset: hovered.value ? \"0\" : \"1000\",\r\n strokeDasharray: \"1000\",\r\n transition: \"stroke-dashoffset 4s ease-in-out, stroke-dasharray 4s ease-in-out\",\r\n}));\r\n\r\nfunction handleMouseEnter() {\r\n hovered.value = true;\r\n}\r\n\r\nfunction handleMouseLeave() {\r\n hovered.value = false;\r\n}\r\n\r\nfunction handleMouseMove(e: MouseEvent) {\r\n cursor.x = e.clientX;\r\n cursor.y = e.clientY;\r\n}\r\n\r\n// Touch support\r\nfunction handleTouchStart(e: TouchEvent) {\r\n hovered.value = true;\r\n handleTouchMove(e); // Update the position on touch start\r\n}\r\n\r\nfunction handleTouchMove(e: TouchEvent) {\r\n const touch = e.touches[0];\r\n cursor.x = touch.clientX;\r\n cursor.y = touch.clientY;\r\n}\r\n\r\nfunction handleTouchEnd() {\r\n hovered.value = false;\r\n}\r\n</script>\r\n\r\n<style scoped>\r\n.select-none {\r\n user-select: none;\r\n}\r\n</style>\r\n"
12
+ }
13
+ ],
14
+ "fileCount": 2,
15
+ "contentHash": "771a321fbf3b04835564cc4a4ea81662e97fd170"
16
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "text-reveal-card",
3
+ "dependencies": [],
4
+ "files": [
5
+ {
6
+ "path": "index.ts",
7
+ "content": "export { default as TextRevealCard } from \"./TextRevealCard.vue\";\r\n"
8
+ },
9
+ {
10
+ "path": "TextRevealCard.vue",
11
+ "content": "<template>\r\n <div\r\n ref=\"cardRef\"\r\n :class=\"[\r\n 'relative w-full max-w-[40rem] overflow-hidden rounded-lg border border-white/[0.08] bg-[#1d1c20] p-4 md:p-8 sm:p-6',\r\n props.class,\r\n ]\"\r\n @mouseenter=\"mouseEnterHandler\"\r\n @mouseleave=\"mouseLeaveHandler\"\r\n @mousemove=\"mouseMoveHandler\"\r\n @touchstart=\"mouseEnterHandler\"\r\n @touchend=\"mouseLeaveHandler\"\r\n @touchmove=\"touchMoveHandler\"\r\n >\r\n <slot name=\"header\"></slot>\r\n <div class=\"relative flex h-40 items-center overflow-hidden\">\r\n <div\r\n :style=\"{\r\n width: '100%',\r\n opacity: widthPercentage > 0 ? 1 : 0,\r\n clipPath: `inset(0 ${100 - widthPercentage}% 0 0)`,\r\n transition: isMouseOver ? 'none' : 'all 0.4s ease-out',\r\n }\"\r\n class=\"absolute z-20 bg-[#1d1c20] will-change-transform\"\r\n >\r\n <slot name=\"text\" />\r\n </div>\r\n\r\n <div\r\n :style=\"{\r\n left: `${widthPercentage}%`,\r\n transform: `rotate(${rotateDeg}deg)`,\r\n opacity: widthPercentage > 0 ? 1 : 0,\r\n transition: isMouseOver ? 'none' : 'all 0.4s ease-out',\r\n }\"\r\n class=\"absolute z-50 h-40 w-[8px] bg-gradient-to-b from-transparent via-neutral-800 to-transparent will-change-transform\"\r\n ></div>\r\n\r\n <div\r\n class=\"overflow-hidden [mask-image:linear-gradient(to_bottom,transparent,white,transparent)]\"\r\n >\r\n <slot name=\"revealText\"></slot>\r\n\r\n <TextRevealStars\r\n :stars-count=\"starsCount\"\r\n :class=\"starsClass\"\r\n />\r\n </div>\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { ref, computed, onMounted } from \"vue\";\r\nimport type { HTMLAttributes } from \"vue\";\r\n\r\ninterface Props {\r\n class?: HTMLAttributes[\"class\"];\r\n starsCount?: number;\r\n starsClass?: HTMLAttributes[\"class\"];\r\n}\r\nconst props = withDefaults(defineProps<Props>(), {\r\n starsCount: 130,\r\n});\r\n\r\nconst cardRef = ref<HTMLElement | null>(null);\r\nconst widthPercentage = ref(0);\r\nconst left = ref(0);\r\nconst localWidth = ref(0);\r\nconst isMouseOver = ref(false);\r\n\r\nconst rotateDeg = computed(() => (widthPercentage.value - 50) * 0.1);\r\n\r\nonMounted(() => {\r\n if (cardRef.value) {\r\n const rect = cardRef.value.getBoundingClientRect();\r\n left.value = rect.left;\r\n localWidth.value = rect.width;\r\n }\r\n\r\n window.addEventListener(\"resize\", updateMeasurements);\r\n});\r\n\r\nfunction updateMeasurements() {\r\n if (cardRef.value) {\r\n const rect = cardRef.value.getBoundingClientRect();\r\n left.value = rect.left;\r\n localWidth.value = rect.width;\r\n }\r\n}\r\n\r\nfunction mouseMoveHandler(event: MouseEvent) {\r\n event.preventDefault();\r\n if (cardRef.value) {\r\n const rect = cardRef.value.getBoundingClientRect(); // Get current position\r\n const relativeX = event.clientX - rect.left;\r\n widthPercentage.value = (relativeX / rect.width) * 100;\r\n }\r\n}\r\n\r\nfunction mouseLeaveHandler() {\r\n isMouseOver.value = false;\r\n setTimeout(() => {\r\n if (!isMouseOver.value) {\r\n widthPercentage.value = 0;\r\n }\r\n }, 100);\r\n}\r\n\r\nfunction mouseEnterHandler() {\r\n isMouseOver.value = true;\r\n}\r\n\r\nfunction touchMoveHandler(event: TouchEvent) {\r\n event.preventDefault();\r\n if (cardRef.value) {\r\n const rect = cardRef.value.getBoundingClientRect();\r\n const relativeX = event.touches[0]!.clientX - rect.left;\r\n widthPercentage.value = (relativeX / rect.width) * 100;\r\n }\r\n}\r\n</script>\r\n"
12
+ },
13
+ {
14
+ "path": "TextRevealStars.vue",
15
+ "content": "<template>\r\n <div class=\"absolute inset-0\">\r\n <Motion\r\n is=\"span\"\r\n v-for=\"i in starsCount\"\r\n :key=\"`star-${i}`\"\r\n :initial=\"generatePosition()\"\r\n :animate=\"generateEnterAnimation()\"\r\n :transition=\"{\r\n duration: randomDuration,\r\n repeat: Infinity,\r\n ease: 'linear',\r\n }\"\r\n :class=\"cn('inline-block absolute w-0.5 h-0.5 bg-white rounded-full z-[1]', props.class)\"\r\n />\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { cn } from \"@/lib/utils\";\r\nimport type { HTMLAttributes } from \"vue\";\r\n\r\ninterface Props {\r\n starsCount?: number;\r\n class?: HTMLAttributes[\"class\"];\r\n}\r\n\r\nconst props = withDefaults(defineProps<Props>(), {\r\n starsCount: 130,\r\n});\r\n\r\nfunction randomMove() {\r\n return Math.random() * 4 - 2;\r\n}\r\n\r\nfunction randomOpacity() {\r\n return Math.random();\r\n}\r\n\r\nfunction random() {\r\n return Math.random();\r\n}\r\n\r\nfunction generatePosition() {\r\n return {\r\n top: `calc(${random() * 100}% + ${randomMove()}px)`,\r\n left: `calc(${random() * 100}% + ${randomMove()}px)`,\r\n };\r\n}\r\n\r\nfunction generateEnterAnimation() {\r\n return {\r\n top: `calc(${random() * 100}% + ${randomMove()}px)`,\r\n left: `calc(${random() * 100}% + ${randomMove()}px)`,\r\n opacity: randomOpacity(),\r\n scale: [1, 1.2, 0],\r\n };\r\n}\r\n\r\nconst randomDuration = random() * 10 + 20;\r\n</script>\r\n"
16
+ }
17
+ ],
18
+ "fileCount": 3,
19
+ "contentHash": "300b6eee738ef95d17f2fe98ce48d57ba50d5f0a"
20
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "text-reveal",
3
+ "dependencies": [
4
+ "gsap"
5
+ ],
6
+ "files": [
7
+ {
8
+ "path": "index.ts",
9
+ "content": "export { default as TextReveal } from \"./TextReveal.vue\";\r\n"
10
+ },
11
+ {
12
+ "path": "TextReveal.vue",
13
+ "content": "<template>\r\n <div :class=\"['overflow-hidden', props.containerClass]\">\r\n <div\r\n ref=\"textContainer\"\r\n :class=\"[props.class]\"\r\n >\r\n <slot />\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script lang=\"ts\" setup>\r\nimport { ref, onMounted, onUnmounted } from \"vue\";\r\nimport { gsap } from \"gsap\";\r\nimport { SplitText } from \"gsap/SplitText\";\r\nimport type { HTMLAttributes } from \"vue\";\r\n\r\ninterface Props {\r\n class?: HTMLAttributes[\"class\"];\r\n containerClass?: HTMLAttributes[\"class\"];\r\n duration?: number;\r\n delay?: number;\r\n stagger?: number;\r\n}\r\n\r\ngsap.registerPlugin(SplitText);\r\n\r\nconst props = withDefaults(defineProps<Props>(), {\r\n duration: 0.6,\r\n delay: 0.2,\r\n stagger: 0.1,\r\n});\r\n\r\nconst textContainer = ref<HTMLElement | null>(null);\r\nlet split: gsap.core.Tween;\r\n\r\nonMounted(() => {\r\n if (!textContainer.value) return;\r\n\r\n gsap.set(textContainer.value, { opacity: 1 });\r\n\r\n SplitText.create(textContainer.value, {\r\n type: \"words,lines\",\r\n linesClass: \"line\",\r\n autoSplit: true,\r\n mask: \"lines\",\r\n onSplit: (splitText) => {\r\n split = gsap.from(splitText.lines, {\r\n duration: props.duration,\r\n delay: props.delay,\r\n yPercent: 100,\r\n opacity: 0,\r\n stagger: props.stagger,\r\n ease: \"expo.out\",\r\n });\r\n },\r\n });\r\n});\r\n\r\nonUnmounted(() => {\r\n split?.kill();\r\n});\r\n</script>\r\n\r\n<style scoped>\r\n.line {\r\n overflow: hidden;\r\n}\r\n</style>\r\n"
14
+ }
15
+ ],
16
+ "fileCount": 2,
17
+ "contentHash": "ebfc7654b0f29b8d197cf5496a57fa34e79c7c89"
18
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "text-scroll-reveal",
3
+ "dependencies": [],
4
+ "files": [
5
+ {
6
+ "path": "index.ts",
7
+ "content": "export { default as TextScrollReveal } from \"./TextScrollReveal.vue\";\r\n"
8
+ },
9
+ {
10
+ "path": "ScrollWord.vue",
11
+ "content": "<template>\r\n <span class=\"xl:lg-3 relative mx-1 lg:mx-2.5\">\r\n <span class=\"absolute opacity-30 dark:opacity-70\">{{ word }}</span>\r\n <span\r\n :style=\"{ opacity: computedOpacity }\"\r\n class=\"text-black dark:text-white\"\r\n >\r\n {{ word }}\r\n </span>\r\n </span>\r\n</template>\r\n\r\n<script lang=\"ts\" setup>\r\nimport { computed } from \"vue\";\r\n\r\ninterface Props {\r\n word: string;\r\n progress: number;\r\n range: Array<number>;\r\n}\r\n\r\nconst props = defineProps<Props>();\r\n\r\nconst computedOpacity = computed(() => {\r\n const [start, end] = props.range;\r\n const progress = props.progress;\r\n\r\n // Calculate opacity based on the progress and range\r\n if (progress < start) return 0;\r\n if (progress > end) return 1;\r\n\r\n // Linear interpolation for opacity between 0 and 1\r\n return (progress - start) / (end - start);\r\n});\r\n</script>\r\n"
12
+ },
13
+ {
14
+ "path": "TextScrollReveal.vue",
15
+ "content": "<template>\r\n <div\r\n ref=\"textScrollRevealRef\"\r\n :class=\"cn('relative z-0 h-[200vh]', $props.class)\"\r\n >\r\n <div class=\"sticky top-0 mx-auto flex h-1/2 max-w-4xl items-center bg-transparent px-4 py-20\">\r\n <p\r\n class=\"flex flex-wrap p-5 text-2xl font-bold text-black/20 xl:text-5xl lg:p-10 lg:text-4xl md:p-8 md:text-3xl dark:text-white/20\"\r\n >\r\n <ScrollWord\r\n v-for=\"(word, i) in words\"\r\n :key=\"i\"\r\n :word=\"word\"\r\n :progress=\"scrollYProgress\"\r\n :range=\"[i / words.length, (i + 1) / words.length]\"\r\n />\r\n </p>\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script lang=\"ts\" setup>\r\nimport { ref, computed, onMounted, onUnmounted } from \"vue\";\r\nimport { cn } from \"@/lib/utils\";\r\n\r\ninterface Props {\r\n class?: string;\r\n text: string;\r\n}\r\n\r\n// Props\r\nconst props = defineProps<Props>();\r\n\r\nconst textScrollRevealRef = ref<HTMLElement | null>(null);\r\n\r\nconst words = computed(() => props.text.split(\" \"));\r\n\r\nconst scrollYProgress = ref(0);\r\n\r\nfunction updateScrollYProgress() {\r\n if (textScrollRevealRef.value) {\r\n const boundingRect = textScrollRevealRef.value.getBoundingClientRect();\r\n const windowHeight = window.innerHeight;\r\n\r\n scrollYProgress.value = (boundingRect.y / windowHeight) * -1;\r\n }\r\n}\r\n\r\nonMounted(() => {\r\n window.addEventListener(\"scroll\", updateScrollYProgress);\r\n window.addEventListener(\"resize\", updateScrollYProgress);\r\n updateScrollYProgress();\r\n});\r\n\r\nonUnmounted(() => {\r\n window.removeEventListener(\"scroll\", updateScrollYProgress);\r\n window.removeEventListener(\"resize\", updateScrollYProgress);\r\n});\r\n</script>\r\n"
16
+ }
17
+ ],
18
+ "fileCount": 3,
19
+ "contentHash": "b23631027a070a2dea158c66e6ba68563dcfcf86"
20
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "timeline",
3
+ "dependencies": [
4
+ "motion-v"
5
+ ],
6
+ "files": [
7
+ {
8
+ "path": "index.ts",
9
+ "content": "export { default as Timeline } from \"./Timeline.vue\";\r\n"
10
+ },
11
+ {
12
+ "path": "Timeline.vue",
13
+ "content": "<template>\r\n <div\r\n ref=\"timelineContainerRef\"\r\n class=\"w-full bg-white font-sans md:px-10 dark:bg-neutral-950\"\r\n >\r\n <div class=\"mx-auto max-w-7xl px-4 py-20 lg:px-10 md:px-8\">\r\n <h2 class=\"mb-4 max-w-4xl text-lg text-black md:text-4xl dark:text-white\">\r\n {{ title }}\r\n </h2>\r\n <p class=\"max-w-sm text-sm text-neutral-700 md:text-base dark:text-neutral-300\">\r\n {{ description }}\r\n </p>\r\n </div>\r\n\r\n <div\r\n ref=\"timelineRef\"\r\n class=\"relative z-0 mx-auto max-w-7xl pb-20\"\r\n >\r\n <div\r\n v-for=\"(item, index) in props.items\"\r\n :key=\"item.id + index\"\r\n class=\"flex justify-start pt-10 md:gap-10 md:pt-40\"\r\n >\r\n <div\r\n class=\"sticky top-40 z-40 flex max-w-xs flex-col items-center self-start lg:max-w-sm md:w-full md:flex-row\"\r\n >\r\n <div\r\n class=\"absolute left-3 flex size-10 items-center justify-center rounded-full bg-white md:left-3 dark:bg-black\"\r\n >\r\n <div\r\n class=\"size-4 rounded-full border border-neutral-300 bg-neutral-200 p-2 dark:border-neutral-700 dark:bg-neutral-800\"\r\n />\r\n </div>\r\n <h3\r\n class=\"hidden text-xl font-bold text-neutral-500 md:block md:pl-20 md:text-5xl dark:text-neutral-500\"\r\n >\r\n {{ item.label }}\r\n </h3>\r\n </div>\r\n <slot :name=\"item.id\"></slot>\r\n </div>\r\n <div\r\n :style=\"{\r\n height: height + 'px',\r\n }\"\r\n class=\"absolute left-8 top-0 w-[2px] overflow-hidden bg-[linear-gradient(to_bottom,var(--tw-gradient-stops))] from-transparent from-0% via-neutral-200 to-transparent to-[99%] [mask-image:linear-gradient(to_bottom,transparent_0%,black_10%,black_90%,transparent_100%)] md:left-8 dark:via-neutral-700\"\r\n >\r\n <Motion\r\n as=\"div\"\r\n :style=\"{\r\n height: heightTransform,\r\n opacity: opacityTransform,\r\n }\"\r\n class=\"absolute inset-x-0 top-0 w-[2px] rounded-full bg-gradient-to-t from-purple-500 from-0% via-blue-500 via-10% to-transparent\"\r\n >\r\n </Motion>\r\n </div>\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script lang=\"ts\" setup>\r\nimport { Motion, useScroll, useTransform } from \"motion-v\";\r\nimport type { HTMLAttributes } from \"vue\";\r\n\r\ninterface Props {\r\n containerClass?: HTMLAttributes[\"class\"];\r\n class?: HTMLAttributes[\"class\"];\r\n items?: {\r\n id: string;\r\n label: string;\r\n }[];\r\n title?: string;\r\n description?: string;\r\n}\r\n\r\nconst props = withDefaults(defineProps<Props>(), {\r\n items: () => [],\r\n});\r\n\r\nconst timelineContainerRef = ref<HTMLElement | null>(null);\r\nconst timelineRef = ref<HTMLElement | null>(null);\r\nconst height = ref(0);\r\n\r\nonMounted(async () => {\r\n await nextTick();\r\n if (timelineRef.value) {\r\n const rect = timelineRef.value.getBoundingClientRect();\r\n height.value = rect.height;\r\n }\r\n});\r\n\r\nconst { scrollYProgress } = useScroll({\r\n target: timelineRef,\r\n offset: [\"start 10%\", \"end 50%\"],\r\n});\r\n\r\nconst opacityTransform = useTransform(scrollYProgress, [0, 0.1], [0, 1]);\r\nconst heightTransform = ref(useTransform(scrollYProgress, [0, 1], [0, 0]));\r\n\r\nwatch(height, (newHeight) => {\r\n heightTransform.value = useTransform(scrollYProgress, [0, 1], [0, newHeight]);\r\n});\r\n</script>\r\n"
14
+ }
15
+ ],
16
+ "fileCount": 2,
17
+ "contentHash": "fd93acc7efd2b3591b1931c6c597a8e5f1d1c902"
18
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "tracing-beam",
3
+ "dependencies": [
4
+ "motion-v",
5
+ "vue-use-spring"
6
+ ],
7
+ "files": [
8
+ {
9
+ "path": "index.ts",
10
+ "content": "export { default as TracingBeam } from \"./TracingBeam.vue\";\r\n"
11
+ },
12
+ {
13
+ "path": "TracingBeam.vue",
14
+ "content": "<template>\r\n <div\r\n ref=\"tracingBeamRef\"\r\n :class=\"cn('relative w-full max-w-4xl mx-auto h-full', $props.class)\"\r\n >\r\n <div class=\"absolute -left-4 top-3 md:-left-12\">\r\n <div\r\n :style=\"{\r\n boxShadow: scrollYProgress > 0 ? 'none' : 'rgba(0, 0, 0, 0.24) 0px 3px 8px',\r\n }\"\r\n class=\"border-netural-200 ml-[27px] flex size-4 items-center justify-center rounded-full border shadow-sm\"\r\n >\r\n <Motion\r\n :animate=\"{\r\n backgroundColor: scrollYProgress > 0 ? 'white' : 'var(--emerald-500)',\r\n borderColor: scrollYProgress > 0 ? 'white' : 'var(--emerald-600)',\r\n }\"\r\n class=\"size-2 rounded-full border border-neutral-300 bg-white\"\r\n />\r\n </div>\r\n <svg\r\n :viewBox=\"`0 0 20 ${svgHeight}`\"\r\n width=\"20\"\r\n :height=\"svgHeight\"\r\n class=\"ml-4 block\"\r\n aria-hidden=\"true\"\r\n >\r\n <path\r\n :d=\"`M 1 0V -36 l 18 24 V ${svgHeight * 0.8} l -18 24V ${svgHeight}`\"\r\n fill=\"none\"\r\n stroke=\"#9091A0\"\r\n stroke-opacity=\"0.16\"\r\n ></path>\r\n <path\r\n :d=\"`M 1 0V -36 l 18 24 V ${svgHeight * 0.8} l -18 24V ${svgHeight}`\"\r\n fill=\"none\"\r\n stroke=\"url(#gradient)\"\r\n stroke-width=\"1.25\"\r\n class=\"motion-reduce:hidden\"\r\n ></path>\r\n <defs>\r\n <linearGradient\r\n id=\"gradient\"\r\n gradientUnits=\"userSpaceOnUse\"\r\n x1=\"0\"\r\n x2=\"0\"\r\n :y1=\"spring.y1\"\r\n :y2=\"spring.y2\"\r\n >\r\n <stop\r\n stop-color=\"#18CCFC\"\r\n stop-opacity=\"0\"\r\n ></stop>\r\n <stop stop-color=\"#18CCFC\"></stop>\r\n <stop\r\n offset=\"0.325\"\r\n stop-color=\"#6344F5\"\r\n ></stop>\r\n <stop\r\n offset=\"1\"\r\n stop-color=\"#AE48FF\"\r\n stop-opacity=\"0\"\r\n ></stop>\r\n </linearGradient>\r\n </defs>\r\n </svg>\r\n </div>\r\n <div ref=\"tracingBeamContentRef\">\r\n <slot />\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script lang=\"ts\" setup>\r\nimport { Motion } from \"motion-v\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport { useSpring } from \"vue-use-spring\";\r\nimport { ref, computed, onMounted, onUnmounted, watch } from \"vue\";\r\n\r\ndefineProps({\r\n class: String,\r\n});\r\n\r\nconst tracingBeamRef = ref<HTMLDivElement>();\r\nconst tracingBeamContentRef = ref<HTMLDivElement>();\r\n\r\nconst scrollYProgress = ref(0);\r\nconst svgHeight = ref(0);\r\nconst scrollPercentage = ref(0);\r\n\r\nconst computedY1 = computed(\r\n () =>\r\n mapRange(scrollYProgress.value, 0, 0.8, scrollYProgress.value, svgHeight.value) *\r\n (1.4 - scrollPercentage.value),\r\n);\r\n\r\nconst computedY2 = computed(\r\n () =>\r\n mapRange(scrollYProgress.value, 0, 1, scrollYProgress.value, svgHeight.value - 500) *\r\n (1.4 - scrollPercentage.value),\r\n);\r\n\r\nconst spring = useSpring(\r\n { y1: computedY1.value, y2: computedY2.value },\r\n { tension: 80, friction: 26, precision: 0.01 },\r\n);\r\n\r\nwatch(computedY1, (newY1) => {\r\n spring.y1 = newY1;\r\n});\r\n\r\nwatch(computedY2, (newY2) => {\r\n spring.y2 = newY2;\r\n});\r\n\r\nfunction updateScrollYProgress() {\r\n if (tracingBeamRef.value) {\r\n const boundingRect = tracingBeamRef.value.getBoundingClientRect();\r\n const windowHeight = window.innerHeight;\r\n const elementHeight = boundingRect.height;\r\n\r\n scrollPercentage.value = (windowHeight - boundingRect.top) / (windowHeight + elementHeight);\r\n\r\n scrollYProgress.value = (boundingRect.y / windowHeight) * -1;\r\n }\r\n}\r\n\r\nonMounted(() => {\r\n window.addEventListener(\"scroll\", updateScrollYProgress);\r\n window.addEventListener(\"resize\", updateScrollYProgress);\r\n updateScrollYProgress();\r\n\r\n const resizeObserver = new ResizeObserver(function () {\r\n updateSVGHeight();\r\n });\r\n\r\n resizeObserver.observe(tracingBeamContentRef.value!);\r\n\r\n updateSVGHeight();\r\n});\r\n\r\nonUnmounted(() => {\r\n tracingBeamRef.value?.removeEventListener(\"scroll\", updateScrollYProgress);\r\n window.removeEventListener(\"resize\", updateScrollYProgress);\r\n});\r\n\r\nfunction updateSVGHeight() {\r\n if (!tracingBeamContentRef.value) return;\r\n\r\n svgHeight.value = tracingBeamContentRef.value.offsetHeight;\r\n}\r\n\r\nfunction mapRange(\r\n value: number,\r\n inMin: number,\r\n inMax: number,\r\n outMin: number,\r\n outMax: number,\r\n): number {\r\n return ((value - inMin) * (outMax - outMin)) / (inMax - inMin) + outMin;\r\n}\r\n</script>\r\n"
15
+ }
16
+ ],
17
+ "fileCount": 2,
18
+ "contentHash": "24ee105d2713af163e94d167170b460079189612"
19
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "vanishing-input",
3
+ "dependencies": [
4
+ "@vueuse/core"
5
+ ],
6
+ "files": [
7
+ {
8
+ "path": "index.ts",
9
+ "content": "export { default as VanishingInput } from \"./VanishingInput.vue\";\r\n"
10
+ },
11
+ {
12
+ "path": "VanishingInput.vue",
13
+ "content": "<template>\r\n <form\r\n :class=\"[\r\n 'relative mx-auto h-12 w-full max-w-xl overflow-hidden rounded-full bg-white shadow-[0px_2px_3px_-1px_rgba(0,0,0,0.1),_0px_1px_0px_0px_rgba(25,28,33,0.02),_0px_0px_0px_1px_rgba(25,28,33,0.08)] transition duration-200 dark:bg-zinc-800',\r\n vanishingText && 'bg-gray-50',\r\n ]\"\r\n @submit.prevent=\"handleSubmit\"\r\n >\r\n <!-- Canvas Element -->\r\n <canvas\r\n ref=\"canvasRef\"\r\n :class=\"[\r\n 'pointer-events-none absolute left-2 top-[20%] origin-top-left scale-50 pr-20 text-base invert sm:left-8 dark:invert-0',\r\n animating ? 'opacity-100' : 'opacity-0',\r\n ]\"\r\n />\r\n\r\n <!-- Text Input -->\r\n <input\r\n ref=\"inputRef\"\r\n v-model=\"vanishingText\"\r\n :disabled=\"animating\"\r\n type=\"text\"\r\n class=\"relative z-50 size-full rounded-full border-none bg-transparent pl-4 pr-20 text-sm text-black focus:outline-none focus:ring-0 sm:pl-10 sm:text-base dark:text-white\"\r\n :class=\"{ 'text-transparent dark:text-transparent': animating }\"\r\n @keydown.enter=\"handleKeyDown\"\r\n />\r\n\r\n <!-- Submit Button -->\r\n <button\r\n :disabled=\"!vanishingText\"\r\n type=\"submit\"\r\n class=\"absolute right-2 top-1/2 z-50 flex size-8 -translate-y-1/2 items-center justify-center rounded-full bg-black transition duration-200 disabled:bg-gray-100 dark:bg-zinc-900 dark:disabled:bg-zinc-700\"\r\n >\r\n <svg\r\n xmlns=\"http://www.w3.org/2000/svg\"\r\n width=\"24\"\r\n height=\"24\"\r\n viewBox=\"0 0 24 24\"\r\n fill=\"none\"\r\n stroke=\"currentColor\"\r\n stroke-width=\"2\"\r\n stroke-linecap=\"round\"\r\n stroke-linejoin=\"round\"\r\n class=\"size-4 text-gray-300\"\r\n >\r\n <path\r\n stroke=\"none\"\r\n d=\"M0 0h24v24H0z\"\r\n fill=\"none\"\r\n />\r\n <path\r\n d=\"M5 12l14 0\"\r\n :style=\"{\r\n strokeDasharray: '50%',\r\n strokeDashoffset: vanishingText ? '0' : '50%',\r\n transition: 'stroke-dashoffset 0.3s linear',\r\n }\"\r\n />\r\n <path d=\"M13 18l6 -6\" />\r\n <path d=\"M13 6l6 6\" />\r\n </svg>\r\n </button>\r\n\r\n <!-- Placeholder Text -->\r\n <div class=\"pointer-events-none absolute inset-0 flex items-center rounded-full\">\r\n <Transition\r\n v-show=\"!vanishingText\"\r\n mode=\"out-in\"\r\n enter-active-class=\"transition duration-300 ease-out\"\r\n leave-active-class=\"transition duration-300 ease-in\"\r\n enter-from-class=\"opacity-0 translate-y-4\"\r\n enter-to-class=\"opacity-100 translate-y-0\"\r\n leave-from-class=\"opacity-100 translate-y-0\"\r\n leave-to-class=\"opacity-0 -translate-y-4\"\r\n >\r\n <p\r\n :key=\"currentPlaceholder\"\r\n class=\"w-[calc(100%-2rem)] truncate pl-4 text-left text-sm font-normal text-neutral-500 sm:pl-10 sm:text-base dark:text-zinc-500\"\r\n >\r\n {{ placeholders[currentPlaceholder] }}\r\n </p>\r\n </Transition>\r\n </div>\r\n </form>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { ref, onMounted, watch, onBeforeUnmount } from \"vue\";\r\nimport { templateRef } from \"@vueuse/core\";\r\n\r\n// Define interfaces for props and data structures\r\ninterface Props {\r\n placeholders?: string[];\r\n}\r\n\r\ninterface PixelData {\r\n x: number;\r\n y: number;\r\n color: string;\r\n}\r\n\r\ninterface AnimatedPixel extends PixelData {\r\n r: number;\r\n}\r\n\r\nconst vanishingText = defineModel<string>({\r\n default: \"\",\r\n});\r\nconst emit = defineEmits([\"submit\", \"change\"]);\r\n\r\nconst canvasRef = templateRef<HTMLCanvasElement>(\"canvasRef\");\r\nconst inputRef = templateRef<HTMLInputElement>(\"inputRef\");\r\n\r\n// normal refs\r\nconst currentPlaceholder = ref<number>(0);\r\nconst animating = ref<boolean>(false);\r\nconst intervalRef = ref<number | null>(null);\r\nconst newDataRef = ref<AnimatedPixel[]>([]);\r\nconst animationFrame = ref<number | null>(null);\r\n\r\n// props\r\nconst props = withDefaults(defineProps<Props>(), {\r\n placeholders: () => [\"Placeholder 1\", \"Placeholder 2\", \"Placeholder 3\"],\r\n});\r\n\r\n// Focus on input when mounted\r\nonMounted(() => {\r\n if (!inputRef.value) return;\r\n inputRef.value.focus();\r\n});\r\n\r\nfunction changePlaceholder(): void {\r\n intervalRef.value = window.setInterval(() => {\r\n currentPlaceholder.value = (currentPlaceholder.value + 1) % props.placeholders.length;\r\n }, 3000);\r\n}\r\n\r\nfunction handleVisibilityChange(): void {\r\n if (document.visibilityState !== \"visible\" && intervalRef.value) {\r\n clearInterval(intervalRef.value);\r\n intervalRef.value = null;\r\n } else if (document.visibilityState === \"visible\") {\r\n changePlaceholder();\r\n }\r\n}\r\n\r\nfunction draw(): void {\r\n if (!inputRef.value || !canvasRef.value) return;\r\n\r\n const canvas = canvasRef.value;\r\n const ctx = canvas.getContext(\"2d\");\r\n if (!ctx) return;\r\n\r\n const computedStyles = getComputedStyle(inputRef.value);\r\n\r\n canvas.width = 800;\r\n canvas.height = 800;\r\n ctx.clearRect(0, 0, 800, 800);\r\n\r\n const fontSize = parseFloat(computedStyles.getPropertyValue(\"font-size\"));\r\n ctx.font = `${fontSize * 2}px ${computedStyles.fontFamily}`;\r\n ctx.fillStyle = \"#FFF\";\r\n ctx.fillText(vanishingText.value, 16, 40);\r\n\r\n const imageData = ctx.getImageData(0, 0, 800, 800);\r\n const pixelData = imageData.data;\r\n const newData: PixelData[] = [];\r\n\r\n for (let t = 0; t < 800; t++) {\r\n let i = 4 * t * 800;\r\n for (let n = 0; n < 800; n++) {\r\n let e = i + 4 * n;\r\n if (pixelData[e] !== 0 && pixelData[e + 1] !== 0 && pixelData[e + 2] !== 0) {\r\n newData.push({\r\n x: n,\r\n y: t,\r\n color: `rgba(${pixelData[e]}, ${pixelData[e + 1]}, ${pixelData[e + 2]}, ${pixelData[e + 3]})`,\r\n });\r\n }\r\n }\r\n }\r\n newDataRef.value = newData.map(({ x, y, color }) => ({ x, y, r: 1, color }));\r\n}\r\n\r\nfunction animate(start: number = 0): void {\r\n animationFrame.value = requestAnimationFrame(() => {\r\n const newArr: AnimatedPixel[] = [];\r\n for (const current of newDataRef.value) {\r\n if (current.x < start) {\r\n newArr.push(current);\r\n } else {\r\n if (current.r <= 0) {\r\n current.r = 0;\r\n continue;\r\n }\r\n current.x += Math.random() > 0.5 ? 1 : -1;\r\n current.y += Math.random() > 0.5 ? 1 : -1;\r\n current.r -= 0.05 * Math.random();\r\n newArr.push(current);\r\n }\r\n }\r\n newDataRef.value = newArr;\r\n const ctx = canvasRef.value?.getContext(\"2d\");\r\n if (ctx) {\r\n ctx.clearRect(start, 0, 800, 800);\r\n newDataRef.value.forEach(({ x, y, r, color }) => {\r\n if (x > start) {\r\n ctx.beginPath();\r\n ctx.rect(x, y, r, r);\r\n ctx.fillStyle = color;\r\n ctx.strokeStyle = color;\r\n ctx.stroke();\r\n }\r\n });\r\n }\r\n if (newDataRef.value.length > 0) {\r\n animate(start - 8);\r\n } else {\r\n vanishingText.value = \"\";\r\n animating.value = false;\r\n setTimeout(() => {\r\n // regain focus after animation\r\n inputRef.value.focus();\r\n }, 100);\r\n }\r\n });\r\n}\r\n\r\nfunction handleKeyDown(e: KeyboardEvent): void {\r\n if (vanishingText.value === \"\") return;\r\n if (e.key === \"Enter\" && !animating.value) {\r\n vanishAndSubmit();\r\n }\r\n}\r\n\r\nfunction vanishAndSubmit(): void {\r\n animating.value = true;\r\n draw();\r\n if (vanishingText.value) {\r\n const maxX = Math.max(...newDataRef.value.map(({ x }) => x));\r\n animate(maxX);\r\n emit(\"submit\", vanishingText.value);\r\n }\r\n}\r\n\r\nfunction handleSubmit(): void {\r\n vanishAndSubmit();\r\n}\r\n\r\n// Watch for value changes\r\nwatch(vanishingText, (newVal: string) => {\r\n if (!animating.value) {\r\n emit(\"change\", { target: { value: newVal } });\r\n }\r\n});\r\n\r\nonMounted(() => {\r\n changePlaceholder();\r\n document.addEventListener(\"visibilitychange\", handleVisibilityChange);\r\n});\r\n\r\nonBeforeUnmount(() => {\r\n if (intervalRef.value) {\r\n clearInterval(intervalRef.value);\r\n }\r\n if (animationFrame.value) {\r\n cancelAnimationFrame(animationFrame.value);\r\n }\r\n document.removeEventListener(\"visibilitychange\", handleVisibilityChange);\r\n});\r\n</script>\r\n"
14
+ }
15
+ ],
16
+ "fileCount": 2,
17
+ "contentHash": "ba5236b6bf224bbe235f66c6e01d6b319cf35615"
18
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "video-text",
3
+ "dependencies": [],
4
+ "files": [
5
+ {
6
+ "path": "index.ts",
7
+ "content": "export { default as VideoText } from \"./VideoText.vue\";\r\n"
8
+ },
9
+ {
10
+ "path": "VideoText.vue",
11
+ "content": "<template>\r\n <div\r\n class=\"relative size-full\"\r\n :class=\"props.class\"\r\n >\r\n <div\r\n class=\"absolute inset-0 flex items-center justify-center\"\r\n :style=\"{\r\n maskImage: dataUrlMask,\r\n WebkitMaskImage: dataUrlMask,\r\n WebkitMaskSize: 'contain',\r\n maskRepeat: 'no-repeat',\r\n WebkitMaskRepeat: 'no-repeat',\r\n maskPosition: 'center',\r\n WebkitMaskPosition: 'center',\r\n }\"\r\n >\r\n <video\r\n class=\"size-full object-cover\"\r\n :autoplay=\"autoPlay\"\r\n :muted=\"muted\"\r\n :loop=\"loop\"\r\n :preload=\"preload\"\r\n >\r\n <source :src=\"src\" />\r\n Your browser does not support the video tag.\r\n </video>\r\n </div>\r\n <span class=\"sr-only\">{{ content }}</span>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { onMounted, onUnmounted, ref, watch, computed } from \"vue\";\r\n\r\ninterface Props {\r\n src: string;\r\n class?: string;\r\n autoPlay?: boolean;\r\n muted?: boolean;\r\n loop?: boolean;\r\n preload?: \"auto\" | \"metadata\" | \"none\";\r\n fontSize?: string | number;\r\n fontWeight?: string | number;\r\n textAnchor?: string;\r\n dominantBaseline?: string;\r\n fontFamily?: string;\r\n}\r\n\r\nconst props = withDefaults(defineProps<Props>(), {\r\n class: \"\",\r\n autoPlay: true,\r\n muted: true,\r\n loop: true,\r\n preload: \"auto\",\r\n fontSize: 20,\r\n fontWeight: \"bold\",\r\n textAnchor: \"middle\",\r\n dominantBaseline: \"middle\",\r\n fontFamily: \"sans-serif\",\r\n});\r\n\r\nconst defaultSlot = useSlots().default;\r\n\r\nconst content = computed(\r\n () =>\r\n defaultSlot?.()\r\n .map((vnode) => vnode.children)\r\n .join(\"\") ?? \"\",\r\n);\r\n\r\nconst svgMask = ref(\"\");\r\n\r\nconst dataUrlMask = computed(\r\n () => `url(\"data:image/svg+xml,${encodeURIComponent(svgMask.value)}\")`,\r\n);\r\n\r\nfunction updateSvgMask() {\r\n const responsiveFontSize =\r\n typeof props.fontSize === \"number\" ? `${props.fontSize}vw` : props.fontSize;\r\n svgMask.value = `<svg xmlns='http://www.w3.org/2000/svg' width='100%' height='100%'><text x='50%' y='50%' font-size='${responsiveFontSize}' font-weight='${props.fontWeight}' text-anchor='${props.textAnchor}' dominant-baseline='${props.dominantBaseline}' font-family='${props.fontFamily}'>${content.value}</text></svg>`;\r\n}\r\n\r\nwatch(content, updateSvgMask);\r\n\r\nwatch(\r\n () => [\r\n props.fontSize,\r\n props.fontWeight,\r\n props.textAnchor,\r\n props.dominantBaseline,\r\n props.fontFamily,\r\n ],\r\n updateSvgMask,\r\n);\r\n\r\nonMounted(() => {\r\n updateSvgMask();\r\n window.addEventListener(\"resize\", updateSvgMask);\r\n});\r\n\r\nonUnmounted(() => {\r\n window.removeEventListener(\"resize\", updateSvgMask);\r\n});\r\n</script>\r\n"
12
+ }
13
+ ],
14
+ "fileCount": 2,
15
+ "contentHash": "843615dc73bd1e2dc84859e6dd11fdea1b04bce6"
16
+ }