import type { ComponentType } from 'react'; import type { AccessibilityState, StyleProp, ViewStyle } from 'react-native'; import { Pressable, View } from 'react-native'; import type { IconProps } from 'rn-motion-ui-icons/icon-props'; import { useReducedMotion } from '../../../hooks/use-reduced-motion'; import { cn } from '../../../lib/cn'; import { elevated as elevatedSurface, type SurfaceElevation } from '../../../lib/elevated'; import { FOCUS_VISIBLE_RING } from '../../../lib/focus-ring'; import { hitSlopFor } from '../../../lib/radius'; import { MotiView } from '../../../moti/components/view'; import type { MotiTransitionProp } from '../../../theme/motion'; import { MOTION_SNAPPY, mergeTransition } from '../../../theme/motion'; import { useThemeColors } from '../../../theme/use-theme-color'; import { Surface } from '../../display/Surface/surface'; import { ButtonRipples, ButtonSpinner, usePressRipples } from '../Button/button-internals'; import { type PressMode, pressAnimate } from '../Button/button-press'; import { BUTTON_SIZE, type ButtonShape, buttonRadius, FLOATING_ICON_SIDE, type RampSize } from '../Button/button-scale'; import { BUTTON_HOVER_CLASS } from '../Button/button-variants'; // ── Per-size metrics ───────────────────────────────────────────────────────── /** * Tile dimensions and inner icon size when `iconBackgroundColor` is set. The * tile stays proportionate to the compact button, leaving only a narrow ring. */ const ICON_TILE: Record = { xs: { tileClass: 'h-[18px] w-[18px] rounded-[4px]', iconSize: 12 }, sm: { tileClass: 'h-6 w-6 rounded-md', iconSize: 16 }, md: { tileClass: 'h-[30px] w-[30px] rounded-lg', iconSize: 20 }, lg: { tileClass: 'h-9 w-9 rounded-lg', iconSize: 24 }, }; /** Spinner diameter per button size. */ const SPINNER_SIZE: Record = { xs: 10, sm: 12, md: 16, lg: 20 }; /** Floating controls leave more clear space around the glyph than resting icon buttons. */ const FLOATING_GLYPH_SIZE: Record = { xs: 16, sm: 18, md: 20, lg: 24 }; /** Resting controls keep the original icon scale. */ const ICON_BUTTON_GLYPH_SIZE: Record = { xs: 18, sm: 24, md: 28, lg: 32 }; const COMPACT_SIDE: Record = { xs: 28, sm: 36, md: 44, lg: 52 }; // ── Component ──────────────────────────────────────────────────────────────── export type IconButtonProps = { /** Icon component — any function accepting {@link IconProps} (e.g. a MingCute icon from `rn-motion-ui-icons`). */ icon: ComponentType; /** * When set, the icon is placed inside a coloured rounded-square tile * (iOS Settings style), inside the button's touch area. */ iconBackgroundColor?: string; /** * Icon stroke colour. When `iconBackgroundColor` is set this defaults to * `'white'`; otherwise it uses the plain foreground token. */ iconColor?: string; /** * Swap the plate's ladder shadow for the shared compact floating shadow * (`shadow-floating`) — the recipe {@link Input}'s `floating` prop wears. * It replaces the `shadow-elevated-N` rung rather than adding to it, so the * plate keeps its `elevation` tint but trades the layered drop for the halo. * @default false */ floating?: boolean; /** * Surface elevation level (0–3) — drives the background tint (`bg-surface-N`) * and the `shadow-elevated-N` recipe. `0` is the flat resting surface — a * `surface-3` fill with no shadow or border. @default 0 */ elevation?: SurfaceElevation; /** Button size — the square, and the icon or tile inside it. Floating compact uses 28/34/40/48px; comfortable follows Button's shared size ramp. @default 'md' */ size?: RampSize; /** Compact visual padding, preserving a 44px touch target through hitSlop. @default 'compact' */ density?: 'compact' | 'comfortable'; /** Override the glyph size without changing the touch target. */ iconSize?: number; /** Corner shape. @default 'pill' */ shape?: ButtonShape; /** * Backdrop blur radius in px/dp. `0` keeps the plate a solid surface; any * positive value frosts it — a `glass` tint over a backdrop blur (with the * specular edge light when `rim` is set). @default 0 */ blurRadius?: number; /** Opacity of the frosted tint (0–1); only thins the fill when `blurRadius` is set. @default 0.8 */ opacity?: number; /** Draw the glass edge light — enabled by default for floating plates. @default floating */ rim?: boolean; /** Rim width in px/dp. @default 1 */ rimWidth?: number; /** Peak alpha (0–1) of the rim's specular highlight — lower is subtler. @default 0.5 */ intensity?: number; // ── Interaction ──────────────────────────────────────────────────────────── onPress?: () => void; disabled?: boolean; loading?: boolean; /** Spawn a Material-style ripple from the press point. On by default. */ ripple?: boolean; /** Settled scale for the uniform `scaleUp`/`scaleDown` press modes. Omit to * use the mode's own default (1.05 growing, 0.93 shrinking). */ pressScale?: number; /** * Shape of the press animation. * - `scaleUp` (default) — grows to `pressScale` (uniform, default 1.05). * - `scaleDown` — shrinks to `pressScale` (uniform, default 0.93). * - `scaleY` — compresses vertically and nudges down. * - `scaleX` — compresses horizontally. * - `none` — no press animation at all. */ pressMode?: PressMode; /** When true, skip the 0.5 opacity applied to disabled buttons. */ noDisabledOpacity?: boolean; /** Override the press-scale spring. Partial — only the fields you pass are changed. */ pressTransition?: Partial; /** Stretch the button to fill its container width. */ fitWidth?: boolean; // ── Layout ───────────────────────────────────────────────────────────────── /** Additional UniWind class names merged onto the outer wrapper. */ className?: string; /** Tailwind classes merged onto the Pressable container. */ contentClassName?: string; style?: StyleProp; // ── Accessibility ────────────────────────────────────────────────────────── /** * Accessible name announced to assistive tech. **Required** — an icon-only * button without a label is invisible to screen readers. */ accessibilityLabel: string; /** State of a controlled surface, such as an expanded action menu. */ accessibilityState?: AccessibilityState; /** Web relationship to a menu or dialog opened by this button. */ 'aria-haspopup'?: 'menu' | 'dialog'; 'aria-controls'?: string; testID?: string; }; /** * A purpose-built icon-only button — a square pressable that displays an icon, * on a surface plate whose depth comes from `elevation` and whose shadow recipe * can be swapped for the input halo via `floating`, with the same * `icon`/`iconBackgroundColor`/`iconColor` API as {@link MenuItem}. * * Supersedes `