import { forwardRef, useMemo } from 'react'; import type { View } from 'react-native'; import Animated from 'react-native-reanimated'; import { AnimationSettingsProvider } from '../../helpers/contexts/animation-settings-context'; import { useThemeColor } from '../../helpers/theme'; import { createContext, getElementWithDefault } from '../../helpers/utils'; import * as ActivityIndicatorPrimitives from '../../primitives/activity-indicator'; import { SpinnerIcon } from './spinner-icon'; import { useSpinnerIndicatorAnimation, useSpinnerRootAnimation, } from './spinner.animation'; import { DISPLAY_NAME, SPINNER_SIZE_MAP } from './spinner.constants'; import spinnerStyles from './spinner.styles'; import type { SpinnerContextValue, SpinnerIndicatorProps, SpinnerProps, } from './spinner.types'; const AnimatedRoot = Animated.createAnimatedComponent( ActivityIndicatorPrimitives.Root ); const AnimatedIndicator = Animated.createAnimatedComponent( ActivityIndicatorPrimitives.Indicator ); const [SpinnerProvider, useSpinnerContext] = createContext( { name: 'SpinnerContext', } ); const SpinnerRoot = forwardRef((props, ref) => { const { children, size = 'md', color = 'default', isLoading = true, className, animation, ...restProps } = props; const tvStyles = spinnerStyles.root({ size, className, }); const { entering, exiting, isAllAnimationsDisabled } = useSpinnerRootAnimation({ animation, }); const indicatorElement = useMemo( () => getElementWithDefault( children, DISPLAY_NAME.INDICATOR, ), [children] ); const contextValue = useMemo( () => ({ size, color, isLoading, }), [size, color, isLoading] ); const animationSettingsContextValue = useMemo( () => ({ isAllAnimationsDisabled, }), [isAllAnimationsDisabled] ); return ( {children || indicatorElement} ); }); // -------------------------------------------------- const SpinnerIndicator = forwardRef( (props, ref) => { const { children, className, style, iconProps, animation, isAnimatedStyleActive = true, ...restProps } = props; const { size, color, isLoading } = useSpinnerContext(); const [ themeColorAccent, themeColorSuccess, themeColorWarning, themeColorDanger, ] = useThemeColor(['accent', 'success', 'warning', 'danger']); const indicatorClassName = spinnerStyles.indicator({ className, }); const iconSize = SPINNER_SIZE_MAP[size]; const colorMap: Record = { default: themeColorAccent, success: themeColorSuccess, warning: themeColorWarning, danger: themeColorDanger, }; const iconColor = colorMap[color] || color; const { rContainerStyle } = useSpinnerIndicatorAnimation({ animation, isLoading, }); const indicatorStyle = isAnimatedStyleActive ? [rContainerStyle, style] : style; if (!isLoading) { return null; } return ( {children || ( )} ); } ); // -------------------------------------------------- SpinnerRoot.displayName = DISPLAY_NAME.ROOT; SpinnerIndicator.displayName = DISPLAY_NAME.INDICATOR; /** * Compound Spinner component with sub-components * * @component Spinner - Main container that controls loading state, size, and color. * Renders a default animated indicator if no children provided. * * @component Spinner.Indicator - Optional sub-component for customizing animation configuration * and icon appearance. Accepts custom children to replace the default icon. * When omitted, Spinner uses a default indicator with standard animation settings. * * Props flow from Spinner to Indicator via context (size, color, isLoading). * The indicator only renders when isLoading is true. * * @see Full documentation: https://heroui.com/components/spinner */ const CompoundSpinner = Object.assign(SpinnerRoot, { /** @optional Customize animation configuration and icon appearance */ Indicator: SpinnerIndicator, }); export default CompoundSpinner;