{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "apple-carousel",
  "title": "Apple Carousel",
  "author": "ncdai <dai@chanhdai.com>",
  "description": "Infinite autoplay carousel with progress dots, inspired by Apple’s website.",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "src/registry/components/apple-carousel/apple-carousel.tsx",
      "content": "\"use client\"\n\nimport {\n  Children,\n  createContext,\n  isValidElement,\n  use,\n  useEffect,\n  useEffectEvent,\n  useId,\n  useLayoutEffect,\n  useRef,\n  useState,\n  useSyncExternalStore,\n} from \"react\"\nimport type { ComponentProps, KeyboardEvent, RefObject } from \"react\"\nimport {\n  animate,\n  motion,\n  useInView,\n  useMotionValue,\n  useMotionValueEvent,\n} from \"motion/react\"\nimport type { AnimationPlaybackControls, Transition } from \"motion/react\"\n\nimport { cn } from \"@/lib/utils\"\n\n// More than anyone can fling past before the wrap runs, so the loop's seam\n// never scrolls into view.\nconst CLONE_COUNT = 4\n\n// Debounce for \"scroll settled\", and how long scroll events after our own\n// writes are ignored.\nconst SETTLE_DELAY = 150\n\n// Spring visualDuration (s), longer for long jumps so they don't whip past.\nconst SCROLL_BASE_DURATION = 0.5\nconst SCROLL_EXTRA_CARD_DURATION = 0.1\nconst SCROLL_MAX_DURATION = 0.9\n\nconst REDUCED_MOTION_QUERY = \"(prefers-reduced-motion: reduce)\"\n\nfunction subscribeReducedMotion(callback: () => void) {\n  const mql = window.matchMedia(REDUCED_MOTION_QUERY)\n  mql.addEventListener(\"change\", callback)\n  return () => mql.removeEventListener(\"change\", callback)\n}\n\nfunction usePrefersReducedMotion() {\n  return useSyncExternalStore(\n    subscribeReducedMotion,\n    () => window.matchMedia(REDUCED_MOTION_QUERY).matches,\n    () => false\n  )\n}\n\nconst subscribeNothing = () => () => {}\n\nfunction useIsHydrated() {\n  return useSyncExternalStore(\n    subscribeNothing,\n    () => true,\n    () => false\n  )\n}\n\nfunction getSlides(viewport: HTMLElement) {\n  return Array.from(viewport.firstElementChild?.children ?? []) as HTMLElement[]\n}\n\nfunction getSnapLeft(viewport: HTMLElement, slide: HTMLElement) {\n  if (getComputedStyle(slide).scrollSnapAlign.startsWith(\"start\")) {\n    const padding = parseFloat(\n      getComputedStyle(viewport).scrollPaddingInlineStart\n    )\n    return slide.offsetLeft - (padding || 0)\n  }\n\n  return slide.offsetLeft - (viewport.clientWidth - slide.offsetWidth) / 2\n}\n\nfunction getNearestSlide(viewport: HTMLElement) {\n  let nearest: HTMLElement | undefined\n  let minDistance = Infinity\n\n  for (const slide of getSlides(viewport)) {\n    const distance = Math.abs(\n      getSnapLeft(viewport, slide) - viewport.scrollLeft\n    )\n    if (distance < minDistance) {\n      minDistance = distance\n      nearest = slide\n    }\n  }\n\n  return nearest\n}\n\nfunction getRealSlide(viewport: HTMLElement, index: number) {\n  return getSlides(viewport).find(\n    (slide) =>\n      !slide.hasAttribute(\"data-clone\") && slide.dataset.index === String(index)\n  )\n}\n\ntype AppleCarouselContextValue = {\n  id: string\n  index: number\n  isPlaying: boolean\n  prefersReducedMotion: boolean\n  viewportRef: RefObject<HTMLDivElement | null>\n  progressFillRef: RefObject<HTMLSpanElement | null>\n  goTo: (index: number) => void\n  play: () => void\n  pause: () => void\n}\n\nconst AppleCarouselContext = createContext<AppleCarouselContextValue | null>(\n  null\n)\n\nfunction useAppleCarousel() {\n  const context = use(AppleCarouselContext)\n  if (!context) {\n    throw new Error(\"AppleCarousel parts must be used within AppleCarouselRoot\")\n  }\n  return context\n}\n\ntype AppleCarouselItemState = {\n  /** Position of the item among the items passed to `AppleCarouselContent`. */\n  index: number\n  /** Whether the item is the one being shown. */\n  isCurrent: boolean\n  /** Whether this copy is a clone rendered to keep the loop seamless. */\n  isClone: boolean\n}\n\nconst AppleCarouselItemContext = createContext<Omit<\n  AppleCarouselItemState,\n  \"isCurrent\"\n> | null>(null)\n\n/**\n * State of the enclosing item. Content also renders in loop clones, so check\n * `isClone` before starting media, fetching, or tracking impressions.\n */\nfunction useAppleCarouselItem(): AppleCarouselItemState {\n  const { index: currentIndex } = useAppleCarousel()\n  const item = use(AppleCarouselItemContext)\n  if (!item) {\n    throw new Error(\n      \"useAppleCarouselItem must be used inside an item of AppleCarouselContent\"\n    )\n  }\n\n  return { ...item, isCurrent: item.index === currentIndex }\n}\n\nconst AppleCarouselTabContext = createContext<{\n  index: number\n  count: number\n} | null>(null)\n\n// `auto` holds back under reduced motion. Pressing play is an explicit opt-in,\n// so `playing` ignores that preference and cards jump instead of sliding.\ntype Playback = \"auto\" | \"playing\" | \"paused\"\n\ntype AppleCarouselRootProps = ComponentProps<\"div\"> & {\n  /**\n   * Time each card stays up during autoplay (ms).\n   * @defaultValue 5000\n   */\n  duration?: number\n}\n\nfunction AppleCarouselRoot({\n  duration = 5000,\n  className,\n  ...props\n}: AppleCarouselRootProps) {\n  const id = useId()\n  const viewportRef = useRef<HTMLDivElement>(null)\n  const progressFillRef = useRef<HTMLSpanElement>(null)\n\n  const [index, setIndex] = useState(0)\n  const [playback, setPlayback] = useState<Playback>(\"auto\")\n  const isInView = useInView(viewportRef, { amount: 0.25 })\n  const prefersReducedMotion = usePrefersReducedMotion()\n  const isPlaying =\n    isInView &&\n    (playback === \"playing\" || (playback === \"auto\" && !prefersReducedMotion))\n\n  const ignoreScrollUntilRef = useRef(0)\n  const scrollAnimationRef = useRef<AnimationPlaybackControls | null>(null)\n  const progressAnimationRef = useRef<Animation | null>(null)\n  const scrollLeft = useMotionValue(0)\n\n  const play = () => setPlayback(\"playing\")\n  const pause = () => setPlayback(\"paused\")\n\n  const isOwnScroll = () =>\n    scrollAnimationRef.current !== null ||\n    performance.now() < ignoreScrollUntilRef.current\n\n  const writeScrollLeft = (value: number) => {\n    const viewport = viewportRef.current\n    if (!viewport) return\n    ignoreScrollUntilRef.current = performance.now() + SETTLE_DELAY\n    viewport.scrollLeft = value\n  }\n\n  useMotionValueEvent(scrollLeft, \"change\", writeScrollLeft)\n\n  const setScrollAnimation = (controls: AnimationPlaybackControls | null) => {\n    scrollAnimationRef.current = controls\n    // Mandatory snapping would pull every intermediate scrollLeft back.\n    viewportRef.current?.toggleAttribute(\"data-animating\", controls !== null)\n  }\n\n  const stopScrollAnimation = () => {\n    scrollAnimationRef.current?.stop()\n    setScrollAnimation(null)\n  }\n\n  const syncFromScroll = () => {\n    // An animation already committed its destination, so the cards it passes\n    // through must not flash their dots.\n    if (scrollAnimationRef.current) return\n    const viewport = viewportRef.current\n    const slide = viewport && getNearestSlide(viewport)\n    if (slide) setIndex(Number(slide.dataset.index))\n  }\n\n  // Clone and real card look identical, so the jump is invisible.\n  const wrapFromClone = () => {\n    const viewport = viewportRef.current\n    const slide = viewport && getNearestSlide(viewport)\n    if (!viewport || !slide?.hasAttribute(\"data-clone\")) return\n\n    const realSlide = getRealSlide(viewport, Number(slide.dataset.index))\n    if (realSlide) writeScrollLeft(getSnapLeft(viewport, realSlide))\n  }\n\n  const scrollToSlide = (slide: HTMLElement) => {\n    const viewport = viewportRef.current\n    if (!viewport) return\n\n    setIndex(Number(slide.dataset.index))\n\n    const to = getSnapLeft(viewport, slide)\n    const distance = Math.abs(to - viewport.scrollLeft)\n\n    if (prefersReducedMotion || distance < 1) {\n      stopScrollAnimation()\n      writeScrollLeft(to)\n      wrapFromClone()\n      return\n    }\n\n    // A running spring keeps its velocity when retargeted; only an idle one\n    // needs to pick up where the user left the scroll position.\n    if (!scrollAnimationRef.current) scrollLeft.jump(viewport.scrollLeft)\n\n    const cardsToTravel = Math.max(distance / slide.offsetWidth, 1)\n    const controls = animate(scrollLeft, to, {\n      type: \"spring\",\n      bounce: 0,\n      visualDuration: Math.min(\n        SCROLL_BASE_DURATION + (cardsToTravel - 1) * SCROLL_EXTRA_CARD_DURATION,\n        SCROLL_MAX_DURATION\n      ),\n      onComplete: () => {\n        if (scrollAnimationRef.current !== controls) return\n        setScrollAnimation(null)\n        wrapFromClone()\n      },\n    })\n    setScrollAnimation(controls)\n  }\n\n  const goTo = (next: number) => {\n    const viewport = viewportRef.current\n    const slide = viewport && getRealSlide(viewport, next)\n    if (slide) scrollToSlide(slide)\n  }\n\n  const goNext = () => {\n    const viewport = viewportRef.current\n    const next = viewport && getNearestSlide(viewport)?.nextElementSibling\n    if (next instanceof HTMLElement) scrollToSlide(next)\n  }\n\n  const onViewportScroll = useEffectEvent(() => {\n    if (isPlaying && !isOwnScroll()) pause()\n    syncFromScroll()\n  })\n\n  const onViewportSettled = useEffectEvent(() => {\n    if (!isOwnScroll()) wrapFromClone()\n  })\n\n  const onViewportInput = useEffectEvent(() => {\n    stopScrollAnimation()\n    pause()\n  })\n\n  useEffect(() => {\n    const viewport = viewportRef.current\n    if (!viewport) return\n\n    let frame: number | null = null\n    let settleTimer: ReturnType<typeof setTimeout> | undefined\n\n    const handleScroll = () => {\n      if (frame === null) {\n        frame = requestAnimationFrame(() => {\n          frame = null\n          onViewportScroll()\n        })\n      }\n      // `scrollend` is not available everywhere, so a debounce backs it up.\n      // Whichever fires first wraps; the other finds a real card and no-ops.\n      clearTimeout(settleTimer)\n      settleTimer = setTimeout(onViewportSettled, SETTLE_DELAY)\n    }\n\n    const handleWheel = (event: WheelEvent) => {\n      // Vertical wheels scroll the page, not the gallery.\n      if (Math.abs(event.deltaX) > Math.abs(event.deltaY)) onViewportInput()\n    }\n\n    viewport.addEventListener(\"scroll\", handleScroll, { passive: true })\n    viewport.addEventListener(\"scrollend\", onViewportSettled)\n    viewport.addEventListener(\"pointerdown\", onViewportInput)\n    viewport.addEventListener(\"wheel\", handleWheel, { passive: true })\n\n    return () => {\n      viewport.removeEventListener(\"scroll\", handleScroll)\n      viewport.removeEventListener(\"scrollend\", onViewportSettled)\n      viewport.removeEventListener(\"pointerdown\", onViewportInput)\n      viewport.removeEventListener(\"wheel\", handleWheel)\n      if (frame !== null) cancelAnimationFrame(frame)\n      clearTimeout(settleTimer)\n    }\n  }, [])\n\n  const onProgressFinish = useEffectEvent(goNext)\n\n  // The progress fill doubles as the autoplay clock: composited, pauses in\n  // place, no per-frame JS. Without a tab list it still runs, targeting nothing.\n  useEffect(() => {\n    const animation = new Animation(\n      new KeyframeEffect(\n        progressFillRef.current,\n        [{ scale: \"0 1\" }, { scale: \"1 1\" }],\n        { duration, fill: \"forwards\" }\n      )\n    )\n    animation.onfinish = onProgressFinish\n    progressAnimationRef.current = animation\n\n    return () => {\n      animation.cancel()\n      progressAnimationRef.current = null\n    }\n  }, [index, duration])\n\n  useEffect(() => {\n    const animation = progressAnimationRef.current\n    if (!animation) return\n    if (isPlaying) animation.play()\n    else animation.pause()\n  }, [index, duration, isPlaying])\n\n  return (\n    <AppleCarouselContext\n      value={{\n        id,\n        index,\n        isPlaying,\n        prefersReducedMotion,\n        viewportRef,\n        progressFillRef,\n        goTo,\n        play,\n        pause,\n      }}\n    >\n      <div\n        data-slot=\"apple-carousel\"\n        data-playing={isPlaying ? \"\" : undefined}\n        className={cn(\"@container relative w-full\", className)}\n        {...props}\n      />\n    </AppleCarouselContext>\n  )\n}\n\nfunction AppleCarouselContent({\n  className,\n  children,\n  ...props\n}: ComponentProps<\"div\">) {\n  const { index, viewportRef } = useAppleCarousel()\n  const isHydrated = useIsHydrated()\n\n  const items = Children.toArray(children).filter(isValidElement)\n  const count = items.length\n  // Clones wait for hydration: without them the first card already sits at\n  // scrollLeft 0, so the server markup renders the right card with no script.\n  const cloneCount = isHydrated && count > 1 ? Math.min(CLONE_COUNT, count) : 0\n\n  const alignToCurrent = useEffectEvent(() => {\n    const viewport = viewportRef.current\n    const slide = viewport && getRealSlide(viewport, index)\n    if (viewport && slide) viewport.scrollLeft = getSnapLeft(viewport, slide)\n  })\n\n  useLayoutEffect(() => {\n    if (cloneCount > 0) alignToCurrent()\n  }, [cloneCount])\n\n  return (\n    <div\n      ref={viewportRef}\n      data-slot=\"apple-carousel-content\"\n      className={cn(\n        \"relative no-scrollbar snap-x snap-mandatory overflow-x-auto overflow-y-hidden data-animating:snap-none\",\n        \"[--apple-carousel-gap:--spacing(5)] [--apple-carousel-item-width:min(max(87.5cqw,var(--container-3xs)),var(--container-6xl))] [--apple-carousel-padding:max(6.25cqw,(100cqw-var(--container-6xl))/2)]\",\n        \"@max-3xl:scroll-ps-(--apple-carousel-padding) @max-3xl:[--apple-carousel-item-width:calc(max(87.5cqw,var(--container-3xs))-var(--apple-carousel-gap))]\",\n        className\n      )}\n      {...props}\n    >\n      <ul\n        role=\"list\"\n        data-slot=\"apple-carousel-list\"\n        className=\"mx-auto grid w-fit grid-flow-col gap-(--apple-carousel-gap) px-(--apple-carousel-padding)\"\n      >\n        {items.slice(count - cloneCount).map((item, i) => (\n          <AppleCarouselItemContext\n            key={`${item.key}-leading`}\n            value={{ index: count - cloneCount + i, isClone: true }}\n          >\n            {item}\n          </AppleCarouselItemContext>\n        ))}\n        {items.map((item, i) => (\n          <AppleCarouselItemContext\n            key={item.key}\n            value={{ index: i, isClone: false }}\n          >\n            {item}\n          </AppleCarouselItemContext>\n        ))}\n        {items.slice(0, cloneCount).map((item, i) => (\n          <AppleCarouselItemContext\n            key={`${item.key}-trailing`}\n            value={{ index: i, isClone: true }}\n          >\n            {item}\n          </AppleCarouselItemContext>\n        ))}\n      </ul>\n    </div>\n  )\n}\n\nfunction AppleCarouselItem({ className, ...props }: ComponentProps<\"li\">) {\n  const { id } = useAppleCarousel()\n  const { index, isCurrent, isClone } = useAppleCarouselItem()\n\n  return (\n    <li\n      id={isClone ? undefined : `${id}-item-${index}`}\n      role={isClone ? undefined : \"tabpanel\"}\n      aria-labelledby={isClone ? undefined : `${id}-tab-${index}`}\n      // Peeking cards stay out of reach: a click there would otherwise follow\n      // a link on a card the user isn't looking at.\n      inert={isClone || !isCurrent}\n      tabIndex={isCurrent && !isClone ? 0 : -1}\n      data-slot=\"apple-carousel-item\"\n      data-index={index}\n      data-clone={isClone ? \"\" : undefined}\n      data-current={isCurrent ? \"\" : undefined}\n      className={cn(\n        \"relative h-152 w-(--apple-carousel-item-width) snap-center overflow-hidden rounded-4xl bg-muted data-clone:snap-always\",\n        \"@max-5xl:h-140 @max-3xl:h-152 @max-3xl:snap-start\",\n        \"focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ring\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\ntype AppleCarouselControlsProps = ComponentProps<\"div\"> & {\n  /**\n   * Size of the tab list and play button inside.\n   * @defaultValue \"default\"\n   */\n  size?: \"default\" | \"sm\"\n}\n\nfunction AppleCarouselControls({\n  size = \"default\",\n  className,\n  ...props\n}: AppleCarouselControlsProps) {\n  return (\n    <div\n      data-slot=\"apple-carousel-controls\"\n      data-size={size}\n      className={cn(\n        \"group/apple-carousel-controls flex items-center justify-center gap-3 pt-6 data-[size=sm]:gap-2 data-[size=sm]:pt-4\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction AppleCarouselTabList({\n  className,\n  children,\n  ...props\n}: ComponentProps<\"div\">) {\n  const tabs = Children.toArray(children).filter(isValidElement)\n\n  return (\n    <div\n      role=\"tablist\"\n      data-slot=\"apple-carousel-tab-list\"\n      className={cn(\n        \"flex h-14 items-center rounded-full bg-muted px-4 group-data-[size=sm]/apple-carousel-controls:h-11 group-data-[size=sm]/apple-carousel-controls:px-3\",\n        className\n      )}\n      {...props}\n    >\n      {tabs.map((tab, index) => (\n        <AppleCarouselTabContext\n          key={tab.key}\n          value={{ index, count: tabs.length }}\n        >\n          {tab}\n        </AppleCarouselTabContext>\n      ))}\n    </div>\n  )\n}\n\nfunction AppleCarouselTab({\n  className,\n  children,\n  onClick,\n  onFocus,\n  onKeyDown,\n  ...props\n}: ComponentProps<\"button\">) {\n  const {\n    id,\n    index: currentIndex,\n    isPlaying,\n    progressFillRef,\n    goTo,\n    pause,\n  } = useAppleCarousel()\n  const tab = use(AppleCarouselTabContext)\n  if (!tab) {\n    throw new Error(\n      \"AppleCarouselTab must be a direct child of AppleCarouselTabList\"\n    )\n  }\n\n  const { index, count } = tab\n  const isCurrent = index === currentIndex\n\n  const handleKeyDown = (event: KeyboardEvent<HTMLButtonElement>) => {\n    onKeyDown?.(event)\n    if (event.defaultPrevented) return\n\n    let next: number\n    switch (event.key) {\n      case \"ArrowLeft\":\n        next = (index - 1 + count) % count\n        break\n      case \"ArrowRight\":\n        next = (index + 1) % count\n        break\n      case \"Home\":\n        next = 0\n        break\n      case \"End\":\n        next = count - 1\n        break\n      default:\n        return\n    }\n\n    event.preventDefault()\n    goTo(next)\n    event.currentTarget\n      .closest('[role=\"tablist\"]')\n      ?.querySelectorAll<HTMLElement>('[role=\"tab\"]')\n      [next]?.focus()\n  }\n\n  return (\n    <button\n      type=\"button\"\n      role=\"tab\"\n      id={`${id}-tab-${index}`}\n      aria-controls={`${id}-item-${index}`}\n      aria-selected={isCurrent}\n      tabIndex={isCurrent ? 0 : -1}\n      data-slot=\"apple-carousel-tab\"\n      data-current={isCurrent ? \"\" : undefined}\n      className={cn(\n        \"relative mx-2 size-2 overflow-hidden rounded-full bg-muted-foreground not-data-current:hover:bg-foreground\",\n        // Width morphs on screen; the hover color is a quick, small change.\n        \"transition-[width,background-color] duration-[400ms,150ms] ease-[cubic-bezier(0.645,0.045,0.355,1),ease]\",\n        \"motion-reduce:transition-colors motion-reduce:duration-150 motion-reduce:ease-[ease]\",\n        \"data-current:w-12 group-data-[size=sm]/apple-carousel-controls:data-current:w-6 @max-3xl:data-current:w-8\",\n        \"focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-ring\",\n        className\n      )}\n      onClick={(event) => {\n        onClick?.(event)\n        if (!event.defaultPrevented) goTo(index)\n      }}\n      onFocus={(event) => {\n        onFocus?.(event)\n        if (isPlaying && event.currentTarget.matches(\":focus-visible\")) pause()\n      }}\n      onKeyDown={handleKeyDown}\n      {...props}\n    >\n      <span\n        ref={isCurrent ? progressFillRef : undefined}\n        aria-hidden\n        data-slot=\"apple-carousel-tab-progress\"\n        className=\"absolute inset-0 origin-left scale-x-0 rounded-[inherit] bg-foreground\"\n      />\n      <span className=\"sr-only\">{children}</span>\n    </button>\n  )\n}\n\n// Points sit half a stroke inside the outline, so the round joins soften the\n// corners without growing the icon. Changing the width means re-insetting them.\nconst ICON_STROKE_WIDTH = 2.5\n\n// Matching M L L L Z commands let motion morph `d`: the play triangle is split\n// at x = 13, one half per pause bar.\nconst PAUSE_ICON = {\n  left: \"M5.5 5.5L8.5 5.5L8.5 18.5L5.5 18.5Z\",\n  right: \"M15.5 5.5L18.5 5.5L18.5 18.5L15.5 18.5Z\",\n} as const\n\nconst PLAY_ICON = {\n  left: \"M7.5 5.87L13 9.08L13 14.92L7.5 18.13Z\",\n  right: \"M13 9.08L18.01 12L18.01 12L13 14.92Z\",\n} as const\n\nconst ICON_TRANSITION: Transition = {\n  type: \"spring\",\n  visualDuration: 0.3,\n  bounce: 0,\n}\n\nconst INSTANT_TRANSITION: Transition = { duration: 0 }\n\nfunction AppleCarouselPlayButton({\n  className,\n  onClick,\n  ...props\n}: ComponentProps<\"button\">) {\n  const { isPlaying, prefersReducedMotion, play, pause } = useAppleCarousel()\n  // The icon names the action, so a playing gallery shows pause.\n  const icon = isPlaying ? PAUSE_ICON : PLAY_ICON\n  const transition = prefersReducedMotion ? INSTANT_TRANSITION : ICON_TRANSITION\n\n  return (\n    <button\n      type=\"button\"\n      aria-label={isPlaying ? \"Pause gallery\" : \"Play gallery\"}\n      data-slot=\"apple-carousel-play-button\"\n      className={cn(\n        \"flex size-14 items-center justify-center rounded-full bg-muted text-foreground group-data-[size=sm]/apple-carousel-controls:size-11\",\n        \"transition-[background-color,scale] duration-150 ease-out hover:bg-muted/80 active:scale-97 motion-reduce:active:scale-100\",\n        \"focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-ring\",\n        className\n      )}\n      onClick={(event) => {\n        onClick?.(event)\n        if (event.defaultPrevented) return\n        if (isPlaying) pause()\n        else play()\n      }}\n      {...props}\n    >\n      <svg\n        aria-hidden\n        viewBox=\"0 0 24 24\"\n        fill=\"currentColor\"\n        stroke=\"currentColor\"\n        strokeWidth={ICON_STROKE_WIDTH}\n        strokeLinejoin=\"round\"\n        className=\"size-6 group-data-[size=sm]/apple-carousel-controls:size-5\"\n      >\n        <motion.path\n          initial={false}\n          animate={{ d: icon.left }}\n          transition={transition}\n        />\n        <motion.path\n          initial={false}\n          animate={{ d: icon.right }}\n          transition={transition}\n        />\n      </svg>\n    </button>\n  )\n}\n\nexport {\n  AppleCarouselContent,\n  AppleCarouselControls,\n  AppleCarouselItem,\n  AppleCarouselPlayButton,\n  AppleCarouselRoot,\n  AppleCarouselTab,\n  AppleCarouselTabList,\n  useAppleCarouselItem,\n}\nexport type {\n  AppleCarouselControlsProps,\n  AppleCarouselItemState,\n  AppleCarouselRootProps,\n}\n",
      "type": "registry:component",
      "target": "@components/apple-carousel.tsx"
    }
  ],
  "docs": "https://chanhdai.com/components/apple-carousel",
  "categories": [
    "marketing"
  ],
  "type": "registry:component"
}