{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "drawer-action",
  "type": "registry:ui",
  "title": "DrawerAction",
  "description": "A single action in a BottomDrawer footer: plain, form-submit, or tap-again-confirm.",
  "categories": [
    "actions",
    "overlays"
  ],
  "registryDependencies": [
    "https://whiskeyjack.net/r/announce.json",
    "https://whiskeyjack.net/r/utils.json"
  ],
  "files": [
    {
      "path": "components/ui/drawer-action.tsx",
      "type": "registry:ui",
      "target": "components/ui/drawer-action.tsx",
      "content": "import * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { announce } from \"@/lib/announce\";\n\nexport type DrawerActionTone = \"danger\" | \"warning\";\n\nexport interface DrawerActionProps {\n  /** Icon shown above the label (mobile) or beside it (desktop). Phosphor or any node; sized to 20px. */\n  icon: React.ReactNode;\n  /** Text label rendered below the icon. */\n  label: string;\n  /** Click handler for plain actions (e.g. Cancel). */\n  onClick?: () => void;\n  /**\n   * Button type. Use `\"submit\"` together with `form` so a Save action can\n   * submit a `<form>` rendered in the drawer's scrollable content (the footer\n   * sits outside that form in the DOM).\n   */\n  type?: \"button\" | \"submit\";\n  /** Associates a `type=\"submit\"` action with a form by its `id`. */\n  form?: string;\n  /** Disable the action (e.g. Save until the form is dirty/valid). */\n  disabled?: boolean;\n  /**\n   * Turns this into a tap-again-to-confirm action. The first activation arms it\n   * (icon + label swap to this confirm variant, tinted by `tone`); the second\n   * runs `onConfirm`. Pair with `armed` / `onArmedChange` to keep sibling\n   * destructive actions mutually exclusive and to reset on drawer close.\n   */\n  confirm?: {\n    icon: React.ReactNode;\n    label: string;\n    /** @default \"danger\" */\n    tone?: DrawerActionTone;\n  };\n  onConfirm?: () => void;\n  /** Controlled armed state. When omitted the component manages its own. */\n  armed?: boolean;\n  onArmedChange?: (armed: boolean) => void;\n  /** Accessible label when the visible label needs supplementing. */\n  \"aria-label\"?: string;\n  className?: string;\n  /** Inline styles for the root element. */\n  style?: React.CSSProperties;\n}\n\n/**\n * A single action in a `BottomDrawer` footer: an icon stacked above the label\n * on mobile (bottom-nav item style) and inline beside it on desktop, mirroring\n * how the app nav shifts from the mobile bottom bar to the desktop header.\n * Footers lay these out in an even flex row (most-destructive leftmost), e.g.\n * `Delete | Reset | Cancel | Save`.\n *\n * Two modes:\n * - **Plain** -- `onClick` (Cancel), or `type=\"submit\"` + `form` (Save).\n * - **Confirm** -- pass `confirm` + `onConfirm` for tap-again-to-confirm.\n *   At rest the action is untinted; only the armed confirmation step tints\n *   (`tone=\"danger\"` red, `tone=\"warning\"` amber).\n *\n * @example\n * ```tsx\n * <DrawerAction icon={<X />} label={t('common.cancel')} onClick={onClose} />\n * <DrawerAction icon={<Check />} label={t('common.save')} type=\"submit\" form=\"edit-goal\" disabled={!dirty} />\n * <DrawerAction\n *   icon={<Trash />} label={t('settings.removeGoal')}\n *   confirm={{ icon: <Warning />, label: t('common.confirm'), tone: 'danger' }}\n *   onConfirm={handleRemove} armed={removeArmed} onArmedChange={setRemoveArmed}\n * />\n * ```\n */\nexport const DrawerAction = React.forwardRef<HTMLButtonElement, DrawerActionProps>(function DrawerAction({\n  icon,\n  label,\n  onClick,\n  type = \"button\",\n  form,\n  disabled,\n  confirm,\n  onConfirm,\n  armed: controlledArmed,\n  onArmedChange,\n  className,\n  style,\n  ...rest\n}, ref) {\n  const [internalArmed, setInternalArmed] = React.useState(false);\n  const isControlled = controlledArmed !== undefined;\n  const armed = isControlled ? controlledArmed : internalArmed;\n\n  const setArmed = React.useCallback(\n    (next: boolean) => {\n      if (!isControlled) setInternalArmed(next);\n      onArmedChange?.(next);\n    },\n    [isControlled, onArmedChange]\n  );\n\n  const isConfirm = Boolean(confirm && onConfirm);\n\n  const handleClick = () => {\n    if (isConfirm) {\n      if (armed) {\n        onConfirm!();\n        setArmed(false);\n      } else {\n        setArmed(true);\n        // Announce the confirm label assertively so screen-reader users hear\n        // \"press again to confirm\" before the destructive second activation.\n        announce(confirm!.label, \"assertive\");\n      }\n      return;\n    }\n    onClick?.();\n  };\n\n  const showConfirm = isConfirm && armed;\n  const tone = confirm?.tone ?? \"danger\";\n\n  return (\n    <button\n      ref={ref}\n      type={type}\n      form={form}\n      disabled={disabled}\n      // A submit action lets the form handle activation; everything else\n      // (plain + confirm) runs through handleClick.\n      onClick={type === \"submit\" && !isConfirm ? onClick : handleClick}\n      style={style}\n      className={cn(\n        // Stacked (icon over label) on mobile -- like a bottom-nav item -- and\n        // inline (icon beside label) on desktop, mirroring how the app nav goes\n        // from the mobile bottom bar to the desktop header.\n        \"flex flex-1 min-w-0 flex-col md:flex-row items-center justify-center gap-1 md:gap-2\",\n        \"rounded-xl px-1.5 md:px-3 py-2.5 text-xs md:text-sm font-medium transition-colors\",\n        \"[&_svg]:size-5 wj-focus-ring\",\n        disabled\n          ? \"opacity-40 pointer-events-none text-[var(--color-text-muted-light)] dark:text-[var(--color-text-muted-dark)]\"\n          : showConfirm\n            ? tone === \"warning\"\n              ? \"text-[var(--color-warning-600)] dark:text-[var(--color-warning-400)]\"\n              : \"text-[var(--color-error-600)] dark:text-[var(--color-error-400)]\"\n            : \"text-[var(--color-text-secondary-light)] dark:text-[var(--color-text-secondary-dark)] hover:text-[var(--color-text-primary-light)] dark:hover:text-[var(--color-text-primary-dark)]\",\n        className\n      )}\n      {...rest}\n    >\n      <span aria-hidden className=\"flex items-center justify-center\">\n        {showConfirm ? confirm!.icon : icon}\n      </span>\n      <span className=\"leading-tight text-center\">\n        {showConfirm ? confirm!.label : label}\n      </span>\n    </button>\n  );\n});\n\nDrawerAction.displayName = \"DrawerAction\"\n"
    }
  ],
  "docs": "Modal actions belong in the drawer's sticky footer, never in scrollable content. Lay them out in an even row, most-destructive leftmost and primary rightmost: Delete, Reset, Cancel, Save. A Save uses type=\"submit\" with form=\"<id>\" so it can submit a form rendered in drawer content, and stays disabled until the form is dirty and valid.",
  "meta": {
    "group": "actions",
    "related": [
      "bottom-drawer",
      "confirm-button"
    ],
    "exports": [
      "DrawerAction"
    ],
    "siteSlug": "drawer-action"
  }
}
