{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "carousel-tailwind",
  "type": "registry:item",
  "title": "Carousel (Tailwind)",
  "description": "A slideshow component for cycling through content.",
  "files": [
    {
      "path": "registry/brook/tailwind/ui/carousel.tsx",
      "content": "\"use client\";\n\nimport { useControlled } from \"@base-ui/utils/useControlled\";\nimport { createContext, useCallback, useContext, useEffect, useId, useRef, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\ntype CarouselContextValue = {\n  currentIndex: number;\n  setCurrentIndex: (index: number) => void;\n  totalItems: number;\n  gap: number;\n  variant: \"default\" | \"inset\";\n  goToIndex: (index: number) => void;\n  nextSlide: () => void;\n  prevSlide: () => void;\n  canGoNext: boolean;\n  canGoPrev: boolean;\n  viewportRef: React.RefObject<HTMLDivElement | null>;\n  slidesId: string;\n};\n\nconst CarouselContext = createContext<CarouselContextValue | null>(null);\n\nfunction useCarousel() {\n  const context = useContext(CarouselContext);\n  if (!context) {\n    throw new Error(\"Carousel components must be used within Carousel.Root\");\n  }\n  return context;\n}\n\nexport type CarouselRootProps = React.ComponentProps<\"div\"> & {\n  totalItems: number;\n  gap?: number;\n  index?: number;\n  defaultIndex?: number;\n  onIndexChange?: (index: number) => void;\n  align?: \"start\" | \"center\";\n  variant?: \"default\" | \"inset\";\n};\n\nexport function Root({\n  children,\n  totalItems,\n  gap = 16,\n  index: indexProp,\n  defaultIndex = 0,\n  onIndexChange,\n  align = \"start\",\n  variant = \"default\",\n  className,\n  ...props\n}: CarouselRootProps) {\n  const [currentIndex, setCurrentIndexInternal] = useControlled({\n    controlled: indexProp,\n    default: defaultIndex,\n    name: \"Carousel\",\n    state: \"index\",\n  });\n\n  const viewportRef = useRef<HTMLDivElement>(null);\n  const bleedRefFromContext = useBleedRef();\n  const [insetPaddingLeft, setInsetPaddingLeft] = useState(0);\n  const [insetPaddingRight, setInsetPaddingRight] = useState(0);\n  const slidesId = `${useId()}-slides`;\n\n  const maxIndex = totalItems - 1;\n\n  const setCurrentIndex = useCallback(\n    (index: number) => {\n      setCurrentIndexInternal(index);\n      onIndexChange?.(index);\n    },\n    [setCurrentIndexInternal, onIndexChange]\n  );\n\n  const goToIndex = useCallback(\n    (index: number) => {\n      const viewport = viewportRef.current;\n      if (!viewport) {\n        return;\n      }\n\n      const slides = viewport.querySelectorAll('[role=\"group\"]');\n      const targetSlide = slides[index] as HTMLElement;\n\n      if (targetSlide) {\n        let targetScroll = targetSlide.offsetLeft;\n\n        if (variant === \"inset\" && bleedRefFromContext?.current) {\n          const parent = bleedRefFromContext.current.parentElement;\n          if (parent) {\n            const parentRect = parent.getBoundingClientRect();\n            const leftPadding = parentRect.left;\n            targetScroll = targetSlide.offsetLeft - leftPadding;\n          }\n        }\n\n        viewport.scrollTo({ left: targetScroll, behavior: \"smooth\" });\n      }\n\n      setCurrentIndex(index);\n    },\n    [setCurrentIndex, variant, bleedRefFromContext]\n  );\n\n  const getVisibleItemsCount = useCallback(() => {\n    const viewport = viewportRef.current;\n    if (!viewport) {\n      return 1;\n    }\n\n    const slides = viewport.querySelectorAll('[role=\"group\"]');\n    if (slides.length === 0) {\n      return 1;\n    }\n\n    const viewportRect = viewport.getBoundingClientRect();\n    let visibleCount = 0;\n    const VISIBILITY_THRESHOLD = 0.5;\n\n    for (const slide of slides) {\n      const slideRect = slide.getBoundingClientRect();\n      const visibleWidth = Math.min(slideRect.right, viewportRect.right) - Math.max(slideRect.left, viewportRect.left);\n      const slideWidth = slideRect.width;\n\n      if (visibleWidth / slideWidth >= VISIBILITY_THRESHOLD) {\n        visibleCount++;\n      }\n    }\n\n    return Math.max(1, visibleCount);\n  }, []);\n\n  const visibleItemsForNav = getVisibleItemsCount();\n  const canGoNext = currentIndex + visibleItemsForNav <= maxIndex;\n  const canGoPrev = currentIndex > 0;\n\n  const nextSlide = useCallback(() => {\n    const visibleItems = getVisibleItemsCount();\n    const newIndex = Math.min(currentIndex + visibleItems, maxIndex);\n    goToIndex(newIndex);\n  }, [currentIndex, maxIndex, goToIndex, getVisibleItemsCount]);\n\n  const prevSlide = useCallback(() => {\n    const visibleItems = getVisibleItemsCount();\n    const newIndex = Math.max(currentIndex - visibleItems, 0);\n    goToIndex(newIndex);\n  }, [currentIndex, goToIndex, getVisibleItemsCount]);\n\n  const value: CarouselContextValue = {\n    currentIndex,\n    setCurrentIndex,\n    totalItems,\n    gap,\n    variant,\n    goToIndex,\n    nextSlide,\n    prevSlide,\n    canGoNext,\n    canGoPrev,\n    viewportRef,\n    slidesId,\n  };\n\n  useEffect(() => {\n    const viewport = viewportRef.current;\n    if (!viewport) {\n      return;\n    }\n\n    const handleScroll = () => {\n      const slides = viewport.querySelectorAll('[role=\"group\"]');\n      if (slides.length === 0) {\n        return;\n      }\n\n      const viewportRect = viewport.getBoundingClientRect();\n      let closestIndex = 0;\n      let closestDistance = Number.POSITIVE_INFINITY;\n\n      slides.forEach((slide, index) => {\n        const slideRect = slide.getBoundingClientRect();\n        const distance = Math.abs(slideRect.left - viewportRect.left);\n\n        if (distance < closestDistance) {\n          closestDistance = distance;\n          closestIndex = index;\n        }\n      });\n\n      if (closestIndex !== currentIndex) {\n        setCurrentIndexInternal(closestIndex);\n      }\n    };\n\n    viewport.addEventListener(\"scroll\", handleScroll, { passive: true });\n    return () => viewport.removeEventListener(\"scroll\", handleScroll);\n  }, [currentIndex, setCurrentIndexInternal]);\n\n  useEffect(() => {\n    if (variant !== \"inset\" || !bleedRefFromContext?.current || !viewportRef.current) {\n      return;\n    }\n\n    const calculatePadding = () => {\n      const bleed = bleedRefFromContext.current;\n      const viewport = viewportRef.current;\n      if (!(bleed && viewport)) {\n        return;\n      }\n\n      const parent = bleed.parentElement;\n      if (!parent) {\n        return;\n      }\n\n      const parentRect = parent.getBoundingClientRect();\n      const viewportRect = viewport.getBoundingClientRect();\n\n      const parentStyles = window.getComputedStyle(parent);\n      const parentPaddingLeft = Number.parseFloat(parentStyles.paddingLeft);\n      const parentPaddingRight = Number.parseFloat(parentStyles.paddingRight);\n\n      const leftPadding = Math.max(0, parentRect.left + parentPaddingLeft - viewportRect.left - gap);\n\n      const rightPadding = Math.max(0, viewportRect.right - (parentRect.right - parentPaddingRight));\n\n      setInsetPaddingLeft(leftPadding);\n      setInsetPaddingRight(rightPadding);\n    };\n\n    calculatePadding();\n\n    window.addEventListener(\"resize\", calculatePadding);\n    return () => window.removeEventListener(\"resize\", calculatePadding);\n  }, [variant, bleedRefFromContext, gap]);\n\n  return (\n    <CarouselContext.Provider value={value}>\n      <div\n        className={cn(\n          \"relative mx-auto w-full overflow-visible rounded-lg\",\n          \"focus-visible:outline-2 focus-visible:outline-[color:var(--ring)] focus-visible:outline-offset-2\",\n          \"data-[align=center]:flex data-[align=center]:flex-col data-[align=center]:items-center\",\n          className\n        )}\n        data-align={align}\n        data-slot=\"carousel\"\n        style={\n          {\n            \"--page-max-width\": \"1280px\",\n            \"--page-padding-left\": \"1rem\",\n            \"--carousel-gap\": \"16px\",\n            \"--edge\": \"calc((100vw - var(--page-max-width)) / 2)\",\n            \"--min-edge\": \"calc(var(--edge) - var(--carousel-gap))\",\n            \"--min-padding\": \"calc(var(--page-padding-left) - var(--carousel-gap))\",\n            \"--calculated-inset-padding-left\": `${insetPaddingLeft}px`,\n            \"--calculated-inset-padding-right\": `${insetPaddingRight}px`,\n          } as React.CSSProperties\n        }\n        {...props}\n      >\n        {children}\n        <div\n          aria-atomic=\"true\"\n          aria-live=\"polite\"\n          className=\"-m-px absolute h-px w-px overflow-hidden whitespace-nowrap border-0 p-0\"\n          style={{ clip: \"rect(0, 0, 0, 0)\" }}\n        >\n          Slide {currentIndex + 1} of {totalItems}\n        </div>\n      </div>\n    </CarouselContext.Provider>\n  );\n}\n\nexport type CarouselBleedProps = React.ComponentProps<\"div\">;\n\nconst BleedRefContext = createContext<React.RefObject<HTMLDivElement | null> | null>(null);\n\nexport function useBleedRef() {\n  return useContext(BleedRefContext);\n}\n\nexport function Bleed({ className, children, ...props }: CarouselBleedProps) {\n  const bleedRef = useRef<HTMLDivElement | null>(null);\n\n  return (\n    <BleedRefContext.Provider value={bleedRef}>\n      <div\n        className={cn(\"-ml-[50vw] -mr-[50vw] relative right-1/2 left-1/2 w-screen\", className)}\n        ref={bleedRef}\n        {...props}\n      >\n        {children}\n      </div>\n    </BleedRefContext.Provider>\n  );\n}\n\nexport type CarouselViewportProps = React.ComponentProps<\"div\">;\n\nexport function Viewport({ className, children, ...props }: CarouselViewportProps) {\n  const { viewportRef, slidesId } = useCarousel();\n\n  return (\n    <div\n      className={cn(\n        \"[scroll-snap-stop:always] relative w-full overflow-y-hidden overflow-x-scroll overscroll-x-contain\",\n        \"py-[calc(var(--focus-ring-width,2px)+var(--focus-ring-offset,2px))] [-ms-overflow-style:none] [scrollbar-width:none]\",\n        \"[&::-webkit-scrollbar]:hidden\",\n        className\n      )}\n      id={slidesId}\n      ref={viewportRef}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n\nexport type CarouselContentProps = React.ComponentProps<\"div\">;\n\nexport function Content({ className, children, ...props }: CarouselContentProps) {\n  const { gap, variant } = useCarousel();\n\n  return (\n    <div\n      className={cn(\n        \"flex items-stretch\",\n        \"before:w-[var(--inset-padding-left,0)] before:flex-shrink-0 before:content-['']\",\n        \"after:w-[var(--inset-padding-right,0)] after:flex-shrink-0 after:content-['']\",\n        className\n      )}\n      style={\n        {\n          gap: `${gap}px`,\n          \"--inset-padding-left\":\n            variant === \"inset\"\n              ? \"var(--calculated-inset-padding-left, max(var(--min-edge), var(--min-padding)))\"\n              : undefined,\n          \"--inset-padding-right\":\n            variant === \"inset\"\n              ? \"var(--calculated-inset-padding-right, max(var(--min-edge), var(--min-padding)))\"\n              : undefined,\n        } as React.CSSProperties\n      }\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n\nexport type CarouselItemProps = React.ComponentProps<\"div\"> & {\n  index: number;\n};\n\nexport function Item({ index, className, children, ...props }: CarouselItemProps) {\n  const { totalItems, goToIndex, nextSlide, prevSlide, canGoNext, canGoPrev, viewportRef } = useCarousel();\n  const itemRef = useRef<HTMLDivElement>(null);\n  const [isVisible, setIsVisible] = useState(false);\n\n  useEffect(() => {\n    const el = itemRef.current;\n    const root = viewportRef.current;\n    if (!(el && root)) {\n      return;\n    }\n    const observer = new IntersectionObserver(\n      ([entry]) => setIsVisible(entry.intersectionRatio >= 0.5),\n      { root, threshold: [0, 0.5, 1] }\n    );\n    observer.observe(el);\n    return () => observer.disconnect();\n  }, [viewportRef]);\n\n  const handleKeyDown = useCallback(\n    (e: React.KeyboardEvent) => {\n      switch (e.key) {\n        case \"ArrowLeft\":\n          if (canGoPrev) {\n            e.preventDefault();\n            prevSlide();\n          }\n          break;\n        case \"ArrowRight\":\n          if (canGoNext) {\n            e.preventDefault();\n            nextSlide();\n          }\n          break;\n        case \"Home\":\n          e.preventDefault();\n          goToIndex(0);\n          break;\n        case \"End\":\n          e.preventDefault();\n          goToIndex(totalItems - 1);\n          break;\n        default:\n          break;\n      }\n    },\n    [canGoPrev, canGoNext, prevSlide, nextSlide, goToIndex, totalItems]\n  );\n\n  return (\n    // biome-ignore lint/a11y/noNoninteractiveElementInteractions: Keyboard navigation is required for carousel accessibility\n    <div\n      aria-label={`Slide ${index + 1} of ${totalItems}`}\n      aria-roledescription=\"slide\"\n      className={cn(\n        \"relative flex-shrink-0\",\n        \"focus-visible:rounded-[var(--radius)] focus-visible:outline-2 focus-visible:outline-[color:var(--ring)] focus-visible:outline-offset-[1px]\",\n        className\n      )}\n      onKeyDown={handleKeyDown}\n      ref={itemRef}\n      role=\"group\"\n      tabIndex={isVisible ? 0 : -1}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n\nexport type CarouselPreviousProps = React.ComponentProps<\"button\">;\n\nexport function Previous({ className, children, ...props }: CarouselPreviousProps) {\n  const { prevSlide, canGoPrev, slidesId } = useCarousel();\n\n  return (\n    <button\n      aria-controls={slidesId}\n      aria-label=\"Previous slide\"\n      className={cn(\n        \"relative h-10 w-10 rounded-full border-[0.5px] border-[color:oklch(from_var(--border)_l_c_h_/_0.8)]\",\n        \"flex cursor-pointer items-center justify-center bg-[color:var(--card)] text-[color:var(--foreground)]\",\n        \"opacity-90 shadow-[var(--shadow-md)] transition-all duration-200 ease-[var(--ease-out-quad)]\",\n        \"hover:bg-[color:var(--muted)] hover:opacity-100\",\n        \"focus-visible:outline-2 focus-visible:outline-[color:var(--ring)] focus-visible:outline-offset-2\",\n        \"active:scale-95\",\n        \"disabled:pointer-events-none disabled:cursor-default disabled:bg-[color:var(--muted)] disabled:text-[color:var(--muted-foreground)] disabled:opacity-30\",\n        \"disabled:hover:bg-[color:var(--muted)] disabled:hover:opacity-30 disabled:hover:[transform:none]\",\n        \"motion-reduce:transition-none [&_svg]:h-4 [&_svg]:w-4\",\n        className\n      )}\n      disabled={!canGoPrev}\n      onClick={prevSlide}\n      type=\"button\"\n      {...props}\n    >\n      {children || (\n        <svg aria-hidden=\"true\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" viewBox=\"0 0 24 24\">\n          <path d=\"m15 18-6-6 6-6\" />\n        </svg>\n      )}\n    </button>\n  );\n}\n\nexport type CarouselNextProps = React.ComponentProps<\"button\">;\n\nexport function Next({ className, children, ...props }: CarouselNextProps) {\n  const { nextSlide, canGoNext, slidesId } = useCarousel();\n\n  return (\n    <button\n      aria-controls={slidesId}\n      aria-label=\"Next slide\"\n      className={cn(\n        \"relative h-10 w-10 rounded-full border-[0.5px] border-[color:oklch(from_var(--border)_l_c_h_/_0.8)]\",\n        \"flex cursor-pointer items-center justify-center bg-[color:var(--card)] text-[color:var(--foreground)]\",\n        \"opacity-90 shadow-[var(--shadow-md)] transition-all duration-200 ease-[var(--ease-out-quad)]\",\n        \"hover:bg-[color:var(--muted)] hover:opacity-100\",\n        \"focus-visible:outline-2 focus-visible:outline-[color:var(--ring)] focus-visible:outline-offset-2\",\n        \"active:scale-95\",\n        \"disabled:pointer-events-none disabled:cursor-default disabled:bg-[color:var(--muted)] disabled:text-[color:var(--muted-foreground)] disabled:opacity-30\",\n        \"disabled:hover:bg-[color:var(--muted)] disabled:hover:opacity-30 disabled:hover:[transform:none]\",\n        \"motion-reduce:transition-none [&_svg]:h-4 [&_svg]:w-4\",\n        className\n      )}\n      disabled={!canGoNext}\n      onClick={nextSlide}\n      type=\"button\"\n      {...props}\n    >\n      {children || (\n        <svg aria-hidden=\"true\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" viewBox=\"0 0 24 24\">\n          <path d=\"m9 18 6-6-6-6\" />\n        </svg>\n      )}\n    </button>\n  );\n}\n\nexport type CarouselNavigationProps = React.ComponentProps<\"div\">;\n\nexport function Navigation({ className, children, ...props }: CarouselNavigationProps) {\n  const { totalItems, variant } = useCarousel();\n\n  if (totalItems <= 1) {\n    return null;\n  }\n\n  return (\n    <div\n      className={cn(\"mt-12 flex justify-center gap-2\", className)}\n      style={\n        variant === \"inset\"\n          ? ({\n              \"--inset-padding\": \"var(--calculated-inset-padding, 0)\",\n            } as React.CSSProperties)\n          : undefined\n      }\n      {...props}\n    >\n      {children || (\n        <>\n          <Previous />\n          <Next />\n        </>\n      )}\n    </div>\n  );\n}\n\nexport type CarouselIndicatorsProps = React.ComponentProps<\"div\">;\n\nexport function Indicators({ className, ...props }: CarouselIndicatorsProps) {\n  const { totalItems, currentIndex, goToIndex, slidesId } = useCarousel();\n\n  if (totalItems <= 1) {\n    return null;\n  }\n\n  return (\n    <div\n      aria-label=\"Choose slide to display\"\n      className={cn(\"-translate-x-1/2 absolute bottom-4 left-1/2 z-10 flex gap-2\", className)}\n      role=\"tablist\"\n      {...props}\n    >\n      {Array.from({ length: totalItems }, (_, index) => (\n        <button\n          aria-controls={slidesId}\n          aria-label={`Go to slide ${index + 1}`}\n          aria-selected={currentIndex === index}\n          className={cn(\n            \"relative h-3 w-3 cursor-pointer rounded-full border-none\",\n            \"bg-[color:oklch(from_var(--foreground)_l_c_h_/_0.3)] transition-all duration-200 ease-[ease]\",\n            \"hover:scale-110 hover:bg-[color:oklch(from_var(--foreground)_l_c_h_/_0.5)]\",\n            \"focus-visible:outline-2 focus-visible:outline-[color:var(--ring)] focus-visible:outline-offset-2\",\n            \"data-[active]:scale-[1.2] data-[active]:bg-[color:var(--primary)] data-[active]:hover:bg-[color:var(--primary)]\",\n            \"motion-reduce:transition-none\"\n          )}\n          data-active={currentIndex === index ? \"\" : undefined}\n          // biome-ignore lint/suspicious/noArrayIndexKey: Indicators are stable and don't reorder\n          key={`indicator-${index}`}\n          onClick={() => goToIndex(index)}\n          role=\"tab\"\n          type=\"button\"\n        />\n      ))}\n    </div>\n  );\n}\n\nexport const Carousel = {\n  Root,\n  Bleed,\n  Viewport,\n  Content,\n  Item,\n  Previous,\n  Next,\n  Navigation,\n  Indicators,\n};\n",
      "type": "registry:file",
      "target": "~/components/ui/carousel/carousel.tsx"
    }
  ]
}