{
  "$schema": "https://shadcn-vue.com/schema/registry-item.json",
  "name": "carousel",
  "title": "Carousel",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-vue/components/carousel/Carousel.vue",
      "content": "<!--\n  Carousel Component\n  \n  A flexible carousel/slider for cycling through content.\n  \n  @example - Basic carousel with images\n  <Carousel>\n    <CarouselContent>\n      <CarouselItem v-for=\"img in images\" :key=\"img.id\">\n        <img :src=\"img.src\" :alt=\"img.alt\" class=\"w-full h-full object-cover\" />\n      </CarouselItem>\n    </CarouselContent>\n  </Carousel>\n  \n  @example - Carousel with navigation controls\n  <Carousel v-model:active-index=\"activeIndex\" :loop=\"true\">\n    <CarouselContent>\n      <CarouselItem v-for=\"slide in slides\" :key=\"slide.id\">\n        <SlideContent :content=\"slide\" />\n      </CarouselItem>\n    </CarouselContent>\n    <template #footer>\n      <CarouselPrevious />\n      <CarouselIndicators />\n      <CarouselNext />\n    </template>\n  </Carousel>\n  \n  @example - Vertical carousel\n  <Carousel orientation=\"vertical\" :loop=\"false\">\n    <CarouselContent>\n      <CarouselItem v-for=\"item in items\" :key=\"item.id\">\n        {{ item }}\n      </CarouselItem>\n    </CarouselContent>\n  </Carousel>\n-->\n<script setup lang=\"ts\">\nimport { provide, computed, watch, onMounted, onUnmounted, type HTMLAttributes } from 'vue'\nimport { cn } from '@/lib/utils'\nimport { useCarousel } from './useCarousel'\nimport { carouselVariants } from './carousel.variants'\n\nexport interface CarouselProps {\n  modelValue?: number\n  orientation?: 'horizontal' | 'vertical'\n  loop?: boolean\n  class?: HTMLAttributes['class']\n}\n\nconst props = withDefaults(defineProps<CarouselProps>(), {\n  modelValue: 0,\n  orientation: 'horizontal',\n  loop: false,\n})\n\nconst emit = defineEmits<{\n  'update:modelValue': [value: number]\n}>()\n\nconst carousel = useCarousel({\n  loop: props.loop,\n  orientation: props.orientation,\n})\n\n// Sync modelValue → scroll when controlled externally\nwatch(\n  () => props.modelValue,\n  (newVal) => {\n    if (newVal !== carousel.activeIndex.value) {\n      carousel.scrollTo(newVal, false)\n    }\n  },\n)\n\n// Emit scroll position changes back to parent\nfunction onScroll() {\n  const newIndex = carousel.activeIndex.value\n  if (newIndex !== props.modelValue) {\n    emit('update:modelValue', newIndex)\n  }\n}\n\nonMounted(() => {\n  if (carousel.rootRef.value) {\n    carousel.rootRef.value.addEventListener('scroll', onScroll, { passive: true })\n  }\n})\n\nonUnmounted(() => {\n  if (carousel.rootRef.value) {\n    carousel.rootRef.value.removeEventListener('scroll', onScroll)\n  }\n})\n\n// Provide to children\nprovide('carousel', {\n  ...carousel,\n  orientation: computed(() => props.orientation),\n  loop: computed(() => props.loop),\n})\n</script>\n\n<template>\n  <div\n    data-uipkge\n    data-slot=\"carousel\"\n    :class=\"cn(carouselVariants({ orientation: props.orientation }), props.class)\"\n    role=\"region\"\n    aria-label=\"Carousel\"\n  >\n    <slot />\n  </div>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/carousel/Carousel.vue"
    },
    {
      "path": "packages/registry-vue/components/carousel/CarouselContent.vue",
      "content": "<!--\n  CarouselContent\n  \n  The scrollable container for carousel items.\n  Uses CSS scroll-snap for smooth sliding behavior.\n  \n  @example\n  <CarouselContent>\n    <CarouselItem v-for=\"slide in slides\" :key=\"slide.id\">\n      <img :src=\"slide.src\" :alt=\"slide.alt\" />\n    </CarouselItem>\n  </CarouselContent>\n-->\n<script setup lang=\"ts\">\nimport { inject, computed, type HTMLAttributes } from 'vue'\nimport { cn } from '@/lib/utils'\n\ninterface Props {\n  class?: HTMLAttributes['class']\n}\n\nconst props = defineProps<Props>()\n\nconst carousel = inject<{\n  rootRef: { value: HTMLElement | null }\n  orientation: { value: 'horizontal' | 'vertical' }\n} | null>('carousel', null)\n\nconst isHorizontal = computed(() => carousel?.orientation?.value !== 'vertical')\n\nconst scrollClass = computed(() => {\n  return isHorizontal.value\n    ? 'flex overflow-x-auto scroll-smooth snap-x snap-mandatory'\n    : 'flex flex-col overflow-y-auto snap-y snap-mandatory'\n})\n</script>\n\n<template>\n  <div\n    :ref=\"\n      (el) => {\n        if (carousel) carousel.rootRef.value = el as HTMLElement\n      }\n    \"\n    data-uipkge\n    data-slot=\"carousel-content\"\n    :class=\"\n      cn(\n        scrollClass,\n        'relative h-full w-full',\n        '[-ms-overflow-style:none] [scrollbar-width:none]',\n        '[&::-webkit-scrollbar]:hidden',\n        props.class,\n      )\n    \"\n    :aria-orientation=\"carousel?.orientation?.value\"\n    role=\"group\"\n  >\n    <slot />\n  </div>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/carousel/CarouselContent.vue"
    },
    {
      "path": "packages/registry-vue/components/carousel/CarouselFooter.vue",
      "content": "<!--\n  CarouselFooter\n  \n  Wrapper for carousel footer content (pagination, indicators).\n  \n  @example\n  <CarouselFooter>\n    <CarouselIndicators />\n  </CarouselFooter>\n-->\n<script setup lang=\"ts\">\nimport type { HTMLAttributes } from 'vue'\nimport { cn } from '@/lib/utils'\n\ninterface Props {\n  class?: HTMLAttributes['class']\n}\n\nconst props = defineProps<Props>()\n</script>\n\n<template>\n  <div data-uipkge data-slot=\"carousel-footer\" :class=\"cn('flex items-center justify-between px-1 pt-2', props.class)\">\n    <slot />\n  </div>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/carousel/CarouselFooter.vue"
    },
    {
      "path": "packages/registry-vue/components/carousel/CarouselHeader.vue",
      "content": "<!--\n  CarouselHeader\n  \n  Wrapper for carousel header content (title, controls).\n  \n  @example\n  <CarouselHeader>\n    <h3>Featured Products</h3>\n  </CarouselHeader>\n-->\n<script setup lang=\"ts\">\nimport type { HTMLAttributes } from 'vue'\nimport { cn } from '@/lib/utils'\n\ninterface Props {\n  class?: HTMLAttributes['class']\n}\n\nconst props = defineProps<Props>()\n</script>\n\n<template>\n  <div data-uipkge data-slot=\"carousel-header\" :class=\"cn('flex items-center justify-between px-1 pb-2', props.class)\">\n    <slot />\n  </div>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/carousel/CarouselHeader.vue"
    },
    {
      "path": "packages/registry-vue/components/carousel/CarouselIndicators.vue",
      "content": "<!--\n  CarouselIndicators\n  \n  Pagination dots showing current slide position.\n  Clicking a dot navigates to that slide.\n  \n  @example\n  <CarouselIndicators />\n-->\n<script setup lang=\"ts\">\nimport { inject, type HTMLAttributes } from 'vue'\nimport { cn } from '@/lib/utils'\n\ninterface Props {\n  class?: HTMLAttributes['class']\n}\n\ndefineProps<Props>()\n\nconst carousel = inject<{\n  activeIndex: { value: number }\n  scrollSnaps: { value: number[] }\n  scrollTo: (index: number, smooth?: boolean) => void\n} | null>('carousel', null)\n</script>\n\n<template>\n  <div\n    v-if=\"carousel\"\n    data-uipkge\n    data-slot=\"carousel-indicators\"\n    class=\"flex items-center justify-center gap-1.5 py-2\"\n    role=\"tablist\"\n    aria-label=\"Carousel navigation\"\n  >\n    <button\n      v-for=\"(_, index) in carousel.scrollSnaps.value\"\n      :key=\"index\"\n      type=\"button\"\n      role=\"tab\"\n      :aria-label=\"`Go to slide ${index + 1}`\"\n      :aria-selected=\"index === carousel.activeIndex.value\"\n      :class=\"\n        cn(\n          'h-2 w-2 rounded-full transition-all duration-200',\n          index === carousel.activeIndex.value\n            ? 'bg-primary w-6'\n            : 'bg-muted-foreground/30 hover:bg-muted-foreground/50',\n        )\n      \"\n      @click=\"carousel.scrollTo(index, true)\"\n    />\n  </div>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/carousel/CarouselIndicators.vue"
    },
    {
      "path": "packages/registry-vue/components/carousel/CarouselItem.vue",
      "content": "<!--\n  CarouselItem\n  \n  Individual slide within the carousel.\n  Each item snaps into view when scrolled.\n  \n  @example\n  <CarouselItem>\n    <img src=\"/img1.jpg\" alt=\"Slide 1\" />\n  </CarouselItem>\n-->\n<script setup lang=\"ts\">\nimport { inject, computed, type HTMLAttributes } from 'vue'\nimport { cn } from '@/lib/utils'\nimport { carouselItemVariants } from './index'\n\ninterface Props {\n  class?: HTMLAttributes['class']\n  active?: boolean\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  active: false,\n})\n\nconst carousel = inject<{\n  orientation: { value: 'horizontal' | 'vertical' }\n} | null>('carousel', null)\n\nconst itemClass = computed(() => {\n  const orientation = carousel?.orientation?.value ?? 'horizontal'\n  return carouselItemVariants({ orientation })\n})\n</script>\n\n<template>\n  <div\n    data-uipkge\n    data-slot=\"carousel-item\"\n    :class=\"cn(itemClass, 'shrink-0 grow-0 basis-full', 'snap-start', 'relative', props.class)\"\n    :aria-hidden=\"!props.active\"\n    role=\"group\"\n    :aria-roledescription=\"'slide'\"\n  >\n    <slot />\n  </div>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/carousel/CarouselItem.vue"
    },
    {
      "path": "packages/registry-vue/components/carousel/CarouselNext.vue",
      "content": "<!--\n  CarouselNext\n  \n  Navigation button to go to the next slide.\n  \n  @example\n  <CarouselNext label=\"Next slide\" />\n-->\n<script setup lang=\"ts\">\nimport { inject, computed, type HTMLAttributes } from 'vue'\nimport { ChevronRight } from 'lucide-vue-next'\nimport { cn } from '@/lib/utils'\nimport { Button } from '@/components/ui/button'\n\ninterface Props {\n  class?: HTMLAttributes['class']\n  label?: string\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  label: 'Next slide',\n})\n\nconst carousel = inject<{\n  canScrollNext: { value: boolean }\n  scrollToNext: (smooth?: boolean) => void\n  orientation: { value: 'horizontal' | 'vertical' }\n} | null>('carousel', null)\n\nconst isHorizontal = computed(() => carousel?.orientation?.value !== 'vertical')\n</script>\n\n<template>\n  <Button\n    type=\"button\"\n    variant=\"outline\"\n    size=\"icon\"\n    :class=\"\n      cn(\n        'absolute z-10 size-8 shrink-0 rounded-full',\n        'bg-background/80 border shadow-md backdrop-blur-sm',\n        'hover:bg-accent hover:text-accent-foreground',\n        'disabled:pointer-events-none disabled:opacity-50',\n        isHorizontal ? 'top-1/2 -right-3 -translate-y-1/2' : '-bottom-3 left-1/2 -translate-x-1/2 rotate-90',\n        props.class,\n      )\n    \"\n    :disabled=\"!carousel?.canScrollNext?.value\"\n    :aria-label=\"props.label\"\n    @click=\"carousel?.scrollToNext(true)\"\n  >\n    <slot>\n      <ChevronRight class=\"size-4\" aria-hidden=\"true\" />\n    </slot>\n  </Button>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/carousel/CarouselNext.vue"
    },
    {
      "path": "packages/registry-vue/components/carousel/CarouselPrevious.vue",
      "content": "<!--\n  CarouselPrevious\n  \n  Navigation button to go to the previous slide.\n  \n  @example\n  <CarouselPrevious label=\"Previous slide\" />\n-->\n<script setup lang=\"ts\">\nimport { inject, computed, type HTMLAttributes } from 'vue'\nimport { ChevronLeft } from 'lucide-vue-next'\nimport { cn } from '@/lib/utils'\nimport { Button } from '@/components/ui/button'\n\ninterface Props {\n  class?: HTMLAttributes['class']\n  label?: string\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  label: 'Previous slide',\n})\n\nconst carousel = inject<{\n  canScrollPrev: { value: boolean }\n  scrollToPrev: (smooth?: boolean) => void\n  orientation: { value: 'horizontal' | 'vertical' }\n} | null>('carousel', null)\n\nconst isHorizontal = computed(() => carousel?.orientation?.value !== 'vertical')\n</script>\n\n<template>\n  <Button\n    type=\"button\"\n    variant=\"outline\"\n    size=\"icon\"\n    :class=\"\n      cn(\n        'absolute z-10 size-8 shrink-0 rounded-full',\n        'bg-background/80 border shadow-md backdrop-blur-sm',\n        'hover:bg-accent hover:text-accent-foreground',\n        'disabled:pointer-events-none disabled:opacity-50',\n        isHorizontal ? 'top-1/2 -left-3 -translate-y-1/2' : '-top-3 left-1/2 -translate-x-1/2 rotate-90',\n        props.class,\n      )\n    \"\n    :disabled=\"!carousel?.canScrollPrev?.value\"\n    :aria-label=\"props.label\"\n    @click=\"carousel?.scrollToPrev(true)\"\n  >\n    <slot>\n      <ChevronLeft class=\"size-4\" aria-hidden=\"true\" />\n    </slot>\n  </Button>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/carousel/CarouselPrevious.vue"
    },
    {
      "path": "packages/registry-vue/components/carousel/carousel.variants.ts",
      "content": "import type { VariantProps } from 'class-variance-authority'\nimport { cva } from 'class-variance-authority'\n\n/**\n * Variant definitions live in their own file (rather than the package\n * `index.ts` or inline in the SFC) so `Carousel.vue` / `CarouselItem.vue`\n * can import them without creating a circular dependency back through\n * the index. The circular form caused intermittent\n * `$setup.carouselVariants is not a function` errors during dev SSR.\n */\nexport const carouselVariants = cva('relative overflow-hidden', {\n  variants: {\n    orientation: {\n      horizontal: 'w-full',\n      vertical: 'h-full flex-col',\n    },\n  },\n  defaultVariants: {\n    orientation: 'horizontal',\n  },\n})\n\nexport const carouselItemVariants = cva('flex shrink-0 grow-0 basis-full flex-col', {\n  variants: {\n    orientation: {\n      horizontal: 'w-full',\n      vertical: 'h-full',\n    },\n  },\n  defaultVariants: {\n    orientation: 'horizontal',\n  },\n})\n\nexport type CarouselVariants = VariantProps<typeof carouselVariants>\nexport type CarouselItemVariants = VariantProps<typeof carouselItemVariants>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/carousel/carousel.variants.ts"
    },
    {
      "path": "packages/registry-vue/components/carousel/useCarousel.ts",
      "content": "/**\n * useCarousel\n *\n * Lightweight carousel composable using native CSS scroll-snap.\n * No external dependencies required.\n */\nimport { ref, computed, onMounted, onUnmounted, type Ref } from 'vue'\n\nexport interface CarouselOptions {\n  loop?: boolean\n  orientation?: 'horizontal' | 'vertical'\n}\n\nexport interface UseCarouselReturn {\n  activeIndex: Ref<number>\n  scrollSnaps: Ref<number[]>\n  canScrollPrev: Ref<boolean>\n  canScrollNext: Ref<boolean>\n  scrollTo: (index: number, smooth?: boolean) => void\n  scrollToPrev: (smooth?: boolean) => void\n  scrollToNext: (smooth?: boolean) => void\n  rootRef: Ref<HTMLElement | null>\n  orientation: Ref<'horizontal' | 'vertical'>\n}\n\nexport function useCarousel(options: CarouselOptions = {}): UseCarouselReturn {\n  const { loop = false, orientation = 'horizontal' } = options\n\n  const rootRef = ref<HTMLElement | null>(null)\n  const activeIndex = ref(0)\n  const scrollSnaps = ref<number[]>([])\n  const canScrollPrev = ref(false)\n  const canScrollNext = ref(false)\n\n  const isHorizontal = computed(() => orientation === 'horizontal')\n\n  function getScrollPosition(): number {\n    if (!rootRef.value) return 0\n    return isHorizontal.value ? rootRef.value.scrollLeft : rootRef.value.scrollTop\n  }\n\n  function getItemSize(): number {\n    if (!rootRef.value) return 0\n    return isHorizontal.value ? rootRef.value.offsetWidth : rootRef.value.offsetHeight\n  }\n\n  function updateScrollState() {\n    if (!rootRef.value) return\n\n    const el = rootRef.value\n    const itemSize = getItemSize()\n    if (itemSize === 0) return\n\n    const scrollPos = getScrollPosition()\n    const scrollWidth = isHorizontal.value ? el.scrollWidth : el.scrollHeight\n    const viewportSize = isHorizontal.value ? el.offsetWidth : el.offsetHeight\n\n    // Calculate active index\n    activeIndex.value = Math.round(scrollPos / itemSize)\n\n    // Update scroll snaps\n    const newSnaps: number[] = []\n    const itemCount = Math.ceil(scrollWidth / itemSize)\n    for (let i = 0; i < itemCount; i++) {\n      newSnaps.push(i * itemSize)\n    }\n    scrollSnaps.value = newSnaps\n\n    // Update navigation state\n    canScrollPrev.value = loop || scrollPos > 0\n    canScrollNext.value = loop || scrollPos < scrollWidth - viewportSize - 1\n  }\n\n  function scrollTo(index: number, smooth = true) {\n    if (!rootRef.value || index < 0) return\n\n    const el = rootRef.value\n    const itemSize = getItemSize()\n    if (itemSize === 0) return\n\n    const targetScroll = index * itemSize\n    el.scrollTo({\n      left: isHorizontal.value ? targetScroll : 0,\n      top: isHorizontal.value ? 0 : targetScroll,\n      behavior: smooth ? 'smooth' : 'auto',\n    })\n  }\n\n  function scrollToPrev(smooth = true) {\n    const nextIndex = Math.max(0, activeIndex.value - 1)\n    scrollTo(nextIndex, smooth)\n  }\n\n  function scrollToNext(smooth = true) {\n    const maxIndex = scrollSnaps.value.length - 1\n    const nextIndex = loop ? (activeIndex.value + 1) % (maxIndex + 1) : Math.min(maxIndex, activeIndex.value + 1)\n    scrollTo(nextIndex, smooth)\n  }\n\n  let scrollHandler: (() => void) | null = null\n  let resizeHandler: (() => void) | null = null\n\n  onMounted(() => {\n    if (rootRef.value) {\n      updateScrollState()\n\n      scrollHandler = () => updateScrollState()\n      rootRef.value.addEventListener('scroll', scrollHandler, { passive: true })\n\n      resizeHandler = () => updateScrollState()\n      window.addEventListener('resize', resizeHandler, { passive: true })\n    }\n  })\n\n  onUnmounted(() => {\n    if (rootRef.value && scrollHandler) {\n      rootRef.value.removeEventListener('scroll', scrollHandler)\n    }\n    if (resizeHandler) {\n      window.removeEventListener('resize', resizeHandler)\n    }\n  })\n\n  return {\n    activeIndex,\n    scrollSnaps,\n    canScrollPrev,\n    canScrollNext,\n    scrollTo,\n    scrollToPrev,\n    scrollToNext,\n    rootRef,\n    orientation: ref(orientation),\n  }\n}\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/carousel/useCarousel.ts"
    },
    {
      "path": "packages/registry-vue/components/carousel/index.ts",
      "content": "export { default as Carousel } from './Carousel.vue'\nexport { default as CarouselContent } from './CarouselContent.vue'\nexport { default as CarouselItem } from './CarouselItem.vue'\nexport { default as CarouselPrevious } from './CarouselPrevious.vue'\nexport { default as CarouselNext } from './CarouselNext.vue'\nexport { default as CarouselIndicators } from './CarouselIndicators.vue'\nexport { default as CarouselHeader } from './CarouselHeader.vue'\nexport { default as CarouselFooter } from './CarouselFooter.vue'\n\n// Re-export variant API from the sibling file (kept separate to avoid the\n// Carousel.vue <-> index.ts circular import that broke dev SSR).\nexport {\n  carouselVariants,\n  carouselItemVariants,\n  type CarouselVariants,\n  type CarouselItemVariants,\n} from './carousel.variants'\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/carousel/index.ts"
    }
  ],
  "dependencies": [
    "class-variance-authority",
    "lucide-vue-next"
  ],
  "devDependencies": [],
  "registryDependencies": [],
  "description": "Horizontal or vertical scroller with previous/next controls. Hand-rolled `useCarousel` composable backed by native CSS scroll-snap — no external carousel library, just the browser. Drop in images, cards, or any custom slide content.",
  "categories": [
    "data-display"
  ]
}