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": "colourful-text",
3
+ "dependencies": [
4
+ "motion-v"
5
+ ],
6
+ "files": [
7
+ {
8
+ "path": "ColourfulText.vue",
9
+ "content": "<template>\r\n <motion.span\r\n v-for=\"(char, index) in props.text\"\r\n :key=\"`${char}-${count}-${index}`\"\r\n :initial=\"{\r\n y: 0,\r\n opacity: 0.2,\r\n color: props.startColor,\r\n scale: 1,\r\n filter: 'blur(5px)',\r\n }\"\r\n :transition=\"{\r\n duration: props.duration,\r\n delay: index * 0.05,\r\n }\"\r\n :animate=\"{\r\n y: [0, -3, 0],\r\n opacity: [1, 0.8, 1],\r\n scale: [1, 1.01, 1],\r\n filter: ['blur(0px)', 'blur(5px)', 'blur(0px)'],\r\n color: currentColors[index % currentColors.length],\r\n }\"\r\n :exit=\"{\r\n y: -3,\r\n opacity: 1,\r\n scale: 1,\r\n filter: 'blur(5px)',\r\n color: props.startColor,\r\n }\"\r\n >\r\n {{ char }}\r\n </motion.span>\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 Props {\r\n text: string;\r\n colors?: string[];\r\n startColor?: string;\r\n duration?: number;\r\n}\r\n\r\nconst props = withDefaults(defineProps<Props>(), {\r\n startColor: \"rgb(255,255,255)\",\r\n duration: 0.5,\r\n colors: () => [\r\n \"rgb(131, 179, 32)\",\r\n \"rgb(47, 195, 106)\",\r\n \"rgb(42, 169, 210)\",\r\n \"rgb(4, 112, 202)\",\r\n \"rgb(107, 10, 255)\",\r\n \"rgb(183, 0, 218)\",\r\n \"rgb(218, 0, 171)\",\r\n \"rgb(230, 64, 92)\",\r\n \"rgb(232, 98, 63)\",\r\n \"rgb(249, 129, 47)\",\r\n ],\r\n});\r\n\r\nconst currentColors = ref(props.colors);\r\nconst count = ref(0);\r\nconst lastHidden = ref(0);\r\n\r\n// eslint-disable-next-line no-undef\r\nlet intervalId: undefined | NodeJS.Timeout = undefined;\r\nonMounted(() => {\r\n intervalId = setInterval(() => {\r\n const shuffled = [...props.colors].sort(() => 0.5 - Math.random());\r\n currentColors.value = shuffled;\r\n\r\n if (document.visibilityState === \"visible\") {\r\n if (Date.now() - lastHidden.value > 500) {\r\n count.value++;\r\n }\r\n } else {\r\n lastHidden.value = Date.now();\r\n }\r\n }, 5000);\r\n});\r\n\r\nonUnmounted(() => {\r\n clearInterval(intervalId);\r\n});\r\n</script>\r\n\r\n<style scoped></style>\r\n"
10
+ },
11
+ {
12
+ "path": "index.ts",
13
+ "content": "export { default as ColourfulText } from \"./ColourfulText.vue\";\r\n"
14
+ }
15
+ ],
16
+ "fileCount": 2,
17
+ "contentHash": "f3c6482eac5818c5cdd72acbe85fe89fca1e25c6"
18
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "compare",
3
+ "dependencies": [
4
+ "@vueuse/core"
5
+ ],
6
+ "files": [
7
+ {
8
+ "path": "Compare.vue",
9
+ "content": "<template>\r\n <div\r\n ref=\"sliderRef\"\r\n :class=\"cn('w-[400px] h-[400px] overflow-hidden', props.class)\"\r\n :style=\"{\r\n position: 'relative',\r\n cursor: props.slideMode === 'drag' ? 'grab' : 'col-resize',\r\n }\"\r\n @mousemove=\"handleMouseMove\"\r\n @mouseleave=\"mouseLeaveHandler\"\r\n @mouseenter=\"mouseEnterHandler\"\r\n @mousedown=\"handleMouseDown\"\r\n @mouseup=\"handleEnd\"\r\n @touchstart=\"handleTouchStart\"\r\n @touchend=\"handleTouchEnd\"\r\n @touchmove=\"handleTouchMove\"\r\n >\r\n <!-- Slider Line -->\r\n <Transition>\r\n <div\r\n v-show=\"true\"\r\n class=\"absolute top-0 z-30 m-auto h-full w-px bg-gradient-to-b from-transparent from-5% via-indigo-500 to-transparent to-95%\"\r\n :style=\"{\r\n left: `${sliderXPercent}%`,\r\n top: '0',\r\n zIndex: 40,\r\n pointerEvents: 'none',\r\n }\"\r\n >\r\n <!-- Decorative Effects -->\r\n <div\r\n class=\"absolute left-0 top-1/2 z-20 h-full w-36 -translate-y-1/2 bg-gradient-to-r from-indigo-400 via-transparent to-transparent opacity-50 [mask-image:radial-gradient(100px_at_left,white,transparent)]\"\r\n />\r\n <div\r\n class=\"absolute left-0 top-1/2 z-10 h-1/2 w-10 -translate-y-1/2 bg-gradient-to-r from-cyan-400 via-transparent to-transparent opacity-100 [mask-image:radial-gradient(50px_at_left,white,transparent)]\"\r\n />\r\n <div\r\n class=\"absolute -right-10 top-1/2 h-3/4 w-10 -translate-y-1/2 [mask-image:radial-gradient(100px_at_left,white,transparent)]\"\r\n >\r\n <StarField\r\n :stars-count=\"120\"\r\n class=\"size-full\"\r\n />\r\n </div>\r\n\r\n <!-- Custom Handle Slot -->\r\n <slot name=\"handle\">\r\n <div\r\n v-if=\"props.showHandlebar\"\r\n class=\"pointer-events-auto absolute -right-2.5 top-1/2 z-30 flex size-5 -translate-y-1/2 cursor-grab items-center justify-center rounded-md bg-white shadow-[0px_-1px_0px_0px_#FFFFFF40]\"\r\n >\r\n <Icon\r\n name=\"heroicons:ellipsis-vertical\"\r\n class=\"size-4 text-black\"\r\n />\r\n </div>\r\n </slot>\r\n </div>\r\n </Transition>\r\n\r\n <!-- First Content -->\r\n <div\r\n class=\"relative z-20 size-full overflow-hidden\"\r\n :style=\"{ pointerEvents: isInteracting ? 'none' : 'auto' }\"\r\n >\r\n <Transition>\r\n <div\r\n v-show=\"true\"\r\n :class=\"\r\n cn(\r\n 'absolute inset-0 z-20 rounded-2xl flex-shrink-0 w-full h-full select-none overflow-hidden',\r\n props.firstContentClass,\r\n )\r\n \"\r\n :style=\"{\r\n clipPath: `inset(0 ${100 - sliderXPercent}% 0 0)`,\r\n }\"\r\n >\r\n <slot name=\"first-content\">\r\n <img\r\n v-if=\"props.firstImage\"\r\n :alt=\"props.firstImageAlt\"\r\n :src=\"props.firstImage\"\r\n :class=\"\r\n cn(\r\n 'absolute inset-0 z-20 rounded-2xl flex-shrink-0 w-full h-full select-none',\r\n firstContentClass,\r\n )\r\n \"\r\n :draggable=\"false\"\r\n />\r\n </slot>\r\n </div>\r\n </Transition>\r\n </div>\r\n\r\n <!-- Second Content -->\r\n <Transition>\r\n <div\r\n v-show=\"true\"\r\n :class=\"\r\n cn(\r\n 'absolute top-0 left-0 z-[19] rounded-2xl w-full h-full select-none',\r\n props.secondContentClass,\r\n )\r\n \"\r\n :style=\"{ pointerEvents: isInteracting ? 'none' : 'auto' }\"\r\n >\r\n <slot name=\"second-content\">\r\n <img\r\n v-if=\"props.secondImage\"\r\n :alt=\"props.secondImageAlt\"\r\n :src=\"props.secondImage\"\r\n :class=\"cn('w-full h-full object-cover', secondContentClass)\"\r\n :draggable=\"false\"\r\n />\r\n </slot>\r\n </div>\r\n </Transition>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { cn } from \"@/lib/utils\";\r\nimport { ref, onMounted, onUnmounted, watch } from \"vue\";\r\nimport { templateRef } from \"@vueuse/core\";\r\n\r\ninterface Props {\r\n firstImage?: string;\r\n secondImage?: string;\r\n firstImageAlt?: string;\r\n secondImageAlt?: string;\r\n class?: string;\r\n firstContentClass?: string;\r\n secondContentClass?: string;\r\n initialSliderPercentage?: number;\r\n slideMode?: \"hover\" | \"drag\";\r\n showHandlebar?: boolean;\r\n autoplay?: boolean;\r\n autoplayDuration?: number;\r\n}\r\n\r\nconst props = withDefaults(defineProps<Props>(), {\r\n firstImage: \"\",\r\n secondImage: \"\",\r\n firstImageAlt: \"First image\",\r\n secondImageAlt: \"Second image\",\r\n class: \"\",\r\n firstContentClass: \"\",\r\n secondContentClass: \"\",\r\n initialSliderPercentage: 50,\r\n slideMode: \"hover\",\r\n showHandlebar: true,\r\n autoplay: false,\r\n autoplayDuration: 5000,\r\n});\r\n\r\nconst emit = defineEmits<{\r\n (e: \"update:percentage\", value: number): void;\r\n (e: \"drag:start\"): void;\r\n (e: \"drag:end\"): void;\r\n (e: \"hover:enter\"): void;\r\n (e: \"hover:leave\"): void;\r\n}>();\r\n\r\nconst sliderRef = templateRef<HTMLDivElement | null>(\"sliderRef\");\r\nconst sliderXPercent = ref(props.initialSliderPercentage);\r\nconst isDragging = ref(false);\r\nconst isMouseOver = ref(false);\r\nconst isInteracting = ref(false);\r\nlet autoplayTimeout: ReturnType<typeof setTimeout> | null = null;\r\nlet autoplayRAF: number | null = null;\r\n\r\nfunction startAutoplay(): void {\r\n if (!props.autoplay || isMouseOver.value || isDragging.value) return;\r\n\r\n const startTime = Date.now();\r\n function animate(): void {\r\n if (isMouseOver.value || isDragging.value) {\r\n if (autoplayRAF) cancelAnimationFrame(autoplayRAF);\r\n return;\r\n }\r\n\r\n const elapsedTime = Date.now() - startTime;\r\n const progress = (elapsedTime % (props.autoplayDuration * 2)) / props.autoplayDuration;\r\n const percentage = progress <= 1 ? progress * 100 : (2 - progress) * 100;\r\n\r\n sliderXPercent.value = percentage;\r\n emit(\"update:percentage\", percentage);\r\n autoplayRAF = requestAnimationFrame(animate);\r\n }\r\n\r\n animate();\r\n}\r\n\r\nfunction stopAutoplay(): void {\r\n if (autoplayTimeout) {\r\n clearTimeout(autoplayTimeout);\r\n autoplayTimeout = null;\r\n }\r\n if (autoplayRAF) {\r\n cancelAnimationFrame(autoplayRAF);\r\n autoplayRAF = null;\r\n }\r\n}\r\n\r\nfunction mouseEnterHandler(): void {\r\n isMouseOver.value = true;\r\n emit(\"hover:enter\");\r\n if (props.autoplay) {\r\n stopAutoplay();\r\n }\r\n}\r\n\r\nfunction mouseLeaveHandler(): void {\r\n isMouseOver.value = false;\r\n isInteracting.value = false;\r\n emit(\"hover:leave\");\r\n\r\n if (props.slideMode === \"hover\") {\r\n sliderXPercent.value = props.initialSliderPercentage;\r\n emit(\"update:percentage\", props.initialSliderPercentage);\r\n }\r\n if (props.slideMode === \"drag\") {\r\n isDragging.value = false;\r\n }\r\n\r\n if (props.autoplay) {\r\n startAutoplay();\r\n }\r\n}\r\n\r\nfunction handleStart(): void {\r\n if (props.slideMode === \"drag\") {\r\n isDragging.value = true;\r\n isInteracting.value = true;\r\n emit(\"drag:start\");\r\n stopAutoplay();\r\n }\r\n}\r\n\r\nfunction handleEnd(): void {\r\n if (props.slideMode === \"drag\") {\r\n isDragging.value = false;\r\n isInteracting.value = false;\r\n emit(\"drag:end\");\r\n if (props.autoplay && !isMouseOver.value) {\r\n startAutoplay();\r\n }\r\n }\r\n}\r\n\r\nfunction handleMove(clientX: number): void {\r\n if (!sliderRef.value) return;\r\n\r\n if (props.slideMode === \"hover\" || (props.slideMode === \"drag\" && isDragging.value)) {\r\n isInteracting.value = true;\r\n stopAutoplay();\r\n\r\n const rect = sliderRef.value.getBoundingClientRect();\r\n const x = clientX - rect.left;\r\n const percent = (x / rect.width) * 100;\r\n\r\n requestAnimationFrame(() => {\r\n const newPercent = Math.max(0, Math.min(100, percent));\r\n sliderXPercent.value = newPercent;\r\n emit(\"update:percentage\", newPercent);\r\n });\r\n }\r\n}\r\n\r\nfunction handleMouseDown(e: MouseEvent): void {\r\n handleStart();\r\n}\r\n\r\nfunction handleMouseMove(e: MouseEvent): void {\r\n handleMove(e.clientX);\r\n}\r\n\r\nfunction handleTouchStart(e: TouchEvent): void {\r\n if (!props.autoplay) handleStart();\r\n}\r\n\r\nfunction handleTouchEnd(): void {\r\n if (!props.autoplay) handleEnd();\r\n}\r\n\r\nfunction handleTouchMove(e: TouchEvent): void {\r\n if (!props.autoplay) handleMove(e.touches[0].clientX);\r\n}\r\n\r\nonMounted(() => {\r\n startAutoplay();\r\n});\r\n\r\nonUnmounted(() => {\r\n stopAutoplay();\r\n});\r\n\r\nwatch(\r\n () => props.initialSliderPercentage,\r\n (newValue) => {\r\n sliderXPercent.value = newValue;\r\n },\r\n);\r\n\r\nwatch(\r\n () => props.autoplay,\r\n (newValue) => {\r\n if (newValue && !isMouseOver.value && !isDragging.value) {\r\n startAutoplay();\r\n } else {\r\n stopAutoplay();\r\n }\r\n },\r\n);\r\n</script>\r\n"
10
+ },
11
+ {
12
+ "path": "index.ts",
13
+ "content": "export { default as Compare } from \"./Compare.vue\";\r\nexport { default as StarField } from \"./StarField.vue\";\r\n"
14
+ },
15
+ {
16
+ "path": "StarField.vue",
17
+ "content": "<template>\r\n <div class=\"absolute inset-0 overflow-hidden\">\r\n <div\r\n v-for=\"star in stars\"\r\n :key=\"star.id\"\r\n :style=\"{\r\n top: star.top,\r\n left: star.left,\r\n width: `${star.size}px`,\r\n height: `${star.size}px`,\r\n '--inspira-twinkle-duration': `${star.twinkleDuration}s`,\r\n '--inspira-drift-duration': `${star.driftDuration}s`,\r\n '--inspira-drift-direction': `${star.driftDirection}px`,\r\n '--inspira-opacity-start': star.opacityStart,\r\n '--inspira-opacity-end': star.opacityEnd,\r\n }\"\r\n class=\"star absolute rounded-full bg-white\"\r\n />\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { computed } from \"vue\";\r\n\r\ninterface Props {\r\n starsCount?: number;\r\n}\r\n\r\ninterface Star {\r\n id: number;\r\n top: string;\r\n left: string;\r\n size: number;\r\n twinkleDuration: number;\r\n driftDuration: number;\r\n driftDirection: number;\r\n opacityStart: number;\r\n opacityEnd: number;\r\n}\r\n\r\nconst props = withDefaults(defineProps<Props>(), {\r\n starsCount: 130,\r\n});\r\n\r\nfunction random(min: number, max: number): number {\r\n return Math.random() * (max - min) + min;\r\n}\r\n\r\nfunction randomSize(): number {\r\n // Randomly return either 1 or 2\r\n return Math.random() < 0.5 ? 1 : 2;\r\n}\r\n\r\nconst stars = computed(() =>\r\n Array.from(\r\n { length: props.starsCount },\r\n (_, i): Star => ({\r\n id: i,\r\n top: `${random(0, 100)}%`,\r\n left: `${random(0, 100)}%`,\r\n size: randomSize(),\r\n twinkleDuration: random(2, 4),\r\n driftDuration: random(5, 10),\r\n driftDirection: random(-50, 50),\r\n opacityStart: random(0.1, 0.3),\r\n opacityEnd: random(0.7, 1),\r\n }),\r\n ),\r\n);\r\n</script>\r\n\r\n<style scoped>\r\n.star {\r\n opacity: var(--inspira-opacity-start);\r\n animation:\r\n twinkle var(--inspira-twinkle-duration) ease-in-out infinite alternate,\r\n drift var(--inspira-drift-duration) linear infinite;\r\n}\r\n\r\n@keyframes twinkle {\r\n 0% {\r\n opacity: var(--inspira-opacity-start);\r\n }\r\n 100% {\r\n opacity: var(--inspira-opacity-end);\r\n }\r\n}\r\n\r\n@keyframes drift {\r\n 0% {\r\n transform: translate(0, 0);\r\n }\r\n 25% {\r\n transform: translate(var(--inspira-drift-direction), calc(var(--inspira-drift-direction) / 2));\r\n }\r\n 50% {\r\n transform: translate(calc(var(--inspira-drift-direction) / 2), var(--inspira-drift-direction));\r\n }\r\n 75% {\r\n transform: translate(\r\n calc(var(--inspira-drift-direction) * -1),\r\n calc(var(--inspira-drift-direction) / 2)\r\n );\r\n }\r\n 100% {\r\n transform: translate(0, 0);\r\n }\r\n}\r\n</style>\r\n"
18
+ }
19
+ ],
20
+ "fileCount": 3,
21
+ "contentHash": "3f6ae4f8f7692d1080bb136df17087678d8491b9"
22
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "confetti",
3
+ "dependencies": [
4
+ "canvas-confetti"
5
+ ],
6
+ "files": [
7
+ {
8
+ "path": "Confetti.vue",
9
+ "content": "<template>\r\n <div>\r\n <canvas\r\n ref=\"canvasRef\"\r\n :class=\"$props.class\"\r\n ></canvas>\r\n <slot />\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport {\r\n create,\r\n type GlobalOptions as ConfettiGlobalOptions,\r\n type Options as ConfettiOptions,\r\n type CreateTypes as ConfettiInstance,\r\n} from \"canvas-confetti\";\r\nimport { ref, onMounted, onUnmounted, provide } from \"vue\";\r\n\r\ntype Api = {\r\n fire: (options?: ConfettiOptions) => void;\r\n};\r\n\r\ntype ConfettiProps = {\r\n options?: ConfettiOptions;\r\n globalOptions?: ConfettiGlobalOptions;\r\n manualstart?: boolean;\r\n class?: string;\r\n};\r\n\r\nconst props = defineProps<ConfettiProps>();\r\n\r\nconst instanceRef = ref<ConfettiInstance | null>(null);\r\nconst canvasRef = ref<HTMLCanvasElement | null>(null);\r\n\r\n// Confetti API\r\nfunction fire(opts: ConfettiOptions = {}) {\r\n instanceRef.value?.({ ...props.options, ...opts });\r\n}\r\n\r\nconst api: Api = { fire };\r\n\r\nprovide(\"ConfettiContext\", api);\r\n\r\n// Initialize confetti when mounted\r\nonMounted(() => {\r\n if (canvasRef.value) {\r\n instanceRef.value = create(canvasRef.value, {\r\n ...props.globalOptions,\r\n resize: true,\r\n });\r\n\r\n if (!props.manualstart) {\r\n fire();\r\n }\r\n }\r\n});\r\n\r\n// Cleanup when unmounted\r\nonUnmounted(() => {\r\n if (instanceRef.value) {\r\n instanceRef.value.reset();\r\n instanceRef.value = null;\r\n }\r\n});\r\n\r\ndefineExpose({\r\n fire,\r\n});\r\n</script>\r\n"
10
+ },
11
+ {
12
+ "path": "ConfettiButton.vue",
13
+ "content": "<template>\r\n <button @click=\"handleClick\">\r\n <slot />\r\n </button>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport confetti from \"canvas-confetti\";\r\nimport type { Options as ConfettiOptions } from \"canvas-confetti\";\r\nimport { inject } from \"vue\";\r\n\r\ntype ConfettiButtonProps = {\r\n options?: ConfettiOptions & { canvas?: HTMLCanvasElement };\r\n};\r\n\r\nconst props = defineProps<ConfettiButtonProps>();\r\nconst confettiContext = inject<{ fire: (opts?: ConfettiOptions) => void }>(\"ConfettiContext\");\r\n\r\n// Handle click event\r\nfunction handleClick(event: MouseEvent) {\r\n const target = event.target as HTMLElement;\r\n const rect = target.getBoundingClientRect();\r\n const x = rect.left + rect.width / 2;\r\n const y = rect.top + rect.height / 2;\r\n\r\n confetti({\r\n ...props.options,\r\n origin: {\r\n x: x / window.innerWidth,\r\n y: y / window.innerHeight,\r\n },\r\n });\r\n\r\n if (confettiContext) {\r\n confettiContext.fire({ ...props.options });\r\n }\r\n}\r\n</script>\r\n"
14
+ },
15
+ {
16
+ "path": "index.ts",
17
+ "content": "export { default as Confetti } from \"./Confetti.vue\";\r\nexport { default as ConfettiButton } from \"./ConfettiButton.vue\";\r\n"
18
+ }
19
+ ],
20
+ "fileCount": 3,
21
+ "contentHash": "f98587a12f63487887f44a2717863ce2d8e100d8"
22
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "container-scroll",
3
+ "dependencies": [
4
+ "@vueuse/core"
5
+ ],
6
+ "files": [
7
+ {
8
+ "path": "ContainerScroll.vue",
9
+ "content": "<template>\r\n <div\r\n ref=\"containerRef\"\r\n class=\"relative flex h-[60rem] items-center justify-center p-2 md:h-[80rem] md:p-20\"\r\n >\r\n <div\r\n class=\"relative w-full py-10 md:py-40\"\r\n style=\"perspective: 1000px\"\r\n >\r\n <ContainerScrollTitle :translate=\"translateY\">\r\n <slot name=\"title\"></slot>\r\n </ContainerScrollTitle>\r\n <ContainerScrollCard\r\n :rotate=\"rotate\"\r\n :scale=\"scale\"\r\n >\r\n <slot name=\"card\" />\r\n </ContainerScrollCard>\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { useWindowSize, useScroll, useElementBounding } from \"@vueuse/core\";\r\nimport { ref, onMounted, onUnmounted, computed } from \"vue\";\r\n\r\nconst containerRef = ref(null);\r\nconst isMobile = ref(false);\r\n\r\nfunction updateIsMobile() {\r\n isMobile.value = window.innerWidth <= 768;\r\n}\r\n\r\nonMounted(() => {\r\n updateIsMobile();\r\n window.addEventListener(\"resize\", updateIsMobile);\r\n});\r\n\r\nonUnmounted(() => {\r\n window.removeEventListener(\"resize\", updateIsMobile);\r\n});\r\n\r\nconst { height } = useWindowSize();\r\nconst { y: scrollY } = useScroll(window);\r\nconst { bottom } = useElementBounding(containerRef);\r\n\r\nconst scrollYProgress = computed(() => {\r\n if (!bottom.value) return 0;\r\n return 1 - Math.max(0, bottom.value - scrollY.value) / height.value;\r\n});\r\n\r\nconst scaleDimensions = computed(() => (isMobile.value ? [0.7, 0.9] : [1.05, 1]));\r\n\r\nconst rotate = computed(() => 20 * (1 - scrollYProgress.value));\r\nconst scale = computed(() => {\r\n const [start, end] = scaleDimensions.value;\r\n return start + (end - start) * scrollYProgress.value;\r\n});\r\nconst translateY = computed(() => -100 * scrollYProgress.value);\r\n</script>\r\n"
10
+ },
11
+ {
12
+ "path": "ContainerScrollCard.vue",
13
+ "content": "<template>\r\n <div\r\n :style=\"{\r\n transform: `rotateX(${rotate}deg) scale(${scale})`,\r\n boxShadow:\r\n '0 0 #0000004d, 0 9px 20px #0000004a, 0 37px 37px #00000042, 0 84px 50px #00000026, 0 149px 60px #0000000a, 0 233px 65px #00000003',\r\n }\"\r\n class=\"mx-auto -mt-12 h-[30rem] w-full max-w-5xl rounded-[30px] border-4 border-[#6C6C6C] bg-[#222222] p-2 shadow-2xl md:h-[40rem] md:p-6\"\r\n >\r\n <div\r\n class=\"size-full overflow-hidden rounded-2xl bg-gray-100 md:rounded-2xl md:p-4 dark:bg-zinc-900\"\r\n >\r\n <slot></slot>\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\ndefineProps({\r\n rotate: Number,\r\n scale: Number,\r\n});\r\n</script>\r\n"
14
+ },
15
+ {
16
+ "path": "ContainerScrollTitle.vue",
17
+ "content": "<template>\r\n <div\r\n :style=\"{ transform: `translateY(${translate}px)` }\"\r\n class=\"mx-auto max-w-5xl text-center\"\r\n >\r\n <slot></slot>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\ndefineProps({\r\n translate: Number,\r\n});\r\n</script>\r\n"
18
+ },
19
+ {
20
+ "path": "index.ts",
21
+ "content": "export { default as ContainerScroll } from \"./ContainerScroll.vue\";\r\nexport { default as ContainerScrollCard } from \"./ContainerScrollCard.vue\";\r\nexport { default as ContainerScrollTitle } from \"./ContainerScrollTitle.vue\";\r\n"
22
+ }
23
+ ],
24
+ "fileCount": 4,
25
+ "contentHash": "740ae97fa27853565e7fa7c3a88e9cbff80c3710"
26
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "container-text-flip",
3
+ "dependencies": [
4
+ "@vueuse/core",
5
+ "motion-v"
6
+ ],
7
+ "files": [
8
+ {
9
+ "path": "ContainerTextFlip.vue",
10
+ "content": "<template>\r\n <Motion\r\n :key=\"words[currentWordIndex]\"\r\n as=\"p\"\r\n :layout-id=\"`words-here-${id}`\"\r\n :animate=\"{ width }\"\r\n :transition=\"{ duration: props.animationDuration / 2000 }\"\r\n :class=\"\r\n cn(\r\n 'relative inline-block rounded-lg pt-2 pb-3 px-4 text-center text-4xl font-bold text-black md:text-7xl dark:text-white',\r\n '[background:linear-gradient(to_bottom,#f3f4f6,#e5e7eb)]',\r\n 'shadow-[inset_0_-1px_#d1d5db,inset_0_0_0_1px_#d1d5db,_0_4px_8px_#d1d5db]',\r\n 'dark:[background:linear-gradient(to_bottom,#374151,#1f2937)]',\r\n 'dark:shadow-[inset_0_-1px_#10171e,inset_0_0_0_1px_hsla(205,89%,46%,.24),_0_4px_8px_#00000052]',\r\n props.class,\r\n )\r\n \"\r\n >\r\n <Motion\r\n ref=\"textRef\"\r\n as=\"div\"\r\n :transition=\"{\r\n duration: animationDuration / 1000,\r\n ease: 'easeInOut',\r\n }\"\r\n :class=\"cn('inline-block', props.textClass)\"\r\n :layout-id=\"`word-div-${words[currentWordIndex]}-${id}`\"\r\n >\r\n <Motion\r\n as=\"div\"\r\n class=\"inline-block\"\r\n >\r\n <Motion\r\n v-for=\"(letter, index) in words[currentWordIndex]\"\r\n :key=\"index\"\r\n as=\"span\"\r\n :initial=\"{\r\n opacity: 0,\r\n filter: 'blur(10px)',\r\n }\"\r\n :animate=\"{\r\n opacity: 1,\r\n filter: 'blur(0px)',\r\n }\"\r\n :transition=\"{\r\n delay: index * 0.02,\r\n }\"\r\n >\r\n {{ letter }}\r\n </Motion>\r\n </Motion>\r\n </Motion>\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 { useIntervalFn } from \"@vueuse/core\";\r\n\r\nconst props = withDefaults(\r\n defineProps<{\r\n words?: string[];\r\n interval?: number;\r\n animationDuration?: number;\r\n class?: string;\r\n textClass?: string;\r\n }>(),\r\n {\r\n words: () => [\"better\", \"modern\", \"beautiful\", \"awesome\"],\r\n interval: 3000,\r\n animationDuration: 700,\r\n },\r\n);\r\n\r\nconst id = useId();\r\n\r\nconst currentWordIndex = ref(0);\r\nconst textRef = templateRef<HTMLDivElement>(\"textRef\", null);\r\n\r\nconst width = computed(() => {\r\n if (textRef.value) {\r\n return textRef.value.scrollWidth + 30;\r\n }\r\n return 100;\r\n});\r\n\r\nuseIntervalFn(() => {\r\n currentWordIndex.value = (currentWordIndex.value + 1) % props.words.length;\r\n}, props.interval);\r\n</script>\r\n\r\n<style scoped></style>\r\n"
11
+ },
12
+ {
13
+ "path": "index.ts",
14
+ "content": "export { default as ContainerTextFlip } from \"./ContainerTextFlip.vue\";\r\n"
15
+ }
16
+ ],
17
+ "fileCount": 2,
18
+ "contentHash": "4047d11d581717b5fc4727af211702c87e4421ed"
19
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "cosmic-portal",
3
+ "dependencies": [
4
+ "three"
5
+ ],
6
+ "files": [
7
+ {
8
+ "path": "CosmicPortal.vue",
9
+ "content": "<template>\r\n <div :class=\"['relative overflow-hidden h-full w-full', props.containerClass]\">\r\n <canvas\r\n ref=\"canvasRef\"\r\n :class=\"['absolute inset-0 h-full w-full block', props.class]\"\r\n />\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport * as THREE from \"three\";\r\nimport { OrbitControls } from \"three/addons/controls/OrbitControls.js\";\r\nimport { EffectComposer } from \"three/addons/postprocessing/EffectComposer.js\";\r\nimport { RenderPass } from \"three/addons/postprocessing/RenderPass.js\";\r\nimport { UnrealBloomPass } from \"three/addons/postprocessing/UnrealBloomPass.js\";\r\nimport { ShaderPass } from \"three/addons/postprocessing/ShaderPass.js\";\r\nimport { FXAAShader } from \"three/addons/shaders/FXAAShader.js\";\r\n\r\ninterface PortalParams {\r\n portalComplexity: number;\r\n crystalCount: number;\r\n primaryColor: string;\r\n secondaryColor: string;\r\n accentColor: string;\r\n vortexColor: string;\r\n rotationSpeed: number;\r\n bloomStrength: number;\r\n bloomRadius: number;\r\n bloomThreshold: number;\r\n dimensionShift: number;\r\n}\r\n\r\nconst props = withDefaults(\r\n defineProps<Partial<PortalParams> & { class?: string; containerClass?: string }>(),\r\n {\r\n portalComplexity: 4,\r\n crystalCount: 12,\r\n primaryColor: \"#9b59b6\",\r\n secondaryColor: \"#3498db\",\r\n accentColor: \"#e74c3c\",\r\n vortexColor: \"#2ecc71\",\r\n rotationSpeed: 0.3,\r\n bloomStrength: 1.2,\r\n bloomRadius: 0.7,\r\n bloomThreshold: 0.2,\r\n dimensionShift: 4,\r\n class: \"\",\r\n containerClass: \"\",\r\n },\r\n);\r\n\r\nconst params = ref<PortalParams>({\r\n portalComplexity: props.portalComplexity,\r\n crystalCount: props.crystalCount,\r\n primaryColor: props.primaryColor,\r\n secondaryColor: props.secondaryColor,\r\n accentColor: props.accentColor,\r\n vortexColor: props.vortexColor,\r\n rotationSpeed: props.rotationSpeed,\r\n bloomStrength: props.bloomStrength,\r\n bloomRadius: props.bloomRadius,\r\n bloomThreshold: props.bloomThreshold,\r\n dimensionShift: props.dimensionShift,\r\n});\r\n\r\nconst canvasRef = ref<HTMLCanvasElement>();\r\n\r\n// Three.js core objects\r\nlet scene: THREE.Scene;\r\nlet camera: THREE.PerspectiveCamera;\r\nlet renderer: THREE.WebGLRenderer;\r\nlet composer: EffectComposer;\r\nlet controls: OrbitControls;\r\nlet bloomPass: UnrealBloomPass;\r\nlet fxaaPass: ShaderPass;\r\nlet clock: THREE.Clock;\r\n\r\n// Portal objects\r\nlet meshes: THREE.Object3D[] = [];\r\nlet materials: THREE.Material[] = [];\r\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\r\nlet portalMaterials: any[] = [];\r\nlet portalLights: THREE.Light[] = [];\r\nlet animationId: number;\r\nlet time = 0;\r\n\r\nlet resizeObserver: ResizeObserver;\r\n\r\nfunction initThreeJS() {\r\n if (!canvasRef.value) return;\r\n\r\n // Scene setup\r\n scene = new THREE.Scene();\r\n scene.background = new THREE.Color(0x0a0015);\r\n scene.fog = new THREE.FogExp2(0x1a0033, 0.001);\r\n\r\n // Camera setup - get container dimensions first\r\n const container = canvasRef.value.parentElement;\r\n const width = container ? container.clientWidth : window.innerWidth;\r\n const height = container ? container.clientHeight : window.innerHeight;\r\n\r\n camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000);\r\n camera.position.set(0, 0, 15);\r\n\r\n // Lighting\r\n scene.add(new THREE.AmbientLight(0x330066, 0.2));\r\n const mainLight = new THREE.DirectionalLight(0xffffff, 0.6);\r\n mainLight.position.set(10, 10, 5);\r\n scene.add(mainLight);\r\n\r\n // Portal lights\r\n const lightColors = [\r\n params.value.primaryColor,\r\n params.value.secondaryColor,\r\n params.value.accentColor,\r\n params.value.vortexColor,\r\n ];\r\n for (let i = 0; i < 6; i++) {\r\n const light = new THREE.PointLight(new THREE.Color(lightColors[i % 4]), 0.8, 20);\r\n scene.add(light);\r\n portalLights.push(light);\r\n }\r\n\r\n // Renderer setup - use the container dimensions\r\n renderer = new THREE.WebGLRenderer({\r\n canvas: canvasRef.value,\r\n antialias: true,\r\n powerPreference: \"high-performance\",\r\n });\r\n renderer.setSize(width, height);\r\n renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));\r\n renderer.outputColorSpace = THREE.SRGBColorSpace;\r\n renderer.toneMapping = THREE.ACESFilmicToneMapping;\r\n renderer.toneMappingExposure = 1.2;\r\n\r\n // Controls\r\n controls = new OrbitControls(camera, canvasRef.value);\r\n controls.enableDamping = true;\r\n controls.dampingFactor = 0.08;\r\n controls.autoRotate = true;\r\n controls.autoRotateSpeed = 0.5;\r\n controls.minDistance = 8;\r\n controls.maxDistance = 40;\r\n\r\n // Post-processing\r\n composer = new EffectComposer(renderer);\r\n composer.addPass(new RenderPass(scene, camera));\r\n\r\n bloomPass = new UnrealBloomPass(\r\n new THREE.Vector2(width, height),\r\n params.value.bloomStrength,\r\n params.value.bloomRadius,\r\n params.value.bloomThreshold,\r\n );\r\n composer.addPass(bloomPass);\r\n\r\n fxaaPass = new ShaderPass(FXAAShader);\r\n const pixelRatio = renderer.getPixelRatio();\r\n fxaaPass.material.uniforms[\"resolution\"].value.set(\r\n 1 / (width * pixelRatio),\r\n 1 / (height * pixelRatio),\r\n );\r\n composer.addPass(fxaaPass);\r\n\r\n clock = new THREE.Clock();\r\n}\r\n\r\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\r\nfunction addPortalShader(material: any) {\r\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\r\n material.onBeforeCompile = (shader: any) => {\r\n shader.uniforms.time = { value: 0 };\r\n shader.uniforms.pulseTime = { value: -1000 };\r\n shader.uniforms.portalSpeed = { value: 8.0 };\r\n shader.uniforms.portalColor = { value: new THREE.Color(params.value.accentColor) };\r\n shader.uniforms.dimensionShift = { value: 0 };\r\n\r\n shader.vertexShader = `varying vec3 vWorldPosition;\\n` + shader.vertexShader;\r\n\r\n shader.fragmentShader =\r\n `\r\n uniform float time;\r\n uniform float pulseTime;\r\n uniform float portalSpeed;\r\n uniform vec3 portalColor;\r\n uniform float dimensionShift;\r\n varying vec3 vWorldPosition;\\n` + shader.fragmentShader;\r\n\r\n shader.vertexShader = shader.vertexShader.replace(\r\n \"#include <begin_vertex>\",\r\n `#include <begin_vertex>\r\n vWorldPosition = (modelMatrix * vec4(transformed, 1.0)).xyz;`,\r\n );\r\n\r\n shader.fragmentShader = shader.fragmentShader.replace(\r\n \"#include <emissivemap_fragment>\",\r\n `#include <emissivemap_fragment>\r\n float timeSincePortal = time - pulseTime;\r\n if(timeSincePortal > 0.0 && timeSincePortal < 3.0) {\r\n float portalRadius = timeSincePortal * portalSpeed;\r\n float currentRadius = length(vWorldPosition);\r\n float portalWidth = 1.5;\r\n float portalEffect = smoothstep(portalRadius - portalWidth, portalRadius, currentRadius) -\r\n smoothstep(portalRadius, portalRadius + portalWidth, currentRadius);\r\n vec3 dimensionalColor = mix(portalColor, vec3(1.0, 0.5, 1.0), sin(dimensionShift * 3.14159) * 0.5 + 0.5);\r\n totalEmissiveRadiance += dimensionalColor * portalEffect * 4.0;\r\n }`,\r\n );\r\n portalMaterials.push(shader);\r\n };\r\n}\r\n\r\nfunction createCosmicBackground() {\r\n const count = 4000;\r\n const geo = new THREE.BufferGeometry();\r\n const positions = new Float32Array(count * 3);\r\n const colors = new Float32Array(count * 3);\r\n\r\n for (let i = 0; i < count; i++) {\r\n const i3 = i * 3;\r\n const radius = 80 + Math.random() * 50;\r\n const theta = Math.random() * Math.PI * 2;\r\n const phi = Math.acos(2 * Math.random() - 1);\r\n\r\n positions[i3] = radius * Math.sin(phi) * Math.cos(theta);\r\n positions[i3 + 1] = radius * Math.sin(phi) * Math.sin(theta);\r\n positions[i3 + 2] = radius * Math.cos(phi);\r\n\r\n const temp = Math.random();\r\n const color = new THREE.Color();\r\n if (temp < 0.15) color.setHSL(0.8, 0.8, 0.9);\r\n else if (temp < 0.4) color.setHSL(0.6, 0.6, 0.8);\r\n else if (temp < 0.7) color.setHSL(0.1, 0.3, 0.9);\r\n else color.setHSL(0.3, 0.7, 0.6);\r\n\r\n color.toArray(colors, i3);\r\n }\r\n\r\n geo.setAttribute(\"position\", new THREE.BufferAttribute(positions, 3));\r\n geo.setAttribute(\"color\", new THREE.BufferAttribute(colors, 3));\r\n\r\n const mat = new THREE.PointsMaterial({\r\n size: 0.3,\r\n vertexColors: true,\r\n sizeAttenuation: true,\r\n blending: THREE.AdditiveBlending,\r\n depthWrite: false,\r\n transparent: true,\r\n });\r\n\r\n const stars = new THREE.Points(geo, mat);\r\n scene.add(stars);\r\n meshes.push(stars);\r\n materials.push(mat);\r\n}\r\n\r\nfunction createPortalCore() {\r\n const geo = new THREE.SphereGeometry(0.8, 32, 32);\r\n const mat = new THREE.ShaderMaterial({\r\n uniforms: {\r\n time: { value: 0 },\r\n pulseTime: { value: -1000 },\r\n dimensionShift: { value: 0 },\r\n color1: { value: new THREE.Color(params.value.primaryColor) },\r\n color2: { value: new THREE.Color(params.value.secondaryColor) },\r\n color3: { value: new THREE.Color(params.value.accentColor) },\r\n },\r\n vertexShader: `\r\n uniform float time;\r\n uniform float dimensionShift;\r\n varying vec3 vPos;\r\n varying vec3 vNorm;\r\n void main() {\r\n vPos = position;\r\n vNorm = normal;\r\n float warp = sin(position.x * 10.0 + time * 3.0) * 0.1;\r\n float shift = sin(dimensionShift * 6.28318) * 0.3;\r\n vec3 p = position * (1.0 + warp + shift);\r\n gl_Position = projectionMatrix * modelViewMatrix * vec4(p, 1.0);\r\n }\r\n `,\r\n fragmentShader: `\r\n uniform float time;\r\n uniform float pulseTime;\r\n uniform float dimensionShift;\r\n uniform vec3 color1;\r\n uniform vec3 color2;\r\n uniform vec3 color3;\r\n varying vec3 vPos;\r\n varying vec3 vNorm;\r\n void main() {\r\n float noise = sin(vPos.x * 20.0 + time * 4.0) * cos(vPos.z * 15.0 + time * 3.0);\r\n vec3 baseColor = mix(color1, color2, 0.5 + 0.5 * sin(time * 2.0 + dimensionShift));\r\n vec3 finalColor = mix(baseColor, color3, noise * 0.3);\r\n\r\n float fresnel = pow(1.0 - abs(dot(vNorm, normalize(cameraPosition - vPos))), 3.0);\r\n finalColor = mix(finalColor, vec3(1.0), fresnel * 0.5);\r\n\r\n float timeSincePortal = time - pulseTime;\r\n if(timeSincePortal > 0.0 && timeSincePortal < 1.0) {\r\n float burst = 1.0 - timeSincePortal;\r\n finalColor += vec3(1.0) * burst * 3.0;\r\n }\r\n\r\n gl_FragColor = vec4(finalColor, 0.9);\r\n }\r\n `,\r\n transparent: true,\r\n });\r\n\r\n portalMaterials.push(mat);\r\n const mesh = new THREE.Mesh(geo, mat);\r\n scene.add(mesh);\r\n meshes.push(mesh);\r\n}\r\n\r\nfunction createVortexRings() {\r\n const colors = [\r\n params.value.primaryColor,\r\n params.value.secondaryColor,\r\n params.value.accentColor,\r\n params.value.vortexColor,\r\n ];\r\n\r\n for (let ring = 0; ring < 5; ring++) {\r\n const radius = 2 + ring * 0.8;\r\n const geo = new THREE.TorusGeometry(radius, 0.05, 16, 64);\r\n const mat = new THREE.MeshPhysicalMaterial({\r\n color: new THREE.Color(colors[ring % colors.length]),\r\n transparent: true,\r\n opacity: 0.7,\r\n metalness: 0.8,\r\n roughness: 0.2,\r\n clearcoat: 0.8,\r\n clearcoatRoughness: 0.1,\r\n emissive: new THREE.Color(colors[ring % colors.length]).multiplyScalar(0.2),\r\n });\r\n\r\n addPortalShader(mat);\r\n const mesh = new THREE.Mesh(geo, mat);\r\n mesh.rotation.x = Math.PI * 0.1 * ring;\r\n mesh.rotation.z = Math.PI * 0.15 * ring;\r\n scene.add(mesh);\r\n meshes.push(mesh);\r\n }\r\n}\r\n\r\nfunction createFloatingCrystals() {\r\n const colors = [\r\n params.value.accentColor,\r\n params.value.vortexColor,\r\n params.value.primaryColor,\r\n params.value.secondaryColor,\r\n ];\r\n\r\n for (let i = 0; i < params.value.crystalCount; i++) {\r\n const geo = new THREE.OctahedronGeometry(0.3 + Math.random() * 0.4, 1);\r\n const mat = new THREE.MeshPhysicalMaterial({\r\n color: new THREE.Color(colors[i % colors.length]),\r\n transparent: true,\r\n opacity: 0.8,\r\n metalness: 0.9,\r\n roughness: 0.1,\r\n clearcoat: 1.0,\r\n clearcoatRoughness: 0.0,\r\n emissive: new THREE.Color(colors[i % colors.length]).multiplyScalar(0.3),\r\n });\r\n\r\n addPortalShader(mat);\r\n const mesh = new THREE.Mesh(geo, mat);\r\n const angle = (i / params.value.crystalCount) * Math.PI * 2;\r\n const radius = 6 + Math.random() * 4;\r\n mesh.position.set(\r\n Math.cos(angle) * radius,\r\n (Math.random() - 0.5) * 8,\r\n Math.sin(angle) * radius,\r\n );\r\n mesh.rotation.set(Math.random() * Math.PI, Math.random() * Math.PI, Math.random() * Math.PI);\r\n scene.add(mesh);\r\n meshes.push(mesh);\r\n }\r\n}\r\n\r\nfunction createDimensionalStreams() {\r\n const colors = [params.value.vortexColor, params.value.primaryColor, params.value.secondaryColor];\r\n\r\n for (let i = 0; i < 8; i++) {\r\n const points = [];\r\n const segments = 120;\r\n\r\n for (let j = 0; j <= segments; j++) {\r\n const t = j / segments;\r\n const angle = t * Math.PI * 12 + i * Math.PI * 0.25;\r\n const radius = 3 + Math.sin(t * Math.PI * 6) * 1.5;\r\n const height = (t - 0.5) * 15;\r\n\r\n points.push(new THREE.Vector3(Math.cos(angle) * radius, height, Math.sin(angle) * radius));\r\n }\r\n\r\n const curve = new THREE.CatmullRomCurve3(points);\r\n const geo = new THREE.TubeGeometry(curve, segments, 0.02, 8, false);\r\n const mat = new THREE.MeshPhysicalMaterial({\r\n color: new THREE.Color(colors[i % colors.length]),\r\n transparent: true,\r\n opacity: 0.6,\r\n metalness: 1.0,\r\n roughness: 0.0,\r\n emissive: new THREE.Color(colors[i % colors.length]).multiplyScalar(0.4),\r\n });\r\n\r\n addPortalShader(mat);\r\n const stream = new THREE.Mesh(geo, mat);\r\n scene.add(stream);\r\n meshes.push(stream);\r\n }\r\n}\r\n\r\nfunction createPortalFrame() {\r\n const frameGeo = new THREE.TorusGeometry(7, 0.2, 16, 64);\r\n const frameMat = new THREE.MeshPhysicalMaterial({\r\n color: new THREE.Color(params.value.primaryColor),\r\n transparent: true,\r\n opacity: 0.4,\r\n metalness: 1.0,\r\n roughness: 0.1,\r\n clearcoat: 1.0,\r\n clearcoatRoughness: 0.0,\r\n emissive: new THREE.Color(params.value.primaryColor).multiplyScalar(0.5),\r\n });\r\n\r\n addPortalShader(frameMat);\r\n const frame = new THREE.Mesh(frameGeo, frameMat);\r\n scene.add(frame);\r\n meshes.push(frame);\r\n}\r\n\r\nfunction createEnergyParticles() {\r\n const count = 1500;\r\n const geo = new THREE.BufferGeometry();\r\n const positions = new Float32Array(count * 3);\r\n\r\n for (let i = 0; i < count; i++) {\r\n const r = 2 + Math.random() * 8;\r\n const theta = Math.random() * Math.PI * 2;\r\n const phi = Math.acos(2 * Math.random() - 1);\r\n\r\n positions[i * 3] = r * Math.sin(phi) * Math.cos(theta);\r\n positions[i * 3 + 1] = r * Math.sin(phi) * Math.sin(theta);\r\n positions[i * 3 + 2] = r * Math.cos(phi);\r\n }\r\n\r\n geo.setAttribute(\"position\", new THREE.BufferAttribute(positions, 3));\r\n\r\n const mat = new THREE.PointsMaterial({\r\n size: 0.08,\r\n color: params.value.vortexColor,\r\n blending: THREE.AdditiveBlending,\r\n transparent: true,\r\n opacity: 0.8,\r\n });\r\n\r\n const particles = new THREE.Points(geo, mat);\r\n scene.add(particles);\r\n meshes.push(particles);\r\n materials.push(mat);\r\n}\r\n\r\nfunction createSpaceDistortion() {\r\n const geo = new THREE.SphereGeometry(12, 64, 64);\r\n const mat = new THREE.ShaderMaterial({\r\n uniforms: {\r\n time: { value: 0 },\r\n dimensionShift: { value: 0 },\r\n color1: { value: new THREE.Color(params.value.primaryColor) },\r\n color2: { value: new THREE.Color(params.value.vortexColor) },\r\n },\r\n vertexShader: `\r\n uniform float time;\r\n uniform float dimensionShift;\r\n varying vec3 vNorm;\r\n varying vec3 vPos;\r\n void main() {\r\n vNorm = normal;\r\n vPos = position;\r\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\r\n }\r\n `,\r\n fragmentShader: `\r\n uniform float time;\r\n uniform float dimensionShift;\r\n uniform vec3 color1;\r\n uniform vec3 color2;\r\n varying vec3 vNorm;\r\n varying vec3 vPos;\r\n void main() {\r\n vec3 viewDir = normalize(cameraPosition - vPos);\r\n float fresnel = pow(1.0 - abs(dot(vNorm, viewDir)), 4.0);\r\n\r\n float distortion = sin(vPos.x * 0.5 + time * 2.0) * cos(vPos.y * 0.7 + time * 1.5);\r\n vec3 color = mix(color1, color2, distortion * 0.5 + 0.5 + dimensionShift * 0.3);\r\n\r\n gl_FragColor = vec4(color, fresnel * 0.3);\r\n }\r\n `,\r\n transparent: true,\r\n blending: THREE.AdditiveBlending,\r\n depthWrite: false,\r\n });\r\n\r\n const distortion = new THREE.Mesh(geo, mat);\r\n scene.add(distortion);\r\n meshes.push(distortion);\r\n materials.push(mat);\r\n}\r\n\r\nfunction createPortalScene() {\r\n // Clean up existing meshes\r\n meshes.forEach((mesh) => scene.remove(mesh));\r\n materials.forEach((mat) => mat.dispose());\r\n meshes = [];\r\n materials = [];\r\n portalMaterials = [];\r\n\r\n // Create portal components\r\n createCosmicBackground();\r\n createPortalCore();\r\n createVortexRings();\r\n createFloatingCrystals();\r\n createDimensionalStreams();\r\n createPortalFrame();\r\n createEnergyParticles();\r\n createSpaceDistortion();\r\n}\r\n\r\nfunction handleResize() {\r\n if (!camera || !renderer || !composer || !canvasRef.value) return;\r\n\r\n const container = canvasRef.value.parentElement;\r\n if (!container) return;\r\n\r\n const width = container.clientWidth;\r\n const height = container.clientHeight;\r\n\r\n // Update camera\r\n camera.aspect = width / height;\r\n camera.updateProjectionMatrix();\r\n\r\n // Update renderer and composer\r\n renderer.setSize(width, height);\r\n composer.setSize(width, height);\r\n\r\n // Update FXAA pass\r\n const pixelRatio = renderer.getPixelRatio();\r\n fxaaPass.material.uniforms[\"resolution\"].value.set(\r\n 1 / (width * pixelRatio),\r\n 1 / (height * pixelRatio),\r\n );\r\n}\r\n\r\nfunction animate() {\r\n animationId = requestAnimationFrame(animate);\r\n\r\n const delta = clock.getDelta();\r\n time = clock.getElapsedTime();\r\n\r\n // Update shader uniforms\r\n portalMaterials.forEach((shader) => {\r\n if (shader.uniforms) {\r\n if (shader.uniforms.time) shader.uniforms.time.value = time;\r\n if (shader.uniforms.dimensionShift)\r\n shader.uniforms.dimensionShift.value = params.value.dimensionShift;\r\n }\r\n });\r\n\r\n materials.forEach((mat) => {\r\n if (mat.uniforms) {\r\n if (mat.uniforms.time) mat.uniforms.time.value = time;\r\n if (mat.uniforms.dimensionShift)\r\n mat.uniforms.dimensionShift.value = params.value.dimensionShift;\r\n }\r\n });\r\n\r\n // Animate portal lights\r\n portalLights.forEach((light, i) => {\r\n const angle = time * 0.3 + (i / 6) * Math.PI * 2;\r\n const radius = 10 + Math.sin(time * 0.5 + i) * 3;\r\n light.position.x = Math.cos(angle) * radius;\r\n light.position.z = Math.sin(angle) * radius;\r\n light.position.y = Math.sin(time * 0.4 + i * 0.7) * 5;\r\n });\r\n\r\n // Animate meshes\r\n meshes.forEach((mesh, i) => {\r\n if (!mesh.rotation) return;\r\n const speed = params.value.rotationSpeed;\r\n mesh.rotation.y += delta * speed * (i % 2 ? -1 : 1) * 0.3;\r\n mesh.rotation.x += delta * speed * 0.1;\r\n\r\n // Animate particle positions\r\n if (mesh.material && mesh.material.type === \"PointsMaterial\") {\r\n const positions = mesh.geometry.attributes.position.array;\r\n for (let j = 0; j < positions.length; j += 3) {\r\n positions[j] += Math.sin(time + j) * 0.001;\r\n positions[j + 1] += Math.cos(time + j) * 0.001;\r\n positions[j + 2] += Math.sin(time * 0.7 + j) * 0.001;\r\n }\r\n mesh.geometry.attributes.position.needsUpdate = true;\r\n }\r\n });\r\n\r\n controls.update();\r\n composer.render();\r\n}\r\n\r\nfunction cleanup() {\r\n if (animationId) {\r\n cancelAnimationFrame(animationId);\r\n }\r\n\r\n // Dispose of Three.js objects\r\n meshes.forEach((mesh) => {\r\n if (mesh.geometry) mesh.geometry.dispose();\r\n if (mesh.material) {\r\n if (Array.isArray(mesh.material)) {\r\n mesh.material.forEach((mat) => mat.dispose());\r\n } else {\r\n mesh.material.dispose();\r\n }\r\n }\r\n });\r\n\r\n materials.forEach((mat) => mat.dispose());\r\n\r\n if (renderer) {\r\n renderer.dispose();\r\n }\r\n\r\n if (controls) {\r\n controls.dispose();\r\n }\r\n\r\n window.removeEventListener(\"resize\", handleResize);\r\n}\r\n\r\n// Exposed methods\r\nfunction activatePortal() {\r\n portalMaterials.forEach((mat) => {\r\n if (mat.uniforms && mat.uniforms.pulseTime) {\r\n mat.uniforms.pulseTime.value = time;\r\n }\r\n });\r\n}\r\n\r\nfunction shiftDimensions() {\r\n const colors = [\r\n \"#9b59b6\",\r\n \"#3498db\",\r\n \"#e74c3c\",\r\n \"#2ecc71\",\r\n \"#f39c12\",\r\n \"#e67e22\",\r\n \"#1abc9c\",\r\n \"#34495e\",\r\n ];\r\n params.value.primaryColor = colors[Math.floor(Math.random() * colors.length)];\r\n params.value.secondaryColor = colors[Math.floor(Math.random() * colors.length)];\r\n params.value.accentColor = colors[Math.floor(Math.random() * colors.length)];\r\n params.value.vortexColor = colors[Math.floor(Math.random() * colors.length)];\r\n params.value.dimensionShift = Math.random();\r\n createPortalScene();\r\n}\r\n\r\n// Expose methods to parent component\r\ndefineExpose({\r\n activatePortal,\r\n shiftDimensions,\r\n});\r\n\r\nonMounted(() => {\r\n initThreeJS();\r\n createPortalScene();\r\n // Use ResizeObserver instead of window resize\r\n if (canvasRef.value?.parentElement) {\r\n resizeObserver = new ResizeObserver(() => {\r\n handleResize();\r\n });\r\n resizeObserver.observe(canvasRef.value.parentElement);\r\n }\r\n animate();\r\n});\r\n\r\nonUnmounted(() => {\r\n cleanup();\r\n if (resizeObserver) {\r\n resizeObserver.disconnect();\r\n }\r\n});\r\n</script>\r\n"
10
+ },
11
+ {
12
+ "path": "index.ts",
13
+ "content": "export { default as CosmicPortal } from \"./CosmicPortal.vue\";\r\n"
14
+ }
15
+ ],
16
+ "fileCount": 2,
17
+ "contentHash": "003357594a4790361c3800b269f700cf37220842"
18
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "direction-aware-hover",
3
+ "dependencies": [],
4
+ "files": [
5
+ {
6
+ "path": "DirectionAwareHover.vue",
7
+ "content": "<template>\r\n <div\r\n ref=\"divRef\"\r\n :class=\"containerClass\"\r\n @mouseenter=\"handleMouseEnter\"\r\n @mouseleave=\"handleMouseLeave\"\r\n @touchstart=\"handleTouchStart\"\r\n @touchend=\"handleTouchEnd\"\r\n >\r\n <div class=\"relative size-full overflow-hidden\">\r\n <transition name=\"fade\">\r\n <div\r\n v-show=\"direction !== null\"\r\n :class=\"overlayClass\"\r\n />\r\n </transition>\r\n <div\r\n class=\"relative size-full bg-gray-50 transition-transform duration-300 dark:bg-black\"\r\n :class=\"imageContainerClass\"\r\n >\r\n <img\r\n :src=\"imageUrl\"\r\n alt=\"image\"\r\n :class=\"imageClass\"\r\n width=\"1000\"\r\n height=\"1000\"\r\n />\r\n </div>\r\n <transition name=\"fade\">\r\n <div\r\n v-show=\"direction !== null || isTouched\"\r\n :class=\"childrenClass\"\r\n >\r\n <slot />\r\n </div>\r\n </transition>\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { ref, computed, onMounted, onUnmounted } from \"vue\";\r\nimport { cn } from \"@/lib/utils\";\r\n\r\ninterface Props {\r\n imageUrl: string;\r\n childrenClass?: string;\r\n imageClass?: string;\r\n class?: string;\r\n}\r\n\r\nconst props = withDefaults(defineProps<Props>(), {\r\n childrenClass: undefined,\r\n imageClass: undefined,\r\n class: undefined,\r\n});\r\n\r\nconst divRef = ref<HTMLDivElement | null>(null);\r\nconst direction = ref<\"top\" | \"bottom\" | \"left\" | \"right\" | null>(null);\r\nconst isTouched = ref(false);\r\nconst isMobile = ref(false);\r\n\r\n// Touch timer for mobile interactions\r\nlet touchTimer: ReturnType<typeof setTimeout> | null = null;\r\n\r\nfunction detectMobile() {\r\n isMobile.value = window.matchMedia(\"(max-width: 768px)\").matches || \"ontouchstart\" in window;\r\n}\r\n\r\nfunction handleMouseEnter(event: MouseEvent) {\r\n if (isMobile.value) return;\r\n\r\n if (!divRef.value) return;\r\n const fetchedDirection = getDirection(event, divRef.value);\r\n\r\n switch (fetchedDirection) {\r\n case 0:\r\n direction.value = \"top\";\r\n break;\r\n case 1:\r\n direction.value = \"right\";\r\n break;\r\n case 2:\r\n direction.value = \"bottom\";\r\n break;\r\n case 3:\r\n direction.value = \"left\";\r\n break;\r\n default:\r\n direction.value = \"left\";\r\n break;\r\n }\r\n}\r\n\r\nfunction handleMouseLeave() {\r\n if (isMobile.value) return;\r\n direction.value = null;\r\n}\r\n\r\nfunction handleTouchStart(event: TouchEvent) {\r\n if (!isMobile.value) return;\r\n\r\n isTouched.value = true;\r\n\r\n if (!divRef.value) return;\r\n const touch = event.touches[0];\r\n const mouseEvent = new MouseEvent(\"mouseenter\", {\r\n clientX: touch.clientX,\r\n clientY: touch.clientY,\r\n });\r\n\r\n const fetchedDirection = getDirection(mouseEvent, divRef.value);\r\n\r\n switch (fetchedDirection) {\r\n case 0:\r\n direction.value = \"top\";\r\n break;\r\n case 1:\r\n direction.value = \"right\";\r\n break;\r\n case 2:\r\n direction.value = \"bottom\";\r\n break;\r\n case 3:\r\n direction.value = \"left\";\r\n break;\r\n default:\r\n direction.value = \"left\";\r\n break;\r\n }\r\n\r\n // Auto-hide after 3 seconds on mobile\r\n if (touchTimer) clearTimeout(touchTimer);\r\n touchTimer = setTimeout(() => {\r\n handleTouchEnd();\r\n }, 3000);\r\n}\r\n\r\nfunction handleTouchEnd() {\r\n if (touchTimer) {\r\n clearTimeout(touchTimer);\r\n touchTimer = null;\r\n }\r\n\r\n setTimeout(() => {\r\n direction.value = null;\r\n isTouched.value = false;\r\n }, 300);\r\n}\r\n\r\nfunction getDirection(ev: MouseEvent, obj: HTMLElement) {\r\n const { width: w, height: h, left, top } = obj.getBoundingClientRect();\r\n const x = ev.clientX - left - (w / 2) * (w > h ? h / w : 1);\r\n const y = ev.clientY - top - (h / 2) * (h > w ? w / h : 1);\r\n const d = Math.round(Math.atan2(y, x) / 1.57079633 + 5) % 4;\r\n return d;\r\n}\r\n\r\nconst containerClass = computed(() =>\r\n cn(\r\n \"group/card relative overflow-hidden rounded-lg bg-transparent transition-all duration-300\",\r\n // Mobile first responsive sizing\r\n \"h-48 w-48\", // Base mobile size\r\n \"xs:h-56 xs:w-56\", // Extra small screens\r\n \"sm:h-64 sm:w-64\", // Small screens\r\n \"md:h-80 md:w-80\", // Medium screens\r\n \"lg:h-96 lg:w-96\", // Large screens\r\n \"xl:h-[28rem] xl:w-[28rem]\", // Extra large screens\r\n // Mobile touch improvements\r\n \"touch-manipulation\",\r\n \"active:scale-[0.98]\",\r\n \"md:active:scale-100\", // Disable scale on desktop\r\n props.class,\r\n ),\r\n);\r\n\r\nconst imageClass = computed(() =>\r\n cn(\r\n \"h-full w-full object-cover transition-transform duration-300\",\r\n // Responsive scaling\r\n \"scale-125\", // Mobile\r\n \"sm:scale-135\", // Small screens\r\n \"md:scale-150\", // Desktop\r\n props.imageClass,\r\n ),\r\n);\r\n\r\nconst childrenClass = computed(() =>\r\n cn(\r\n \"absolute z-40 text-white transition-opacity duration-300\",\r\n // Responsive positioning\r\n \"bottom-2 left-2 text-sm\", // Mobile\r\n \"sm:bottom-3 sm:left-3 sm:text-base\", // Small screens\r\n \"md:bottom-4 md:left-4 md:text-lg\", // Desktop\r\n props.childrenClass,\r\n ),\r\n);\r\n\r\nconst overlayClass = computed(() => {\r\n const baseClasses = \"absolute inset-0 z-10 transition-all duration-300\";\r\n const backgroundClasses = \"bg-black/40 dark:bg-black/60\";\r\n\r\n let transformClasses = \"\";\r\n\r\n switch (direction.value) {\r\n case \"top\":\r\n transformClasses = \"-translate-y-full\";\r\n break;\r\n case \"bottom\":\r\n transformClasses = \"translate-y-full\";\r\n break;\r\n case \"left\":\r\n transformClasses = \"-translate-x-full\";\r\n break;\r\n case \"right\":\r\n transformClasses = \"translate-x-full\";\r\n break;\r\n default:\r\n transformClasses = \"\";\r\n }\r\n\r\n return cn(baseClasses, backgroundClasses, transformClasses);\r\n});\r\n\r\nconst imageContainerClass = computed(() => ({\r\n // Reduced movement on mobile for better UX\r\n \"translate-y-2 md:translate-y-5\": direction.value === \"top\",\r\n \"-translate-y-2 md:-translate-y-5\": direction.value === \"bottom\",\r\n \"translate-x-2 md:translate-x-5\": direction.value === \"left\",\r\n \"-translate-x-2 md:-translate-x-5\": direction.value === \"right\",\r\n}));\r\n\r\nonMounted(() => {\r\n detectMobile();\r\n window.addEventListener(\"resize\", detectMobile);\r\n});\r\n\r\nonUnmounted(() => {\r\n window.removeEventListener(\"resize\", detectMobile);\r\n if (touchTimer) {\r\n clearTimeout(touchTimer);\r\n }\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.3s ease;\r\n}\r\n\r\n.fade-enter-from,\r\n.fade-leave-to {\r\n opacity: 0;\r\n}\r\n\r\n/* Enhanced mobile touch targets */\r\n@media (max-width: 768px) {\r\n .group\\/card {\r\n min-height: 44px; /* iOS minimum touch target */\r\n min-width: 44px;\r\n }\r\n}\r\n\r\n/* Smooth transitions for mobile */\r\n@media (prefers-reduced-motion: reduce) {\r\n * {\r\n transition-duration: 0.1s !important;\r\n }\r\n}\r\n</style>\r\n"
8
+ },
9
+ {
10
+ "path": "index.ts",
11
+ "content": "export { default as DirectionAwareHover } from \"./DirectionAwareHover.vue\";\r\n"
12
+ }
13
+ ],
14
+ "fileCount": 2,
15
+ "contentHash": "9fad5325de2882f22193c88535ecd1e8e595f72e"
16
+ }
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "dock",
3
+ "dependencies": [],
4
+ "files": [
5
+ {
6
+ "path": "Dock.vue",
7
+ "content": "<template>\r\n <div\r\n ref=\"dockRef\"\r\n :class=\"\r\n cn(\r\n 'supports-backdrop-blur:bg-white/10 supports-backdrop-blur:dark:bg-black/10 mx-auto mt-8 flex h-[58px] w-max rounded-2xl border p-2 backdrop-blur-md transition-all gap-4',\r\n orientation === 'vertical' && 'flex-col w-[58px] h-max',\r\n props.class,\r\n dockClass,\r\n )\r\n \"\r\n @mousemove=\"onMouseMove\"\r\n @mouseleave=\"onMouseLeave\"\r\n >\r\n <slot />\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { ref, computed, provide, type HTMLAttributes } from \"vue\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport type { DataOrientation, Direction } from \"./types\";\r\nimport {\r\n MOUSE_X_INJECTION_KEY,\r\n MOUSE_Y_INJECTION_KEY,\r\n MAGNIFICATION_INJECTION_KEY,\r\n DISTANCE_INJECTION_KEY,\r\n ORIENTATION_INJECTION_KEY,\r\n} from \"./injectionKeys\";\r\n\r\ninterface DockProps {\r\n class?: HTMLAttributes[\"class\"];\r\n magnification?: number;\r\n distance?: number;\r\n direction?: Direction;\r\n orientation?: DataOrientation;\r\n}\r\n\r\nconst props = withDefaults(defineProps<DockProps>(), {\r\n magnification: 60,\r\n distance: 140,\r\n direction: \"middle\",\r\n orientation: \"horizontal\",\r\n});\r\n\r\nconst dockRef = ref<HTMLElement | null>(null);\r\nconst mouseX = ref(Infinity);\r\nconst mouseY = ref(Infinity);\r\nconst magnification = computed(() => props.magnification);\r\nconst distance = computed(() => props.distance);\r\n\r\nconst dockClass = computed(() => ({\r\n \"items-start\": props.direction === \"top\",\r\n \"items-center\": props.direction === \"middle\",\r\n \"items-end\": props.direction === \"bottom\",\r\n}));\r\n\r\nfunction onMouseMove(e: MouseEvent) {\r\n requestAnimationFrame(() => {\r\n mouseX.value = e.pageX;\r\n mouseY.value = e.pageY;\r\n });\r\n}\r\n\r\nfunction onMouseLeave() {\r\n requestAnimationFrame(() => {\r\n mouseX.value = Infinity;\r\n mouseY.value = Infinity;\r\n });\r\n}\r\nprovide(MOUSE_X_INJECTION_KEY, mouseX);\r\nprovide(MOUSE_Y_INJECTION_KEY, mouseY);\r\nprovide(ORIENTATION_INJECTION_KEY, props.orientation);\r\nprovide(MAGNIFICATION_INJECTION_KEY, magnification);\r\nprovide(DISTANCE_INJECTION_KEY, distance);\r\n</script>\r\n"
8
+ },
9
+ {
10
+ "path": "DockIcon.vue",
11
+ "content": "<template>\r\n <div\r\n ref=\"iconRef\"\r\n class=\"flex aspect-square cursor-pointer items-center justify-center rounded-full transition-all duration-200 ease-out\"\r\n :style=\"{\r\n width: `${iconWidth}px`,\r\n height: `${iconWidth}px`,\r\n }\"\r\n :hovered=\"{\r\n marginLeft: margin,\r\n marginRight: margin,\r\n }\"\r\n >\r\n <slot />\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { ref, inject, computed } from \"vue\";\r\nimport {\r\n MOUSE_X_INJECTION_KEY,\r\n MOUSE_Y_INJECTION_KEY,\r\n MAGNIFICATION_INJECTION_KEY,\r\n DISTANCE_INJECTION_KEY,\r\n ORIENTATION_INJECTION_KEY,\r\n} from \"./injectionKeys\";\r\n\r\nconst iconRef = ref<HTMLDivElement | null>(null);\r\n\r\nconst mouseX = inject(MOUSE_X_INJECTION_KEY, ref(Infinity));\r\nconst mouseY = inject(MOUSE_Y_INJECTION_KEY, ref(Infinity));\r\nconst distance = inject(DISTANCE_INJECTION_KEY);\r\nconst orientation = inject(ORIENTATION_INJECTION_KEY, \"vertical\");\r\nconst magnification = inject(MAGNIFICATION_INJECTION_KEY);\r\nconst isVertical = computed(() => orientation === \"vertical\");\r\n\r\nconst margin = ref(0);\r\n\r\nfunction calculateDistance(val: number) {\r\n if (isVertical.value) {\r\n const bounds = iconRef.value?.getBoundingClientRect() || {\r\n y: 0,\r\n height: 0,\r\n };\r\n return val - bounds.y - bounds.height / 2;\r\n }\r\n const bounds = iconRef.value?.getBoundingClientRect() || { x: 0, width: 0 };\r\n return val - bounds.x - bounds.width / 2;\r\n}\r\n\r\nconst iconWidth = computed(() => {\r\n const distanceCalc = isVertical.value\r\n ? calculateDistance(mouseY.value)\r\n : calculateDistance(mouseX.value);\r\n if (!distance?.value || !magnification?.value) return 40;\r\n if (Math.abs(distanceCalc) < distance?.value) {\r\n return (1 - Math.abs(distanceCalc) / distance?.value) * magnification?.value + 40;\r\n }\r\n\r\n return 40;\r\n});\r\n</script>\r\n"
12
+ },
13
+ {
14
+ "path": "DockSeparator.vue",
15
+ "content": "<template>\r\n <div\r\n :class=\"\r\n cn('relative block bg-secondary', orientation === 'vertical' ? 'w-4/5 h-0.5' : 'h-4/5 w-0.5')\r\n \"\r\n ></div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { ORIENTATION_INJECTION_KEY } from \"./injectionKeys\";\r\nimport { cn } from \"~/lib/utils\";\r\n\r\nconst orientation = inject(ORIENTATION_INJECTION_KEY, \"vertical\");\r\n</script>\r\n"
16
+ },
17
+ {
18
+ "path": "index.ts",
19
+ "content": "export { default as Dock } from \"./Dock.vue\";\r\nexport { default as DockIcon } from \"./DockIcon.vue\";\r\nexport { default as DockSeparator } from \"./DockSeparator.vue\";\r\n"
20
+ },
21
+ {
22
+ "path": "injectionKeys.ts",
23
+ "content": "import type { Ref, InjectionKey, ComputedRef } from \"vue\";\r\nimport type { DataOrientation } from \"./types\";\r\n\r\nexport const MOUSE_X_INJECTION_KEY = Symbol(\"mouse-x\") as InjectionKey<Ref<number>>;\r\nexport const MOUSE_Y_INJECTION_KEY = Symbol(\"mouse-y\") as InjectionKey<Ref<number>>;\r\n\r\nexport const MAGNIFICATION_INJECTION_KEY = Symbol(\"magnification\") as InjectionKey<\r\n ComputedRef<number>\r\n>;\r\n\r\nexport const DISTANCE_INJECTION_KEY = Symbol(\"distance\") as InjectionKey<ComputedRef<number>>;\r\n\r\nexport const ORIENTATION_INJECTION_KEY = Symbol(\"orientation\") as InjectionKey<DataOrientation>;\r\n"
24
+ },
25
+ {
26
+ "path": "types.ts",
27
+ "content": "export type DataOrientation = \"vertical\" | \"horizontal\";\r\nexport type Direction = \"top\" | \"middle\" | \"bottom\";\r\n"
28
+ }
29
+ ],
30
+ "fileCount": 6,
31
+ "contentHash": "347c358234456aed6af263cebaf8ead5179c3428"
32
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "expandable-gallery",
3
+ "dependencies": [],
4
+ "files": [
5
+ {
6
+ "path": "ExpandableGallery.vue",
7
+ "content": "<template>\r\n <div :class=\"cn('flex h-96 w-full gap-2', props.class)\">\r\n <div\r\n v-for=\"image in images\"\r\n :key=\"image\"\r\n class=\"relative flex h-full flex-1 cursor-pointer overflow-hidden rounded-xl transition-all duration-500 ease-in-out hover:flex-[3]\"\r\n >\r\n <img\r\n class=\"relative h-full object-cover\"\r\n :src=\"image\"\r\n :alt=\"image\"\r\n />\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script lang=\"ts\" setup>\r\nimport type { HTMLAttributes } from \"vue\";\r\nimport { cn } from \"@/lib/utils\";\r\n\r\ninterface Props {\r\n images: string[];\r\n class?: HTMLAttributes[\"class\"];\r\n}\r\n\r\nconst props = defineProps<Props>();\r\n</script>\r\n"
8
+ },
9
+ {
10
+ "path": "index.ts",
11
+ "content": "export { default as ExpandableGallery } from \"./ExpandableGallery.vue\";\r\n"
12
+ }
13
+ ],
14
+ "fileCount": 2,
15
+ "contentHash": "c3a3a7a359c36414e013b4f92111c6877edc6ffd"
16
+ }
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "file-tree",
3
+ "dependencies": [],
4
+ "files": [
5
+ {
6
+ "path": "File.vue",
7
+ "content": "<template>\r\n <button\r\n ref=\"fileRef\"\r\n type=\"button\"\r\n :disabled=\"!isSelectable\"\r\n :class=\"[\r\n cn(\r\n 'flex w-fit items-center gap-1 rounded-sm pr-1 text-sm duration-200 ease-in-out rtl:pl-1 rtl:pr-0',\r\n isSelected && isSelectable ? 'bg-muted' : '',\r\n isSelectable ? 'cursor-pointer' : 'cursor-not-allowed opacity-50',\r\n $props.class,\r\n ),\r\n ]\"\r\n :dir=\"direction\"\r\n @click=\"onClickHandler\"\r\n >\r\n <Icon\r\n :name=\"fileIcon\"\r\n size=\"16\"\r\n />\r\n <span class=\"select-none\">{{ name }}</span>\r\n </button>\r\n</template>\r\n\r\n<script lang=\"ts\" setup>\r\nimport { cn } from \"@/lib/utils\";\r\nimport { type TreeContextProps, type FileProps, TREE_CONTEXT_SYMBOL } from \"./index\";\r\nimport { inject, computed, toRefs } from \"vue\";\r\n\r\nconst props = withDefaults(defineProps<FileProps>(), {\r\n isSelectable: true,\r\n});\r\n\r\nconst { id, name, isSelectable, isSelect } = toRefs(props);\r\n\r\nconst treeContext = inject<TreeContextProps>(TREE_CONTEXT_SYMBOL);\r\nif (!treeContext) {\r\n throw new Error(\"[File] must be used inside <Tree>\");\r\n}\r\n\r\nconst { selectedId, selectItem, direction, fileIcon } = treeContext;\r\n\r\nconst isSelected = computed<boolean>(() => {\r\n return isSelect.value || selectedId.value === id.value;\r\n});\r\n\r\nfunction onClickHandler() {\r\n if (!isSelectable.value) return;\r\n selectItem(id.value);\r\n}\r\n</script>\r\n"
8
+ },
9
+ {
10
+ "path": "Folder.vue",
11
+ "content": "<template>\r\n <div class=\"relative h-full overflow-hidden\">\r\n <div\r\n class=\"flex cursor-pointer items-center gap-1 rounded-md text-sm transition-all duration-200\"\r\n :class=\"[\r\n cn(\r\n 'flex cursor-pointer items-center gap-1 rounded-md text-sm',\r\n isSelect && isSelectable ? 'bg-muted' : '',\r\n !isSelectable ? 'cursor-not-allowed opacity-50' : '',\r\n $props.class,\r\n ),\r\n ]\"\r\n :dir=\"direction\"\r\n @click=\"onTriggerClick\"\r\n >\r\n <Icon\r\n v-if=\"isExpanded\"\r\n :name=\"openIcon\"\r\n size=\"16\"\r\n />\r\n <Icon\r\n v-else\r\n :name=\"closeIcon\"\r\n size=\"16\"\r\n />\r\n\r\n <span class=\"select-none\">{{ name }}</span>\r\n </div>\r\n\r\n <div\r\n v-if=\"isExpanded\"\r\n class=\"relative text-sm\"\r\n >\r\n <TreeIndicator\r\n v-if=\"name && indicator\"\r\n aria-hidden=\"true\"\r\n />\r\n <div\r\n class=\"ml-5 flex flex-col gap-1 py-1 rtl:mr-5\"\r\n :dir=\"direction\"\r\n >\r\n <slot />\r\n </div>\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script lang=\"ts\" setup>\r\nimport { cn } from \"@/lib/utils\";\r\nimport { type TreeContextProps, type FolderProps, TREE_CONTEXT_SYMBOL } from \"./index\";\r\nimport { inject, computed, toRefs } from \"vue\";\r\n\r\nconst props = withDefaults(defineProps<FolderProps>(), {\r\n isSelectable: true,\r\n});\r\n\r\nconst { id, name, isSelectable, isSelect } = toRefs(props);\r\n\r\nconst treeContext = inject<TreeContextProps>(TREE_CONTEXT_SYMBOL);\r\nif (!treeContext) {\r\n throw new Error(\"[Folder] must be used inside <Tree>\");\r\n}\r\n\r\nconst { expandedItems, handleExpand, openIcon, closeIcon, direction, indicator } = treeContext;\r\n\r\nconst isExpanded = computed<boolean>(() => {\r\n return !!expandedItems.value?.includes(id.value);\r\n});\r\n\r\nfunction onTriggerClick() {\r\n if (!isSelectable.value) return;\r\n handleExpand(id.value);\r\n}\r\n</script>\r\n"
12
+ },
13
+ {
14
+ "path": "index.ts",
15
+ "content": "import type { HTMLAttributes } from \"vue\";\r\n\r\nexport interface TreeViewElement {\r\n id: string;\r\n name: string;\r\n isSelectable?: boolean;\r\n children?: TreeViewElement[];\r\n}\r\n\r\nexport interface TreeProps {\r\n class?: HTMLAttributes[\"class\"];\r\n initialSelectedId: string;\r\n indicator?: boolean;\r\n elements: TreeViewElement[];\r\n initialExpandedItems: string[];\r\n openIcon?: string;\r\n closeIcon?: string;\r\n fileIcon?: string;\r\n direction?: \"rtl\" | \"ltr\";\r\n}\r\n\r\nexport interface TreeContextProps {\r\n selectedId: Ref<string | undefined>;\r\n expandedItems: Ref<string[] | undefined>;\r\n indicator: boolean;\r\n openIcon: string;\r\n closeIcon: string;\r\n fileIcon: string;\r\n direction: \"rtl\" | \"ltr\";\r\n handleExpand: (id: string) => void;\r\n selectItem: (id: string) => void;\r\n setExpandedItems: (items: string[] | undefined) => void;\r\n}\r\n\r\nexport interface BaseItemProps {\r\n class?: HTMLAttributes[\"class\"];\r\n id: string;\r\n name: string;\r\n isSelectable?: boolean;\r\n isSelect?: boolean;\r\n}\r\n\r\nexport interface FolderProps extends BaseItemProps {}\r\n\r\nexport interface FileProps extends BaseItemProps {}\r\n\r\nexport const TREE_CONTEXT_SYMBOL = Symbol(\"TREE_CONTEXT_SYMBOL\");\r\n\r\nexport { default as Tree } from \"./Tree.vue\";\r\nexport { default as Folder } from \"./Folder.vue\";\r\nexport { default as File } from \"./File.vue\";\r\nexport { default as TreeIndicator } from \"./TreeIndicator.vue\";\r\n"
16
+ },
17
+ {
18
+ "path": "Tree.vue",
19
+ "content": "<template>\r\n <div :class=\"cn('size-full', $props.class)\">\r\n <div\r\n ref=\"rootRef\"\r\n class=\"relative h-full overflow-auto px-2\"\r\n :dir=\"direction\"\r\n >\r\n <div class=\"flex flex-col gap-1\">\r\n <slot />\r\n </div>\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script lang=\"ts\" setup>\r\nimport { cn } from \"@/lib/utils\";\r\nimport {\r\n type TreeContextProps,\r\n type TreeViewElement,\r\n type TreeProps,\r\n TREE_CONTEXT_SYMBOL,\r\n} from \"./index\";\r\nimport { inject, computed, toRefs, ref, onMounted, provide } from \"vue\";\r\n\r\nconst props = withDefaults(defineProps<TreeProps>(), {\r\n indicator: true,\r\n dir: \"ltr\",\r\n openIcon: \"lucide:folder-open\",\r\n closeIcon: \"lucide:folder\",\r\n fileIcon: \"lucide:file\",\r\n});\r\n\r\nconst {\r\n initialSelectedId,\r\n indicator,\r\n elements,\r\n initialExpandedItems,\r\n openIcon,\r\n closeIcon,\r\n fileIcon,\r\n direction,\r\n} = toRefs(props);\r\n\r\nconst selectedId = ref<string | undefined>(initialSelectedId.value);\r\nconst expandedItems = ref<string[] | undefined>(initialExpandedItems.value);\r\n\r\nfunction handleExpand(id: string) {\r\n expandedItems.value = expandedItems.value ?? [];\r\n if (expandedItems.value.includes(id)) {\r\n // If already expanded, collapse it\r\n expandedItems.value = expandedItems.value.filter((item) => item !== id);\r\n } else {\r\n // Otherwise, expand it\r\n expandedItems.value.push(id);\r\n }\r\n}\r\n\r\nfunction selectItem(id: string) {\r\n selectedId.value = id;\r\n}\r\n\r\nfunction setExpandedItemsFn(items: string[] | undefined) {\r\n expandedItems.value = items;\r\n}\r\n\r\nprovide<TreeContextProps>(TREE_CONTEXT_SYMBOL, {\r\n selectedId,\r\n expandedItems,\r\n indicator: indicator.value,\r\n openIcon: openIcon.value,\r\n closeIcon: closeIcon.value,\r\n fileIcon: fileIcon.value,\r\n direction: direction.value === \"rtl\" ? \"rtl\" : \"ltr\",\r\n handleExpand,\r\n selectItem,\r\n setExpandedItems: setExpandedItemsFn,\r\n});\r\n\r\nfunction expandSpecificTargetedElements(list?: TreeViewElement[], selectId?: string) {\r\n if (!list || !selectId) return;\r\n function findParent(current: TreeViewElement, path: string[] = []) {\r\n const isSelectable = current.isSelectable ?? true;\r\n const newPath = [...path, current.id];\r\n if (current.id === selectId) {\r\n if (isSelectable) {\r\n expandedItems.value = [...(expandedItems.value ?? []), ...newPath];\r\n } else {\r\n // if not selectable, pop the last item (itself)\r\n newPath.pop();\r\n expandedItems.value = [...(expandedItems.value ?? []), ...newPath];\r\n }\r\n return;\r\n }\r\n if (current.children?.length) {\r\n current.children.forEach((child: TreeViewElement) => findParent(child, newPath));\r\n }\r\n }\r\n list.forEach((element: TreeViewElement) => findParent(element));\r\n}\r\n\r\nonMounted(() => {\r\n if (initialSelectedId.value) {\r\n expandSpecificTargetedElements(elements.value, initialSelectedId.value);\r\n }\r\n});\r\n</script>\r\n"
20
+ },
21
+ {
22
+ "path": "TreeIndicator.vue",
23
+ "content": "<template>\r\n <div\r\n :dir=\"direction\"\r\n class=\"absolute left-1.5 h-full w-px rounded-md bg-muted py-3 duration-300 ease-in-out hover:bg-slate-300 rtl:right-1.5\"\r\n />\r\n</template>\r\n\r\n<script lang=\"ts\" setup>\r\nimport { type TreeContextProps, TREE_CONTEXT_SYMBOL } from \"./index\";\r\nimport { inject } from \"vue\";\r\n\r\nconst treeContext = inject<TreeContextProps>(TREE_CONTEXT_SYMBOL);\r\nif (!treeContext) {\r\n throw new Error(\"[TreeIndicator] must be used inside <Tree>\");\r\n}\r\n\r\nconst { direction } = treeContext;\r\n</script>\r\n"
24
+ }
25
+ ],
26
+ "fileCount": 5,
27
+ "contentHash": "d4283e8ddced6a697d02ee957a3ddbd971763f96"
28
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "file-upload",
3
+ "dependencies": [
4
+ "motion-v"
5
+ ],
6
+ "files": [
7
+ {
8
+ "path": "FileUpload.vue",
9
+ "content": "<template>\r\n <ClientOnly>\r\n <div\r\n :class=\"cn('w-full', $props.class)\"\r\n @dragover.prevent=\"handleEnter\"\r\n @dragleave=\"handleLeave\"\r\n @drop.prevent=\"handleDrop\"\r\n @mouseover=\"handleEnter\"\r\n @mouseleave=\"handleLeave\"\r\n >\r\n <div\r\n class=\"group/file relative block w-full cursor-pointer overflow-hidden rounded-lg p-10\"\r\n @click=\"handleClick\"\r\n >\r\n <input\r\n ref=\"fileInputRef\"\r\n type=\"file\"\r\n class=\"hidden\"\r\n @change=\"onFileChange\"\r\n />\r\n\r\n <!-- Grid pattern -->\r\n <div\r\n class=\"pointer-events-none absolute inset-0 [mask-image:radial-gradient(ellipse_at_center,white,transparent)]\"\r\n >\r\n <slot />\r\n </div>\r\n\r\n <!-- Content -->\r\n <div class=\"flex flex-col items-center justify-center\">\r\n <p\r\n class=\"relative z-20 font-sans text-base font-bold text-neutral-700 dark:text-neutral-300\"\r\n >\r\n Upload file\r\n </p>\r\n <p\r\n class=\"relative z-20 mt-2 font-sans text-base font-normal text-neutral-400 dark:text-neutral-400\"\r\n >\r\n Drag or drop your files here or click to upload\r\n </p>\r\n\r\n <div class=\"relative mx-auto mt-10 w-full max-w-xl space-y-4\">\r\n <Motion\r\n v-for=\"(file, idx) in files\"\r\n :key=\"`file-${idx}`\"\r\n :initial=\"{ opacity: 0, scaleX: 0 }\"\r\n :animate=\"{ opacity: 1, scaleX: 1 }\"\r\n class=\"relative z-40 mx-auto flex w-full flex-col items-start justify-start overflow-hidden rounded-md bg-white p-4 shadow-sm md:h-24 dark:bg-neutral-900\"\r\n >\r\n <div class=\"flex w-full items-center justify-between gap-4\">\r\n <Motion\r\n as=\"p\"\r\n :initial=\"{ opacity: 0 }\"\r\n :animate=\"{ opacity: 1 }\"\r\n class=\"max-w-xs truncate text-base text-neutral-700 dark:text-neutral-300\"\r\n >\r\n {{ file.name }}\r\n </Motion>\r\n <Motion\r\n as=\"p\"\r\n :initial=\"{ opacity: 0 }\"\r\n :animate=\"{ opacity: 1 }\"\r\n class=\"w-fit shrink-0 rounded-lg px-2 py-1 text-sm text-neutral-600 shadow-input dark:bg-neutral-800 dark:text-white\"\r\n >\r\n {{ (file.size / (1024 * 1024)).toFixed(2) }} MB\r\n </Motion>\r\n </div>\r\n\r\n <div\r\n class=\"mt-2 flex w-full flex-col items-start justify-between text-sm text-neutral-600 md:flex-row md:items-center dark:text-neutral-400\"\r\n >\r\n <Motion\r\n as=\"p\"\r\n :initial=\"{ opacity: 0 }\"\r\n :animate=\"{ opacity: 1 }\"\r\n class=\"rounded-md bg-gray-100 px-1.5 py-1 text-sm dark:bg-neutral-800\"\r\n >\r\n {{ file.type || \"unknown type\" }}\r\n </Motion>\r\n <Motion\r\n as=\"p\"\r\n :initial=\"{ opacity: 0 }\"\r\n :animate=\"{ opacity: 1 }\"\r\n >\r\n modified {{ new Date(file.lastModified).toLocaleDateString() }}\r\n </Motion>\r\n </div>\r\n </Motion>\r\n\r\n <template v-if=\"!files.length\">\r\n <Motion\r\n as=\"div\"\r\n class=\"relative z-40 mx-auto mt-4 flex h-32 w-full max-w-32 items-center justify-center rounded-md bg-white shadow-[0px_10px_50px_rgba(0,0,0,0.1)] group-hover/file:shadow-2xl dark:bg-neutral-900\"\r\n :initial=\"{\r\n x: 0,\r\n y: 0,\r\n opacity: 1,\r\n }\"\r\n :transition=\"{\r\n type: 'spring',\r\n stiffness: 300,\r\n damping: 20,\r\n }\"\r\n :animate=\"\r\n isActive\r\n ? {\r\n x: 20,\r\n y: -20,\r\n opacity: 0.9,\r\n }\r\n : {}\r\n \"\r\n >\r\n <Icon\r\n name=\"heroicons:arrow-up-tray-20-solid\"\r\n class=\"text-neutral-600 dark:text-neutral-400\"\r\n size=\"20\"\r\n />\r\n </Motion>\r\n\r\n <div\r\n class=\"absolute inset-0 z-30 mx-auto mt-4 flex h-32 w-full max-w-32 items-center justify-center rounded-md border border-dashed border-sky-400 bg-transparent transition-opacity\"\r\n :class=\"{ 'opacity-100': isActive, 'opacity-0': !isActive }\"\r\n ></div>\r\n </template>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n </ClientOnly>\r\n</template>\r\n\r\n<script lang=\"ts\" setup>\r\nimport type { HTMLAttributes } from \"vue\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport { Motion } from \"motion-v\";\r\nimport { ref } from \"vue\";\r\n\r\ninterface FileUploadProps {\r\n class?: HTMLAttributes[\"class\"];\r\n}\r\n\r\ndefineProps<FileUploadProps>();\r\n\r\nconst emit = defineEmits<{\r\n (e: \"onChange\", files: File[]): void;\r\n}>();\r\n\r\nconst fileInputRef = ref<HTMLInputElement | null>(null);\r\nconst files = ref<File[]>([]);\r\nconst isActive = ref<boolean>(false);\r\n\r\nfunction handleFileChange(newFiles: File[]) {\r\n files.value = [...files.value, ...newFiles];\r\n emit(\"onChange\", files.value);\r\n}\r\n\r\nfunction onFileChange(e: Event) {\r\n const input = e.target as HTMLInputElement;\r\n if (!input.files) return;\r\n handleFileChange(Array.from(input.files));\r\n}\r\n\r\nfunction handleClick() {\r\n fileInputRef.value?.click();\r\n}\r\n\r\nfunction handleEnter() {\r\n isActive.value = true;\r\n}\r\nfunction handleLeave() {\r\n isActive.value = false;\r\n}\r\nfunction handleDrop(e: DragEvent) {\r\n isActive.value = false;\r\n const droppedFiles = e.dataTransfer?.files ? Array.from(e.dataTransfer.files) : [];\r\n if (droppedFiles.length) handleFileChange(droppedFiles);\r\n}\r\n</script>\r\n\r\n<style scoped>\r\n.group-hover\\/file\\:shadow-2xl:hover {\r\n box-shadow: 0px 10px 20px rgba(0, 0, 0, 0.25);\r\n}\r\n\r\n.transition-opacity {\r\n transition: opacity 0.3s ease;\r\n}\r\n</style>\r\n"
10
+ },
11
+ {
12
+ "path": "FileUploadGrid.vue",
13
+ "content": "<template>\r\n <div\r\n :class=\"\r\n cn(\r\n 'flex shrink-0 scale-105 flex-wrap items-center justify-center gap-px bg-gray-100 dark:bg-neutral-900',\r\n $props.class,\r\n )\r\n \"\r\n >\r\n <template v-for=\"row in ROWS\">\r\n <template\r\n v-for=\"col in COLUMNS\"\r\n :key=\"`${row}-${col}`\"\r\n >\r\n <div\r\n :class=\"\r\n cn(\r\n 'w-10 h-10 flex flex-shrink-0 rounded-[2px]',\r\n ((row - 1) * COLUMNS + (col - 1)) % 2 === 0\r\n ? 'bg-gray-50 dark:bg-neutral-950'\r\n : 'bg-gray-50 dark:bg-neutral-950 shadow-[0px_0px_1px_3px_rgba(255,255,255,1)_inset] dark:shadow-[0px_0px_1px_3px_rgba(0,0,0,1)_inset]',\r\n )\r\n \"\r\n />\r\n </template>\r\n </template>\r\n </div>\r\n</template>\r\n\r\n<script lang=\"ts\" setup>\r\nimport type { HTMLAttributes } from \"vue\";\r\nimport { cn } from \"@/lib/utils\";\r\n\r\ninterface FileUploadGridProps {\r\n class?: HTMLAttributes[\"class\"];\r\n}\r\n\r\ndefineProps<FileUploadGridProps>();\r\n\r\nconst ROWS = 11;\r\nconst COLUMNS = 41;\r\n</script>\r\n"
14
+ },
15
+ {
16
+ "path": "index.ts",
17
+ "content": "export { default as FileUpload } from \"./FileUpload.vue\";\r\nexport { default as FileUploadGrid } from \"./FileUploadGrid.vue\";\r\n"
18
+ }
19
+ ],
20
+ "fileCount": 3,
21
+ "contentHash": "401ae13bda7164b30500bae050392c82bf913c6c"
22
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "flickering-grid",
3
+ "dependencies": [],
4
+ "files": [
5
+ {
6
+ "path": "FlickeringGrid.vue",
7
+ "content": "<template>\r\n <div\r\n ref=\"containerRef\"\r\n :class=\"cn('w-full h-full', props.class)\"\r\n >\r\n <canvas\r\n ref=\"canvasRef\"\r\n class=\"pointer-events-none\"\r\n :width=\"canvasSize.width\"\r\n :height=\"canvasSize.height\"\r\n />\r\n </div>\r\n</template>\r\n\r\n<script lang=\"ts\" setup>\r\nimport { cn } from \"@/lib/utils\";\r\nimport { ref, onMounted, onBeforeUnmount, toRefs, computed } from \"vue\";\r\n\r\ninterface FlickeringGridProps {\r\n squareSize?: number;\r\n gridGap?: number;\r\n flickerChance?: number;\r\n color?: string;\r\n width?: number;\r\n height?: number;\r\n class?: string;\r\n maxOpacity?: number;\r\n}\r\n\r\nconst props = withDefaults(defineProps<FlickeringGridProps>(), {\r\n squareSize: 4,\r\n gridGap: 6,\r\n flickerChance: 0.3,\r\n color: \"rgb(0, 0, 0)\",\r\n maxOpacity: 0.3,\r\n});\r\n\r\nconst { squareSize, gridGap, flickerChance, color, maxOpacity, width, height } = toRefs(props);\r\n\r\nconst containerRef = ref<HTMLDivElement>();\r\nconst canvasRef = ref<HTMLCanvasElement>();\r\nconst context = ref<CanvasRenderingContext2D>();\r\n\r\nconst isInView = ref(false);\r\nconst canvasSize = ref({ width: 0, height: 0 });\r\n\r\nconst computedColor = computed(() => {\r\n if (!context.value) return \"rgba(255, 0, 0,\";\r\n\r\n const hex = color.value.replace(/^#/, \"\");\r\n const bigint = Number.parseInt(hex, 16);\r\n const r = (bigint >> 16) & 255;\r\n const g = (bigint >> 8) & 255;\r\n const b = bigint & 255;\r\n return `rgba(${r}, ${g}, ${b},`;\r\n});\r\n\r\nfunction setupCanvas(\r\n canvas: HTMLCanvasElement,\r\n width: number,\r\n height: number,\r\n): {\r\n cols: number;\r\n rows: number;\r\n squares: Float32Array;\r\n dpr: number;\r\n} {\r\n const dpr = window.devicePixelRatio || 1;\r\n canvas.width = width * dpr;\r\n canvas.height = height * dpr;\r\n canvas.style.width = `${width}px`;\r\n canvas.style.height = `${height}px`;\r\n\r\n const cols = Math.floor(width / (squareSize.value + gridGap.value));\r\n const rows = Math.floor(height / (squareSize.value + gridGap.value));\r\n\r\n const squares = new Float32Array(cols * rows);\r\n for (let i = 0; i < squares.length; i++) {\r\n squares[i] = Math.random() * maxOpacity.value;\r\n }\r\n return { cols, rows, squares, dpr };\r\n}\r\n\r\nfunction updateSquares(squares: Float32Array, deltaTime: number) {\r\n for (let i = 0; i < squares.length; i++) {\r\n if (Math.random() < flickerChance.value * deltaTime) {\r\n squares[i] = Math.random() * maxOpacity.value;\r\n }\r\n }\r\n}\r\n\r\nfunction drawGrid(\r\n ctx: CanvasRenderingContext2D,\r\n width: number,\r\n height: number,\r\n cols: number,\r\n rows: number,\r\n squares: Float32Array,\r\n dpr: number,\r\n) {\r\n ctx.clearRect(0, 0, width, height);\r\n ctx.fillStyle = \"transparent\";\r\n ctx.fillRect(0, 0, width, height);\r\n for (let i = 0; i < cols; i++) {\r\n for (let j = 0; j < rows; j++) {\r\n const opacity = squares[i * rows + j];\r\n ctx.fillStyle = `${computedColor.value}${opacity})`;\r\n ctx.fillRect(\r\n i * (squareSize.value + gridGap.value) * dpr,\r\n j * (squareSize.value + gridGap.value) * dpr,\r\n squareSize.value * dpr,\r\n squareSize.value * dpr,\r\n );\r\n }\r\n }\r\n}\r\n\r\nconst gridParams = ref<ReturnType<typeof setupCanvas>>();\r\n\r\nfunction updateCanvasSize() {\r\n const newWidth = width.value || containerRef.value!.clientWidth;\r\n const newHeight = height.value || containerRef.value!.clientHeight;\r\n\r\n canvasSize.value = { width: newWidth, height: newHeight };\r\n gridParams.value = setupCanvas(canvasRef.value!, newWidth, newHeight);\r\n}\r\n\r\nlet animationFrameId: number | undefined;\r\nlet resizeObserver: ResizeObserver | undefined;\r\nlet intersectionObserver: IntersectionObserver | undefined;\r\nlet lastTime = 0;\r\n\r\nfunction animate(time: number) {\r\n if (!isInView.value) return;\r\n\r\n const deltaTime = (time - lastTime) / 1000;\r\n lastTime = time;\r\n\r\n updateSquares(gridParams.value!.squares, deltaTime);\r\n drawGrid(\r\n context.value!,\r\n canvasRef.value!.width,\r\n canvasRef.value!.height,\r\n gridParams.value!.cols,\r\n gridParams.value!.rows,\r\n gridParams.value!.squares,\r\n gridParams.value!.dpr,\r\n );\r\n animationFrameId = requestAnimationFrame(animate);\r\n}\r\n\r\nonMounted(() => {\r\n if (!canvasRef.value || !containerRef.value) return;\r\n context.value = canvasRef.value.getContext(\"2d\")!;\r\n if (!context.value) return;\r\n\r\n updateCanvasSize();\r\n\r\n resizeObserver = new ResizeObserver(() => {\r\n updateCanvasSize();\r\n });\r\n intersectionObserver = new IntersectionObserver(\r\n ([entry]) => {\r\n isInView.value = entry.isIntersecting;\r\n animationFrameId = requestAnimationFrame(animate);\r\n },\r\n { threshold: 0 },\r\n );\r\n\r\n resizeObserver.observe(containerRef.value);\r\n intersectionObserver.observe(canvasRef.value);\r\n});\r\n\r\nonBeforeUnmount(() => {\r\n if (animationFrameId) {\r\n cancelAnimationFrame(animationFrameId);\r\n }\r\n resizeObserver?.disconnect();\r\n intersectionObserver?.disconnect();\r\n});\r\n</script>\r\n"
8
+ },
9
+ {
10
+ "path": "index.ts",
11
+ "content": "export { default as FlickeringGrid } from \"./FlickeringGrid.vue\";\r\n"
12
+ }
13
+ ],
14
+ "fileCount": 2,
15
+ "contentHash": "1f43e36cb8ad9f499501b409bd86d59a212eb737"
16
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "flip-card",
3
+ "dependencies": [],
4
+ "files": [
5
+ {
6
+ "path": "FlipCard.vue",
7
+ "content": "<template>\r\n <div :class=\"cn('group h-72 w-56 [perspective:1000px]', props.class)\">\r\n <div\r\n :class=\"\r\n cn(\r\n 'relative h-full rounded-2xl transition-all duration-500 [transform-style:preserve-3d]',\r\n rotation[0],\r\n )\r\n \"\r\n >\r\n <!-- Front -->\r\n <div\r\n class=\"absolute size-full overflow-hidden rounded-2xl border [backface-visibility:hidden]\"\r\n >\r\n <slot />\r\n </div>\r\n\r\n <!-- Back -->\r\n <div\r\n :class=\"\r\n cn(\r\n 'absolute h-full w-full overflow-hidden rounded-2xl border bg-black/80 p-4 text-slate-200 [backface-visibility:hidden]',\r\n rotation[1],\r\n )\r\n \"\r\n >\r\n <slot name=\"back\" />\r\n </div>\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { cn } from \"@/lib/utils\";\r\nimport { computed } from \"vue\";\r\n\r\ninterface FlipCardProps {\r\n rotate?: \"x\" | \"y\";\r\n class?: string;\r\n}\r\n\r\nconst props = withDefaults(defineProps<FlipCardProps>(), {\r\n rotate: \"y\",\r\n});\r\nconst rotationClass = {\r\n x: [\"group-hover:[transform:rotateX(180deg)]\", \"[transform:rotateX(180deg)]\"],\r\n y: [\"group-hover:[transform:rotateY(180deg)]\", \"[transform:rotateY(180deg)]\"],\r\n};\r\n\r\nconst rotation = computed(() => rotationClass[props.rotate]);\r\n</script>\r\n"
8
+ },
9
+ {
10
+ "path": "index.ts",
11
+ "content": "export { default as FlipCard } from \"./FlipCard.vue\";\r\n"
12
+ }
13
+ ],
14
+ "fileCount": 2,
15
+ "contentHash": "50a02804c0c17ea051e1690f2bb60b537f565077"
16
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "flip-words",
3
+ "dependencies": [],
4
+ "files": [
5
+ {
6
+ "path": "FlipWords.vue",
7
+ "content": "<template>\r\n <div class=\"relative inline-block px-2\">\r\n <Transition\r\n @after-enter=\"$emit('animationStart')\"\r\n @after-leave=\"$emit('animationComplete')\"\r\n >\r\n <div\r\n v-show=\"isVisible\"\r\n :class=\"[\r\n 'relative z-10 inline-block text-left text-neutral-900 dark:text-neutral-100',\r\n props.class,\r\n ]\"\r\n >\r\n <template\r\n v-for=\"(wordObj, wordIndex) in splitWords\"\r\n :key=\"wordObj.word + wordIndex\"\r\n >\r\n <span\r\n class=\"inline-block whitespace-nowrap opacity-0\"\r\n :style=\"{\r\n animation: `fadeInWord 0.3s ease forwards`,\r\n animationDelay: `${wordIndex * 0.3}s`,\r\n }\"\r\n >\r\n <span\r\n v-for=\"(letter, letterIndex) in wordObj.letters\"\r\n :key=\"wordObj.word + letterIndex\"\r\n class=\"inline-block opacity-0\"\r\n :style=\"{\r\n animation: `fadeInLetter 0.2s ease forwards`,\r\n animationDelay: `${wordIndex * 0.3 + letterIndex * 0.05}s`,\r\n }\"\r\n >\r\n {{ letter }}\r\n </span>\r\n <span class=\"inline-block\">&nbsp;</span>\r\n </span>\r\n </template>\r\n </div>\r\n </Transition>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { ref, computed, onMounted, onBeforeUnmount, watch } from \"vue\";\r\n\r\ninterface Props {\r\n words: string[];\r\n duration?: number;\r\n class?: string;\r\n}\r\n\r\nconst props = withDefaults(defineProps<Props>(), {\r\n duration: 3000,\r\n class: \"\",\r\n});\r\n\r\ndefineEmits([\"animationStart\", \"animationComplete\"]);\r\n\r\nconst currentWord = ref(props.words[0]);\r\nconst isVisible = ref(true);\r\nconst timeoutId = ref<number | null>(null);\r\n\r\nfunction startAnimation() {\r\n isVisible.value = false;\r\n\r\n setTimeout(() => {\r\n const currentIndex = props.words.indexOf(currentWord.value);\r\n const nextWord = props.words[currentIndex + 1] || props.words[0];\r\n currentWord.value = nextWord;\r\n isVisible.value = true;\r\n }, 600);\r\n}\r\n\r\nconst splitWords = computed(() => {\r\n return currentWord.value.split(\" \").map((word) => ({\r\n word,\r\n letters: word.split(\"\"),\r\n }));\r\n});\r\n\r\nfunction startTimeout() {\r\n timeoutId.value = window.setTimeout(() => {\r\n startAnimation();\r\n }, props.duration);\r\n}\r\n\r\nonMounted(() => {\r\n startTimeout();\r\n});\r\n\r\nonBeforeUnmount(() => {\r\n if (timeoutId.value) {\r\n clearTimeout(timeoutId.value);\r\n }\r\n});\r\n\r\nwatch(isVisible, (newValue) => {\r\n if (newValue) {\r\n startTimeout();\r\n }\r\n});\r\n</script>\r\n\r\n<style>\r\n@keyframes fadeInWord {\r\n 0% {\r\n opacity: 0;\r\n transform: translateY(10px);\r\n filter: blur(8px);\r\n }\r\n 100% {\r\n opacity: 1;\r\n transform: translateY(0);\r\n filter: blur(0);\r\n }\r\n}\r\n\r\n@keyframes fadeInLetter {\r\n 0% {\r\n opacity: 0;\r\n transform: translateY(10px);\r\n filter: blur(8px);\r\n }\r\n 100% {\r\n opacity: 1;\r\n transform: translateY(0);\r\n filter: blur(0);\r\n }\r\n}\r\n\r\n.v-enter-active {\r\n animation: enterWord 0.6s ease-in-out forwards;\r\n}\r\n\r\n.v-leave-active {\r\n animation: leaveWord 0.6s ease-in-out forwards;\r\n}\r\n\r\n@keyframes enterWord {\r\n 0% {\r\n opacity: 0;\r\n transform: translateY(10px);\r\n }\r\n 100% {\r\n opacity: 1;\r\n transform: translateY(0);\r\n }\r\n}\r\n\r\n@keyframes leaveWord {\r\n 0% {\r\n opacity: 1;\r\n transform: scale(1);\r\n filter: blur(0);\r\n }\r\n 100% {\r\n opacity: 0;\r\n transform: scale(2);\r\n filter: blur(8px);\r\n }\r\n}\r\n</style>\r\n"
8
+ },
9
+ {
10
+ "path": "index.ts",
11
+ "content": "export { default as FlipWords } from \"./FlipWords.vue\";\r\n"
12
+ }
13
+ ],
14
+ "fileCount": 2,
15
+ "contentHash": "7c41b33700ea1ad7d373cb85226e368f4ae0772b"
16
+ }