{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "carousel",
  "title": "Carousel",
  "type": "registry:ui",
  "description": "Horizontal or vertical scroller with previous/next controls. Built on embla-carousel-react (the official shadcn carousel pattern) and wired through a React context. Drop in images, cards, or any custom slide content.",
  "categories": [
    "data-display"
  ],
  "files": [
    {
      "path": "packages/elements-lit/src/components/carousel/carousel.ts",
      "content": "import { LitElement, css, html, nothing } from 'lit'\nimport { ChevronLeft, ChevronRight } from 'lucide'\nimport { cn } from '../../lib/utils'\nimport { icon } from '../../lib/icon'\nimport { tailwind } from '../../lib/styles'\nimport { ThemeController } from '../../lib/theme'\n\nexport type CarouselOrientation = 'horizontal' | 'vertical'\n\n/* ------------------------------------------------------------------ Carousel */\n\nexport class UipCarousel extends LitElement {\n  static styles = [\n    tailwind,\n    css`\n      :host {\n        display: block;\n        position: relative;\n      }\n    `,\n  ]\n\n  static properties = {\n    orientation: { type: String, reflect: true },\n    loop: { type: Boolean },\n    canScrollPrev: { type: Boolean, state: true },\n    canScrollNext: { type: Boolean, state: true },\n    selectedIndex: { type: Number, state: true },\n    slideCount: { type: Number, state: true },\n  }\n\n  orientation: CarouselOrientation = 'horizontal'\n  loop = false\n  canScrollPrev = false\n  canScrollNext = true\n  selectedIndex = 0\n  slideCount = 0\n\n  constructor() {\n    super()\n    new ThemeController(this)\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'carousel')\n    this.setAttribute('role', 'region')\n    this.setAttribute('aria-roledescription', 'carousel')\n    this.setAttribute('aria-label', 'Carousel')\n    this.addEventListener('keydown', this.handleKeyDown.bind(this))\n  }\n\n  firstUpdated() {\n    this.updateSlides()\n  }\n\n  updateSlides() {\n    const content = this.querySelector('uip-carousel-content') as UipCarouselContent | null\n    if (content) {\n      const items = content.querySelectorAll('uip-carousel-item')\n      this.slideCount = items.length\n      this.syncState()\n    }\n  }\n\n  syncState() {\n    if (this.loop) {\n      this.canScrollPrev = true\n      this.canScrollNext = true\n    } else {\n      this.canScrollPrev = this.selectedIndex > 0\n      this.canScrollNext = this.selectedIndex < this.slideCount - 1\n    }\n    this.dispatchEvent(\n      new CustomEvent('select', {\n        detail: { index: this.selectedIndex },\n        bubbles: true,\n        composed: true,\n      }),\n    )\n  }\n\n  scrollPrev() {\n    if (this.selectedIndex > 0) {\n      this.scrollToIndex(this.selectedIndex - 1)\n    } else if (this.loop && this.slideCount > 0) {\n      this.scrollToIndex(this.slideCount - 1)\n    }\n  }\n\n  scrollNext() {\n    if (this.selectedIndex < this.slideCount - 1) {\n      this.scrollToIndex(this.selectedIndex + 1)\n    } else if (this.loop && this.slideCount > 0) {\n      this.scrollToIndex(0)\n    }\n  }\n\n  scrollToIndex(index: number) {\n    if (index < 0 || index >= this.slideCount) return\n    this.selectedIndex = index\n    this.syncState()\n    const content = this.querySelector('uip-carousel-content') as UipCarouselContent | null\n    content?.scrollToSlide(index)\n  }\n\n  private handleKeyDown(e: KeyboardEvent) {\n    const isVertical = this.orientation === 'vertical'\n    if ((!isVertical && e.key === 'ArrowLeft') || (isVertical && e.key === 'ArrowUp')) {\n      e.preventDefault()\n      this.scrollPrev()\n    } else if ((!isVertical && e.key === 'ArrowRight') || (isVertical && e.key === 'ArrowDown')) {\n      e.preventDefault()\n      this.scrollNext()\n    }\n  }\n\n  render() {\n    return html`\n      <div\n        part=\"base\"\n        class=${cn(\n          'relative overflow-hidden',\n          this.orientation === 'horizontal' ? 'w-full' : 'h-full flex-col',\n        )}\n      >\n        <slot></slot>\n      </div>\n    `\n  }\n}\n\n/* ---------------------------------------------------------- Carousel Content */\n\nexport class UipCarouselContent extends LitElement {\n  static styles = [\n    tailwind,\n    css`\n      :host {\n        display: block;\n        width: 100%;\n        height: 100%;\n      }\n    `,\n  ]\n\n  constructor() {\n    super()\n    new ThemeController(this)\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'carousel-content')\n  }\n\n  get orientation(): CarouselOrientation {\n    const carousel = this.closest('uip-carousel') as UipCarousel | null\n    return carousel?.orientation ?? 'horizontal'\n  }\n\n  scrollToSlide(index: number) {\n    const scrollContainer = this.renderRoot.querySelector('[part=scroll]') as HTMLElement | null\n    const items = this.querySelectorAll('uip-carousel-item')\n    const targetItem = items[index] as HTMLElement | undefined\n    if (scrollContainer && targetItem) {\n      if (this.orientation === 'horizontal') {\n        scrollContainer.scrollTo({ left: targetItem.offsetLeft, behavior: 'smooth' })\n      } else {\n        scrollContainer.scrollTo({ top: targetItem.offsetTop, behavior: 'smooth' })\n      }\n    }\n  }\n\n  private onSlotChange() {\n    const carousel = this.closest('uip-carousel') as UipCarousel | null\n    carousel?.updateSlides()\n  }\n\n  render() {\n    const isHorizontal = this.orientation === 'horizontal'\n    return html`\n      <div\n        part=\"scroll\"\n        class=${cn(\n          isHorizontal\n            ? 'flex snap-x snap-mandatory overflow-x-auto scroll-smooth'\n            : 'flex snap-y snap-mandatory flex-col overflow-y-auto scroll-smooth',\n          'relative h-full w-full',\n          '[-ms-overflow-style:none] [scrollbar-width:none]',\n          '[&::-webkit-scrollbar]:hidden',\n        )}\n        aria-orientation=${this.orientation}\n        role=\"group\"\n      >\n        <div\n          part=\"track\"\n          class=${cn(\n            'flex',\n            isHorizontal ? 'w-full' : 'flex-col',\n          )}\n        >\n          <slot @slotchange=${this.onSlotChange}></slot>\n        </div>\n      </div>\n    `\n  }\n}\n\n/* ------------------------------------------------------------- Carousel Item */\n\nexport class UipCarouselItem extends LitElement {\n  static styles = [\n    tailwind,\n    css`\n      :host {\n        display: flex;\n        flex-shrink: 0;\n        flex-grow: 0;\n        flex-basis: 100%;\n        width: 100%;\n      }\n      :host([orientation='vertical']) {\n        height: 100%;\n      }\n    `,\n  ]\n\n  constructor() {\n    super()\n    new ThemeController(this)\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'carousel-item')\n    this.setAttribute('role', 'group')\n    this.setAttribute('aria-roledescription', 'slide')\n\n    const carousel = this.closest('uip-carousel') as UipCarousel | null\n    if (carousel?.orientation === 'vertical') {\n      this.setAttribute('orientation', 'vertical')\n    }\n  }\n\n  render() {\n    return html`\n      <div part=\"base\" class=\"relative flex w-full flex-col snap-start\">\n        <slot></slot>\n      </div>\n    `\n  }\n}\n\n/* --------------------------------------------------------- Carousel Previous */\n\nexport class UipCarouselPrevious extends LitElement {\n  static styles = [\n    tailwind,\n    css`\n      :host {\n        display: inline-flex;\n      }\n    `,\n  ]\n\n  static properties = {\n    label: { type: String },\n  }\n\n  label = 'Previous slide'\n\n  constructor() {\n    super()\n    new ThemeController(this)\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'carousel-previous')\n  }\n\n  private onClick() {\n    const carousel = this.closest('uip-carousel') as UipCarousel | null\n    carousel?.scrollPrev()\n  }\n\n  render() {\n    const carousel = this.closest('uip-carousel') as UipCarousel | null\n    const isHorizontal = (carousel?.orientation ?? 'horizontal') !== 'vertical'\n    const disabled = carousel ? !carousel.canScrollPrev : false\n\n    return html`\n      <button\n        type=\"button\"\n        part=\"base\"\n        class=${cn(\n          'focus-visible:ring-ring inline-flex size-8 shrink-0 items-center justify-center rounded-full border shadow-md transition-colors backdrop-blur-sm focus-visible:ring-2 focus-visible:outline-none',\n          'bg-background/80 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        )}\n        ?disabled=${disabled}\n        aria-label=${this.label}\n        @click=${this.onClick}\n      >\n        <slot>${icon(ChevronLeft, 'chevron-left', 'size-4')}</slot>\n      </button>\n    `\n  }\n}\n\n/* ------------------------------------------------------------- Carousel Next */\n\nexport class UipCarouselNext extends LitElement {\n  static styles = [\n    tailwind,\n    css`\n      :host {\n        display: inline-flex;\n      }\n    `,\n  ]\n\n  static properties = {\n    label: { type: String },\n  }\n\n  label = 'Next slide'\n\n  constructor() {\n    super()\n    new ThemeController(this)\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'carousel-next')\n  }\n\n  private onClick() {\n    const carousel = this.closest('uip-carousel') as UipCarousel | null\n    carousel?.scrollNext()\n  }\n\n  render() {\n    const carousel = this.closest('uip-carousel') as UipCarousel | null\n    const isHorizontal = (carousel?.orientation ?? 'horizontal') !== 'vertical'\n    const disabled = carousel ? !carousel.canScrollNext : false\n\n    return html`\n      <button\n        type=\"button\"\n        part=\"base\"\n        class=${cn(\n          'focus-visible:ring-ring inline-flex size-8 shrink-0 items-center justify-center rounded-full border shadow-md transition-colors backdrop-blur-sm focus-visible:ring-2 focus-visible:outline-none',\n          'bg-background/80 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        )}\n        ?disabled=${disabled}\n        aria-label=${this.label}\n        @click=${this.onClick}\n      >\n        <slot>${icon(ChevronRight, 'chevron-right', 'size-4')}</slot>\n      </button>\n    `\n  }\n}\n\n/* ----------------------------------------------------------- Carousel Header */\n\nexport class UipCarouselHeader extends LitElement {\n  static styles = [\n    tailwind,\n    css`\n      :host {\n        display: block;\n      }\n    `,\n  ]\n\n  constructor() {\n    super()\n    new ThemeController(this)\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'carousel-header')\n  }\n\n  render() {\n    return html`\n      <div part=\"base\" class=\"flex items-center justify-between px-1 pb-2\">\n        <slot></slot>\n      </div>\n    `\n  }\n}\n\n/* ----------------------------------------------------------- Carousel Footer */\n\nexport class UipCarouselFooter extends LitElement {\n  static styles = [\n    tailwind,\n    css`\n      :host {\n        display: block;\n      }\n    `,\n  ]\n\n  constructor() {\n    super()\n    new ThemeController(this)\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'carousel-footer')\n  }\n\n  render() {\n    return html`\n      <div part=\"base\" class=\"flex items-center justify-between px-1 pt-2\">\n        <slot></slot>\n      </div>\n    `\n  }\n}\n\n/* ------------------------------------------------------- Carousel Indicators */\n\nexport class UipCarouselIndicators extends LitElement {\n  static styles = [\n    tailwind,\n    css`\n      :host {\n        display: flex;\n        justify-content: center;\n      }\n    `,\n  ]\n\n  constructor() {\n    super()\n    new ThemeController(this)\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'carousel-indicators')\n    this.setAttribute('role', 'tablist')\n    this.setAttribute('aria-label', 'Carousel navigation')\n  }\n\n  render() {\n    const carousel = this.closest('uip-carousel') as UipCarousel | null\n    const count = carousel?.slideCount ?? 0\n    const selected = carousel?.selectedIndex ?? 0\n\n    return html`\n      <div part=\"base\" class=\"flex items-center justify-center gap-1.5 py-2\">\n        ${Array.from({ length: count }).map(\n          (_, i) => html`\n            <button\n              type=\"button\"\n              role=\"tab\"\n              aria-label=\"Go to slide ${i + 1}\"\n              aria-selected=${i === selected ? 'true' : 'false'}\n              class=${cn(\n                'h-2 rounded-full transition-all duration-200 focus-visible:ring-ring focus-visible:outline-none focus-visible:ring-1',\n                i === selected ? 'bg-primary w-6' : 'bg-muted-foreground/30 hover:bg-muted-foreground/50 w-2',\n              )}\n              @click=${() => carousel?.scrollToIndex(i)}\n            ></button>\n          `,\n        )}\n      </div>\n    `\n  }\n}\n\n/* -------------------------------------------------------------- Registration */\n\ncustomElements.get('uip-carousel') || customElements.define('uip-carousel', UipCarousel)\ncustomElements.get('uip-carousel-content') || customElements.define('uip-carousel-content', UipCarouselContent)\ncustomElements.get('uip-carousel-item') || customElements.define('uip-carousel-item', UipCarouselItem)\ncustomElements.get('uip-carousel-previous') || customElements.define('uip-carousel-previous', UipCarouselPrevious)\ncustomElements.get('uip-carousel-next') || customElements.define('uip-carousel-next', UipCarouselNext)\ncustomElements.get('uip-carousel-header') || customElements.define('uip-carousel-header', UipCarouselHeader)\ncustomElements.get('uip-carousel-footer') || customElements.define('uip-carousel-footer', UipCarouselFooter)\ncustomElements.get('uip-carousel-indicators') || customElements.define('uip-carousel-indicators', UipCarouselIndicators)\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'uip-carousel': UipCarousel\n    'uip-carousel-content': UipCarouselContent\n    'uip-carousel-item': UipCarouselItem\n    'uip-carousel-previous': UipCarouselPrevious\n    'uip-carousel-next': UipCarouselNext\n    'uip-carousel-header': UipCarouselHeader\n    'uip-carousel-footer': UipCarouselFooter\n    'uip-carousel-indicators': UipCarouselIndicators\n  }\n}\n",
      "type": "registry:file",
      "target": "src/uip/components/carousel/carousel.ts"
    }
  ],
  "dependencies": [
    "lit",
    "lucide"
  ],
  "registryDependencies": [
    "https://uipkge.dev/r/lit/lit-core.json"
  ]
}
