{
  "$schema": "https://zinmin232.github.io/advui/schema/registry-item.json",
  "name": "button",
  "title": "Button",
  "type": "registry:component",
  "description": "Triggers an action or event, such as submitting a form or opening a dialog.",
  "category": "buttons",
  "status": "stable",
  "since": "0.1.0",
  "platforms": [
    "web",
    "ios",
    "android"
  ],
  "package": "@advui/core",
  "files": [
    {
      "path": "components/button/Button.tsx",
      "type": "registry:ui",
      "sha256": "32689b743a0260aace377b590ce115305c8f263cc6e3eff81150cef1a9701127",
      "content": "import { IconDefaults } from '@advui/icons'\nimport { type ReactNode, forwardRef, useContext } from 'react'\nimport {\n  type GetProps,\n  type TamaguiElement,\n  Text,\n  View,\n  createStyledContext,\n  isWeb,\n  styled,\n  withStaticProperties,\n} from 'tamagui/unstyled'\nimport { useRipple } from '../../hooks/useRipple'\nimport { ButtonGroupContext, ButtonGroupItemContext, attachedStyle } from './groupContext'\nimport { Spinner } from '../spinner'\nimport { isTextContent } from '../../utils/isTextContent'\nimport { ariaState } from '../../utils/ariaState'\nimport { colorsOpacity } from '../../utils/transition'\nimport type { ControlStateProps } from '../../hooks/useFieldControl'\n\nexport type ButtonVariant = 'default' | 'secondary' | 'outline' | 'ghost' | 'destructive' | 'link'\nexport type ButtonSize = 'sm' | 'md' | 'lg' | 'icon'\n\nexport const ButtonContext = createStyledContext<{ variant: ButtonVariant; size: ButtonSize }>({\n  variant: 'default',\n  size: 'md',\n})\n\n/** Label color per variant — shared by text, icons and the loading spinner. */\nexport const buttonForeground = {\n  default: 'primaryForeground',\n  secondary: 'secondaryForeground',\n  outline: 'foreground',\n  ghost: 'foreground',\n  destructive: 'destructiveForeground',\n  link: 'primaryText',\n} as const satisfies Record<ButtonVariant, `${string}`>\n\n// Resting fill per variant, kept while pressed when the Android ripple is on.\nconst buttonBackground = {\n  default: 'primary',\n  secondary: 'secondary',\n  outline: 'background',\n  ghost: 'transparent',\n  destructive: 'destructive',\n  link: 'transparent',\n} as const satisfies Record<ButtonVariant, string>\n\nconst iconSizes: Record<ButtonSize, number> = { sm: 14, md: 16, lg: 18, icon: 16 }\n\nexport const ButtonFrame = styled(View, {\n  displayName: 'Button',\n  // Tamagui 3 no longer adds an `is_<name>` class; GlobalStyles and apps target it.\n  className: 'is_Button',\n  context: ButtonContext,\n  role: 'button',\n  render: 'button',\n  tabIndex: 0,\n  flexDirection: 'row',\n  alignItems: 'center',\n  justifyContent: 'center',\n  flexShrink: 0,\n  gap: '2',\n  borderWidth: 1,\n  borderColor: 'transparent',\n  borderRadius: 'button',\n  cursor: 'pointer',\n  userSelect: 'none',\n  transition: colorsOpacity('quick'),\n  outlineColor: 'focus-visible:ring',\n  outlineStyle: 'focus-visible:solid',\n  outlineWidth: 'focus-visible:2px',\n  outlineOffset: 'focus-visible:2px',\n  variants: {\n    variant: {\n      default: {\n        backgroundColor: 'primary hover:primaryHover press:primaryPress',\n      },\n      secondary: {\n        backgroundColor: 'secondary hover:secondaryHover press:secondaryPress',\n      },\n      outline: {\n        backgroundColor: 'background hover:accent press:accentHover',\n        borderColor: 'input hover:borderStrong',\n      },\n      ghost: {\n        backgroundColor: 'transparent hover:accent press:accentHover',\n      },\n      destructive: {\n        backgroundColor: 'destructive hover:destructiveHover press:destructivePress',\n      },\n      link: {\n        backgroundColor: 'transparent',\n        paddingHorizontal: 0,\n        height: 'auto',\n        opacity: 'hover:0.8 press:0.7',\n      },\n    },\n\n    size: {\n      sm: {\n        height: '8',\n        paddingHorizontal: '3',\n        gap: '1-5',\n      },\n      md: {\n        height: '10',\n        paddingHorizontal: '4',\n      },\n      lg: {\n        height: '12',\n        paddingHorizontal: '6',\n        borderRadius: 'buttonLg',\n      },\n      icon: {\n        height: '10',\n        width: '10',\n        paddingHorizontal: 0,\n      },\n    },\n\n    fullWidth: {\n      true: { width: '100%', alignSelf: 'stretch' },\n    },\n\n    disabled: {\n      true: { opacity: 0.5, pointerEvents: isWeb ? 'none' : 'auto', cursor: 'default' },\n    },\n  } as const,\n  defaultVariants: {\n    variant: 'default',\n    size: 'md',\n  },\n})\n\nexport const ButtonText = styled(Text, {\n  displayName: 'ButtonText',\n  context: ButtonContext,\n  fontFamily: 'body',\n  fontWeight: '500',\n  userSelect: 'none',\n  flexShrink: 1,\n  numberOfLines: 1,\n  variants: {\n    variant: {\n      default: { color: buttonForeground.default },\n      secondary: { color: buttonForeground.secondary },\n      outline: { color: buttonForeground.outline },\n      ghost: { color: buttonForeground.ghost },\n      destructive: { color: buttonForeground.destructive },\n      link: { color: buttonForeground.link, textDecorationLine: 'underline' },\n    },\n    size: {\n      sm: {\n        fontSize: '2',\n        lineHeight: '2',\n      },\n      md: {\n        fontSize: '2',\n        lineHeight: '2',\n      },\n      lg: {\n        fontSize: '3',\n        lineHeight: '3',\n      },\n      icon: {\n        fontSize: '2',\n        lineHeight: '2',\n      },\n    },\n  } as const,\n})\n\ntype FrameProps = GetProps<typeof ButtonFrame>\n\nexport interface ButtonProps\n  extends Omit<FrameProps, 'variant' | 'size' | 'disabled'>, Pick<ControlStateProps, 'disabled'> {\n  variant?: ButtonVariant\n  size?: ButtonSize\n  /** Shows a spinner, sets `aria-busy` and blocks presses while keeping the label for layout. */\n  loading?: boolean\n  /** Icon element rendered before the label. */\n  icon?: ReactNode\n  /** Icon element rendered after the label. */\n  iconAfter?: ReactNode\n  children?: ReactNode\n}\n\n// Touch targets: extend the pressable area to ≥44pt on native without changing layout.\nconst nativeHitSlop: Record<ButtonSize, number> = { sm: 6, md: 2, lg: 0, icon: 2 }\n\nconst ButtonImpl = forwardRef<TamaguiElement, ButtonProps>(function Button(\n  {\n    variant: variantProp,\n    size: sizeProp,\n    loading = false,\n    disabled = false,\n    icon,\n    iconAfter,\n    children,\n    onPressIn,\n    onPressOut,\n    ...props\n  },\n  ref,\n) {\n  const group = useContext(ButtonGroupContext)\n  const position = useContext(ButtonGroupItemContext)\n  const variant = variantProp ?? group?.variant ?? 'default'\n  const size = sizeProp ?? group?.size ?? 'md'\n  const inactive = disabled || loading\n  const foreground = buttonForeground[variant]\n  const attached =\n    group?.attached && position && attachedStyle(position, group.orientation, variant)\n  // Material gives text links no ripple, so links keep their own press style.\n  const ripple = useRipple({\n    color: foreground,\n    // The frame's corners: its size's (see ButtonFrame), then a group's, then yours.\n    radius: { borderRadius: size === 'lg' ? 'buttonLg' : 'button', ...attached, ...props },\n    disabled: inactive || variant === 'link',\n    onPressIn,\n    onPressOut,\n  })\n  const content = isTextContent(children) ? <ButtonText>{children}</ButtonText> : children\n  // Android writes a busy view's description from its label. Without one it\n  // reads only \"busy\", and goes on reading it after loading ends, so a text\n  // button names itself with its text on native.\n  const nativeLabel = !isWeb && isTextContent(children) ? [children].flat().join('') : undefined\n\n  return (\n    <ButtonFrame\n      ref={ref}\n      variant={variant}\n      size={size}\n      disabled={inactive}\n      aria-disabled={ariaState(inactive)}\n      aria-busy={ariaState(loading)}\n      hitSlop={isWeb ? undefined : nativeHitSlop[size]}\n      {...(isWeb ? { type: 'button' } : null)}\n      {...(nativeLabel && { 'aria-label': nativeLabel })}\n      {...attached}\n      {...(ripple.active && {\n        backgroundColor: `press:${buttonBackground[variant]}`,\n        // The ripple is clipped inside the border: drop the transparent one\n        // so it reaches the edge. Outline buttons keep their visible border.\n        ...(variant !== 'outline' && { borderWidth: 0 }),\n      })}\n      {...props}\n      {...ripple.props}\n    >\n      {ripple.element}\n      <IconDefaults size={iconSizes[size]} color={foreground}>\n        {loading ? <Spinner size=\"sm\" color={foreground} label=\"Loading\" /> : icon}\n        {content}\n        {iconAfter}\n      </IconDefaults>\n    </ButtonFrame>\n  )\n})\n\n/**\n * Triggers an action. Renders a native `<button>` on web and a pressable\n * with the `button` role on iOS/Android.\n *\n * @example\n * <Button variant=\"outline\" icon={<PlusIcon />}>New project</Button>\n */\nexport const Button = withStaticProperties(ButtonImpl, {\n  Text: ButtonText,\n  Frame: ButtonFrame,\n})\n"
    },
    {
      "path": "components/button/groupContext.ts",
      "type": "registry:ui",
      "sha256": "555f432dc3242d3ed02b93d419c26762a5160709eadbae193d3bf4f3f513ba7f",
      "content": "import { createContext } from 'react'\nimport type { ButtonVariant } from './Button'\n\nexport type ButtonGroupOrientation = 'horizontal' | 'vertical'\nexport type ButtonGroupSize = 'sm' | 'md' | 'lg'\ntype Position = 'first' | 'middle' | 'last' | 'only'\n\nexport interface ButtonGroupContextValue {\n  variant?: ButtonVariant\n  size?: ButtonGroupSize\n  attached: boolean\n  orientation: ButtonGroupOrientation\n}\n\n/** Group-wide defaults that buttons inside a ButtonGroup read. */\nexport const ButtonGroupContext = createContext<ButtonGroupContextValue | null>(null)\n\n/** Where a button sits in its group, for joined corners. */\nexport const ButtonGroupItemContext = createContext<Position | null>(null)\n\nexport function positionOf(index: number, count: number): Position {\n  if (count === 1) return 'only'\n  if (index === 0) return 'first'\n  return index === count - 1 ? 'last' : 'middle'\n}\n\nconst ring = {\n  outlineColor: 'focus-visible:ring',\n  outlineStyle: 'focus-visible:solid',\n  outlineWidth: 'focus-visible:2px',\n  outlineOffset: 'focus-visible:2px',\n} as const\n\n/**\n * Styles that join a button to its neighbours: square inner corners, and a\n * shared 1px border for outline buttons or a 1px gap between filled ones.\n */\nexport function attachedStyle(\n  position: Position,\n  orientation: ButtonGroupOrientation,\n  variant: ButtonVariant,\n) {\n  const horizontal = orientation === 'horizontal'\n  const start = position === 'first' || position === 'only'\n  const end = position === 'last' || position === 'only'\n  return {\n    // Lifts the focused button so its ring is not covered by its neighbour.\n    position: 'relative',\n    ...ring,\n    zIndex: 'focus-visible:1',\n    ...(!start && {\n      ...(horizontal\n        ? { borderTopLeftRadius: 0, borderBottomLeftRadius: 0 }\n        : { borderTopLeftRadius: 0, borderTopRightRadius: 0 }),\n      [horizontal ? 'marginLeft' : 'marginTop']: variant === 'outline' ? -1 : 1,\n    }),\n    ...(!end &&\n      (horizontal\n        ? { borderTopRightRadius: 0, borderBottomRightRadius: 0 }\n        : { borderBottomLeftRadius: 0, borderBottomRightRadius: 0 })),\n  } as const\n}\n"
    },
    {
      "path": "components/button/index.ts",
      "type": "registry:ui",
      "sha256": "be65b66a396eb776e9f176e60418d275272bb7de6aeb8f8cee5ce8293b3bc701",
      "content": "export {\n  Button,\n  ButtonContext,\n  ButtonFrame,\n  ButtonText,\n  buttonForeground,\n  type ButtonProps,\n  type ButtonSize,\n  type ButtonVariant,\n} from './Button'\n"
    }
  ],
  "examples": [
    {
      "name": "basic",
      "title": "Basic",
      "code": "import { Button } from '@advui/core'\n\nexport default function ButtonBasic() {\n  return <Button onPress={() => console.log('Saved')}>Save changes</Button>\n}\n"
    },
    {
      "name": "variants",
      "title": "Variants",
      "code": "import { Button, HStack } from '@advui/core'\n\nexport default function ButtonVariants() {\n  return (\n    <HStack gap=\"2\" flexWrap=\"wrap\">\n      <Button>Default</Button>\n      <Button variant=\"secondary\">Secondary</Button>\n      <Button variant=\"outline\">Outline</Button>\n      <Button variant=\"ghost\">Ghost</Button>\n      <Button variant=\"destructive\">Destructive</Button>\n      <Button variant=\"link\">Link</Button>\n    </HStack>\n  )\n}\n"
    },
    {
      "name": "sizes",
      "title": "Sizes",
      "code": "import { Button, HStack } from '@advui/core'\n\nexport default function ButtonSizes() {\n  return (\n    <HStack gap=\"2\" flexWrap=\"wrap\">\n      <Button size=\"sm\">Small</Button>\n      <Button size=\"md\">Medium</Button>\n      <Button size=\"lg\">Large</Button>\n    </HStack>\n  )\n}\n"
    },
    {
      "name": "with-icon",
      "title": "With icon",
      "code": "import { Button, HStack } from '@advui/core'\nimport { ArrowRightIcon, MailIcon, PlusIcon } from '@advui/icons'\n\nexport default function ButtonWithIcon() {\n  return (\n    <HStack gap=\"2\" flexWrap=\"wrap\">\n      <Button icon={<MailIcon />}>Login with email</Button>\n      <Button variant=\"outline\" icon={<PlusIcon />}>\n        New project\n      </Button>\n      <Button variant=\"secondary\" iconAfter={<ArrowRightIcon />}>\n        Continue\n      </Button>\n    </HStack>\n  )\n}\n"
    },
    {
      "name": "loading",
      "title": "Loading",
      "code": "import { Button, HStack } from '@advui/core'\nimport { useState } from 'react'\n\nexport default function ButtonLoading() {\n  const [loading, setLoading] = useState(false)\n  const submit = () => {\n    setLoading(true)\n    setTimeout(() => setLoading(false), 1500)\n  }\n  return (\n    <HStack gap=\"2\" flexWrap=\"wrap\">\n      <Button loading={loading} onPress={submit}>\n        {loading ? 'Saving…' : 'Save'}\n      </Button>\n      <Button variant=\"outline\" loading>\n        Please wait\n      </Button>\n    </HStack>\n  )\n}\n"
    },
    {
      "name": "disabled",
      "title": "Disabled",
      "code": "import { Button, HStack } from '@advui/core'\n\nexport default function ButtonDisabled() {\n  return (\n    <HStack gap=\"2\" flexWrap=\"wrap\">\n      <Button disabled>Default</Button>\n      <Button variant=\"outline\" disabled>\n        Outline\n      </Button>\n    </HStack>\n  )\n}\n"
    },
    {
      "name": "full-width",
      "title": "Full width (mobile)",
      "code": "import { Button, VStack } from '@advui/core'\n\nexport default function ButtonFullWidth() {\n  return (\n    <VStack gap=\"2\" width=\"100%\" maxWidth=\"80\">\n      <Button fullWidth size=\"lg\">\n        Create account\n      </Button>\n      <Button fullWidth size=\"lg\" variant=\"ghost\">\n        I already have an account\n      </Button>\n    </VStack>\n  )\n}\n"
    }
  ],
  "docs": "/docs/components/button",
  "dependencies": [
    "@advui/icons"
  ],
  "peerDependencies": [
    "react",
    "tamagui"
  ],
  "registryDependencies": [
    "hooks",
    "spinner",
    "utils"
  ],
  "version": "0.14.0"
}
