{
  "name": "carousel",
  "type": "registry:component",
  "description": "Carousel container with controls and slots",
  "files": [
    {
      "path": "ui/Carousel.astro",
      "type": "registry:component",
      "content": "---\nimport { Icon } from 'astro-icon/components';\n\ninterface BreakpointSlides {\n  base?: number;\n  sm?: number;\n  md?: number;\n  lg?: number;\n  xl?: number;\n}\n\nexport interface Props {\n  slidesPerView?: number | BreakpointSlides;\n  gap?: string;\n  snap?: 'start' | 'center' | 'none';\n  loop?: boolean;\n  autoPlay?: boolean;\n  autoPlayInterval?: number;\n  showControls?: boolean;\n  class?: string;\n  controlPrevLabel?: string;\n  controlNextLabel?: string;\n}\n\nconst {\n  slidesPerView = {\n    base: 1,\n  } satisfies BreakpointSlides,\n  gap = '1.5rem',\n  snap = 'start',\n  loop = false,\n  autoPlay = false,\n  autoPlayInterval = 5000,\n  showControls = true,\n  class: className = '',\n  controlPrevLabel = 'Previous slide',\n  controlNextLabel = 'Next slide',\n} = Astro.props as Props;\n\nconst slidesConfig = typeof slidesPerView === 'number' ? { base: slidesPerView } : slidesPerView;\n---\n\n<div\n  class={`carousel-root relative ${className}`}\n  data-carousel-root\n  data-carousel-initialized=\"false\"\n>\n  <div class=\"carousel-viewport\">\n    <div\n      class=\"carousel-track\"\n      data-carousel-track\n      data-loop={loop ? \"true\" : undefined}\n      data-autoplay={autoPlay ? \"true\" : undefined}\n      data-autoplay-interval={autoPlay ? autoPlayInterval : undefined}\n      data-slides-config={JSON.stringify(slidesConfig)}\n      style={`gap: ${gap};`}\n    >\n      <slot />\n    </div>\n  </div>\n\n  {\n    showControls && (\n      <div class=\"carousel-controls pointer-events-none absolute right-4 top-4 flex items-center justify-end\">\n        <div class=\"pointer-events-auto flex gap-2\">\n          <button\n            type=\"button\"\n            class=\"carousel-control-btn group\"\n            data-carousel-prev\n            aria-label={controlPrevLabel}\n          >\n            <span\n              data-scheme=\"bg\"\n              class=\"shadow-2 inline-flex h-12 w-12 items-center justify-center rounded-full\"\n            >\n              <Icon name=\"lucide:chevron-left\" size={20} />\n            </span>\n          </button>\n          <button\n            type=\"button\"\n            class=\"carousel-control-btn group\"\n            data-carousel-next\n            aria-label={controlNextLabel}\n          >\n            <span\n              data-scheme=\"bg\"\n              class=\"shadow-2 inline-flex h-12 w-12 items-center justify-center rounded-full\"\n            >\n              <Icon name=\"lucide:chevron-right\" size={20} />\n            </span>\n          </button>\n        </div>\n      </div>\n    )\n  }\n</div>\n\n<style>\n  @layer components {\n  .carousel-root {\n    width: 100%;\n  }\n\n  .carousel-viewport {\n    overflow: hidden;\n    position: relative;\n  }\n\n  .carousel-track {\n    display: flex;\n    transition: transform 500ms cubic-bezier(0.4, 0, 0.2, 1);\n    will-change: transform;\n  }\n\n  .carousel-track.no-transition {\n    transition: none;\n  }\n\n  .carousel-item {\n    flex-shrink: 0;\n    min-width: 0;\n  }\n\n  .carousel-control-btn {\n    cursor: pointer;\n    transition: transform 0.15s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.15s cubic-bezier(0.4, 0, 0.2, 1);\n  }\n\n  .carousel-control-btn:hover:not(:disabled) {\n    transform: translateY(1px) scale(0.99);\n    opacity: 0.9;\n  }\n\n  .carousel-control-btn:active:not(:disabled) {\n    transform: translateY(2px) scale(0.98);\n    opacity: 0.8;\n  }\n\n  .carousel-control-btn:disabled {\n    opacity: 0.5;\n    cursor: not-allowed;\n    pointer-events: none;\n  }\n}\n</style>\n\n<script>\n  const setupCarousel = (root) => {\n    if (!(root instanceof HTMLElement)) return;\n    if (root.dataset.carouselInitialized === \"true\") return;\n\n    const track = root.querySelector(\"[data-carousel-track]\");\n    if (!(track instanceof HTMLElement)) return;\n\n    const items = Array.from(track.querySelectorAll(\".carousel-item\"));\n    if (items.length === 0) return;\n\n    const parseSlidesConfig = () => {\n      try {\n        const raw = track.getAttribute(\"data-slides-config\") || \"{}\";\n        return JSON.parse(raw);\n      } catch (error) {\n        console.warn(\"Invalid slides config for carousel\");\n        return { base: 1 };\n      }\n    };\n\n    const slidesConfig = parseSlidesConfig();\n    const isLooping = track.getAttribute(\"data-loop\") === \"true\";\n    const autoPlay = track.getAttribute(\"data-autoplay\") === \"true\";\n    const autoPlayInterval = parseInt(track.getAttribute(\"data-autoplay-interval\") || \"5000\", 10);\n\n    let index = 0;\n    let active = 0;\n    let isPaused = false;\n    let dragStartX = 0;\n    let dragDeltaX = 0;\n    let isDragging = false;\n\n    const applySlidesPerView = () => {\n      const breakpoints = [\n        { key: \"xl\", minWidth: 1280 },\n        { key: \"lg\", minWidth: 1024 },\n        { key: \"md\", minWidth: 768 },\n        { key: \"sm\", minWidth: 640 },\n      ];\n\n      let slidesToShow = slidesConfig.base ?? 1;\n\n      for (const breakpoint of breakpoints) {\n        if (\n          window.innerWidth >= breakpoint.minWidth &&\n          slidesConfig[breakpoint.key]\n        ) {\n          slidesToShow = slidesConfig[breakpoint.key];\n          break;\n        }\n      }\n\n      slidesToShow = Math.max(1, Number(slidesToShow) || 1);\n\n      const widthPercent = 100 / slidesToShow;\n      for (const item of items) {\n        if (item instanceof HTMLElement) {\n          item.style.flex = `0 0 ${widthPercent}%`;\n          item.style.maxWidth = `${widthPercent}%`;\n        }\n      }\n    };\n\n    applySlidesPerView();\n    window.addEventListener(\"resize\", applySlidesPerView);\n\n    // インデックスを循環させる\n    const normalizeIndex = (i) => {\n      const len = items.length;\n      return ((i % len) + len) % len;\n    };\n\n    // スライドを更新\n    const updateSlide = (withTransition = true) => {\n      if (!withTransition) {\n        track.classList.add(\"no-transition\");\n      }\n      \n      const offset = -active * 100 + (isDragging ? (dragDeltaX / track.offsetWidth) * 100 : 0);\n      track.style.transform = `translateX(${offset}%)`;\n\n      if (!withTransition) {\n        requestAnimationFrame(() => {\n          track.classList.remove(\"no-transition\");\n        });\n      }\n    };\n\n    // インデックスを設定\n    const setIndex = (newIndex) => {\n      index = newIndex;\n      active = normalizeIndex(index);\n      updateSlide(true);\n    };\n\n    // 前へ\n    const goPrev = () => {\n      if (!isLooping && active === 0) return;\n      setIndex(index - 1);\n    };\n\n    // 次へ\n    const goNext = () => {\n      if (!isLooping && active === items.length - 1) return;\n      setIndex(index + 1);\n    };\n\n    // ボタン\n    const prevBtn = root.querySelector(\"[data-carousel-prev]\");\n    const nextBtn = root.querySelector(\"[data-carousel-next]\");\n\n    prevBtn?.addEventListener(\"click\", goPrev);\n    nextBtn?.addEventListener(\"click\", goNext);\n\n    // ドラッグ/スワイプ\n    const handlePointerDown = (e) => {\n      isDragging = true;\n      dragStartX = e.clientX || e.touches?.[0]?.clientX || 0;\n      dragDeltaX = 0;\n      track.style.cursor = \"grabbing\";\n    };\n\n    const handlePointerMove = (e) => {\n      if (!isDragging) return;\n      const currentX = e.clientX || e.touches?.[0]?.clientX || 0;\n      dragDeltaX = currentX - dragStartX;\n      \n      // 最大±80pxに制限\n      dragDeltaX = Math.max(-80, Math.min(80, dragDeltaX));\n      \n      updateSlide(false);\n    };\n\n    const handlePointerUp = () => {\n      if (!isDragging) return;\n      isDragging = false;\n      track.style.cursor = \"\";\n\n      const threshold = track.offsetWidth * 0.15;\n      \n      if (Math.abs(dragDeltaX) > threshold) {\n        if (dragDeltaX > 0) {\n          goPrev();\n        } else {\n          goNext();\n        }\n      } else {\n        updateSlide(true);\n      }\n      \n      dragDeltaX = 0;\n    };\n\n    track.addEventListener(\"pointerdown\", handlePointerDown);\n    track.addEventListener(\"pointermove\", handlePointerMove);\n    track.addEventListener(\"pointerup\", handlePointerUp);\n    track.addEventListener(\"pointercancel\", handlePointerUp);\n    track.addEventListener(\"touchstart\", handlePointerDown, { passive: true });\n    track.addEventListener(\"touchmove\", handlePointerMove, { passive: true });\n    track.addEventListener(\"touchend\", handlePointerUp);\n\n    // キーボード\n    root.addEventListener(\"keydown\", (e) => {\n      if (e.key === \"ArrowLeft\") {\n        e.preventDefault();\n        goPrev();\n      } else if (e.key === \"ArrowRight\") {\n        e.preventDefault();\n        goNext();\n      }\n    });\n\n    // 自動再生\n    let autoplayTimer = null;\n\n    const startAutoplay = () => {\n      if (!autoPlay || autoplayTimer) return;\n      autoplayTimer = setInterval(() => {\n        if (!isPaused) {\n          goNext();\n        }\n      }, autoPlayInterval);\n    };\n\n    const stopAutoplay = () => {\n      if (autoplayTimer) {\n        clearInterval(autoplayTimer);\n        autoplayTimer = null;\n      }\n    };\n\n    if (autoPlay) {\n      startAutoplay();\n      root.addEventListener(\"mouseenter\", () => { isPaused = true; });\n      root.addEventListener(\"mouseleave\", () => { isPaused = false; });\n      root.addEventListener(\"focusin\", () => { isPaused = true; });\n      root.addEventListener(\"focusout\", () => { isPaused = false; });\n    }\n\n    // 初期表示\n    updateSlide(false);\n\n    root.dataset.carouselInitialized = \"true\";\n  };\n\n  const init = () => {\n    document\n      .querySelectorAll(\"[data-carousel-root]\")\n      .forEach((root) => setupCarousel(root));\n  };\n\n  if (document.readyState === \"loading\") {\n    document.addEventListener(\"DOMContentLoaded\", init, { once: true });\n  } else {\n    init();\n  }\n</script>\n"
    },
    {
      "path": "ui/CarouselItem.astro",
      "type": "registry:component",
      "content": "---\ninterface Props {\n  class?: string;\n}\n\nconst { class: className = '' } = Astro.props;\n---\n\n<div\n  class={`carousel-item flex-shrink-0 basis-full ${className}`}\n  role=\"group\"\n>\n  <slot />\n</div>\n"
    }
  ],
  "category": "ui",
  "registryDependencies": [
    "carousel-item"
  ]
}