{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "carousel-kit",
  "title": "Carousel Kit",
  "description": "The parts every tealmac carousel is built from: a labelled carousel region, viewport, numbered slides, arrows, dots, a counter and the hooks to drive effects, on Embla.",
  "dependencies": [
    "embla-carousel-react@^8.6.0",
    "embla-carousel@^8.6.0",
    "lucide-react"
  ],
  "files": [
    {
      "path": "registry/tealmac/carousel-kit.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport type { EmblaCarouselType, EmblaOptionsType } from \"embla-carousel\"\nimport useEmblaCarousel from \"embla-carousel-react\"\nimport { ChevronLeft, ChevronRight } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nexport type CarouselApi = EmblaCarouselType\nexport type CarouselOrientation = \"horizontal\" | \"vertical\"\n\n/** Every word the kit puts on screen or reads out. Replace the ones you need to translate. */\nexport interface CarouselLabels {\n  previous: string\n  next: string\n  /** Name of the group of dots. */\n  choose: string\n  /** Name of a dot. */\n  goTo: (position: number) => string\n  /** Name of a slide, for example \"2 of 5\". */\n  slide: (position: number, total: number) => string\n  /** Read out for the counter, for example \"Slide 2 of 5\". */\n  position: (position: number, total: number) => string\n  /** Announced politely when the person moves to another slide. */\n  announce: (position: number, total: number) => string\n}\n\nexport const defaultCarouselLabels: CarouselLabels = {\n  previous: \"Previous slide\",\n  next: \"Next slide\",\n  choose: \"Choose a slide\",\n  goTo: (position) => `Go to slide ${position}`,\n  slide: (position, total) => `${position} of ${total}`,\n  position: (position, total) => `Slide ${position} of ${total}`,\n  announce: (position, total) => `Slide ${position} of ${total}`,\n}\n\n/**\n * True when text runs right to left at this element. An explicit dir attribute\n * wins (it also works where CSS is not computed), then the computed direction.\n */\nexport function isRtlElement(element: Element | null | undefined) {\n  if (!element) return false\n  const dir = element.closest(\"[dir]\")?.getAttribute(\"dir\")\n  if (dir === \"rtl\") return true\n  if (dir === \"ltr\") return false\n  return getComputedStyle(element).direction === \"rtl\"\n}\n\n/** The state every carousel part reads. Embla-backed or not, the parts are the same. */\nexport interface CarouselState {\n  api: EmblaCarouselType | undefined\n  /** Selected snap (the page the person is on). */\n  index: number\n  /** Number of snaps (pages). */\n  count: number\n  canPrev: boolean\n  canNext: boolean\n  orientation: CarouselOrientation\n  /** True when the page direction is right to left. */\n  rtl: boolean\n  label: string\n  labels: CarouselLabels\n  reducedMotion: boolean\n  scrollTo: (index: number, jump?: boolean) => void\n  prev: () => void\n  next: () => void\n  /** Tell the track whether the carousel moves by itself (turns its live region off). */\n  /** True while the carousel moves by itself (the live region is off). */\n  rotating: boolean\n  setRotating: (rotating: boolean) => void\n  viewportRef: (node: HTMLElement | null) => void\n}\n\nconst CarouselContext = React.createContext<CarouselState | null>(null)\n\nexport function useCarousel() {\n  const context = React.useContext(CarouselContext)\n  if (!context) throw new Error(\"Carousel parts must be used inside <Carousel>\")\n  return context\n}\n\nexport const CarouselStateProvider = CarouselContext.Provider\n\nconst SlideContext = React.createContext<{ index: number; total: number }>({\n  index: 0,\n  total: 1,\n})\n\n/** True when the person asked the system for less motion. */\nexport function usePrefersReducedMotion() {\n  return React.useSyncExternalStore(\n    (notify) => {\n      if (typeof window === \"undefined\" || !window.matchMedia) return () => {}\n      const query = window.matchMedia(\"(prefers-reduced-motion: reduce)\")\n      query.addEventListener?.(\"change\", notify)\n      return () => query.removeEventListener?.(\"change\", notify)\n    },\n    () =>\n      typeof window !== \"undefined\" && !!window.matchMedia\n        ? window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n        : false,\n    () => false\n  )\n}\n\nfunction isTypingTarget(target: EventTarget | null) {\n  if (!(target instanceof HTMLElement)) return false\n  return (\n    target.isContentEditable ||\n    [\"INPUT\", \"TEXTAREA\", \"SELECT\"].includes(target.tagName) ||\n    // Widgets that use the arrow keys themselves keep them.\n    !!target.closest(\n      '[role=\"slider\"],[role=\"tab\"],[role=\"listbox\"],[role=\"menu\"],[role=\"radiogroup\"],[role=\"spinbutton\"],[data-carousel-ignore-keys]'\n    )\n  )\n}\n\nexport interface CarouselProps extends Omit<\n  React.ComponentProps<\"div\">,\n  \"onSelect\"\n> {\n  /** Accessible name of the carousel. Required: it is announced as \"<label>, carousel\". */\n  label: string\n  orientation?: CarouselOrientation\n  /** Embla options. `axis` and `direction` are set for you. */\n  opts?: EmblaOptionsType\n  plugins?: Parameters<typeof useEmblaCarousel>[1]\n  /** Receive the Embla API once it exists. */\n  setApi?: (api: CarouselApi) => void\n  /** Called with the new snap index whenever the selection changes. */\n  onIndexChange?: (index: number) => void\n  /** Replace any of the built-in words, for example to translate the controls. */\n  labels?: Partial<CarouselLabels>\n}\n\n/**\n * Root of an Embla carousel: a labelled `region` with the carousel role\n * description, arrow/Home/End keys (flipped in RTL) and the shared state.\n */\nexport function Carousel({\n  label,\n  orientation = \"horizontal\",\n  opts,\n  plugins,\n  setApi,\n  onIndexChange,\n  labels: labelOverrides,\n  className,\n  children,\n  onKeyDown,\n  ...props\n}: CarouselProps) {\n  const rootRef = React.useRef<HTMLDivElement | null>(null)\n  const [rtl, setRtl] = React.useState(false)\n  const reducedMotion = usePrefersReducedMotion()\n  const [viewportRef, api] = useEmblaCarousel(\n    {\n      containScroll: \"trimSnaps\",\n      ...opts,\n      axis: orientation === \"vertical\" ? \"y\" : \"x\",\n      direction: rtl ? \"rtl\" : \"ltr\",\n      duration: reducedMotion ? 0 : (opts?.duration ?? 25),\n    },\n    plugins\n  )\n  const [index, setIndex] = React.useState(0)\n  const [count, setCount] = React.useState(0)\n  const [canPrev, setCanPrev] = React.useState(false)\n  const [canNext, setCanNext] = React.useState(false)\n  const [rotating, setRotating] = React.useState(false)\n  const [announcement, setAnnouncement] = React.useState(\"\")\n  const labels = React.useMemo<CarouselLabels>(\n    () => ({ ...defaultCarouselLabels, ...labelOverrides }),\n    [labelOverrides]\n  )\n  const labelsRef = React.useRef(labels)\n  labelsRef.current = labels\n  const rotatingRef = React.useRef(false)\n  // True from a person's action (button, key, drag) until the move settles.\n  const userMove = React.useRef(false)\n  rotatingRef.current = rotating\n  const onIndexChangeRef = React.useRef(onIndexChange)\n  onIndexChangeRef.current = onIndexChange\n\n  React.useEffect(() => {\n    setRtl(isRtlElement(rootRef.current))\n  }, [])\n\n  React.useEffect(() => {\n    if (api && setApi) setApi(api)\n  }, [api, setApi])\n\n  React.useEffect(() => {\n    if (!api) return\n    const sync = () => {\n      setIndex(api.selectedScrollSnap())\n      setCount(api.scrollSnapList().length)\n      setCanPrev(api.canScrollPrev())\n      setCanNext(api.canScrollNext())\n    }\n    const select = () => {\n      sync()\n      const position = api.selectedScrollSnap() + 1\n      // Sliding moves a transform, not text, so a live region on the track says\n      // nothing. Say the new position here, but only for a person's own move:\n      // not while it rotates, and not for a restore or a jump made in code.\n      setAnnouncement(\n        userMove.current && !rotatingRef.current\n          ? labelsRef.current.announce(position, api.scrollSnapList().length)\n          : \"\"\n      )\n      userMove.current = false\n      onIndexChangeRef.current?.(api.selectedScrollSnap())\n    }\n    const grab = () => {\n      userMove.current = true\n    }\n    const settle = () => {\n      userMove.current = false\n    }\n    sync()\n    api\n      .on(\"reInit\", sync)\n      .on(\"select\", select)\n      .on(\"pointerDown\", grab)\n      .on(\"settle\", settle)\n    return () => {\n      api\n        .off(\"reInit\", sync)\n        .off(\"select\", select)\n        .off(\"pointerDown\", grab)\n        .off(\"settle\", settle)\n    }\n  }, [api])\n\n  const scrollTo = React.useCallback(\n    (to: number, jump?: boolean) => {\n      userMove.current = !jump\n      api?.scrollTo(to, jump)\n    },\n    [api]\n  )\n  const prev = React.useCallback(() => {\n    userMove.current = true\n    api?.scrollPrev()\n  }, [api])\n  const next = React.useCallback(() => {\n    userMove.current = true\n    api?.scrollNext()\n  }, [api])\n\n  const state = React.useMemo<CarouselState>(\n    () => ({\n      api,\n      index,\n      count,\n      canPrev,\n      canNext,\n      orientation,\n      rtl,\n      label,\n      labels,\n      reducedMotion,\n      scrollTo,\n      prev,\n      next,\n      rotating,\n      setRotating,\n      viewportRef,\n    }),\n    [\n      api,\n      index,\n      count,\n      canPrev,\n      canNext,\n      orientation,\n      rtl,\n      label,\n      labels,\n      reducedMotion,\n      scrollTo,\n      prev,\n      next,\n      rotating,\n      viewportRef,\n    ]\n  )\n\n  const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {\n    onKeyDown?.(event)\n    if (event.defaultPrevented || isTypingTarget(event.target)) return\n    const vertical = orientation === \"vertical\"\n    const back = vertical ? \"ArrowUp\" : rtl ? \"ArrowRight\" : \"ArrowLeft\"\n    const forward = vertical ? \"ArrowDown\" : rtl ? \"ArrowLeft\" : \"ArrowRight\"\n    if (event.key === back) prev()\n    else if (event.key === forward) next()\n    else if (event.key === \"Home\") scrollTo(0)\n    else if (event.key === \"End\") scrollTo(Math.max(0, count - 1))\n    else return\n    event.preventDefault()\n  }\n\n  return (\n    <CarouselContext.Provider value={state}>\n      <div\n        ref={rootRef}\n        role=\"region\"\n        aria-roledescription=\"carousel\"\n        aria-label={label}\n        data-slot=\"carousel\"\n        data-orientation={orientation}\n        onKeyDownCapture={handleKeyDown}\n        className={cn(\"relative\", className)}\n        {...props}\n      >\n        {children}\n        <div\n          role=\"status\"\n          aria-live=\"polite\"\n          aria-atomic\n          data-slot=\"carousel-announcer\"\n          className=\"sr-only\"\n        >\n          {announcement}\n        </div>\n      </div>\n    </CarouselContext.Provider>\n  )\n}\n\nexport function CarouselViewport({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  const { viewportRef } = useCarousel()\n  return (\n    <div\n      ref={viewportRef}\n      data-slot=\"carousel-viewport\"\n      className={cn(\"overflow-hidden\", className)}\n      {...props}\n    >\n      {children}\n    </div>\n  )\n}\n\n/**\n * The moving strip. Each direct child should be a <CarouselSlide>; the track\n * gives it its position (\"2 of 5\"). It is a polite live region unless the\n * carousel moves by itself.\n */\nexport function CarouselTrack({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  const { orientation, rotating } = useCarousel()\n  const slides = React.Children.toArray(children)\n  const total = slides.length\n  return (\n    <div\n      data-slot=\"carousel-track\"\n      aria-live={rotating ? \"off\" : \"polite\"}\n      className={cn(\n        \"flex\",\n        orientation === \"vertical\" && \"flex-col\",\n        className\n      )}\n      {...props}\n    >\n      {slides.map((child, i) => (\n        <SlideContext.Provider key={i} value={{ index: i, total }}>\n          {child}\n        </SlideContext.Provider>\n      ))}\n    </div>\n  )\n}\n\nexport function CarouselSlide({\n  className,\n  children,\n  onFocus,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  const { index, total } = React.useContext(SlideContext)\n  const { api, scrollTo, labels } = useCarousel()\n  const handleFocus = (event: React.FocusEvent<HTMLDivElement>) => {\n    onFocus?.(event)\n    if (!api) return\n    // The browser scrolls an overflow:hidden box to a focused child. Undo that and let Embla move instead.\n    const viewport = api.rootNode()\n    viewport.scrollLeft = 0\n    viewport.scrollTop = 0\n    const snap = api\n      .internalEngine()\n      .slideRegistry.findIndex((group) => group.includes(index))\n    if (snap >= 0 && snap !== api.selectedScrollSnap()) scrollTo(snap)\n  }\n  return (\n    <div\n      role=\"group\"\n      aria-roledescription=\"slide\"\n      aria-label={labels.slide(index + 1, total)}\n      data-slot=\"carousel-slide\"\n      onFocus={handleFocus}\n      className={cn(\"min-w-0 shrink-0 grow-0 basis-full\", className)}\n      {...props}\n    >\n      {children}\n    </div>\n  )\n}\n\nconst buttonClass =\n  \"inline-flex size-9 items-center justify-center rounded-full border bg-background/90 text-foreground shadow-xs backdrop-blur transition-colors hover:bg-accent focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-40\"\n\nexport function CarouselPrevious({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<\"button\">) {\n  const { prev, canPrev, orientation, labels } = useCarousel()\n  return (\n    <button\n      type=\"button\"\n      data-slot=\"carousel-previous\"\n      aria-label={labels.previous}\n      disabled={!canPrev}\n      onClick={prev}\n      className={cn(buttonClass, className)}\n      {...props}\n    >\n      {children ?? (\n        <ChevronLeft\n          aria-hidden\n          className={cn(\n            \"size-4 rtl:-scale-x-100\",\n            orientation === \"vertical\" && \"rotate-90 rtl:scale-x-100\"\n          )}\n        />\n      )}\n    </button>\n  )\n}\n\nexport function CarouselNext({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<\"button\">) {\n  const { next, canNext, orientation, labels } = useCarousel()\n  return (\n    <button\n      type=\"button\"\n      data-slot=\"carousel-next\"\n      aria-label={labels.next}\n      disabled={!canNext}\n      onClick={next}\n      className={cn(buttonClass, className)}\n      {...props}\n    >\n      {children ?? (\n        <ChevronRight\n          aria-hidden\n          className={cn(\n            \"size-4 rtl:-scale-x-100\",\n            orientation === \"vertical\" && \"rotate-90 rtl:scale-x-100\"\n          )}\n        />\n      )}\n    </button>\n  )\n}\n\n/** One button per page. The current one has aria-current. Each button is 24px or more to tap. */\nexport function CarouselDots({\n  className,\n  label,\n  ...props\n}: React.ComponentProps<\"div\"> & { label?: string }) {\n  const { count, index, scrollTo, labels } = useCarousel()\n  return (\n    <div\n      role=\"group\"\n      aria-label={label ?? labels.choose}\n      data-slot=\"carousel-dots\"\n      className={cn(\"flex items-center justify-center gap-0.5\", className)}\n      {...props}\n    >\n      {Array.from({ length: count }, (_, i) => (\n        <button\n          key={i}\n          type=\"button\"\n          aria-label={labels.goTo(i + 1)}\n          aria-current={i === index ? \"true\" : undefined}\n          onClick={() => scrollTo(i)}\n          className=\"group focus-visible:ring-ring/50 inline-flex size-6 items-center justify-center rounded-full focus-visible:ring-[3px] focus-visible:outline-none\"\n        >\n          <span\n            className={cn(\n              \"bg-foreground/30 group-hover:bg-foreground/60 block h-2 rounded-full transition-all motion-reduce:transition-none\",\n              i === index ? \"bg-foreground w-5\" : \"w-2\"\n            )}\n          />\n        </button>\n      ))}\n    </div>\n  )\n}\n\n/** \"3 / 8\" on screen; read out as \"Slide 3 of 8\". */\nexport function CarouselCounter({\n  className,\n  ...props\n}: React.ComponentProps<\"p\">) {\n  const { index, count, labels } = useCarousel()\n  return (\n    <p\n      data-slot=\"carousel-counter\"\n      className={cn(\"text-muted-foreground text-sm tabular-nums\", className)}\n      {...props}\n    >\n      <span className=\"sr-only\">{labels.position(index + 1, count)}</span>\n      <span aria-hidden>\n        {index + 1} / {count}\n      </span>\n    </p>\n  )\n}\n\nexport interface SlideProximityOptions {\n  /** Called for every slide on every frame. `distance` is 0 when the slide is centred, 1 when one slide-size away. */\n  apply: (node: HTMLElement, distance: number, slideIndex: number) => void\n  enabled?: boolean\n}\n\n/**\n * Measures how far every slide is from the middle of the viewport and calls\n * `apply` on each scroll frame. Reading real positions keeps it correct with\n * loop, RTL and vertical carousels. Use it to drive transforms through CSS\n * variables without re-rendering.\n */\nexport function useSlideProximity(\n  api: EmblaCarouselType | undefined,\n  { apply, enabled = true }: SlideProximityOptions\n) {\n  const applyRef = React.useRef(apply)\n  applyRef.current = apply\n  React.useEffect(() => {\n    if (!api || !enabled) return\n    const update = () => {\n      const vertical = api.internalEngine().options.axis === \"y\"\n      const viewport = api.rootNode().getBoundingClientRect()\n      const mid = vertical\n        ? viewport.top + viewport.height / 2\n        : viewport.left + viewport.width / 2\n      api.slideNodes().forEach((node, i) => {\n        const rect = node.getBoundingClientRect()\n        const size = (vertical ? rect.height : rect.width) || 1\n        const centre = vertical\n          ? rect.top + rect.height / 2\n          : rect.left + rect.width / 2\n        applyRef.current(node, (centre - mid) / size, i)\n      })\n    }\n    update()\n    api.on(\"scroll\", update).on(\"reInit\", update).on(\"resize\", update)\n    return () => {\n      api.off(\"scroll\", update).off(\"reInit\", update).off(\"resize\", update)\n    }\n  }, [api, enabled])\n}\n\n/** Renders nothing. Place inside <Carousel> to run a per-slide effect (see useSlideProximity). */\nexport function CarouselEffect(props: SlideProximityOptions) {\n  const { api } = useCarousel()\n  useSlideProximity(api, props)\n  return null\n}\n\nexport function clampUnit(value: number) {\n  return Math.max(-1, Math.min(1, value))\n}\n\n/** The slide shape the ready-made carousels take. */\nexport interface CarouselItem {\n  id: string\n  /** Short name, used for thumbnails and accessible names. */\n  label: string\n  content: React.ReactNode\n  /** Optional small preview for thumbnail strips. Falls back to the label. */\n  thumb?: React.ReactNode\n}\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}