import * as React from 'react' import {motion, HTMLMotionProps, useAnimationControls} from 'motion/react' /** * Animated wrapper providing native-like tap feedback. You can use it instead of raw div + onClick. * @publicDocs */ export interface TouchableDocProps { /** Click handler */ onClick?: React.MouseEventHandler /** Prevent click event from bubbling to parent elements */ stopPropagation?: boolean /** Content to render inside the touchable area */ children?: React.ReactNode } export interface TouchableProps extends HTMLMotionProps<'div'> { onClick?: React.MouseEventHandler stopPropagation?: boolean } export const Touchable = ({ children, onClick, stopPropagation = false, ...props }: TouchableProps) => { const ref = React.useRef(null) const controls = useAnimationControls() // Filter out props that shouldn't be passed to motion.div // Any other custom props that get added to the component interface should be filtered here const {ref: _, ...motionProps} = props const handleClick = React.useCallback( (event: React.MouseEvent) => { if (stopPropagation) event.stopPropagation() onClick?.(event) }, [stopPropagation, onClick] ) // Handle animations manually when stopPropagation is true to prevent parent from receiving event React.useEffect(() => { if (!stopPropagation || !ref.current) return const element = ref.current const handlePointerDown = (event: PointerEvent) => { event.stopImmediatePropagation() event.stopPropagation() // Animate to pressed state controls.start({ opacity: 0.7, transition: { opacity: {type: 'tween', duration: 0.08, ease: 'linear'}, }, }) } const handlePointerUp = (event: PointerEvent) => { event.stopImmediatePropagation() event.stopPropagation() // Animate back to normal state controls.start({ opacity: 1, transition: { opacity: {type: 'tween', duration: 0.08, ease: 'linear'}, }, }) } // Capture pointer event before Motion element.addEventListener('pointerdown', handlePointerDown, true) element.addEventListener('pointerup', handlePointerUp, true) return () => { element.removeEventListener('pointerdown', handlePointerDown, true) element.removeEventListener('pointerup', handlePointerUp, true) } }, [stopPropagation, controls]) return ( {children} ) }