diff --git a/apps/www/package.json b/apps/www/package.json
index 4d799b54..34efd414 100644
--- a/apps/www/package.json
+++ b/apps/www/package.json
@@ -27,6 +27,7 @@
"clsx": "^2.0.0",
"codesandbox": "^2.2.3",
"date-fns": "^2.30.0",
+ "embla-carousel-vue": "8.0.0-rc17",
"lucide-vue-next": "^0.276.0",
"radix-vue": "^1.2.5",
"tailwindcss-animate": "^1.0.7",
diff --git a/apps/www/src/lib/registry/default/ui/carousel/Carousel.vue b/apps/www/src/lib/registry/default/ui/carousel/Carousel.vue
new file mode 100644
index 00000000..56be5281
--- /dev/null
+++ b/apps/www/src/lib/registry/default/ui/carousel/Carousel.vue
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
diff --git a/apps/www/src/lib/registry/default/ui/carousel/CarouselContent.vue b/apps/www/src/lib/registry/default/ui/carousel/CarouselContent.vue
new file mode 100644
index 00000000..7d141430
--- /dev/null
+++ b/apps/www/src/lib/registry/default/ui/carousel/CarouselContent.vue
@@ -0,0 +1,21 @@
+
+
+
+
+
diff --git a/apps/www/src/lib/registry/default/ui/carousel/CarouselItem.vue b/apps/www/src/lib/registry/default/ui/carousel/CarouselItem.vue
new file mode 100644
index 00000000..5ff44c57
--- /dev/null
+++ b/apps/www/src/lib/registry/default/ui/carousel/CarouselItem.vue
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
diff --git a/apps/www/src/lib/registry/default/ui/carousel/CarouselNext.vue b/apps/www/src/lib/registry/default/ui/carousel/CarouselNext.vue
new file mode 100644
index 00000000..ec03059a
--- /dev/null
+++ b/apps/www/src/lib/registry/default/ui/carousel/CarouselNext.vue
@@ -0,0 +1,25 @@
+
+
+
+
+
diff --git a/apps/www/src/lib/registry/default/ui/carousel/CarouselPrevious.vue b/apps/www/src/lib/registry/default/ui/carousel/CarouselPrevious.vue
new file mode 100644
index 00000000..7a335f61
--- /dev/null
+++ b/apps/www/src/lib/registry/default/ui/carousel/CarouselPrevious.vue
@@ -0,0 +1,25 @@
+
+
+
+
+
diff --git a/apps/www/src/lib/registry/default/ui/carousel/index.ts b/apps/www/src/lib/registry/default/ui/carousel/index.ts
new file mode 100644
index 00000000..339a2e6e
--- /dev/null
+++ b/apps/www/src/lib/registry/default/ui/carousel/index.ts
@@ -0,0 +1,10 @@
+export { default as Carousel } from './Carousel.vue'
+export { default as CarouselContent } from './CarouselContent.vue'
+export { default as CarouselItem } from './CarouselItem.vue'
+export { default as CarouselPrevious } from './CarouselPrevious.vue'
+export { default as CarouselNext } from './CarouselNext.vue'
+export { useCarousel } from './useCarousel'
+
+export {
+ type EmblaCarouselType as CarouselApi,
+} from 'embla-carousel-vue'
diff --git a/apps/www/src/lib/registry/default/ui/carousel/interface.ts b/apps/www/src/lib/registry/default/ui/carousel/interface.ts
new file mode 100644
index 00000000..328d90a5
--- /dev/null
+++ b/apps/www/src/lib/registry/default/ui/carousel/interface.ts
@@ -0,0 +1,10 @@
+import {
+ type EmblaOptionsType as CarouselOptions,
+ type EmblaPluginType as CarouselPlugin,
+} from 'embla-carousel-vue'
+
+export interface CarouselProps {
+ opts?: CarouselOptions
+ plugins?: CarouselPlugin[]
+ orientation?: 'horizontal' | 'vertical'
+}
diff --git a/apps/www/src/lib/registry/default/ui/carousel/useCarousel.ts b/apps/www/src/lib/registry/default/ui/carousel/useCarousel.ts
new file mode 100644
index 00000000..84a2e1e9
--- /dev/null
+++ b/apps/www/src/lib/registry/default/ui/carousel/useCarousel.ts
@@ -0,0 +1,54 @@
+import { createInjectionState } from '@vueuse/core'
+import emblaCarouselVue, {
+ type EmblaCarouselType as CarouselApi,
+} from 'embla-carousel-vue'
+import { onMounted, ref } from 'vue'
+import type { CarouselProps } from './interface'
+
+const [useProvideCarousel, useInjectCarousel] = createInjectionState(
+ ({
+ opts, orientation, plugins,
+ }: CarouselProps) => {
+ const [emblaNode, emblaApi] = emblaCarouselVue({
+ ...opts,
+ axis: orientation === 'horizontal' ? 'x' : 'y',
+ }, plugins)
+
+ function scrollPrev() {
+ emblaApi.value?.scrollPrev()
+ }
+ function scrollNext() {
+ emblaApi.value?.scrollNext()
+ }
+
+ const canScrollNext = ref(true)
+ const canScrollPrev = ref(true)
+
+ function onSelect(api: CarouselApi) {
+ canScrollNext.value = api.canScrollNext()
+ canScrollPrev.value = api.canScrollPrev()
+ }
+
+ onMounted(() => {
+ if (!emblaApi.value)
+ return
+
+ emblaApi.value?.on('init', onSelect)
+ emblaApi.value?.on('reInit', onSelect)
+ emblaApi.value?.on('select', onSelect)
+ })
+
+ return { carouselRef: emblaNode, carouselApi: emblaApi, canScrollPrev, canScrollNext, scrollPrev, scrollNext, orientation }
+ },
+)
+
+function useCarousel() {
+ const carouselState = useInjectCarousel()
+
+ if (!carouselState)
+ throw new Error('useCarousel must be used within a ')
+
+ return carouselState
+}
+
+export { useCarousel, useProvideCarousel }
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index c4c910f4..30745cec 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -83,6 +83,9 @@ importers:
date-fns:
specifier: ^2.30.0
version: 2.30.0
+ embla-carousel-vue:
+ specifier: 8.0.0-rc17
+ version: 8.0.0-rc17(vue@3.3.7)
lucide-vue-next:
specifier: ^0.276.0
version: 0.276.0(vue@3.3.7)
@@ -6895,6 +6898,28 @@ packages:
resolution: {integrity: sha512-L6uRgvZTH+4OF5NE/MBbzQx/WYpru1xCBE9respNj6qznEewGUIfhzmm7horWWxbNO2M0WckQypGctR8lH79xQ==}
dev: false
+ /embla-carousel-reactive-utils@8.0.0-rc17(embla-carousel@8.0.0-rc17):
+ resolution: {integrity: sha512-eluEOK/u5HdjYaTLC4bUG3iTCnyX7RsYix3il0aH4ZECOKa5fS+pVK2vrM17Mgw6C5Hyjcr3r3lfJtGerVzVsQ==}
+ peerDependencies:
+ embla-carousel: 8.0.0-rc17
+ dependencies:
+ embla-carousel: 8.0.0-rc17
+ dev: false
+
+ /embla-carousel-vue@8.0.0-rc17(vue@3.3.7):
+ resolution: {integrity: sha512-+LHBImxj5Z8OhQHuBxbLhBNZ9cyS1UvXpCbPwlYvUllZ5XbzN08eFNiG7aBnOzxx2WzX2mc6tFzToN+HbO3QPg==}
+ peerDependencies:
+ vue: ^3.2.37
+ dependencies:
+ embla-carousel: 8.0.0-rc17
+ embla-carousel-reactive-utils: 8.0.0-rc17(embla-carousel@8.0.0-rc17)
+ vue: 3.3.7(typescript@5.2.2)
+ dev: false
+
+ /embla-carousel@8.0.0-rc17:
+ resolution: {integrity: sha512-evF49b88VOitvqFtlvhvKVSu96Y8A+QSFdhok87Bfm8R7OYuk95FT+o8+M1GQLi/EhGDUlT193HTVAR0Wt2neQ==}
+ dev: false
+
/emoji-regex@10.2.1:
resolution: {integrity: sha512-97g6QgOk8zlDRdgq1WxwgTMgEWGVAQvB5Fdpgc1MkNy56la5SKP9GsMXKDOdqwn90/41a8yPwIGk1Y6WVbeMQA==}
dev: false