import React from 'react'
import {
  View,
  Animated,
  PanResponder,
  StyleSheet,
  Platform,
  Dimensions,
  Easing
} from 'react-native'
import PropTypes from 'prop-types'
import { useThemeTokens } from '../ThemeProvider/useThemeTokens'
import { useTheme } from '../ThemeProvider/useTheme'
import { useViewport } from '../ViewportProvider/useViewport'
import { getTokensPropType } from '../utils/props/tokens'
import { getA11yPropsFromHtmlTag, layoutTags } from '../utils/a11y/semantics'
import { variantProp } from '../utils/props/variantProp'
import { selectSystemProps } from '../utils/props/selectSystemProps'
import { a11yProps } from '../utils/props/a11yProps'
import { viewProps } from '../utils/props/viewProps'
import { useCopy } from '../utils/useCopy'
import { unpackFragment } from '../utils/children'
import { isTouchDevice } from '../utils/isTouchDevice'
import { useResponsiveProp } from '../utils/useResponsiveProp'
import { resolveContentMaxWidth } from '../utils/resolveContentMaxWidth'
import { useA11yInfo } from '../A11yInfoProvider/A11yInfoProvider'
import { CarouselProvider } from './CarouselContext'
import { IconButton } from '../IconButton/IconButton'
import { SkipLink } from '../SkipLink/SkipLink'
import { A11yText } from '../A11yText/A11yText'
import { CarouselStepTracker } from './CarouselStepTracker/CarouselStepTracker'
import { CarouselThumbnailNavigation } from './CarouselThumbnailNavigation'
import { CarouselTabsPanel } from './CarouselTabs/CarouselTabsPanel'
import { CarouselTabsPanelItem } from './CarouselTabs/CarouselTabsPanelItem'
import { dictionary } from './dictionary'
import { Box } from '../Box/Box'
import {
  ITEMS_PER_VIEWPORT_XS_SM,
  ITEMS_PER_VIEWPORT_MD,
  ITEMS_PER_VIEWPORT_LG_XL,
  DEFAULT_POSITION_OFFSET,
  LARGE_VIEWPORT_MARGIN,
  DEFAULT_VIEWPORT_MARGIN,
  PEEKING_MULTIPLIER,
  NEGATIVE_MULTIPLIER,
  TRANSITION_MODES,
  SWIPE_RELEASE_STYLES,
  INSTANT_ANIMATION_DURATION,
  DEFAULT_SWIPE_RELEASE_DURATION,
  POSITION_VARIANTS,
  POSITION_PROPERTIES
} from './Constants'

const staticStyles = StyleSheet.create({
  root: {
    backgroundColor: 'transparent',
    justifyContent: 'center',
    alignItems: 'center',
    position: 'relative',
    top: 0,
    left: 0
  },
  animationControlButton: {
    position: 'absolute',
    zIndex: 1,
    right: Platform.OS === 'web' ? undefined : 40,
    top: 40
  },
  heroMainContainer: {
    position: 'absolute',
    top: 0,
    left: 0,
    right: 0,
    bottom: 0,
    overflow: 'hidden',
    zIndex: -1
  },
  heroSwipeArea: {
    position: 'absolute',
    top: 0,
    left: 0,
    right: 0,
    bottom: 0,
    zIndex: -1,
    flexDirection: 'row'
  }
})

const selectContainerStyles = (width) => ({
  backgroundColor: 'transparent',
  overflow: 'hidden',
  width
})

const selectSwipeAreaStyles = (
  count,
  width,
  enablePeeking,
  enableDisplayMultipleItemsPerSlide
) => ({
  width: width * count,
  justifyContent:
    enablePeeking || enableDisplayMultipleItemsPerSlide ? 'flex-start' : 'space-between',
  flexDirection: 'row'
})

const selectHeroContainerStyles = (width, hidden) => ({
  width,
  visibility: hidden ? 'hidden' : 'visible'
})

const getDynamicPositionProperty = (areStylesAppliedOnPreviousButton) =>
  areStylesAppliedOnPreviousButton ? POSITION_PROPERTIES.LEFT : POSITION_PROPERTIES.RIGHT

const selectControlButtonPositionStyles = ({
  positionVariant,
  buttonWidth,
  positionProperty = getDynamicPositionProperty(),
  spaceBetweenSlideAndButton,
  enablePeeking,
  enableDisplayMultipleItemsPerSlide,
  isAutoPlayEnabled,
  viewport,
  maxWidth,
  containerWidth
}) => {
  const styles = {}

  let positionOffset = 0

  if (positionVariant === POSITION_VARIANTS.EDGE) {
    positionOffset = -1 * (buttonWidth / 2)
  } else if (positionVariant === POSITION_VARIANTS.INSIDE) {
    positionOffset = DEFAULT_POSITION_OFFSET
  } else if (positionVariant === POSITION_VARIANTS.OUTSIDE) {
    if (
      enablePeeking ||
      enableDisplayMultipleItemsPerSlide ||
      (isAutoPlayEnabled && viewport === 'xs')
    ) {
      positionOffset = 0
    } else {
      positionOffset = -1 * (spaceBetweenSlideAndButton + buttonWidth)
    }
  }

  if (enablePeeking) {
    const { peekingMiddleSpace, peekingGap } = getPeekingProps(viewport)
    const clampedMaxWidth = Math.min(maxWidth || containerWidth, containerWidth)
    const slideRightEdge = (containerWidth + clampedMaxWidth) / 2 - peekingMiddleSpace
    const buttonCenter = slideRightEdge + peekingGap / 2
    positionOffset = containerWidth - buttonCenter - buttonWidth / 2
  }

  styles[positionProperty] = positionOffset
  return styles
}

const selectPreviousNextNavigationButtonStyles = (
  previousNextNavigationButtonWidth,
  previousNextNavigationPosition,
  spaceBetweenSlideAndPreviousNextNavigation,
  isFirstSlide,
  isLastSlide,
  areStylesAppliedOnPreviousButton,
  enablePeeking,
  enableDisplayMultipleItemsPerSlide,
  isAutoPlayEnabled,
  viewport,
  maxWidth,
  containerWidth
) => {
  const styles = {
    zIndex: 1,
    position: 'absolute'
  }
  if (isFirstSlide) {
    styles.visibility = areStylesAppliedOnPreviousButton ? 'hidden' : 'visible'
  } else if (isLastSlide) {
    styles.visibility = areStylesAppliedOnPreviousButton ? 'visible' : 'hidden'
  } else {
    styles.visibility = 'visible'
  }

  return {
    ...styles,
    ...selectControlButtonPositionStyles({
      positionVariant: previousNextNavigationPosition,
      buttonWidth: previousNextNavigationButtonWidth,
      positionProperty: getDynamicPositionProperty(areStylesAppliedOnPreviousButton),
      spaceBetweenSlideAndButton: spaceBetweenSlideAndPreviousNextNavigation,
      enablePeeking,
      enableDisplayMultipleItemsPerSlide,
      isAutoPlayEnabled,
      viewport,
      maxWidth,
      containerWidth
    })
  }
}

const getPeekingProps = (viewport) => {
  if (viewport === 'xs' || viewport === 'sm' || viewport === 'md') {
    return {
      peekingFirstSpace: 48,
      peekingGap: 16,
      peekingLastSpace: 48,
      peekingMarginLeft: 24,
      peekingMarginRight: 24,
      peekingMiddleSpace: 28
    }
  }
  return {
    peekingFirstSpace: 48,
    peekingGap: 16,
    peekingLastSpace: 48,
    peekingMarginLeft: 16,
    peekingMarginRight: 16,
    peekingMiddleSpace: 24
  }
}

const getTotalItems = (enableDisplayMultipleItemsPerSlide, childrenArray, viewport) => {
  if (enableDisplayMultipleItemsPerSlide) {
    switch (viewport) {
      case 'xs':
      case 'sm':
        return childrenArray.length
      case 'md':
        return Math.ceil(childrenArray.length / ITEMS_PER_VIEWPORT_MD)
      case 'lg':
      case 'xl':
        return Math.ceil(childrenArray.length / ITEMS_PER_VIEWPORT_LG_XL)
      default:
        break
    }
  }
  return childrenArray.length
}

/**
 * Determines the maximum number of items that can be displayed per slide based on viewport
 *
 * @param {boolean} enableDisplayMultipleItemsPerSlide - Flag indicating whether multiple items per slide is enabled
 * @param {string} viewport - The current viewport size ('xs', 'sm', 'md', 'lg', 'xl')
 * @returns {number} The maximum number of items that can be displayed per slide
 */
const getMaximumItemsForSlide = (enableDisplayMultipleItemsPerSlide, viewport) => {
  if (enableDisplayMultipleItemsPerSlide) {
    switch (viewport) {
      case 'xs':
      case 'sm':
        return ITEMS_PER_VIEWPORT_XS_SM
      case 'md':
        return ITEMS_PER_VIEWPORT_MD
      case 'lg':
      case 'xl':
        return ITEMS_PER_VIEWPORT_LG_XL
      default:
        return ITEMS_PER_VIEWPORT_XS_SM
    }
  }
  return ITEMS_PER_VIEWPORT_XS_SM
}

const selectRootContainerStyles = (enableHero, viewport, enablePeeking) => {
  if (enableHero && viewport === 'xl' && Platform.OS === 'web') {
    return {
      alignItems: 'center'
    }
  }
  if (enableHero) {
    return {
      paddingHorizontal: 16
    }
  }
  if (enablePeeking) {
    return {
      width: '100%'
    }
  }
  return {}
}

const selectMainContainerStyles = (enableHero, viewport, maxWidth, enablePeeking) => {
  if (enableHero && viewport === 'xl' && Platform.OS === 'web' && !enablePeeking) {
    return {
      width: '100%',
      maxWidth: maxWidth || 1200
    }
  }
  if (enablePeeking) {
    return {
      width: '100%'
    }
  }
  if (maxWidth !== null && maxWidth !== undefined) {
    return {
      maxWidth,
      alignSelf: 'center',
      width: '100%'
    }
  }
  return {}
}

const selectNavigationStyles = (tabs, enableHero, viewport, enablePeeking, maxWidth) => {
  let marginHorizontal = 0

  if (enableHero && tabs) {
    marginHorizontal = viewport === 'xl' ? LARGE_VIEWPORT_MARGIN : DEFAULT_VIEWPORT_MARGIN
  }

  const styles = {
    marginHorizontal
  }

  if (enablePeeking && maxWidth) {
    styles.maxWidth = maxWidth
    styles.alignSelf = 'center'
    styles.width = '100%'
  }

  return styles
}

/**
 * Calculates the final width of a carousel container based on the provided parameters.
 *
 * @param {number} containerWidth - The width of the carousel container.
 * @param {boolean} enablePeeking - Flag indicating whether peeking is enabled.
 * @param {Object} viewport - The viewport configuration object used to determine peeking properties.
 * @param {number} maxWidth - The maximum width constraint for the carousel content.
 * @returns {number} The calculated final width of the carousel container.
 */
const calculateFinalWidth = (containerWidth, enablePeeking, viewport, maxWidth) => {
  let finalWidth = containerWidth

  if (enablePeeking) {
    const { peekingGap, peekingMiddleSpace } = getPeekingProps(viewport)
    const baseWidth = Math.min(maxWidth || containerWidth, containerWidth)
    finalWidth = baseWidth - peekingMiddleSpace * PEEKING_MULTIPLIER + peekingGap
  }

  return finalWidth
}

const selectIconStyles = ({ iconBackgroundColor }) => ({ backgroundColor: iconBackgroundColor })

const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, viewProps])

/**
 * Carousel is a general-purpose content slider that can be used to render content in terms of slides.

  ## Usage
  - `Carousel` is a top-level export from `@telus-uds/components-base` which is used to render a Carousel
  - Immediately within `Carousel`, individual slides are wrapped in `CarouselItem` for the top-level `Carousel` to know how to identify an individual slide
  - You can use any UDS component or other platform-specific component, (based on the platform you're rendering) to achieve any desired layout
  - By default, Carousel takea all the `width` available to it and the `height` is determined based on the content in the slide with more content
  - You may want to wrap Carousel in other layout components like `Box`, `FlexGrid` etc, to achieve a responsive layout of your need

  ## `useCarousel` custom hook

  ```jsx
  import { useCarousel } from '@telus-uds/components-base'

  const SomeComponentWithinCarouselItem = () => {
    const {
      activeIndex,
      totalItems,
      width,
      goTo
    } = useCarousel()
    return <Text>Hi!</Text>
  }
  ```

  You can use `useCarousel` to hook into internal state of the Carousel component like:
  - `activeIndex`: Index of the current slide
  - `totalItems`: Total number of items/slides passed to the Carousel
  - `width`: Width of the individual carousel slide
  - `goTo`: A function to go to a particular slide by passing the index of that slide, e.g: goTo(0) where `0` is the index of the first slide

  ## Accessibility

  - Top-level `Carousel` and `CarouselItem` can take all possible React Native's `View` and `a11y` props
  - If your slide contains input elements like buttons, you may want to configure them to be only focusable when `activeIndex` is equal to the current slide index in order to avoid tabbing going between slides

  ## Platform considerations
  The component is available on both native platforms and web.

  ## Other considerations
  - You may want to use the same kind of layout in all your slides to avoid visual and height differences
  - `previous` and `next` navigation buttons are automatically removed in `sm` and `xs` viewports, as these smaller viewports offers swipe functionality

  ## Tokens

  You can override the following tokens in exceptional circumstances:
  - `previousIcon` - Icon of the previous button
  - `nextIcon` - Icon of the next button
  - `showPreviousNextNavigation` - If you want to show/hide the previous/next navigation
  - `showPanelNavigation` - If you want to show/hide the panel navigation
  - `spaceBetweenSlideAndPreviousNextNavigation` - Horizontal space between slide and previous/next navigational buttons
  - `spaceBetweenSlideAndPanelNavigation` - Vertical space between slide area and panel navigation area
 */
export const Carousel = React.forwardRef(
  (
    {
      tokens,
      variant,
      children,
      itemLabel = 'item',
      previousNextNavigationPosition = 'inside',
      stepTrackerVariant,
      progressBarVariant = { style: 'subtle' },
      previousNextIconSize = 'default',
      minDistanceToCapture = 5,
      minDistanceForAction = 0.2,
      onAnimationStart,
      onAnimationEnd,
      onIndexChanged,
      skipLinkHref,
      tabs,
      refocus = Boolean(tabs),
      title = 'carousel',
      springConfig = undefined,
      thumbnails = undefined,
      enableDisplayMultipleItemsPerSlide = false,
      enableHero = false,
      panelNavigation = thumbnails ? (
        <CarouselThumbnailNavigation thumbnails={thumbnails} />
      ) : (
        <CarouselStepTracker
          stepTrackerVariant={stepTrackerVariant}
          progressBarVariant={progressBarVariant}
          enableDisplayMultipleItemsPerSlide={enableDisplayMultipleItemsPerSlide}
        />
      ),
      tag = 'ul',
      accessibilityRole,
      accessibilityLabel,
      accessibilityLiveRegion = 'polite',
      copy,
      slideDuration = 0,
      transitionDuration = 0,
      loopDuration = transitionDuration,
      autoPlay = false,
      enablePeeking = false,
      contentMaxWidth,
      swipeReleaseStyle = SWIPE_RELEASE_STYLES.INSTANT,
      swipeReleaseDuration = DEFAULT_SWIPE_RELEASE_DURATION,
      ...rest
    },
    ref
  ) => {
    let childrenArray = unpackFragment(children)
    const isTransitioningRef = React.useRef(false)
    const viewport = useViewport()
    const { themeOptions } = useTheme()

    const contentMaxWidthValue = useResponsiveProp(contentMaxWidth)
    const responsiveWidth = useResponsiveProp(themeOptions?.contentMaxWidth)

    let maxWidth = null
    if (enablePeeking || contentMaxWidth !== undefined) {
      maxWidth =
        contentMaxWidthValue === undefined
          ? responsiveWidth
          : resolveContentMaxWidth(contentMaxWidthValue, responsiveWidth)
    }

    const totalItems = getTotalItems(enableDisplayMultipleItemsPerSlide, childrenArray, viewport)
    const autoPlayFeatureEnabled =
      autoPlay && slideDuration > 0 && transitionDuration > 0 && totalItems > 1
    // if `Carousel` only has one `CarouselItem`, convert this to a single-item array
    if (!Array.isArray(childrenArray)) {
      childrenArray = [childrenArray]
    }
    const getCopy = useCopy({ dictionary, copy })

    const themeTokens = useThemeTokens('Carousel', tokens, variant, {
      viewport
    })
    const {
      previousIcon,
      nextIcon,
      playIcon,
      pauseIcon,
      showPreviousNextNavigation,
      showPanelNavigation,
      showPanelTabs,
      spaceBetweenSlideAndPreviousNextNavigation
    } = themeTokens
    const [activeIndex, setActiveIndex] = React.useState(0)
    const activeIndexRef = React.useRef(activeIndex)
    const { reduceMotionEnabled } = useA11yInfo()
    const reduceMotionRef = React.useRef(reduceMotionEnabled)
    const [containerLayout, setContainerLayout] = React.useState({
      x: 0,
      y: 0,
      width: 0,
      height: 0
    })
    const containerLayoutRef = React.useRef(containerLayout)
    const [heroContainerLayout, setHeroContainerLayout] = React.useState({
      x: 0,
      y: 0,
      width: 0,
      height: 0
    })
    const heroContainerLayoutRef = React.useRef(heroContainerLayout)

    const [previousNextNavigationButtonWidth, setPreviousNextNavigationButtonWidth] =
      React.useState(0)
    const firstFocusRef = React.useRef(null)
    const pan = React.useRef(new Animated.ValueXY()).current
    const animatedX = React.useRef(0)
    const animatedY = React.useRef(0)
    const heroPan = React.useRef(new Animated.ValueXY()).current
    const heroAnimatedX = React.useRef(0)
    const heroAnimatedY = React.useRef(0)
    const [isAnimating, setIsAnimating] = React.useState(false)
    /**
     * While having the same starting point, `isAutoPlayEnabled` and `isCarouselPlaying` are different states
     *
     * `isAutoPlayEnabled` is a state to determine if the autoplay feature is enabled or disabled
     * `isCarouselPlaying` is a state to determine if the carousel is currently playing or paused
     */
    const [isAutoPlayEnabled, setIsAutoPlayEnabled] = React.useState(autoPlayFeatureEnabled)
    const [isCarouselPlaying, setisCarouselPlaying] = React.useState(autoPlayFeatureEnabled)
    const isSwiping = React.useRef(false)
    const autoPlayRef = React.useRef(null)

    const [rootContainerLayout, setRootContainerLayout] = React.useState({
      x: 0,
      y: 0,
      width: 0,
      height: 0
    })
    const rootContainerLayoutRef = React.useRef(rootContainerLayout)

    const isFirstSlide = !activeIndex
    const isLastSlide = activeIndex + 1 >= totalItems
    const currentViewportWidth = rootContainerLayout.width

    const handleAnimationStart = React.useCallback(
      (...args) => {
        if (typeof onAnimationStart === 'function') onAnimationStart(...args)
        setIsAnimating(true)
      },
      [onAnimationStart]
    )
    const handleAnimationEnd = React.useCallback(
      (...args) => {
        const result = args[args.length - 1]
        if (result?.finished) {
          if (typeof onAnimationEnd === 'function') onAnimationEnd(...args)
          setIsAnimating(false)
          isTransitioningRef.current = false
        }
      },
      [onAnimationEnd, isTransitioningRef]
    )

    const updateOffset = React.useCallback(() => {
      if (enablePeeking) {
        const { peekingGap, peekingMiddleSpace } = getPeekingProps(viewport)

        let finalWidth
        const baseWidth = Math.min(
          maxWidth || containerLayoutRef.current.width,
          containerLayoutRef.current.width
        )
        const slideWide = baseWidth - peekingMiddleSpace * PEEKING_MULTIPLIER

        if (activeIndexRef.current === 0) {
          finalWidth = 0
        } else {
          finalWidth = (slideWide + peekingGap) * activeIndexRef.current
        }

        animatedX.current = finalWidth * NEGATIVE_MULTIPLIER
      } else {
        animatedX.current =
          containerLayoutRef.current.width * activeIndexRef.current * NEGATIVE_MULTIPLIER
      }

      animatedY.current = 0
      pan.setOffset({
        x: animatedX.current,
        y: animatedY.current
      })
      pan.setValue({ x: 0, y: 0 })

      if (enableHero) {
        heroAnimatedX.current =
          heroContainerLayoutRef.current.width * activeIndexRef.current * NEGATIVE_MULTIPLIER
        heroAnimatedY.current = 0
        heroPan.setOffset({
          x: heroAnimatedX.current,
          y: heroAnimatedY.current
        })
        heroPan.setValue({ x: 0, y: 0 })
      }
    }, [pan, animatedX, heroPan, heroAnimatedX, enableHero, viewport, enablePeeking, maxWidth])

    const animate = React.useCallback(
      (panToAnimate, toValue, toIndex, isSwipeRelease = false) => {
        const applicableTransitionDuration =
          isLastSlide && toIndex === 0 ? loopDuration : transitionDuration
        const handleAnimationEndToIndex = (...args) => handleAnimationEnd(toIndex, ...args)
        if (reduceMotionRef.current) {
          Animated.timing(panToAnimate, {
            toValue,
            duration: INSTANT_ANIMATION_DURATION,
            useNativeDriver: false
          }).start(handleAnimationEndToIndex)
        } else if (isSwipeRelease && swipeReleaseStyle === SWIPE_RELEASE_STYLES.EASE_OUT) {
          Animated.timing(panToAnimate, {
            toValue,
            duration: swipeReleaseDuration,
            easing: Easing.out(Easing.cubic),
            useNativeDriver: false
          }).start(handleAnimationEndToIndex)
        } else if (isSwiping.current || isSwipeRelease) {
          Animated.timing(panToAnimate, {
            toValue,
            duration: INSTANT_ANIMATION_DURATION,
            useNativeDriver: false
          }).start(handleAnimationEndToIndex)
        } else if (isAutoPlayEnabled) {
          Animated.timing(panToAnimate, {
            ...springConfig,
            toValue,
            useNativeDriver: false,
            duration: applicableTransitionDuration * 1000
          }).start(handleAnimationEndToIndex)
        } else if (enablePeeking || enableDisplayMultipleItemsPerSlide) {
          Animated.timing(panToAnimate, {
            ...springConfig,
            toValue,
            useNativeDriver: false,
            duration: applicableTransitionDuration ? applicableTransitionDuration * 1000 : 1000
          }).start(handleAnimationEndToIndex)
        } else {
          Animated.spring(panToAnimate, {
            ...springConfig,
            toValue,
            useNativeDriver: false
          }).start(handleAnimationEndToIndex)
        }
      },
      [
        springConfig,
        handleAnimationEnd,
        transitionDuration,
        loopDuration,
        isLastSlide,
        isAutoPlayEnabled,
        enablePeeking,
        enableDisplayMultipleItemsPerSlide,
        swipeReleaseStyle,
        swipeReleaseDuration
      ]
    )

    const stopAutoplay = React.useCallback(() => {
      if (autoPlayRef?.current) {
        clearTimeout(autoPlayRef?.current)
      }
    }, [])

    const triggerRefocus = React.useCallback(() => {
      if (refocus && Platform.OS === 'web') {
        firstFocusRef.current?.focus({ preventScroll: true, focusVisible: false })
      }
    }, [refocus, firstFocusRef])

    const updateIndex = React.useCallback(
      (delta = 1, transitionMode) => {
        const toValue = { x: 0, y: 0 }
        let skipChanges = !delta
        let calcDelta = delta
        const isSwipeRelease = transitionMode === TRANSITION_MODES.SWIPE
        if (activeIndexRef.current <= 0 && delta < 0) {
          skipChanges = transitionMode !== TRANSITION_MODES.AUTOMATIC
          calcDelta = totalItems + delta
        } else if (activeIndexRef.current + 1 >= totalItems && delta > 0) {
          skipChanges = transitionMode !== TRANSITION_MODES.AUTOMATIC
          calcDelta = -1 * activeIndexRef.current + delta - 1
        }

        const index = activeIndexRef.current + calcDelta
        if (skipChanges) {
          isTransitioningRef.current = true
          animate(pan, toValue, index, isSwipeRelease)

          if (enableHero) {
            animate(heroPan, toValue, index, isSwipeRelease)
          }
          return calcDelta
        }

        stopAutoplay()
        setActiveIndex(index)

        const finalWidth = calculateFinalWidth(
          containerLayoutRef.current.width,
          enablePeeking,
          viewport,
          maxWidth
        )

        toValue.x = finalWidth * -1 * calcDelta
        const heroToValue = { x: 0, y: 0 }
        heroToValue.x = heroContainerLayoutRef.current.width * -1 * calcDelta
        isTransitioningRef.current = true
        animate(pan, toValue, index, isSwipeRelease)
        if (enableHero) {
          animate(heroPan, heroToValue, index, isSwipeRelease)
        }
        if (isCarouselPlaying) {
          stopAutoplay()
          if (
            index === 0 &&
            activeIndexRef.current + 1 === totalItems &&
            transitionMode === TRANSITION_MODES.AUTOMATIC
          ) {
            setisCarouselPlaying(false)
          } else if (isAutoPlayEnabled) {
            autoPlayRef.current = setTimeout(() => {
              updateOffset()
              handleAnimationStart(activeIndexRef.current)
              updateIndex(slideDuration < 0 ? -1 : 1, TRANSITION_MODES.AUTOMATIC)
              triggerRefocus()
            }, Math.abs(slideDuration) * 1000)
          }
        }
        if (onIndexChanged) onIndexChanged(calcDelta, index)
        return calcDelta
      },
      [
        handleAnimationStart,
        triggerRefocus,
        slideDuration,
        updateOffset,
        animate,
        totalItems,
        onIndexChanged,
        isCarouselPlaying,
        stopAutoplay,
        isAutoPlayEnabled,
        viewport,
        enablePeeking,
        pan,
        heroPan,
        enableHero,
        maxWidth
      ]
    )

    const startAutoplay = React.useCallback(() => {
      stopAutoplay()
      if (isAutoPlayEnabled) {
        autoPlayRef.current = setTimeout(() => {
          updateOffset()
          handleAnimationStart(activeIndexRef.current)
          updateIndex(slideDuration < 0 ? -1 : 1, TRANSITION_MODES.AUTOMATIC)
          triggerRefocus()
        }, Math.abs(slideDuration) * 1000)
      }
    }, [
      handleAnimationStart,
      triggerRefocus,
      updateIndex,
      updateOffset,
      slideDuration,
      stopAutoplay,
      isAutoPlayEnabled
    ])

    const fixOffsetAndGo = React.useCallback(
      (delta, transitionMode) => {
        updateOffset()
        handleAnimationStart(activeIndexRef.current)
        updateIndex(delta, transitionMode)
        triggerRefocus()
      },
      [updateIndex, updateOffset, handleAnimationStart, triggerRefocus]
    )

    const goToNeighboring = React.useCallback(
      (toPrev = false, transitionMode = TRANSITION_MODES.MANUAL) => {
        fixOffsetAndGo(toPrev ? -1 : 1, transitionMode)
      },
      [fixOffsetAndGo]
    )

    React.useEffect(() => {
      activeIndexRef.current = activeIndex
    }, [activeIndex])

    React.useEffect(() => {
      reduceMotionRef.current = reduceMotionEnabled
    }, [reduceMotionEnabled])

    React.useEffect(() => {
      containerLayoutRef.current = containerLayout
    }, [containerLayout])

    React.useEffect(() => {
      heroContainerLayoutRef.current = heroContainerLayout
    }, [heroContainerLayout])

    React.useEffect(() => {
      rootContainerLayoutRef.current = rootContainerLayout
    }, [rootContainerLayout])

    React.useEffect(() => {
      pan.x.addListener(({ value }) => {
        animatedX.current = value
      })
      pan.y.addListener(({ value }) => {
        animatedY.current = value
      })
      if (enableHero) {
        heroPan.x.addListener(({ value }) => {
          heroAnimatedX.current = value
        })
        heroPan.y.addListener(({ value }) => {
          heroAnimatedY.current = value
        })
      }
      if (isCarouselPlaying) {
        startAutoplay()
      }
      return () => {
        stopAutoplay()
        pan.x.removeAllListeners()
        pan.y.removeAllListeners()
        if (enableHero) {
          heroPan.x.removeAllListeners()
          heroPan.y.removeAllListeners()
        }
      }
    }, [
      pan.x,
      pan.y,
      heroPan.x,
      heroPan.y,
      startAutoplay,
      stopAutoplay,
      isCarouselPlaying,
      enableHero
    ])

    React.useEffect(() => {
      const subscription = Dimensions.addEventListener('change', () => {
        updateOffset()
      })

      return () => {
        if (subscription.remove) {
          subscription.remove()
        } else {
          Dimensions.removeEventListener('change', updateOffset)
        }
      }
    }, [updateOffset])

    React.useEffect(() => {
      setIsAutoPlayEnabled(
        autoPlay && slideDuration > 0 && transitionDuration > 0 && totalItems > 1
      )
    }, [autoPlay, slideDuration, transitionDuration, totalItems])

    React.useEffect(() => {
      return () => {
        stopAutoplay()
      }
    }, [stopAutoplay])

    const onContainerLayout = ({
      nativeEvent: {
        layout: { x, y, width, height }
      }
    }) => setContainerLayout((prevState) => ({ ...prevState, x, y, width, height }))

    const onHeroContainerLayout = ({
      nativeEvent: {
        layout: { x, y, width, height }
      }
    }) => setHeroContainerLayout((prevState) => ({ ...prevState, x, y, width, height }))

    const onPreviousNextNavigationButtonLayout = ({
      nativeEvent: {
        layout: { width }
      }
    }) => setPreviousNextNavigationButtonWidth(width)

    const onRootContainerLayout = ({
      nativeEvent: {
        layout: { x, y, width, height }
      }
    }) => setRootContainerLayout((prevState) => ({ ...prevState, x, y, width, height }))

    const isSwipeAllowed = React.useCallback(() => {
      if (totalItems === 1) {
        return false
      }
      if (Platform.OS === 'web') {
        return !!(viewport === 'xs' || viewport === 'sm' || (viewport === 'md' && isTouchDevice()))
      }
      return true
    }, [viewport, totalItems])

    const panResponder = React.useMemo(
      () =>
        PanResponder.create({
          onPanResponderTerminationRequest: () => false,
          onMoveShouldSetResponderCapture: () => true,
          onMoveShouldSetPanResponderCapture: (_, gestureState) => {
            if (!isSwipeAllowed()) {
              return false
            }

            handleAnimationStart(activeIndexRef.current)

            const allow = Math.abs(gestureState.dx) > minDistanceToCapture

            if (allow) {
              isSwiping.current = true
              stopAutoplay()
            }

            return allow
          },
          onPanResponderGrant: () => {
            updateOffset()
          },
          onPanResponderMove: Animated.event([null, { dx: pan.x }], {
            useNativeDriver: false
          }),
          onPanResponderRelease: (_, gesture) => {
            if (isCarouselPlaying) {
              startAutoplay()
            }
            const correction = gesture.moveX - gesture.x0

            isSwiping.current = false

            if (Math.abs(correction) < containerLayoutRef.current.width * minDistanceForAction) {
              animate(pan, { x: 0, y: 0 }, activeIndexRef.current, true)
            } else {
              const delta = correction > 0 ? -1 : 1
              updateIndex(delta, TRANSITION_MODES.SWIPE)
            }
          }
        }),
      [
        pan,
        updateIndex,
        updateOffset,
        animate,
        isSwipeAllowed,
        minDistanceForAction,
        handleAnimationStart,
        minDistanceToCapture,
        startAutoplay,
        stopAutoplay,
        isCarouselPlaying
      ]
    )

    const heroPanResponder = React.useMemo(
      () =>
        PanResponder.create({
          onPanResponderTerminationRequest: () => false,
          onMoveShouldSetResponderCapture: () => true,
          onMoveShouldSetPanResponderCapture: (_, gestureState) => {
            if (!isSwipeAllowed()) {
              return false
            }

            handleAnimationStart(activeIndexRef.current)

            const allow = Math.abs(gestureState.dx) > minDistanceToCapture

            if (allow) {
              isSwiping.current = true
              stopAutoplay()
            }

            return allow
          },
          onPanResponderGrant: () => {
            updateOffset()
          },
          onPanResponderMove: Animated.event([null, { dx: heroPan.x }], {
            useNativeDriver: false
          }),
          onPanResponderRelease: (_, gesture) => {
            if (isCarouselPlaying) {
              startAutoplay()
            }
            const correction = gesture.moveX - gesture.x0

            isSwiping.current = false

            if (Math.abs(correction) < containerLayoutRef.current.width * minDistanceForAction) {
              animate(heroPan, { x: 0, y: 0 }, activeIndexRef.current, true)
            } else {
              const delta = correction > 0 ? -1 : 1
              updateIndex(delta, TRANSITION_MODES.SWIPE)
            }
          }
        }),
      [
        heroPan,
        updateIndex,
        updateOffset,
        animate,
        isSwipeAllowed,
        minDistanceForAction,
        handleAnimationStart,
        minDistanceToCapture,
        startAutoplay,
        stopAutoplay,
        isCarouselPlaying
      ]
    )

    const goToNext = React.useCallback(() => {
      goToNeighboring()
    }, [goToNeighboring])

    const goToPrev = React.useCallback(() => {
      goToNeighboring(true)
    }, [goToNeighboring])

    const goTo = React.useCallback(
      (index = 0) => {
        const delta = index - activeIndexRef.current
        if (delta) {
          fixOffsetAndGo(delta, TRANSITION_MODES.MANUAL)
        }
      },
      [fixOffsetAndGo]
    )

    // @TODO: - these are Allium-theme variants and won't have any effect in themes that don't implement them.
    // Normally we avoid setting variants of subcomponents, however this could be re-considered.
    // Related discussion - https://github.com/telus/universal-design-system/issues/1549
    const previousNextIconButtonVariants = {
      size: previousNextIconSize,
      raised: true
    }

    const getCopyWithPlaceholders = React.useCallback(
      (copyKey) => {
        const copyText = getCopy(copyKey)
          .replace(/%\{title\}/g, title)
          .replace(/%\{itemLabel\}/g, itemLabel)
          .replace(/%\{stepNumber\}/g, activeIndex + 1)
          .replace(/%\{stepCount\}/g, totalItems)

        // First word might be a lowercase placeholder: capitalize the first letter
        return `${copyText[0].toUpperCase()}${copyText.slice(1)}`
      },
      [activeIndex, itemLabel, getCopy, title, totalItems]
    )

    const activePanelNavigation =
      tabs && showPanelTabs ? <CarouselTabsPanel items={tabs} /> : panelNavigation

    const isFirstFocusContainer = Boolean(refocus && !skipLinkHref)
    const containerRef = (element) => {
      // Apply both firstFocusRef to the container
      firstFocusRef.current = element
      // Also apply forwarded ref if there is one (which could be a function ref)
      if (ref) {
        if (typeof ref === 'object') {
          // eslint-disable-next-line no-param-reassign
          ref.current = element
        } else if (typeof ref === 'function') {
          ref(element)
        }
      }
    }

    const systemProps = selectProps({
      ...rest,
      accessibilityRole,
      accessibilityLabel
    })

    // If container isn't used for focus, give it a label of title if none is passed in,
    // otherwise read the current position on focus
    const containerAccessibilityLabel =
      systemProps.accessibilityLabel ?? isFirstFocusContainer
        ? `${title ? `${title} ` : ''}${getCopyWithPlaceholders('stepTrackerLabel')}`
        : title
    const containerProps = {
      accessibilityLabel: containerAccessibilityLabel,
      // If used for focus, attach the ref and draw a focus box around the whole carousel
      ...(isFirstFocusContainer && { ref: containerRef, focusable: true })
    }

    const onAnimationControlButtonPress = React.useCallback(() => {
      if (isCarouselPlaying) {
        stopAutoplay()
      } else {
        startAutoplay()
      }
      setisCarouselPlaying((prevState) => !prevState)
    }, [isCarouselPlaying, stopAutoplay, startAutoplay])

    const handleKeyDown = React.useCallback(
      (event) => {
        if (isTransitioningRef.current && event.key === 'Tab') {
          event.preventDefault()
        }
      },
      [isTransitioningRef]
    )

    return (
      <View
        style={selectRootContainerStyles(enableHero, viewport, enablePeeking)}
        onLayout={onRootContainerLayout}
      >
        <View style={selectMainContainerStyles(enableHero, viewport, maxWidth, enablePeeking)}>
          <CarouselProvider
            activeIndex={activeIndex}
            goTo={goTo}
            getCopyWithPlaceholders={getCopyWithPlaceholders}
            itemLabel={itemLabel}
            totalItems={totalItems}
            themeTokens={themeTokens}
            firstFocusRef={firstFocusRef}
            refocus={refocus}
            width={containerLayout.width}
            maximumItemsForSlide={getMaximumItemsForSlide(
              enableDisplayMultipleItemsPerSlide,
              viewport
            )}
            maxWidth={maxWidth}
            viewportWidth={currentViewportWidth}
          >
            <View
              style={[
                staticStyles.root,
                {
                  ...(Platform.OS === 'web' ? { outline: 'none' } : {})
                }
              ]}
              onLayout={onContainerLayout}
              ref={ref}
              {...systemProps}
              {...containerProps}
              {...(Platform.OS === 'web' ? { onKeyDown: handleKeyDown } : {})}
            >
              {isAutoPlayEnabled ? (
                <View
                  style={[
                    staticStyles.animationControlButton,
                    selectControlButtonPositionStyles({
                      positionVariant: previousNextNavigationPosition,
                      buttonWidth: previousNextNavigationButtonWidth,
                      positionProperty: getDynamicPositionProperty(),
                      spaceBetweenSlideAndButton: spaceBetweenSlideAndPreviousNextNavigation,
                      enablePeeking,
                      enableDisplayMultipleItemsPerSlide,
                      isAutoPlayEnabled,
                      viewport,
                      maxWidth,
                      containerWidth: containerLayout.width
                    })
                  ]}
                >
                  <IconButton
                    icon={isCarouselPlaying ? pauseIcon : playIcon}
                    accessibilityLabel={
                      isCarouselPlaying ? getCopy('pauseButtonLabel') : getCopy('playButtonLabel')
                    }
                    variant={previousNextIconButtonVariants}
                    onPress={onAnimationControlButtonPress}
                  />
                </View>
              ) : null}
              {showPreviousNextNavigation && totalItems > 1 ? (
                <View
                  style={selectPreviousNextNavigationButtonStyles(
                    previousNextNavigationButtonWidth,
                    previousNextNavigationPosition,
                    spaceBetweenSlideAndPreviousNextNavigation,
                    isFirstSlide,
                    isLastSlide,
                    true,
                    enablePeeking,
                    enableDisplayMultipleItemsPerSlide,
                    isAutoPlayEnabled,
                    viewport,
                    maxWidth,
                    containerLayout.width
                  )}
                  testID="previous-button-container"
                >
                  <IconButton
                    onLayout={onPreviousNextNavigationButtonLayout}
                    icon={previousIcon}
                    onPress={goToPrev}
                    variant={previousNextIconButtonVariants}
                    accessibilityLabel={getCopyWithPlaceholders('iconButtonLabel').replace(
                      '%{targetStep}',
                      activeIndex
                    )}
                    tokens={selectIconStyles(themeTokens)}
                  />
                </View>
              ) : null}
              {Boolean(skipLinkHref) && (
                <SkipLink ref={firstFocusRef} href={skipLinkHref}>
                  {getCopyWithPlaceholders('skipLink')}
                </SkipLink>
              )}
              {!isFirstFocusContainer && (
                <A11yText
                  // Read the current slide position to screen readers on slide.
                  // If it's set to refocus and doesn't have a SkipLink to focus to, focus this.
                  accessibilityLiveRegion={!skipLinkHref && refocus ? undefined : 'polite'}
                  text={getCopyWithPlaceholders('stepTrackerLabel')}
                />
              )}
              <View style={selectContainerStyles(containerLayout.width)}>
                <Animated.View
                  style={StyleSheet.flatten([
                    selectSwipeAreaStyles(
                      totalItems,
                      containerLayout.width,
                      enablePeeking,
                      enableDisplayMultipleItemsPerSlide
                    ),
                    {
                      transform: [{ translateX: pan.x }, { translateY: pan.y }]
                    }
                  ])}
                  {...panResponder.panHandlers}
                  {...getA11yPropsFromHtmlTag(tag)}
                  // In iframes on Mac (e.g. in Storybook), this content may be misread or read twice.
                  // This is a known Voiceover bug: https://github.com/phetsims/a11y-research/issues/132
                  accessibilityLiveRegion={accessibilityLiveRegion}
                >
                  {childrenArray.map((element, index) => {
                    let hidden = !isAnimating && index !== activeIndex

                    if (enablePeeking && !isAnimating) {
                      hidden = false
                    } else if (
                      !enablePeeking &&
                      enableDisplayMultipleItemsPerSlide &&
                      !isAnimating
                    ) {
                      const maxItemsForSlide = getMaximumItemsForSlide(
                        enableDisplayMultipleItemsPerSlide,
                        viewport
                      )
                      if (
                        index >= activeIndex * maxItemsForSlide &&
                        index < activeIndex * maxItemsForSlide + maxItemsForSlide
                      ) {
                        hidden = false
                      }
                    }

                    const clonedElement = React.cloneElement(element, {
                      elementIndex: index,
                      hidden,
                      enablePeeking,
                      peekingProps: getPeekingProps(viewport),
                      enableDisplayMultipleItemsPerSlide,
                      viewport,
                      backgroundColor: element.props.backgroundColor
                    })
                    return <React.Fragment key={index.toFixed(2)}>{clonedElement}</React.Fragment>
                  })}
                </Animated.View>
              </View>
              {showPreviousNextNavigation && totalItems > 1 ? (
                <View
                  style={selectPreviousNextNavigationButtonStyles(
                    previousNextNavigationButtonWidth,
                    previousNextNavigationPosition,
                    spaceBetweenSlideAndPreviousNextNavigation,
                    isFirstSlide,
                    isLastSlide,
                    false,
                    enablePeeking,
                    enableDisplayMultipleItemsPerSlide,
                    isAutoPlayEnabled,
                    viewport,
                    maxWidth,
                    containerLayout.width
                  )}
                  testID="next-button-container"
                >
                  <IconButton
                    onLayout={onPreviousNextNavigationButtonLayout}
                    icon={nextIcon}
                    onPress={goToNext}
                    variant={previousNextIconButtonVariants}
                    accessibilityLabel={getCopyWithPlaceholders('iconButtonLabel').replace(
                      '%{targetStep}',
                      activeIndex + 2
                    )}
                    tokens={selectIconStyles(themeTokens)}
                  />
                </View>
              ) : null}
            </View>
            <View
              style={selectNavigationStyles(tabs, enableHero, viewport, enablePeeking, maxWidth)}
            >
              {showPanelNavigation ? activePanelNavigation : null}
            </View>
          </CarouselProvider>
        </View>
        {enableHero && (
          <View style={staticStyles.heroMainContainer}>
            <Animated.View
              style={StyleSheet.flatten([
                staticStyles.heroSwipeArea,
                {
                  transform: [{ translateX: heroPan.x }, { translateY: heroPan.y }]
                }
              ])}
              onLayout={onHeroContainerLayout}
              {...heroPanResponder.panHandlers}
              {...getA11yPropsFromHtmlTag(tag)}
              accessibilityLiveRegion={accessibilityLiveRegion}
            >
              {childrenArray.map((element, index) => {
                const hidden = !isAnimating && index !== activeIndex
                return (
                  <View
                    style={selectHeroContainerStyles(heroContainerLayout.width, hidden)}
                    key={index.toFixed(2)}
                  >
                    <Box flex={1} variant={{ background: element.props.backgroundColor }} />
                  </View>
                )
              })}
            </Animated.View>
          </View>
        )}
      </View>
    )
  }
)

Carousel.propTypes = {
  ...selectedSystemPropTypes,
  tokens: getTokensPropType('Carousel'),
  variant: variantProp.propType,
  /**
   * Prop related to StepTracker Variants
   */
  stepTrackerVariant: variantProp.propType,
  /**
   * Prop related to ProgressBar Variants
   */
  progressBarVariant: variantProp.propType,
  /**
   * Slides to render in Carousel. Wrap individual slides in `CarouselItem`
   */
  children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]).isRequired,
  /**
   * Lowercase language-appropriate user-facing description of what each Carousel slide represents.
   * This is used when generating item labels. For example, if a carousel contains offers,
   * pass itemLabel="summer offer" (or copy="fr" and an appropriate French translation) to genereate
   * accessible labels such as "Summer offer 1 of 3" and "Show summer offer 2 of 3".
   */
  itemLabel: PropTypes.string,
  /**
   * Lowercase language-appropriate name for the carousel itself, used when generating the
   * accessible labels of the carousel container and its navigation. Defaults to `carousel`.
   */
  title: PropTypes.string,
  /**
   * Renders the carousel at the full width of the viewport ("hero" mode).
   */
  enableHero: PropTypes.bool,
  /**
   * `inside` renders the previous and next buttons inside the slide
   * `outside` renders the previous and next buttons outside the slide
   * `edge` renders the previous and next buttons at the edge of the slide
   */
  previousNextNavigationPosition: PropTypes.oneOf(['inside', 'outside', 'edge']),
  /**
   * Defines the size of the `IconButton` which is being used to render next and previous buttons
   */
  previousNextIconSize: PropTypes.oneOf(['default', 'small', 'large']),
  /**
   * Carousel uses `Animated.spring` to animate slide changes, use this option to pass custom animation configuration
   */
  springConfig: PropTypes.object,
  /**
   * An array of objects containing information on the thumbnails to be rendered as navigation panel
   */
  thumbnails: PropTypes.arrayOf(
    PropTypes.shape({
      /** Accessibility label for the thumbnail image, used by assistive technologies. */
      accessibilityLabel: PropTypes.string,
      /** Alternative text for the thumbnail image, displayed when the image cannot be rendered. */
      alt: PropTypes.string,
      /** When true, renders a play icon overlay on the thumbnail to indicate that the slide contains a video. */
      isVideo: PropTypes.bool,
      /** URL or path of the thumbnail image. When used with `isVideo`, this should be the video's poster/preview image. */
      src: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
    })
  ),
  /**
   * Minimal part of slide width must be swiped for changing index.
   * Otherwise animation restore current slide. Default value 0.2 means that 20% must be swiped for change index
   */
  minDistanceForAction: PropTypes.number,
  /**
   * Initiate animation after swipe this distance.
   */
  minDistanceToCapture: PropTypes.number,
  /**
   * Called when active index changed
   * This function is also provided with a parameter indicating changed index (either 1, or -1)
   * Use it as follows:
   * ```js
   *    const onIndexChangedCallback = React.useCallback((changedIndex, currentActiveIndex) => {
   *      console.log(changedIndex)
   *    }, [])  // pass local dependencies as per your component
   *    <Carousel
   *      onIndexChanged={onIndexChangedCallback}
   *    >
   *      <CarouselItem>First Slide</CarouselItem>
   *    </Carousel>
   * ```
   * Caution: Always consider wrapping your callback for `onIndexChanged` in `React.useCallback` in order to avoid bugs and performance issues
   */
  onIndexChanged: PropTypes.func,
  /**
   * If this is a complex carousel with a lot of focusable content, pass a href for a skip link. Typically, this will be an anchor link
   * with the ID of a focusable element immediately after the Carousel, e.g. `'#section-2-heading'`.
   */
  skipLinkHref: PropTypes.string,
  /**
   * If provided, defaults the navigation panel to a CarouselTabsPanel element passing each item as props for one tab.
   *
   * Be careful to ensure that the order of each element in the items array matches the order of each child in the Carousel.
   */
  tabs: PropTypes.arrayOf(PropTypes.shape(CarouselTabsPanelItem.propTypes || {})),
  /**
   * If true, whenever a new slide comes into view, the focus of the Carousel switches to the start.
   *
   * Pass this as true when using carousel items that contain interactive content, so a user can easily tab into that content.
   *
   * If skipLinkHref is passed, the focus target will be the SkipLink; if not, it'll be an empty element before the slide content.
   */
  refocus: PropTypes.bool,
  /**
   * Use this to render a custom panel navigation element instead of the default StepTracker's based navigation
   * You can make use of `useCarousel` within your custom panel navigation component to hook into various Carousel states such as:
   * - activeIndex: index of current slide
   * - totalItems: total number of slides
   * Use it as follows:
   * ```js
   *    <Carousel
   *      panelNavigation={<CustomPanelNavigation />}
   *    >
   *      <CarouselItem>First Slide</CarouselItem>
   *    </Carousel>
   * ```
   */
  panelNavigation: PropTypes.element,
  /**
   * When slide animation start
   * This function is also provided with a parameter indicating the current slide index before animation starts
   * Use it as follows:
   * ```js
   *    const onAnimationStartCallback = React.useCallback((currentIndex) => {
   *      console.log(currentIndex)
   *    }, [])  // pass local dependencies as per your component
   *    <Carousel
   *      onAnimationStart={onAnimationStartCallback}
   *    >
   *      <CarouselItem>First Slide</CarouselItem>
   *    </Carousel>
   * ```
   * Caution: Always consider wrapping your callback for `onAnimationStart` in `React.useCallback` in order to avoid bugs and performance issues
   */
  onAnimationStart: PropTypes.func,
  /**
   * When slide animation end with parameter of current index (after animation ends)
   * This function is also provided with a parameter indicating the updated slide index after animation ends
   * Use it as follows:
   * ```js
   *    const onAnimationEndCallback = React.useCallback((changedIndex) => {
   *      console.log(changedIndex)
   *    }, [])  // pass local dependencies as per your component
   *    <Carousel
   *      onAnimationEnd={onAnimationEndCallback}
   *    >
   *      <CarouselItem>First Slide</CarouselItem>
   *    </Carousel>
   * ```
   * Caution: Always consider wrapping your callback for `onAnimationEnd` in `React.useCallback` in order to avoid bugs and performance issues
   */
  onAnimationEnd: PropTypes.func,
  /**
   * Use this to override the default text for panel navigation
   */
  panelNavigationTextDictionary: PropTypes.shape({
    en: PropTypes.shape({ stepTrackerLabel: PropTypes.string.isRequired }),
    fr: PropTypes.shape({ stepTrackerLabel: PropTypes.string.isRequired })
  }),
  /**
   * Provide custom accessibilityRole for Carousel container
   */
  accessibilityRole: PropTypes.string,
  /**
   * Provide custom accessibilityLabel for Carousel container
   */
  accessibilityLabel: PropTypes.string,
  /**
   * HTML tag to use for the Carousel item's immediate parent. Defaults to `'ul'` so that
   * assistive technology tools know to intepret the carousel as a list.
   *
   * Note that if the immediate Carousel children do not all render as `'li'` elements,
   * this should be changed (e.g. pass tag="div") because only 'li' is a valid child of 'ul'.
   */
  tag: PropTypes.oneOf(layoutTags),
  /**
   * If set to `true`, the Carousel will automatically transition between slides
   * and show the play/pause button
   * - Default value is `false`
   * - `slideDuration` and `transitionDuration` are required to be set for this to work
   */
  autoPlay: PropTypes.bool,
  /**
   * Duration of the time in seconds spent on each slide
   * - Default value is `0`
   * - `autoPlay` and `transitionDuration` are required to be set for this to work
   */
  slideDuration: PropTypes.number,
  /**
   * Duration of the time in seconds between each slide transition
   * - Default value is `0`
   * - `autoPlay` and `slideDuration` are required to be set for this to work
   */
  transitionDuration: PropTypes.number,
  /**
   * Time it takes in seconds to transition from last slide to first slide
   * - Default value equals `transitionDuration`'s value
   * - `autoPlay` and `transitionDuration` are required to be set for this to work
   */
  loopDuration: PropTypes.number,
  /**
   * If set to `true`, the Carousel will show the previous and next slides
   * - Default value is `false`
   */
  enablePeeking: PropTypes.bool,
  /**
   * If set to `true`, the Carousel will show multiple slides at once
   * - Default value is `false`
   */
  enableDisplayMultipleItemsPerSlide: PropTypes.bool,
  /**
   * The maximum width of the content in the Carousel.
   * This prop accepts responsive values for different viewports. If a number is provided,
   * it will be the max content width for the desired viewport.
   * - `xs`: 'max' | 'full' | <number>
   * - `sm`: 'max' | 'full' | <number>
   * - `md`: 'max' | 'full' | <number>
   * - `lg`: 'max' | 'full' | <number>
   * - `xl`: 'max' | 'full' | <number>
   */
  contentMaxWidth: PropTypes.shape({
    xl: PropTypes.oneOfType([PropTypes.oneOf(['max', 'full']), PropTypes.number]),
    lg: PropTypes.oneOfType([PropTypes.oneOf(['max', 'full']), PropTypes.number]),
    md: PropTypes.oneOfType([PropTypes.oneOf(['max', 'full']), PropTypes.number]),
    sm: PropTypes.oneOfType([PropTypes.oneOf(['max', 'full']), PropTypes.number]),
    xs: PropTypes.oneOfType([PropTypes.oneOf(['max', 'full']), PropTypes.number])
  }),
  /**
   * Animation style for swipe release transitions.
   * - `'instant'`: Immediate snap to position (current default)
   * - `'ease-out'`: Smooth deceleration animation
   * - Default value is `'instant'`
   * - Use `swipeReleaseDuration` to customize the animation duration when using `'ease-out'`
   *
   * @deprecated The default will change to `'ease-out'` in Q2 2026 (introduced Jan 2026).
   */
  swipeReleaseStyle: PropTypes.oneOf(['instant', 'ease-out']),
  /**
   * Duration in milliseconds of the ease-out animation when releasing a swipe gesture.
   * Only applies when `swipeReleaseStyle` is set to `'ease-out'`.
   * - Default value is `500` (500ms)
   */
  swipeReleaseDuration: PropTypes.number
}

Carousel.displayName = 'Carousel'
