import { forwardRef, useCallback, useMemo } from 'react'; import type { GestureResponderEvent } from 'react-native'; import { View, type ViewStyle } from 'react-native'; import Animated from 'react-native-reanimated'; import { ThemeBackground, useHasDefaultThemeBackground, } from '../../helpers/internal/components'; import { AnimationSettingsProvider } from '../../helpers/internal/contexts'; import type { ViewRef } from '../../helpers/internal/types'; import { createContext } from '../../helpers/internal/utils'; import * as SwitchPrimitives from '../../primitives/switch'; import * as SwitchPrimitivesTypes from '../../primitives/switch/switch.types'; import { SwitchAnimationProvider, useSwitchRootAnimation, useSwitchThumbAnimation, } from './switch.animation'; import { DISPLAY_NAME } from './switch.constants'; import { switchClassNames, switchStyleSheet } from './switch.styles'; import type { SwitchBackgroundProps, SwitchContentProps, SwitchContextValue, SwitchProps, SwitchRenderProps, SwitchThumbProps, } from './switch.types'; const AnimatedSwitchRoot = Animated.createAnimatedComponent( SwitchPrimitives.Root ); const AnimatedSwitchThumb = Animated.createAnimatedComponent( SwitchPrimitives.Thumb ); const [SwitchProvider, useSwitch] = createContext({ name: 'SwitchContext', }); // -------------------------------------------------- /** * Generic absolute-fill background container rendered behind the switch * content (clipped by the root's `overflow: hidden`). With no `children`, * the active library theme decides the default content: `glass` renders a * `GlassView` blur layer; other themes render nothing. Pass `children` to * host arbitrary content (gradients, images) with the container's * positioning and clipping applied. */ const SwitchBackground = forwardRef( ({ className, ...props }, ref) => { const backgroundClassName = switchClassNames.background({ className }); return ( ); } ); // -------------------------------------------------- const Switch = forwardRef( (props, ref) => { const { children, isDisabled, isSelected, onSelectedChange, className, style, animation, isAnimatedStyleActive = true, onPressIn, onPressOut, background, ...restProps } = props; const hasDefaultThemeBackground = useHasDefaultThemeBackground(); const rootClassName = switchClassNames.root({ isDisabled, className, }); const { rContainerStyle, isSwitchPressed, contentContainerWidth, isAllAnimationsDisabled, } = useSwitchRootAnimation({ animation, isSelected, }); const rootStyle = isAnimatedStyleActive ? [switchStyleSheet.borderCurve, rContainerStyle, style] : [switchStyleSheet.borderCurve, style]; const contextValue = useMemo( () => ({ isSelected, isDisabled, }), [isSelected, isDisabled] ); const animationContextValue = useMemo( () => ({ isSwitchPressed, contentContainerWidth, }), [isSwitchPressed, contentContainerWidth] ); const animationSettingsContextValue = useMemo( () => ({ isAllAnimationsDisabled, }), [isAllAnimationsDisabled] ); const handlePressIn = useCallback( (event: GestureResponderEvent) => { isSwitchPressed.set(true); onPressIn?.(event); }, [isSwitchPressed, onPressIn] ); const handlePressOut = useCallback( (event: GestureResponderEvent) => { isSwitchPressed.set(false); onPressOut?.(event); }, [isSwitchPressed, onPressOut] ); const renderProps: SwitchRenderProps = { isSelected, isDisabled: isDisabled ?? false, }; const content = typeof children === 'function' ? children(renderProps) : (children ?? ); /** * Background layer rendered behind the switch content. * - `undefined`: theme-aware default while unselected (selection paints * its own accent color) when the active theme registers default * background content * - custom node: replaces the default layer * - `null`: removes the layer */ const backgroundElement = background !== undefined ? ( background ) : hasDefaultThemeBackground && !isSelected ? ( ) : null; return ( { contentContainerWidth.set(e.nativeEvent.layout.width); }} {...restProps} > {backgroundElement} {content} ); } ); // -------------------------------------------------- const SwitchThumb = forwardRef< SwitchPrimitivesTypes.ThumbRef, SwitchThumbProps >((props, ref) => { const { children, className, style, animation, isAnimatedStyleActive = true, ...restProps } = props; const { isSelected, isDisabled } = useSwitch(); const thumbClassName = switchClassNames.thumb({ className, }); const { rContainerStyle } = useSwitchThumbAnimation({ animation, style: style as ViewStyle | undefined, className: thumbClassName, isSelected, }); const thumbStyle = isAnimatedStyleActive ? [switchStyleSheet.borderCurve, rContainerStyle, style] : [switchStyleSheet.borderCurve, style]; const renderProps: SwitchRenderProps = { isSelected, isDisabled: isDisabled ?? false, }; const content = typeof children === 'function' ? children(renderProps) : children; return ( {content} ); }); // -------------------------------------------------- const SwitchStartContent = forwardRef( (props, ref) => { const { children, className, ...restProps } = props; const startContentClassName = switchClassNames.startContent({ className, }); return ( {children} ); } ); // -------------------------------------------------- const SwitchEndContent = forwardRef( (props, ref) => { const { children, className, ...restProps } = props; const endContentClassName = switchClassNames.endContent({ className, }); return ( {children} ); } ); // -------------------------------------------------- Switch.displayName = DISPLAY_NAME.SWITCH_ROOT; SwitchThumb.displayName = DISPLAY_NAME.SWITCH_THUMB; SwitchBackground.displayName = DISPLAY_NAME.SWITCH_BACKGROUND; SwitchStartContent.displayName = DISPLAY_NAME.SWITCH_START_CONTENT; SwitchEndContent.displayName = DISPLAY_NAME.SWITCH_END_CONTENT; /** * Compound Switch component with sub-components * * @component Switch - Main container that handles toggle state and user interaction. * Renders default thumb if no children provided. Animates scale (on press) and background * color based on selection state. Acts as a pressable area for toggling. Supports render * function children for dynamic content. * * @component Switch.Thumb - Optional sliding thumb element that moves between positions. * Uses spring animation for smooth transitions. Can contain custom content like icons * or be customized with different styles and animations. Supports render function children. * * @component Switch.StartContent - Optional content displayed on the left side of the switch. * Typically used for icons or text that appear when switch is off. Positioned absolutely * within the switch container. * * @component Switch.EndContent - Optional content displayed on the right side of the switch. * Typically used for icons or text that appear when switch is on. Positioned absolutely * within the switch container. * * @component Switch.Background - Absolute-fill background container behind the * switch content. With no children, the active library theme decides the * default content (e.g. a glass blur layer) * * Props flow from Switch to sub-components via context (isSelected, isDisabled). * The switch supports controlled and uncontrolled modes through isSelected/onSelectedChange. * Animations can be customized or disabled at both root and component levels. * Content components provide visual feedback without affecting the toggle functionality. * Integrates with ControlField for press state sharing and larger touch targets. * * @see Full documentation: https://heroui.com/docs/native/components/switch */ const CompoundSwitch = Object.assign(Switch, { /** @optional Sliding thumb with spring animations */ Thumb: SwitchThumb, /** @optional Content shown when switch is off (left side) */ StartContent: SwitchStartContent, /** @optional Content shown when switch is on (right side) */ EndContent: SwitchEndContent, /** Switch background - absolute-fill container behind the switch content */ Background: SwitchBackground, }); export { useSwitch }; export default CompoundSwitch;