{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "carousel",
  "type": "registry:item",
  "title": "Carousel",
  "description": "A slideshow component for cycling through content.",
  "files": [
    {
      "path": "registry/brook/ui/carousel/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\";\nimport styles from \"./carousel.module.css\";\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(styles.carousel, className)}\n        data-align={align}\n        data-slot=\"carousel\"\n        style={\n          {\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 aria-atomic=\"true\" aria-live=\"polite\" className={styles.srOnly}>\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 className={cn(styles.bleed, className)} ref={bleedRef} {...props}>\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 className={cn(styles.viewport, className)} id={slidesId} ref={viewportRef} {...props}>\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(styles.container, className)}\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(styles.slide, className)}\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(styles.navButton, className)}\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(styles.navButton, className)}\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(styles.navContainer, 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 aria-label=\"Choose slide to display\" className={cn(styles.indicators, className)} role=\"tablist\" {...props}>\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={styles.indicator}\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"
    },
    {
      "path": "registry/brook/ui/carousel/carousel.module.css",
      "content": ".carousel {\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\n  position: relative;\n  width: 100%;\n  margin: 0 auto;\n  border-radius: 0.5rem;\n  overflow: visible;\n\n  &:focus-visible {\n    outline: 2px solid var(--ring);\n    outline-offset: 2px;\n  }\n\n  &[data-align=\"center\"] {\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n  }\n}\n\n.bleed {\n  width: 100vw;\n  margin-left: -50vw;\n  margin-right: -50vw;\n  position: relative;\n  left: 50%;\n  right: 50%;\n}\n\n.viewport {\n  position: relative;\n  width: 100%;\n  overflow-x: scroll;\n  overflow-y: hidden;\n  overscroll-behavior-x: contain;\n  scroll-snap-stop: always;\n  padding-block: calc(\n    var(--focus-ring-width, 2px) +\n    var(--focus-ring-offset, 2px)\n  );\n  scrollbar-width: none;\n  -ms-overflow-style: none;\n\n  &::-webkit-scrollbar {\n    display: none;\n  }\n}\n\n.container {\n  display: flex;\n  align-items: stretch;\n\n  &::before {\n    content: \"\";\n    flex-shrink: 0;\n    width: var(--inset-padding-left, 0);\n  }\n\n  &::after {\n    content: \"\";\n    flex-shrink: 0;\n    width: var(--inset-padding-right, 0);\n  }\n}\n\n.slide {\n  flex-shrink: 0;\n  position: relative;\n\n  &:focus-visible {\n    outline: 2px solid var(--ring);\n    outline-offset: 1px;\n    border-radius: var(--radius);\n  }\n}\n\n.navContainer {\n  display: flex;\n  justify-content: center;\n  gap: 0.5rem;\n  margin-top: 3rem;\n}\n\n.navButton {\n  position: relative;\n  width: 2.5rem;\n  height: 2.5rem;\n  border-radius: 50%;\n  border: 0.5px solid oklch(from var(--border) l c h / 0.8);\n  background: var(--card);\n  color: var(--foreground);\n  cursor: pointer;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  box-shadow: var(--shadow-md);\n  transition: all 0.2s var(--ease-out-quad);\n  opacity: 0.9;\n\n  &:hover {\n    opacity: 1;\n    background: var(--muted);\n  }\n\n  &:focus-visible {\n    outline: 2px solid var(--ring);\n    outline-offset: 2px;\n  }\n\n  &:active {\n    transform: scale(0.95);\n  }\n\n  &:disabled {\n    opacity: 0.3;\n    cursor: default;\n    background: var(--muted);\n    color: var(--muted-foreground);\n    pointer-events: none;\n\n    &:hover {\n      opacity: 0.3;\n      transform: none;\n      background: var(--muted);\n    }\n  }\n\n  & svg {\n    width: 1rem;\n    height: 1rem;\n  }\n\n  @media (prefers-reduced-motion: reduce) {\n    transition: none;\n  }\n}\n\n.indicators {\n  position: absolute;\n  bottom: 1rem;\n  left: 50%;\n  transform: translateX(-50%);\n  display: flex;\n  gap: 0.5rem;\n  z-index: 10;\n}\n\n.indicator {\n  width: 0.75rem;\n  height: 0.75rem;\n  border-radius: 50%;\n  border: none;\n  background: oklch(from var(--foreground) l c h / 0.3);\n  cursor: pointer;\n  transition: all 0.2s ease;\n  position: relative;\n\n  &:hover {\n    background: oklch(from var(--foreground) l c h / 0.5);\n    transform: scale(1.1);\n  }\n\n  &:focus-visible {\n    outline: 2px solid var(--ring);\n    outline-offset: 2px;\n  }\n\n  &[data-active] {\n    background: var(--primary);\n    transform: scale(1.2);\n\n    &:hover {\n      background: var(--primary);\n    }\n  }\n\n  @media (prefers-reduced-motion: reduce) {\n    transition: none;\n  }\n}\n\n.srOnly {\n  position: absolute;\n  width: 1px;\n  height: 1px;\n  padding: 0;\n  margin: -1px;\n  overflow: hidden;\n  clip: rect(0, 0, 0, 0);\n  white-space: nowrap;\n  border: 0;\n}\n",
      "type": "registry:file",
      "target": "~/components/ui/carousel/carousel.module.css"
    }
  ]
}