import { forwardRef, useCallback, useMemo } from 'react'; import { View, type GestureResponderEvent } from 'react-native'; import Animated from 'react-native-reanimated'; import { useIsOnSurface, useThemeColor } from '../../helpers/external/hooks'; import { AnimatedCheckIcon, CheckIcon, ThemeBackground, useHasDefaultThemeBackground, } from '../../helpers/internal/components'; import { AnimationSettingsProvider } from '../../helpers/internal/contexts'; import type { ViewRef } from '../../helpers/internal/types'; import * as CheckboxPrimitives from '../../primitives/checkbox'; import * as CheckboxPrimitivesTypes from '../../primitives/checkbox/checkbox.types'; import { CheckboxAnimationProvider, useCheckboxIndicatorAnimation, useCheckboxRootAnimation, } from './checkbox.animation'; import { DEFAULT_HIT_SLOP, DISPLAY_NAME } from './checkbox.constants'; import { checkboxClassNames, checkboxStyleSheet } from './checkbox.styles'; import type { CheckboxBackgroundProps, CheckboxIndicatorProps, CheckboxProps, CheckboxRenderProps, } from './checkbox.types'; const AnimatedRootView = Animated.createAnimatedComponent( CheckboxPrimitives.Root ); const AnimatedIndicatorView = Animated.createAnimatedComponent( CheckboxPrimitives.Indicator ); const useCheckbox = CheckboxPrimitives.useCheckboxContext; // -------------------------------------------------- /** * Generic absolute-fill background container rendered behind the checkbox * 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 CheckboxBackground = forwardRef( ({ className, ...props }, ref) => { const backgroundClassName = checkboxClassNames.background({ className }); return ( ); } ); // -------------------------------------------------- const CheckboxRoot = forwardRef( (props, ref) => { const { children, isSelected, onSelectedChange, isDisabled = false, isInvalid = false, variant, hitSlop = DEFAULT_HIT_SLOP, className, style, onPressIn, onPressOut, animation, isAnimatedStyleActive = true, background, ...restProps } = props; const hasDefaultThemeBackground = useHasDefaultThemeBackground(); const isOnSurfaceAutoDetected = useIsOnSurface(); const finalVariant = variant !== undefined ? variant : isOnSurfaceAutoDetected ? 'secondary' : 'primary'; const rootClassName = checkboxClassNames.root({ variant: finalVariant, isSelected, isDisabled, isInvalid, className, }); const { rContainerStyle, isCheckboxPressed, isAllAnimationsDisabled } = useCheckboxRootAnimation({ animation, }); const rootStyle = isAnimatedStyleActive ? [rContainerStyle, checkboxStyleSheet.root, style] : [checkboxStyleSheet.root, style]; const animationContextValue = useMemo( () => ({ isCheckboxPressed, }), [isCheckboxPressed] ); const animationSettingsContextValue = useMemo( () => ({ isAllAnimationsDisabled, }), [isAllAnimationsDisabled] ); const handlePressIn = useCallback( (event: GestureResponderEvent) => { isCheckboxPressed.set(true); onPressIn?.(event); }, [isCheckboxPressed, onPressIn] ); const handlePressOut = useCallback( (event: GestureResponderEvent) => { isCheckboxPressed.set(false); onPressOut?.(event); }, [isCheckboxPressed, onPressOut] ); const renderProps: CheckboxRenderProps = { isSelected, isInvalid, isDisabled, }; const content = typeof children === 'function' ? children(renderProps) : (children ?? ); /** * Background layer rendered behind the checkbox content. * - `undefined`: theme-aware default for the secondary variant (while * valid — the invalid unselected state paints a transparent * background) when the active theme registers default background * content * - custom node: replaces the default layer * - `null`: removes the layer */ const backgroundElement = background !== undefined ? ( background ) : hasDefaultThemeBackground && finalVariant === 'secondary' && !isInvalid ? ( ) : null; return ( {backgroundElement} {content} ); } ); // -------------------------------------------------- const CheckboxIndicator = forwardRef< CheckboxPrimitivesTypes.IndicatorRef, CheckboxIndicatorProps >((props, ref) => { const { children, iconProps, className, style, animation, isAnimatedStyleActive = true, ...restProps } = props; const { isSelected, isDisabled, isInvalid } = useCheckbox(); const themeColorAccentForeground = useThemeColor('accent-foreground'); const iconSize = iconProps?.size; const iconStrokeWidth = iconProps?.strokeWidth; const iconColor = iconProps?.color ?? themeColorAccentForeground; const iconEnterDuration = iconProps?.enterDuration; const iconExitDuration = iconProps?.exitDuration; const indicatorClassName = checkboxClassNames.indicator({ isInvalid, className, }); const { rContainerStyle, isAnimationDisabled } = useCheckboxIndicatorAnimation({ animation, isSelected, }); const indicatorStyle = isAnimatedStyleActive ? [rContainerStyle, style] : style; const renderProps: CheckboxRenderProps = { isSelected, isInvalid: isInvalid ?? false, isDisabled: isDisabled ?? false, }; const content = typeof children === 'function' ? children(renderProps) : (children ?? (isAnimationDisabled ? ( ) : ( ))); return ( {content} ); }); // -------------------------------------------------- CheckboxRoot.displayName = DISPLAY_NAME.CHECKBOX_ROOT; CheckboxIndicator.displayName = DISPLAY_NAME.CHECKBOX_INDICATOR; CheckboxBackground.displayName = DISPLAY_NAME.CHECKBOX_BACKGROUND; /** * Compound Checkbox component with sub-components * * @component Checkbox - Main container that handles selection state and user interaction. * Renders default indicator with checkmark if no children provided. * Animates background and border color based on selection state. * * @component Checkbox.Indicator - Optional checkmark container that scales in when selected. * Renders default check icon if no children provided. Handles enter/exit animations * and can be replaced with custom indicators. * * @component Checkbox.Background - Absolute-fill background container behind the * checkbox content. With no children, the active library theme decides the * default content (e.g. a glass blur layer) * * Props flow from Checkbox to sub-components via context (isSelected). * The checkbox supports controlled and uncontrolled modes through isSelected/onSelectedChange. * * @see Full documentation: https://heroui.com/docs/native/components/checkbox */ const CompoundCheckbox = Object.assign(CheckboxRoot, { /** @optional Custom indicator with scale animations */ Indicator: CheckboxIndicator, /** Checkbox background - absolute-fill container behind the checkbox content */ Background: CheckboxBackground, }); export { useCheckbox }; export default CompoundCheckbox;