import React, { type Ref, useCallback, useEffect, useRef, useState, } from 'react' import type { Except } from 'type-fest' import type { DOMElement } from '../core/dom.js' import type { ClickEvent } from '../core/events/click-event.js' import type { FocusEvent } from '../core/events/focus-event.js' import type { KeyboardEvent } from '../core/events/keyboard-event.js' import type { Styles } from '../core/styles.js' import Box from './Box.js' type ButtonState = { focused: boolean hovered: boolean active: boolean } export type Props = Except & { ref?: Ref /** * Called when the button is activated via Enter, Space, or click. */ onAction: () => void /** * Tab order index. Defaults to 0 (in tab order). * Set to -1 for programmatically focusable only. */ tabIndex?: number /** * Focus this button when it mounts. */ autoFocus?: boolean /** * Render prop receiving the interactive state. Use this to * style children based on focus/hover/active — Button itself * is intentionally unstyled. * * If not provided, children render as-is (no state-dependent styling). */ children: ((state: ButtonState) => React.ReactNode) | React.ReactNode } function Button({ onAction, tabIndex = 0, autoFocus, children, ref, ...style }: Props): React.ReactNode { const [isFocused, setIsFocused] = useState(false) const [isHovered, setIsHovered] = useState(false) const [isActive, setIsActive] = useState(false) const activeTimer = useRef | null>(null) useEffect(() => { return () => { if (activeTimer.current) clearTimeout(activeTimer.current) } }, []) const handleKeyDown = useCallback( (e: KeyboardEvent) => { if (e.key === 'return' || e.key === ' ') { e.preventDefault() setIsActive(true) onAction() if (activeTimer.current) clearTimeout(activeTimer.current) activeTimer.current = setTimeout( setter => setter(false), 100, setIsActive, ) } }, [onAction], ) const handleClick = useCallback( (_e: ClickEvent) => { onAction() }, [onAction], ) const handleFocus = useCallback((_e: FocusEvent) => setIsFocused(true), []) const handleBlur = useCallback((_e: FocusEvent) => setIsFocused(false), []) const handleMouseEnter = useCallback(() => setIsHovered(true), []) const handleMouseLeave = useCallback(() => setIsHovered(false), []) const state: ButtonState = { focused: isFocused, hovered: isHovered, active: isActive, } const content = typeof children === 'function' ? children(state) : children return ( {content} ) } export default Button export type { ButtonState }