{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "carousel",
  "title": "Carousel",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-angular/components/carousel/carousel.component.ts",
      "content": "import {\n  Component,\n  EventEmitter,\n  Input,\n  Output,\n  booleanAttribute,\n  inject,\n  ChangeDetectionStrategy,\n} from '@angular/core'\nimport { cn } from '@/lib/utils'\nimport { buttonVariants } from '@/ui/button/button.component'\nimport {\n  carouselItemVariants,\n  carouselVariants,\n  type CarouselItemVariants,\n  type CarouselVariants,\n} from './carousel.variants'\n\nexport type CarouselOrientation = NonNullable<CarouselVariants['orientation']>\nexport type CarouselItemOrientation = NonNullable<CarouselItemVariants['orientation']>\n\nexport interface CarouselOptions {\n  loop?: boolean\n  axis?: 'x' | 'y'\n}\n\n/** Angular port of UIPKGE Carousel. Native CSS scroll-snap scroller with prev/next + indicators. */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-carousel, [ui-carousel]',\n  standalone: true,\n  host: {\n    '[attr.data-slot]': '\"carousel\"',\n    '[attr.data-uipkge]': '\"\"',\n    '[attr.role]': '\"region\"',\n    '[attr.aria-roledescription]': '\"carousel\"',\n    '[attr.aria-label]': '\"Carousel\"',\n    '[class]': 'hostClass',\n  },\n  template: `<ng-content />`,\n})\nexport class UiCarouselComponent {\n  @Input() modelValue = 0\n  @Input() orientation: CarouselOrientation = 'horizontal'\n  @Input({ transform: booleanAttribute }) loop = false\n  @Input() opts?: CarouselOptions\n  @Input('class') className?: string\n\n  @Output() modelValueChange = new EventEmitter<number>()\n\n  activeIndex = 0\n  itemCount = 0\n\n  get isLoop(): boolean {\n    return this.loop || !!this.opts?.loop\n  }\n\n  get hostClass(): string {\n    return cn('block', carouselVariants({ orientation: this.orientation }), this.className)\n  }\n\n  get isHorizontal(): boolean {\n    return this.orientation !== 'vertical'\n  }\n\n  scrollTo(index: number): void {\n    const max = Math.max(0, this.itemCount - 1)\n    let next = Math.max(0, Math.min(max, index))\n    if (this.isLoop && this.itemCount > 0) {\n      next = ((index % this.itemCount) + this.itemCount) % this.itemCount\n    }\n    this.activeIndex = next\n    this.modelValueChange.emit(next)\n  }\n\n  scrollToPrev(): void {\n    this.scrollTo(this.activeIndex - 1)\n  }\n\n  scrollToNext(): void {\n    this.scrollTo(this.activeIndex + 1)\n  }\n}\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-carousel-content, [ui-carousel-content]',\n  standalone: true,\n  host: {\n    '[attr.data-slot]': '\"carousel-content\"',\n    '[attr.data-uipkge]': '\"\"',\n    '[attr.role]': '\"group\"',\n    '[attr.aria-orientation]': 'resolvedOrientation',\n    '[class]': 'hostClass',\n  },\n  template: `<ng-content />`,\n})\nexport class UiCarouselContentComponent {\n  private carousel?: UiCarouselComponent\n\n  constructor() {\n    try {\n      this.carousel = inject(UiCarouselComponent, { optional: true }) ?? undefined\n    } catch {\n      // Instantiated outside injection context (e.g. unit tests)\n    }\n  }\n\n  @Input() orientation?: CarouselOrientation\n  @Input('class') className?: string\n\n  get resolvedOrientation(): CarouselOrientation {\n    return this.orientation ?? this.carousel?.orientation ?? 'horizontal'\n  }\n\n  get hostClass(): string {\n    return cn(\n      this.resolvedOrientation !== 'vertical'\n        ? 'flex overflow-x-auto scroll-smooth snap-x snap-mandatory'\n        : 'flex flex-col overflow-y-auto snap-y snap-mandatory',\n      'relative h-full w-full',\n      '[scrollbar-width:none] [-ms-overflow-style:none]',\n      '[&::-webkit-scrollbar]:hidden',\n      this.className,\n    )\n  }\n}\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-carousel-item, [ui-carousel-item]',\n  standalone: true,\n  host: {\n    '[attr.data-slot]': '\"carousel-item\"',\n    '[attr.data-uipkge]': '\"\"',\n    '[attr.role]': '\"group\"',\n    '[attr.aria-roledescription]': '\"slide\"',\n    '[class]': 'hostClass',\n  },\n  template: `<ng-content />`,\n})\nexport class UiCarouselItemComponent {\n  private carousel?: UiCarouselComponent\n\n  constructor() {\n    try {\n      this.carousel = inject(UiCarouselComponent, { optional: true }) ?? undefined\n    } catch {\n      // Instantiated outside injection context (e.g. unit tests)\n    }\n  }\n\n  @Input() orientation?: CarouselItemOrientation\n  @Input('class') className?: string\n\n  get resolvedOrientation(): CarouselItemOrientation {\n    return this.orientation ?? this.carousel?.orientation ?? 'horizontal'\n  }\n\n  get hostClass(): string {\n    return cn(\n      carouselItemVariants({ orientation: this.resolvedOrientation }),\n      'shrink-0 grow-0 basis-full',\n      'snap-start',\n      'relative',\n      this.className,\n    )\n  }\n}\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-carousel-header, [ui-carousel-header]',\n  standalone: true,\n  host: {\n    '[attr.data-slot]': '\"carousel-header\"',\n    '[attr.data-uipkge]': '\"\"',\n    '[class]': 'hostClass',\n  },\n  template: `<ng-content />`,\n})\nexport class UiCarouselHeaderComponent {\n  @Input('class') className?: string\n\n  get hostClass(): string {\n    return cn('flex items-center justify-between px-1 pb-2', this.className)\n  }\n}\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-carousel-footer, [ui-carousel-footer]',\n  standalone: true,\n  host: {\n    '[attr.data-slot]': '\"carousel-footer\"',\n    '[attr.data-uipkge]': '\"\"',\n    '[class]': 'hostClass',\n  },\n  template: `<ng-content />`,\n})\nexport class UiCarouselFooterComponent {\n  @Input('class') className?: string\n\n  get hostClass(): string {\n    return cn('flex items-center justify-between px-1 pt-2', this.className)\n  }\n}\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-carousel-indicators, [ui-carousel-indicators]',\n  standalone: true,\n  host: {\n    '[attr.data-slot]': '\"carousel-indicators\"',\n    '[attr.data-uipkge]': '\"\"',\n    '[attr.role]': '\"tablist\"',\n    '[attr.aria-label]': '\"Carousel navigation\"',\n    '[class]': 'hostClass',\n  },\n  template: `\n    @for (_ of dots; track $index) {\n      <button\n        type=\"button\"\n        role=\"tab\"\n        [attr.aria-label]=\"'Go to slide ' + ($index + 1)\"\n        [attr.aria-selected]=\"$index === resolvedActiveIndex\"\n        [class]=\"dotClass($index)\"\n        (click)=\"onDotClick($index)\"\n      ></button>\n    }\n  `,\n})\nexport class UiCarouselIndicatorsComponent {\n  private carousel?: UiCarouselComponent\n\n  constructor() {\n    try {\n      this.carousel = inject(UiCarouselComponent, { optional: true }) ?? undefined\n    } catch {\n      // Instantiated outside injection context (e.g. unit tests)\n    }\n  }\n\n  @Input() count = 0\n  @Input() activeIndex = 0\n  @Input('class') className?: string\n\n  @Output() goTo = new EventEmitter<number>()\n\n  get resolvedActiveIndex(): number {\n    return this.carousel?.activeIndex ?? this.activeIndex\n  }\n\n  get resolvedCount(): number {\n    return this.count || this.carousel?.itemCount || 0\n  }\n\n  get dots(): unknown[] {\n    return Array.from({ length: this.resolvedCount })\n  }\n\n  get hostClass(): string {\n    return cn('flex items-center justify-center gap-1.5 py-2', this.className)\n  }\n\n  dotClass(index: number): string {\n    return cn(\n      'h-2 w-2 rounded-full transition-colors duration-200',\n      index === this.resolvedActiveIndex ? 'bg-primary w-6' : 'bg-muted-foreground/30 hover:bg-muted-foreground/50',\n    )\n  }\n\n  onDotClick(index: number): void {\n    this.carousel?.scrollTo(index)\n    this.goTo.emit(index)\n  }\n}\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-carousel-previous, [ui-carousel-previous]',\n  standalone: true,\n  host: {\n    '[attr.data-slot]': '\"carousel-previous\"',\n    '[attr.data-uipkge]': '\"\"',\n    '[attr.type]': '\"button\"',\n    '[attr.aria-label]': 'label',\n    '[attr.disabled]': 'disabled ? \"\" : null',\n    '[class]': 'hostClass',\n    '(click)': 'onClick()',\n  },\n  template: `<ng-content\n    ><svg\n      class=\"size-4\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width=\"24\"\n      height=\"24\"\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      stroke-width=\"2\"\n      stroke-linecap=\"round\"\n      stroke-linejoin=\"round\"\n      aria-hidden=\"true\"\n    >\n      <path d=\"m15 18-6-6 6-6\" /></svg\n  ></ng-content>`,\n})\nexport class UiCarouselPreviousComponent {\n  private carousel?: UiCarouselComponent\n\n  constructor() {\n    try {\n      this.carousel = inject(UiCarouselComponent, { optional: true }) ?? undefined\n    } catch {\n      // Instantiated outside injection context (e.g. unit tests)\n    }\n  }\n\n  @Input() label = 'Previous slide'\n  @Input() orientation?: CarouselOrientation\n  @Input({ transform: booleanAttribute }) disabled = false\n  @Input('class') className?: string\n\n  @Output() prev = new EventEmitter<void>()\n\n  get resolvedOrientation(): CarouselOrientation {\n    return this.orientation ?? this.carousel?.orientation ?? 'horizontal'\n  }\n\n  get hostClass(): string {\n    // buttonVariants(outline, icon) first, like React/Vue render a real Button: cn() lets the\n    // size-8 / rounded-full / bg-background/80 overrides below win via tailwind-merge.\n    return cn(\n      buttonVariants({ variant: 'outline', size: 'icon' }),\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      this.resolvedOrientation !== 'vertical'\n        ? 'top-1/2 -left-3 -translate-y-1/2'\n        : '-top-3 left-1/2 -translate-x-1/2 rotate-90',\n      this.className,\n    )\n  }\n\n  onClick(): void {\n    this.carousel?.scrollToPrev()\n    this.prev.emit()\n  }\n}\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-carousel-next, [ui-carousel-next]',\n  standalone: true,\n  host: {\n    '[attr.data-slot]': '\"carousel-next\"',\n    '[attr.data-uipkge]': '\"\"',\n    '[attr.type]': '\"button\"',\n    '[attr.aria-label]': 'label',\n    '[attr.disabled]': 'disabled ? \"\" : null',\n    '[class]': 'hostClass',\n    '(click)': 'onClick()',\n  },\n  template: `<ng-content\n    ><svg\n      class=\"size-4\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width=\"24\"\n      height=\"24\"\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      stroke-width=\"2\"\n      stroke-linecap=\"round\"\n      stroke-linejoin=\"round\"\n      aria-hidden=\"true\"\n    >\n      <path d=\"m9 18 6-6-6-6\" /></svg\n  ></ng-content>`,\n})\nexport class UiCarouselNextComponent {\n  private carousel?: UiCarouselComponent\n\n  constructor() {\n    try {\n      this.carousel = inject(UiCarouselComponent, { optional: true }) ?? undefined\n    } catch {\n      // Instantiated outside injection context (e.g. unit tests)\n    }\n  }\n\n  @Input() label = 'Next slide'\n  @Input() orientation?: CarouselOrientation\n  @Input({ transform: booleanAttribute }) disabled = false\n  @Input('class') className?: string\n\n  @Output() next = new EventEmitter<void>()\n\n  get resolvedOrientation(): CarouselOrientation {\n    return this.orientation ?? this.carousel?.orientation ?? 'horizontal'\n  }\n\n  get hostClass(): string {\n    // buttonVariants(outline, icon) first, like React/Vue render a real Button: cn() lets the\n    // size-8 / rounded-full / bg-background/80 overrides below win via tailwind-merge.\n    return cn(\n      buttonVariants({ variant: 'outline', size: 'icon' }),\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      this.resolvedOrientation !== 'vertical'\n        ? 'top-1/2 -right-3 -translate-y-1/2'\n        : '-bottom-3 left-1/2 -translate-x-1/2 rotate-90',\n      this.className,\n    )\n  }\n\n  onClick(): void {\n    this.carousel?.scrollToNext()\n    this.next.emit()\n  }\n}\n\nexport { carouselItemVariants, carouselVariants, type CarouselItemVariants, type CarouselVariants }\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/carousel/carousel.component.ts"
    },
    {
      "path": "packages/registry-angular/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": "~/src/app/components/ui/carousel/carousel.variants.ts"
    },
    {
      "path": "packages/registry-angular/components/carousel/index.ts",
      "content": "export {\n  UiCarouselComponent,\n  UiCarouselContentComponent,\n  UiCarouselFooterComponent,\n  UiCarouselHeaderComponent,\n  UiCarouselIndicatorsComponent,\n  UiCarouselItemComponent,\n  UiCarouselNextComponent,\n  UiCarouselPreviousComponent,\n  carouselItemVariants,\n  carouselVariants,\n  type CarouselItemOrientation,\n  type CarouselItemVariants,\n  type CarouselOrientation,\n  type CarouselVariants,\n} from './carousel.component'\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/carousel/index.ts"
    }
  ],
  "dependencies": [
    "class-variance-authority"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/angular/button.json"
  ],
  "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"
  ]
}