{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "row-actions",
  "type": "registry:ui",
  "title": "Row Actions",
  "description": "Quick actions slide in over the row's meta, the rest wait in a menu.",
  "categories": [
    "menus"
  ],
  "dependencies": [
    "@base-ui/react"
  ],
  "registryDependencies": [
    "https://www.stealth.pm/r/tokens.json",
    "https://www.stealth.pm/r/cn.json",
    "https://www.stealth.pm/r/icons.json",
    "https://www.stealth.pm/r/tooltip.json"
  ],
  "files": [
    {
      "path": "components/ui/row-actions.tsx",
      "content": "\"use client\";\nimport { Menu } from \"@base-ui/react/menu\";\nimport { Fragment, useSyncExternalStore } from \"react\";\nimport { cn } from \"@/lib/cn\";\nimport { MoreH } from \"@/lib/icons\";\nimport { Tooltip, TooltipProvider, TooltipShortcut } from \"@/components/ui/tooltip\";\n\nexport type RowAction = {\n  id: string;\n  label: string;\n  icon?: React.ReactNode;\n  onSelect: () => void;\n  /** Shown inline on hover and focus. Everything else lives in the menu; on touch, everything does. */\n  inline?: boolean;\n  /** A single key (e.g. \"e\") that runs the action while the row has focus, shown in tooltips and the menu. */\n  shortcut?: string;\n  tone?: \"default\" | \"danger\";\n  disabled?: boolean;\n  /** Draw a separator above this item in the menu. */\n  separated?: boolean;\n};\n\n// Touch screens have no hover to reveal inline actions, so they move into the menu. The inline\n// buttons hide with CSS (no hydration risk); the menu, which only renders once opened, asks JS.\nconst coarse = \"(hover: none), (pointer: coarse)\";\nconst subscribe = (cb: () => void) => {\n  const mq = window.matchMedia(coarse);\n  mq.addEventListener(\"change\", cb);\n  return () => mq.removeEventListener(\"change\", cb);\n};\nfunction useTouch() {\n  return useSyncExternalStore(subscribe, () => window.matchMedia(coarse).matches, () => false);\n}\n\nexport type RowActionsProps = Omit<React.ComponentProps<\"div\">, \"children\"> & {\n  /** The row's name, used to label its actions for screen readers: \"Actions for Launch review\". */\n  label: string;\n  actions: RowAction[];\n  /** The row's content. Make its primary link or button cover the row with after:absolute after:inset-0. */\n  children: React.ReactNode;\n  /** Trailing meta (a time, a count) that gives way to the actions on hover instead of being pushed aside. */\n  trailing?: React.ReactNode;\n  /** Label of the overflow button. */\n  menuLabel?: string;\n};\n\n/**\n * A list row whose actions slide in from the right when it's hovered or focused, over the\n * trailing meta rather than beside it, so nothing in the row reflows. A menu holds the rest.\n */\nexport function RowActions({ label, actions, children, trailing, menuLabel = \"More actions\", className, onKeyDown, ...rest }: RowActionsProps) {\n  const touch = useTouch();\n  const inline = actions.filter((a) => a.inline);\n  const menu = touch ? actions : actions.filter((a) => !a.inline);\n\n  return (\n    <div\n      data-slot=\"row\"\n      className={cn(\n        \"group/row relative isolate flex min-h-12 items-center gap-3 rounded-lg px-2.5 py-2 transition-colors duration-150\",\n        \"hover:bg-fg/[0.035] focus-within:bg-fg/[0.035] has-data-popup-open:bg-fg/[0.035]\",\n        className,\n      )}\n      onKeyDown={(e) => {\n        onKeyDown?.(e);\n        if (e.defaultPrevented || e.metaKey || e.ctrlKey || e.altKey || e.key.length !== 1) return;\n        if ((e.target as HTMLElement).closest(\"input,textarea,[contenteditable=true],[role=menu]\")) return;\n        const hit = actions.find((a) => a.shortcut?.toLowerCase() === e.key.toLowerCase() && !a.disabled);\n        if (hit) {\n          e.preventDefault();\n          hit.onSelect();\n        }\n      }}\n      {...rest}\n    >\n      <div className=\"min-w-0 flex-1\">{children}</div>\n\n      {/* One grid cell holds both the meta and the actions, so the row keeps its width either way. */}\n      <div className=\"relative grid shrink-0 items-center justify-items-end pointer-coarse:flex pointer-coarse:gap-2\">\n        {trailing != null && (\n          <div\n            className={cn(\n              \"col-start-1 row-start-1 flex items-center text-[12px] text-fg-3 tabular transition-[opacity,translate] duration-150 ease-out-quart\",\n              \"pointer-fine:group-hover/row:-translate-x-1 pointer-fine:group-hover/row:opacity-0\",\n              \"pointer-fine:group-focus-within/row:-translate-x-1 pointer-fine:group-focus-within/row:opacity-0\",\n              \"pointer-fine:group-has-data-popup-open/row:opacity-0\",\n              \"motion-reduce:translate-none\",\n            )}\n          >\n            {trailing}\n          </div>\n        )}\n\n        <div\n          role=\"group\"\n          aria-label={`Actions for ${label}`}\n          className={cn(\n            \"relative z-10 col-start-1 row-start-1 flex items-center gap-0.5\",\n            // Hidden until the row is hovered or focused; still in the tab order, so focusing an action reveals it.\n            \"pointer-fine:opacity-0 pointer-fine:group-hover/row:opacity-100 pointer-fine:group-focus-within/row:opacity-100 pointer-fine:group-has-data-popup-open/row:opacity-100\",\n            \"transition-opacity duration-150\",\n          )}\n        >\n          <TooltipProvider delay={600}>\n            {inline.map((a, i) => (\n              <Tooltip key={a.id} content={a.label} shortcut={a.shortcut ? [a.shortcut] : undefined}>\n                <button\n                  type=\"button\"\n                  aria-label={a.label}\n                  aria-keyshortcuts={a.shortcut?.toUpperCase()}\n                  disabled={a.disabled}\n                  onClick={a.onSelect}\n                  // Buttons arrive from the right, nearest the edge first, 20ms apart; they leave together.\n                  style={{ \"--i\": inline.length - i } as React.CSSProperties}\n                  className={cn(\n                    \"relative hidden size-7 items-center justify-center rounded-md text-fg-3 outline-none pointer-fine:inline-flex\",\n                    \"transition-[opacity,translate,background-color,color,scale] duration-200 ease-out-expo\",\n                    \"translate-x-2 opacity-0 group-hover/row:translate-x-0 group-hover/row:opacity-100 group-focus-within/row:translate-x-0 group-focus-within/row:opacity-100\",\n                    \"group-hover/row:[transition-delay:calc(var(--i)*20ms),calc(var(--i)*20ms),0ms,0ms,0ms]\",\n                    \"hover:bg-fg/[0.07] hover:text-fg active:scale-[0.9] active:duration-75\",\n                    a.tone === \"danger\" && \"hover:bg-danger-soft hover:text-danger\",\n                    \"focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-offset-0 focus-visible:outline-fg-3\",\n                    \"disabled:pointer-events-none disabled:opacity-40\",\n                    \"motion-reduce:translate-x-0 [&_svg]:size-4\",\n                  )}\n                >\n                  {a.icon}\n                </button>\n              </Tooltip>\n            ))}\n          </TooltipProvider>\n\n          {menu.length > 0 && (\n            <Menu.Root>\n              <Menu.Trigger\n                aria-label={menuLabel}\n                className={cn(\n                  \"relative inline-flex size-7 items-center justify-center rounded-md text-fg-3 outline-none\",\n                  \"transition-[background-color,color,scale] duration-150 hover:bg-fg/[0.07] hover:text-fg active:scale-[0.9] active:duration-75\",\n                  \"data-popup-open:bg-fg/[0.09] data-popup-open:text-fg\",\n                  \"focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-offset-0 focus-visible:outline-fg-3\",\n                  \"before:absolute before:-inset-2 before:content-[''] pointer-fine:before:hidden\",\n                )}\n              >\n                <MoreH />\n              </Menu.Trigger>\n              <Menu.Portal>\n                <Menu.Positioner side=\"bottom\" align=\"end\" sideOffset={4} collisionPadding={8} className=\"z-(--z-dropdown)\">\n                  <Menu.Popup\n                    className={cn(\n                      \"min-w-44 max-w-(--available-width) rounded-xl border border-line-2 bg-raised p-1 text-fg shadow-pop outline-none\",\n                      \"origin-(--transform-origin) transition-[opacity,scale,translate] duration-160 ease-out-expo\",\n                      \"data-starting-style:scale-96 data-starting-style:opacity-0 data-starting-style:-translate-y-1\",\n                      \"data-ending-style:scale-98 data-ending-style:opacity-0 data-ending-style:duration-100\",\n                      \"data-instant:transition-none motion-reduce:data-starting-style:scale-100 motion-reduce:data-starting-style:translate-y-0\",\n                    )}\n                  >\n                    {menu.map((a, i) => (\n                      <Fragment key={a.id}>\n                        {i > 0 && (a.separated || (touch && i === inline.length)) && <Menu.Separator className=\"mx-2 my-1 h-px bg-line\" />}\n                        <Menu.Item\n                          disabled={a.disabled}\n                          onClick={a.onSelect}\n                          className={cn(\n                            \"flex h-8 cursor-default select-none items-center gap-2.5 rounded-lg px-2 text-[13px] outline-none pointer-coarse:h-10 pointer-coarse:text-[14px]\",\n                            \"transition-colors duration-75 data-highlighted:bg-fg/[0.06]\",\n                            \"data-disabled:opacity-40 [&_svg]:size-4 [&_svg]:shrink-0\",\n                            a.tone === \"danger\" ? \"text-danger data-highlighted:bg-danger-soft\" : \"text-fg [&_svg]:text-fg-3\",\n                          )}\n                        >\n                          {a.icon}\n                          <span className=\"min-w-0 flex-1 truncate\">{a.label}</span>\n                          {a.shortcut && !touch && <TooltipShortcut keys={[a.shortcut]} className=\"mr-0 opacity-80\" />}\n                        </Menu.Item>\n                      </Fragment>\n                    ))}\n                  </Menu.Popup>\n                </Menu.Positioner>\n              </Menu.Portal>\n            </Menu.Root>\n          )}\n        </div>\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "docs": "https://www.stealth.pm/ui/row-actions"
}
