{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "carousel",
  "title": "Carousel",
  "description": "A swipeable set of pages or inline items with optional looping, autoplay, and composed controls.",
  "dependencies": [
    "react-native-gesture-handler",
    "react-native-reanimated",
    "react-native-worklets"
  ],
  "registryDependencies": [
    "@tetra-ui/button",
    "@tetra-ui/icons"
  ],
  "files": [
    {
      "path": "ui/carousel.tsx",
      "content": "import {\n  Children,\n  cloneElement,\n  createContext,\n  isValidElement,\n  useCallback,\n  useContext,\n  useEffect,\n  useImperativeHandle,\n  useLayoutEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport {\n  AccessibilityInfo,\n  AppState,\n  type LayoutChangeEvent,\n  Pressable,\n  View,\n} from \"react-native\";\nimport { GestureDetector, usePanGesture } from \"react-native-gesture-handler\";\nimport Animated, {\n  cancelAnimation,\n  Extrapolation,\n  interpolate,\n  type SharedValue,\n  useAnimatedReaction,\n  useAnimatedStyle,\n  useSharedValue,\n  withSpring,\n  withTiming,\n} from \"react-native-reanimated\";\nimport { scheduleOnRN } from \"react-native-worklets\";\nimport { cn } from \"@/lib/utils\";\nimport { Button, ButtonIcon } from \"@/components/ui/button\";\nimport { ChevronLeftIcon, ChevronRightIcon } from \"@/components/ui/icons\";\n\n// Constants\nconst DEFAULT_PER_VIEW = 1.2;\nconst DEFAULT_GAP = 12;\nconst DEFAULT_AUTOPLAY_INTERVAL = 4000;\nconst SNAP_SPRING = { damping: 22, mass: 0.45, stiffness: 420 };\nconst SNAP_TIMING_MS = 180;\nconst GESTURE_ACTIVE_OFFSET_X = 8;\nconst GESTURE_FAIL_OFFSET_Y = 24;\nconst VELOCITY_THRESHOLD = 250;\nconst RUBBER_BAND = 0.35;\nconst DOT_SIZE = 8;\nconst DOT_GROW = 10;\n\n// Types\nexport type CarouselVariant = \"page\" | \"inline\";\n\nexport type CarouselRef = {\n  scrollTo: (index: number, animated?: boolean) => void;\n  scrollNext: () => void;\n  scrollPrev: () => void;\n  getIndex: () => number;\n};\n\nexport type CarouselApi = {\n  index: number;\n  count: number;\n  looping: boolean;\n  canScrollPrev: boolean;\n  canScrollNext: boolean;\n  scrollTo: (index: number, animated?: boolean) => void;\n  scrollNext: () => void;\n  scrollPrev: () => void;\n  progress: SharedValue<number>;\n};\n\nexport type CarouselProps = Omit<React.ComponentProps<typeof View>, \"ref\"> & {\n  variant?: CarouselVariant;\n  perView?: number;\n  gap?: number;\n  inset?: number;\n  loop?: boolean;\n  autoplay?: boolean;\n  autoplayInterval?: number;\n  index?: number;\n  defaultIndex?: number;\n  onIndexChange?: (index: number) => void;\n  ref?: React.Ref<CarouselRef>;\n};\n\ntype CarouselContextValue = {\n  gap: number;\n  itemWidth: number;\n  count: number;\n  index: number;\n  looping: boolean;\n  reduceMotion: boolean;\n  canScrollPrev: boolean;\n  canScrollNext: boolean;\n  progress: SharedValue<number>;\n  translateX: SharedValue<number>;\n  dragStartX: SharedValue<number>;\n  strideSV: SharedValue<number>;\n  countSV: SharedValue<number>;\n  loopingSV: SharedValue<boolean>;\n  cloneCountSV: SharedValue<number>;\n  scrollTo: (index: number, animated?: boolean) => void;\n  scrollNext: () => void;\n  scrollPrev: () => void;\n  setCount: (count: number) => void;\n  setContainerWidth: (width: number) => void;\n  onPanStart: () => void;\n  onSettledFromGesture: (visual: number) => void;\n  reduceMotionSV: SharedValue<boolean>;\n  cloneCount: number;\n  peekInsetSV: SharedValue<number>;\n};\n\n// Utils\nconst clampIndex = (value: number, count: number) => {\n  if (count <= 0) {\n    return 0;\n  }\n\n  return Math.min(Math.max(value, 0), count - 1);\n};\n\nconst canEnableLoop = (\n  loop: boolean,\n  variant: CarouselVariant,\n  count: number,\n  perView: number\n) => {\n  if (!loop || count < 2) {\n    return false;\n  }\n\n  if (variant === \"inline\" && count <= perView) {\n    return false;\n  }\n\n  return true;\n};\n\nconst getTrackMetrics = (\n  containerWidth: number,\n  variant: CarouselVariant,\n  perView: number,\n  gap: number,\n  inset: number\n) => {\n  const safeInset = Math.max(0, inset);\n  const layoutWidth = Math.max(0, containerWidth - safeInset * 2);\n\n  if (layoutWidth <= 0) {\n    return { itemWidth: 0, peekInset: 0, stride: 0 };\n  }\n\n  if (variant === \"page\") {\n    return {\n      itemWidth: layoutWidth,\n      peekInset: safeInset,\n      stride: layoutWidth + gap,\n    };\n  }\n\n  const views = Math.max(perView, 1);\n  const itemWidth = (layoutWidth - gap * (views - 1)) / views;\n  const fullItems = Math.max(1, Math.floor(perView));\n  const packed = fullItems * itemWidth + Math.max(0, fullItems - 1) * gap;\n\n  return {\n    itemWidth,\n    peekInset: safeInset + Math.max(0, (layoutWidth - packed) / 2),\n    stride: itemWidth + gap,\n  };\n};\n\nconst offsetForVisual = (visual: number, stride: number, peekInset: number) => {\n  \"worklet\";\n\n  return -visual * stride + peekInset;\n};\n\nconst visualForOffset = (offset: number, stride: number, peekInset: number) => {\n  \"worklet\";\n\n  if (stride === 0) {\n    return 0;\n  }\n\n  return -(offset - peekInset) / stride;\n};\n\nconst getLoopCloneCount = (\n  looping: boolean,\n  variant: CarouselVariant,\n  perView: number,\n  count: number\n) => {\n  if (!looping || count <= 0) {\n    return 0;\n  }\n\n  if (variant === \"page\") {\n    return 1;\n  }\n\n  return Math.min(count, Math.max(1, Math.ceil(perView)));\n};\n\nconst getVisualIndex = (realIndex: number, cloneCount: number) => {\n  return realIndex + cloneCount;\n};\n\nconst getRealIndex = (\n  visualIndex: number,\n  count: number,\n  cloneCount: number\n) => {\n  if (cloneCount === 0) {\n    return clampIndex(visualIndex, count);\n  }\n\n  return (((visualIndex - cloneCount) % count) + count) % count;\n};\n\nconst wrapVisualIndex = (\n  visualIndex: number,\n  count: number,\n  cloneCount: number\n) => {\n  \"worklet\";\n\n  if (cloneCount === 0) {\n    if (count <= 0) {\n      return 0;\n    }\n\n    return Math.min(Math.max(visualIndex, 0), count - 1);\n  }\n\n  if (visualIndex >= cloneCount + count) {\n    return visualIndex - count;\n  }\n\n  if (visualIndex < cloneCount) {\n    return visualIndex + count;\n  }\n\n  return visualIndex;\n};\n\nconst wrapSettledTranslation = (\n  visual: number,\n  translateX: SharedValue<number>,\n  stride: number,\n  count: number,\n  cloneCount: number,\n  peekInset: number\n) => {\n  \"worklet\";\n\n  const wrapped = wrapVisualIndex(visual, count, cloneCount);\n\n  if (wrapped !== visual && stride > 0) {\n    translateX.value = offsetForVisual(wrapped, stride, peekInset);\n  }\n\n  return wrapped;\n};\n\nconst wrapLoopTranslation = (\n  offset: number,\n  stride: number,\n  count: number,\n  cloneCount: number,\n  peekInset: number\n) => {\n  \"worklet\";\n\n  if (stride === 0 || count <= 0 || cloneCount <= 0) {\n    return { offset, shift: 0 };\n  }\n\n  const period = count * stride;\n  const firstReal = offsetForVisual(cloneCount, stride, peekInset);\n  const firstTrailing = offsetForVisual(cloneCount + count, stride, peekInset);\n\n  if (offset > firstReal) {\n    const shift = -Math.ceil((offset - firstReal) / period) * period;\n    return { offset: offset + shift, shift };\n  }\n\n  if (offset <= firstTrailing) {\n    const shift = (Math.floor((firstTrailing - offset) / period) + 1) * period;\n    return { offset: offset + shift, shift };\n  }\n\n  return { offset, shift: 0 };\n};\n\nconst circularDistance = (from: number, to: number, count: number) => {\n  \"worklet\";\n\n  const direct = Math.abs(from - to);\n  return Math.min(direct, count - direct);\n};\n\nconst rubberBandTranslate = (\n  next: number,\n  stride: number,\n  count: number,\n  looping: boolean,\n  peekInset: number\n) => {\n  \"worklet\";\n\n  if (looping || stride === 0 || count <= 1) {\n    return next;\n  }\n\n  const max = peekInset;\n  const min = offsetForVisual(count - 1, stride, peekInset);\n  if (next > max) {\n    return max + (next - max) * RUBBER_BAND;\n  }\n\n  if (next < min) {\n    return min + (next - min) * RUBBER_BAND;\n  }\n\n  return next;\n};\n\nconst getSnapVisual = (\n  offsetX: number,\n  velocityX: number,\n  stride: number,\n  count: number,\n  cloneCount: number,\n  peekInset: number\n) => {\n  \"worklet\";\n\n  if (stride === 0 || count <= 0) {\n    return 0;\n  }\n\n  const currentVisual = visualForOffset(offsetX, stride, peekInset);\n  let target = Math.round(currentVisual);\n\n  if (velocityX < -VELOCITY_THRESHOLD) {\n    target = Math.max(target, Math.ceil(currentVisual - 0.01));\n  } else if (velocityX > VELOCITY_THRESHOLD) {\n    target = Math.min(target, Math.floor(currentVisual + 0.01));\n  }\n\n  const maxVisual = cloneCount === 0 ? count - 1 : cloneCount * 2 + count - 1;\n  return Math.min(Math.max(target, 0), maxVisual);\n};\n\ntype CarouselTrackItem = React.ReactElement<{ className?: string }>;\n\nconst getLoopedTrackItems = (\n  items: CarouselTrackItem[],\n  cloneCount: number\n) => {\n  if (cloneCount <= 0 || items.length === 0) {\n    return items;\n  }\n\n  const leading = items.slice(-cloneCount).map((item) =>\n    cloneElement(item, {\n      key: `carousel-clone-start-${item.key}`,\n    })\n  );\n  const trailing = items.slice(0, cloneCount).map((item) =>\n    cloneElement(item, {\n      key: `carousel-clone-end-${item.key}`,\n    })\n  );\n\n  return [...leading, ...items, ...trailing];\n};\n\nconst isTrackItem = (child: React.ReactNode): child is CarouselTrackItem => {\n  return isValidElement(child);\n};\n\nconst getCarouselItems = (children: React.ReactNode) => {\n  return Children.toArray(children).filter(isTrackItem);\n};\n\nconst usePrefersReducedMotion = () => {\n  const [reduceMotion, setReduceMotion] = useState(false);\n\n  useEffect(() => {\n    AccessibilityInfo.isReduceMotionEnabled().then(setReduceMotion);\n\n    const subscription = AccessibilityInfo.addEventListener(\n      \"reduceMotionChanged\",\n      setReduceMotion\n    );\n\n    return () => {\n      subscription.remove();\n    };\n  }, []);\n\n  return reduceMotion;\n};\n\nconst useAppIsActive = () => {\n  const [appState, setAppState] = useState(AppState.currentState);\n\n  useEffect(() => {\n    const subscription = AppState.addEventListener(\"change\", setAppState);\n    return () => {\n      subscription.remove();\n    };\n  }, []);\n\n  return appState === \"active\";\n};\n\nconst CarouselContext = createContext<CarouselContextValue | null>(null);\n\nconst useCarouselContext = () => {\n  const context = useContext(CarouselContext);\n  if (!context) {\n    throw new Error(\"Carousel components must be used within a Carousel\");\n  }\n  return context;\n};\n\nexport const useCarousel = (): CarouselApi => {\n  const {\n    canScrollNext,\n    canScrollPrev,\n    count,\n    index,\n    looping,\n    progress,\n    scrollNext,\n    scrollPrev,\n    scrollTo,\n  } = useCarouselContext();\n\n  return useMemo(\n    () => ({\n      canScrollNext,\n      canScrollPrev,\n      count,\n      index,\n      looping,\n      progress,\n      scrollNext,\n      scrollPrev,\n      scrollTo,\n    }),\n    [\n      canScrollNext,\n      canScrollPrev,\n      count,\n      index,\n      looping,\n      progress,\n      scrollNext,\n      scrollPrev,\n      scrollTo,\n    ]\n  );\n};\n\nexport const Carousel = ({\n  variant = \"page\",\n  perView = DEFAULT_PER_VIEW,\n  gap = DEFAULT_GAP,\n  inset = 0,\n  loop = false,\n  autoplay = false,\n  autoplayInterval = DEFAULT_AUTOPLAY_INTERVAL,\n  index: indexProp,\n  defaultIndex = 0,\n  onIndexChange,\n  className,\n  children,\n  ref,\n  ...props\n}: CarouselProps) => {\n  const reduceMotion = usePrefersReducedMotion();\n  const appIsActive = useAppIsActive();\n  const [containerWidth, setContainerWidth] = useState(0);\n  const [count, setCount] = useState(0);\n  const [internalIndex, setInternalIndex] = useState(() =>\n    Math.max(defaultIndex, 0)\n  );\n  const [isDragging, setIsDragging] = useState(false);\n\n  const isControlled = indexProp !== undefined;\n  const index = isControlled ? indexProp : internalIndex;\n  const looping = canEnableLoop(loop, variant, count, perView);\n  const cloneCount = getLoopCloneCount(looping, variant, perView, count);\n  const { itemWidth, peekInset, stride } = getTrackMetrics(\n    containerWidth,\n    variant,\n    perView,\n    gap,\n    inset\n  );\n  const canScrollPrev = looping || index > 0;\n  const canScrollNext = looping || index < count - 1;\n\n  const translateX = useSharedValue(0);\n  const dragStartX = useSharedValue(0);\n  const progress = useSharedValue(0);\n  const strideSV = useSharedValue(0);\n  const countSV = useSharedValue(0);\n  const loopingSV = useSharedValue(false);\n  const cloneCountSV = useSharedValue(0);\n  const peekInsetSV = useSharedValue(0);\n  const reduceMotionSV = useSharedValue(false);\n\n  useLayoutEffect(() => {\n    strideSV.value = stride;\n    countSV.value = count;\n    loopingSV.value = looping;\n    cloneCountSV.value = cloneCount;\n    peekInsetSV.value = peekInset;\n    reduceMotionSV.value = reduceMotion;\n  }, [\n    stride,\n    strideSV,\n    count,\n    countSV,\n    looping,\n    loopingSV,\n    cloneCount,\n    cloneCountSV,\n    peekInset,\n    peekInsetSV,\n    reduceMotion,\n    reduceMotionSV,\n  ]);\n\n  const indexRef = useRef(index);\n  const countRef = useRef(count);\n  const loopingRef = useRef(looping);\n  const cloneCountRef = useRef(cloneCount);\n  const strideRef = useRef(stride);\n  const peekInsetRef = useRef(peekInset);\n  const lastCommittedRef = useRef(index);\n\n  indexRef.current = index;\n  countRef.current = count;\n  loopingRef.current = looping;\n  cloneCountRef.current = cloneCount;\n  strideRef.current = stride;\n  peekInsetRef.current = peekInset;\n\n  useLayoutEffect(() => {\n    if (stride === 0) {\n      return;\n    }\n\n    translateX.value = offsetForVisual(\n      getVisualIndex(indexRef.current, cloneCount),\n      stride,\n      peekInset\n    );\n  }, [cloneCount, peekInset, stride, translateX]);\n\n  useAnimatedReaction(\n    () => {\n      const currentStride = strideSV.value;\n      if (currentStride === 0) {\n        return 0;\n      }\n\n      let visual = visualForOffset(\n        translateX.value,\n        currentStride,\n        peekInsetSV.value\n      );\n      const clones = cloneCountSV.value;\n      if (clones === 0) {\n        return visual;\n      }\n\n      const itemCount = countSV.value;\n      if (itemCount <= 0) {\n        return 0;\n      }\n\n      visual -= clones;\n      return ((visual % itemCount) + itemCount) % itemCount;\n    },\n    (value) => {\n      progress.value = value;\n    }\n  );\n\n  const animateToVisual = useCallback(\n    (visual: number, animated: boolean, onComplete: () => void) => {\n      const strideNow = strideRef.current;\n      const peekNow = peekInsetRef.current;\n      const dest = offsetForVisual(visual, strideNow, peekNow);\n\n      if (!animated || strideNow === 0) {\n        translateX.value = offsetForVisual(\n          wrapVisualIndex(visual, countRef.current, cloneCountRef.current),\n          strideNow,\n          peekNow\n        );\n        onComplete();\n        return;\n      }\n\n      const finish = (finished?: boolean) => {\n        \"worklet\";\n\n        if (!finished) {\n          return;\n        }\n\n        wrapSettledTranslation(\n          visual,\n          translateX,\n          strideSV.value,\n          countSV.value,\n          cloneCountSV.value,\n          peekInsetSV.value\n        );\n        scheduleOnRN(onComplete);\n      };\n\n      if (reduceMotionSV.value) {\n        translateX.value = withTiming(\n          dest,\n          { duration: SNAP_TIMING_MS },\n          finish\n        );\n        return;\n      }\n\n      translateX.value = withSpring(dest, SNAP_SPRING, finish);\n    },\n    [cloneCountSV, countSV, peekInsetSV, reduceMotionSV, strideSV, translateX]\n  );\n\n  const commitIndex = useCallback(\n    (nextIndex: number) => {\n      const next = clampIndex(nextIndex, countRef.current);\n      lastCommittedRef.current = next;\n\n      if (!isControlled) {\n        setInternalIndex(next);\n      }\n\n      if (next !== indexRef.current) {\n        onIndexChange?.(next);\n      }\n    },\n    [isControlled, onIndexChange]\n  );\n\n  const settleVisual = useCallback(\n    (visual: number, animated: boolean) => {\n      animateToVisual(visual, animated, () => {\n        commitIndex(\n          getRealIndex(visual, countRef.current, cloneCountRef.current)\n        );\n      });\n    },\n    [animateToVisual, commitIndex]\n  );\n\n  const scrollTo = useCallback(\n    (nextIndex: number, animated = true) => {\n      const next = clampIndex(nextIndex, countRef.current);\n      settleVisual(getVisualIndex(next, cloneCountRef.current), animated);\n    },\n    [settleVisual]\n  );\n\n  const scrollBy = useCallback(\n    (delta: number) => {\n      if (countRef.current <= 1) {\n        return;\n      }\n\n      if (loopingRef.current) {\n        settleVisual(\n          getVisualIndex(indexRef.current, cloneCountRef.current) + delta,\n          true\n        );\n        return;\n      }\n\n      const next = indexRef.current + delta;\n      if (next >= 0 && next < countRef.current) {\n        settleVisual(next, true);\n      }\n    },\n    [settleVisual]\n  );\n\n  const scrollNext = useCallback(() => {\n    scrollBy(1);\n  }, [scrollBy]);\n\n  const scrollPrev = useCallback(() => {\n    scrollBy(-1);\n  }, [scrollBy]);\n\n  const onPanStart = useCallback(() => {\n    setIsDragging(true);\n  }, []);\n\n  const onSettledFromGesture = useCallback(\n    (visual: number) => {\n      setIsDragging(false);\n      commitIndex(\n        getRealIndex(visual, countRef.current, cloneCountRef.current)\n      );\n    },\n    [commitIndex]\n  );\n\n  useEffect(() => {\n    if (indexProp === undefined || stride === 0) {\n      return;\n    }\n\n    if (indexProp === lastCommittedRef.current) {\n      return;\n    }\n\n    lastCommittedRef.current = indexProp;\n    settleVisual(getVisualIndex(indexProp, cloneCountRef.current), true);\n  }, [indexProp, settleVisual, stride]);\n\n  useEffect(() => {\n    if (count === 0) {\n      return;\n    }\n\n    AccessibilityInfo.announceForAccessibility(\n      `Slide ${index + 1} of ${count}`\n    );\n  }, [count, index]);\n\n  const autoplayEnabled =\n    autoplay &&\n    appIsActive &&\n    !reduceMotion &&\n    !isDragging &&\n    count > 1 &&\n    (looping || index < count - 1);\n\n  useEffect(() => {\n    if (!autoplayEnabled) {\n      return;\n    }\n\n    const timerId = setInterval(scrollNext, autoplayInterval);\n    return () => {\n      clearInterval(timerId);\n    };\n  }, [autoplayEnabled, autoplayInterval, scrollNext]);\n\n  useImperativeHandle(\n    ref,\n    () => ({\n      getIndex: () => indexRef.current,\n      scrollNext,\n      scrollPrev,\n      scrollTo,\n    }),\n    [scrollNext, scrollPrev, scrollTo]\n  );\n\n  const setContainerWidthIfChanged = useCallback((width: number) => {\n    setContainerWidth((current) => (current === width ? current : width));\n  }, []);\n\n  const ctx = useMemo(\n    () => ({\n      canScrollNext,\n      canScrollPrev,\n      cloneCount,\n      cloneCountSV,\n      count,\n      countSV,\n      dragStartX,\n      gap,\n      index,\n      itemWidth,\n      looping,\n      loopingSV,\n      onPanStart,\n      onSettledFromGesture,\n      peekInsetSV,\n      progress,\n      reduceMotion,\n      reduceMotionSV,\n      scrollNext,\n      scrollPrev,\n      scrollTo,\n      setContainerWidth: setContainerWidthIfChanged,\n      setCount,\n      strideSV,\n      translateX,\n    }),\n    [\n      canScrollNext,\n      canScrollPrev,\n      cloneCount,\n      cloneCountSV,\n      count,\n      countSV,\n      dragStartX,\n      gap,\n      index,\n      itemWidth,\n      looping,\n      loopingSV,\n      onPanStart,\n      onSettledFromGesture,\n      peekInsetSV,\n      progress,\n      reduceMotion,\n      reduceMotionSV,\n      scrollNext,\n      scrollPrev,\n      scrollTo,\n      setContainerWidthIfChanged,\n      strideSV,\n      translateX,\n    ]\n  );\n\n  return (\n    <CarouselContext.Provider value={ctx}>\n      <View\n        className={cn(\"flex w-full flex-col gap-3\", className)}\n        data-slot=\"carousel\"\n        data-variant={variant}\n        {...props}\n      >\n        {children}\n      </View>\n    </CarouselContext.Provider>\n  );\n};\n\nexport const CarouselContent = ({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<typeof View>) => {\n  const {\n    cloneCount,\n    cloneCountSV,\n    count,\n    countSV,\n    dragStartX,\n    index,\n    loopingSV,\n    onPanStart,\n    onSettledFromGesture,\n    peekInsetSV,\n    reduceMotionSV,\n    scrollNext,\n    scrollPrev,\n    setContainerWidth,\n    setCount,\n    strideSV,\n    translateX,\n  } = useCarouselContext();\n\n  const items = useMemo(() => getCarouselItems(children), [children]);\n\n  useEffect(() => {\n    setCount(items.length);\n  }, [items.length, setCount]);\n\n  const trackItems = useMemo(\n    () => getLoopedTrackItems(items, cloneCount),\n    [cloneCount, items]\n  );\n\n  const onLayout = useCallback(\n    (event: LayoutChangeEvent) => {\n      setContainerWidth(event.nativeEvent.layout.width);\n    },\n    [setContainerWidth]\n  );\n\n  const trackStyle = useAnimatedStyle(() => ({\n    transform: [{ translateX: translateX.value }],\n  }));\n\n  const snapEpoch = useSharedValue(0);\n\n  const pan = usePanGesture({\n    activeOffsetX: [-GESTURE_ACTIVE_OFFSET_X, GESTURE_ACTIVE_OFFSET_X],\n    enabled: count > 1,\n    failOffsetY: [-GESTURE_FAIL_OFFSET_Y, GESTURE_FAIL_OFFSET_Y],\n    onActivate: () => {\n      \"worklet\";\n      snapEpoch.value += 1;\n      cancelAnimation(translateX);\n      dragStartX.value = translateX.value;\n      scheduleOnRN(onPanStart);\n    },\n    onDeactivate: (event) => {\n      \"worklet\";\n      snapEpoch.value += 1;\n      const epoch = snapEpoch.value;\n      const stride = strideSV.value;\n      const itemCount = countSV.value;\n      const clones = cloneCountSV.value;\n      const peekInset = peekInsetSV.value;\n      const target = getSnapVisual(\n        translateX.value,\n        event.canceled ? 0 : event.velocityX,\n        stride,\n        itemCount,\n        clones,\n        peekInset\n      );\n\n      const finish = (finished?: boolean) => {\n        \"worklet\";\n\n        if (!(finished && snapEpoch.value === epoch)) {\n          return;\n        }\n\n        scheduleOnRN(\n          onSettledFromGesture,\n          wrapSettledTranslation(\n            target,\n            translateX,\n            stride,\n            itemCount,\n            clones,\n            peekInset\n          )\n        );\n      };\n\n      if (stride === 0) {\n        finish(true);\n        return;\n      }\n\n      const dest = offsetForVisual(target, stride, peekInset);\n\n      if (reduceMotionSV.value) {\n        translateX.value = withTiming(\n          dest,\n          { duration: SNAP_TIMING_MS },\n          finish\n        );\n        return;\n      }\n\n      translateX.value = withSpring(dest, SNAP_SPRING, finish);\n    },\n    onUpdate: (event) => {\n      \"worklet\";\n      const next = rubberBandTranslate(\n        dragStartX.value + event.translationX,\n        strideSV.value,\n        countSV.value,\n        loopingSV.value,\n        peekInsetSV.value\n      );\n      const wrapped = wrapLoopTranslation(\n        next,\n        strideSV.value,\n        countSV.value,\n        cloneCountSV.value,\n        peekInsetSV.value\n      );\n      dragStartX.value += wrapped.shift;\n      translateX.value = wrapped.offset;\n    },\n  });\n\n  return (\n    <View\n      accessibilityActions={[{ name: \"decrement\" }, { name: \"increment\" }]}\n      accessibilityRole=\"adjustable\"\n      accessibilityValue={{\n        max: Math.max(count, 1),\n        min: 1,\n        now: index + 1,\n      }}\n      className={cn(\"min-h-0 w-full flex-1 overflow-hidden\", className)}\n      data-slot=\"carousel-content\"\n      onAccessibilityAction={(event) => {\n        if (event.nativeEvent.actionName === \"increment\") {\n          scrollNext();\n          return;\n        }\n\n        if (event.nativeEvent.actionName === \"decrement\") {\n          scrollPrev();\n        }\n      }}\n      onLayout={onLayout}\n      {...props}\n    >\n      <GestureDetector gesture={pan}>\n        <Animated.View className=\"h-full w-full\">\n          <Animated.View className=\"h-full flex-row\" style={trackStyle}>\n            {trackItems}\n          </Animated.View>\n        </Animated.View>\n      </GestureDetector>\n    </View>\n  );\n};\n\nexport const CarouselItem = ({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<typeof View>) => {\n  const { gap, itemWidth } = useCarouselContext();\n\n  return (\n    <View\n      className={cn(\"h-full shrink-0\", className)}\n      data-slot=\"carousel-item\"\n      style={{ marginEnd: gap, width: itemWidth }}\n      {...props}\n    >\n      {children}\n    </View>\n  );\n};\n\nexport const CarouselPrevious = ({\n  accessibilityLabel = \"Previous\",\n  disabled,\n  variant = \"outline\",\n  size = \"icon\",\n  ...props\n}: Omit<React.ComponentProps<typeof Button>, \"children\">) => {\n  const { canScrollPrev, scrollPrev } = useCarouselContext();\n\n  return (\n    <Button\n      accessibilityLabel={accessibilityLabel}\n      disabled={disabled ?? !canScrollPrev}\n      onPress={scrollPrev}\n      size={size}\n      variant={variant}\n      {...props}\n    >\n      <ButtonIcon>\n        <ChevronLeftIcon />\n      </ButtonIcon>\n    </Button>\n  );\n};\n\nexport const CarouselNext = ({\n  accessibilityLabel = \"Next\",\n  disabled,\n  variant = \"outline\",\n  size = \"icon\",\n  ...props\n}: Omit<React.ComponentProps<typeof Button>, \"children\">) => {\n  const { canScrollNext, scrollNext } = useCarouselContext();\n\n  return (\n    <Button\n      accessibilityLabel={accessibilityLabel}\n      disabled={disabled ?? !canScrollNext}\n      onPress={scrollNext}\n      size={size}\n      variant={variant}\n      {...props}\n    >\n      <ButtonIcon>\n        <ChevronRightIcon />\n      </ButtonIcon>\n    </Button>\n  );\n};\n\nconst CarouselDot = ({ position }: { position: number }) => {\n  const { count, index, progress, reduceMotion, scrollTo } =\n    useCarouselContext();\n\n  const dotStyle = useAnimatedStyle(() => {\n    if (reduceMotion) {\n      const isActive = index === position;\n      return {\n        opacity: isActive ? 1 : 0.3,\n        width: isActive ? DOT_SIZE + DOT_GROW : DOT_SIZE,\n      };\n    }\n\n    const distance = circularDistance(progress.value, position, count);\n    const influence = interpolate(\n      distance,\n      [0, 1],\n      [1, 0],\n      Extrapolation.CLAMP\n    );\n\n    return {\n      opacity: 0.3 + 0.7 * influence,\n      width: DOT_SIZE + DOT_GROW * influence,\n    };\n  });\n\n  return (\n    <Pressable\n      accessibilityLabel={`Go to slide ${position + 1}`}\n      accessibilityRole=\"button\"\n      accessibilityState={{ selected: index === position }}\n      className=\"items-center justify-center p-1\"\n      onPress={() => {\n        scrollTo(position);\n      }}\n    >\n      <Animated.View\n        className=\"bg-foreground h-2 rounded-full\"\n        style={dotStyle}\n      />\n    </Pressable>\n  );\n};\n\nexport const CarouselDots = ({\n  className,\n  ...props\n}: React.ComponentProps<typeof View>) => {\n  const { count, index } = useCarouselContext();\n\n  const dots = useMemo(\n    () =>\n      Array.from({ length: count }, (_, position) => ({\n        id: `slide-${position}`,\n        position,\n      })),\n    [count]\n  );\n\n  return (\n    <View\n      accessibilityLabel={`Slide ${index + 1} of ${count}`}\n      className={cn(\"flex-row items-center justify-center\", className)}\n      data-slot=\"carousel-dots\"\n      {...props}\n    >\n      {dots.map((dot) => (\n        <CarouselDot key={dot.id} position={dot.position} />\n      ))}\n    </View>\n  );\n};\n",
      "type": "registry:ui",
      "target": "@ui/carousel.tsx"
    }
  ],
  "type": "registry:ui"
}
