{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "row-menu",
  "type": "registry:component",
  "title": "Row menu",
  "description": "A row's verbs behind three dots: icons, destructive items last and red, notes under disabled items; also at a page header.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "@wibo/utils",
    "@wibo/button",
    "@wibo/dropdown-menu"
  ],
  "files": [
    {
      "path": "src/patterns/row-menu.tsx",
      "type": "registry:component",
      "content": "import { useRef, type ReactNode } from \"react\";\nimport { MoreHorizontal } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport { Button } from \"@/components/ui/button\";\nimport { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from \"@/components/ui/dropdown-menu\";\n\n/*\n * A row's verbs behind three dots, on every row of a list that can be acted\n * on, and the same menu at the head of the object's own page (`placement`\n * `header`). Each item leads with its icon; destructive items come last,\n * after a separator, in red, and open a `ConfirmDialog`. A disabled item says\n * why on a `note` line under it (a disabled item takes no pointer, so it can\n * carry no tooltip). A menu with no items draws nothing.\n *\n * An item's `onSelect` runs once the menu has closed and given focus back to\n * its trigger, so the sheet or dialog it opens returns focus to the row when\n * it closes. Holds no copy. It holds state: render it from client components\n * and import it by path (`@wibo/ui/patterns/row-menu`).\n */\n\ntype RowMenuEntry =\n  | {\n      kind: \"item\";\n      key: string;\n      label: ReactNode;\n      icon?: ReactNode;\n      onSelect: () => void;\n      /** Red: the act takes something away. */\n      destructive?: boolean;\n      disabled?: boolean;\n    }\n  | { kind: \"separator\"; key: string }\n  /** A faint line of explanation, typically why the item above is disabled. */\n  | { kind: \"note\"; key: string; text: ReactNode };\n\ninterface RowMenuProps {\n  /** Accessible name of the trigger, naming the row (\"Actions for Olon\"). */\n  label: string;\n  entries: RowMenuEntry[];\n  /** `row` (default): a ghost icon button in a table cell; `header`: an outline icon button among a page header's actions. */\n  placement?: \"row\" | \"header\";\n  disabled?: boolean;\n  className?: string;\n}\n\nfunction RowMenu({ label, entries, placement = \"row\", disabled, className }: RowMenuProps) {\n  const pending = useRef<(() => void) | null>(null);\n  if (!entries.some((entry) => entry.kind === \"item\")) return null;\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger asChild>\n        <Button\n          variant={placement === \"header\" ? \"outline\" : \"ghost\"}\n          size=\"icon-sm\"\n          aria-label={label}\n          title={label}\n          disabled={disabled}\n          data-slot=\"row-menu-trigger\"\n          className={cn(placement === \"row\" && \"-my-1\", className)}\n        >\n          <MoreHorizontal aria-hidden=\"true\" />\n        </Button>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent\n        align=\"end\"\n        className=\"min-w-44\"\n        onCloseAutoFocus={() => {\n          // Focus is about to return to the trigger; the chosen act opens its layer after that.\n          const run = pending.current;\n          pending.current = null;\n          if (run) queueMicrotask(run);\n        }}\n      >\n        {entries.map((entry) =>\n          entry.kind === \"separator\" ? (\n            <DropdownMenuSeparator key={entry.key} />\n          ) : entry.kind === \"note\" ? (\n            <p key={entry.key} className=\"max-w-64 px-3 pt-0.5 pb-1.5 text-xs leading-snug text-muted-foreground\">\n              {entry.text}\n            </p>\n          ) : (\n            <DropdownMenuItem\n              key={entry.key}\n              variant={entry.destructive && !entry.disabled ? \"destructive\" : \"default\"}\n              disabled={entry.disabled}\n              onSelect={() => {\n                pending.current = entry.onSelect;\n              }}\n            >\n              {entry.icon}\n              {entry.label}\n            </DropdownMenuItem>\n          ),\n        )}\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n}\n\nexport { RowMenu };\nexport type { RowMenuEntry, RowMenuProps };\n"
    }
  ]
}
