{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "dropdown-menu-shortcut",
  "title": "Menu-shortcut dropdown",
  "description": "Styled dropdown: the Menu-shortcut set. 5 decorative dropdown variations (KbdMenu, InlineMenu, TrailingMenu, GroupedMenu, CompactMenu) on the ai2 token system, powered by framer-motion, reduced-motion aware and sized sm to xl. Part of the free styled layer.",
  "dependencies": [
    "motion@^12.42.2",
    "lucide-react@^1.23.0"
  ],
  "registryDependencies": [
    "@ai2/tokens"
  ],
  "files": [
    {
      "path": "registry/ai2/styled/dropdown-menu-shortcut.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport {\n  ChevronDown,\n  Clipboard,\n  Copy,\n  FilePlus,\n  FolderOpen,\n  Redo2,\n  Save,\n  Scissors,\n  Search,\n  Undo2,\n} from \"lucide-react\"\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\"\n\nimport { cn } from \"@/lib/utils\"\n\n/* Shortcut dropdown family: 5 presentations of keyboard shortcuts. The shared idea\n   is that every item carries a shortcut - the variants change HOW the shortcut is\n   shown: kbd keys, inline text, a right-aligned trail, grouped sections and a\n   tight/compact list. Each export is a complete dropdown: a relative wrapper + a\n   trigger + a panel absolutely positioned below the trigger. NO radix or portal. It\n   closes on an outside click, on Escape (focus returns to the trigger) and on an\n   item selection. The arrow keys move between items. The shortcut text is only a\n   visual hint; it does not bind a global listener. Color comes ONLY from tokens,\n   via alpha color-mix. */\n\nexport type StyledSize = \"sm\" | \"md\" | \"lg\" | \"xl\"\n\nconst panelWidth: Record<StyledSize, string> = {\n  sm: \"w-52\",\n  md: \"w-60\",\n  lg: \"w-68\",\n  xl: \"w-76\",\n}\n\nconst itemHeight: Record<StyledSize, string> = {\n  sm: \"min-h-8 py-1.5\",\n  md: \"min-h-9 py-2\",\n  lg: \"min-h-10 py-2.5\",\n  xl: \"min-h-11 py-3\",\n}\n\nconst compactHeight: Record<StyledSize, string> = {\n  sm: \"min-h-6 py-0.5\",\n  md: \"min-h-7 py-1\",\n  lg: \"min-h-8 py-1.5\",\n  xl: \"min-h-9 py-2\",\n}\n\nexport interface StyledMenuItem {\n  label: React.ReactNode\n  icon?: React.ReactNode\n  description?: React.ReactNode\n  /* Gorsel kisayol ipucu, or. \"Ctrl+K\" veya \"Ctrl K\". */\n  shortcut?: string\n  onSelect?: () => void\n}\n\ninterface StyledMenuProps {\n  className?: string\n  size?: StyledSize\n  label?: React.ReactNode\n  items?: StyledMenuItem[]\n}\n\nconst triggerBtn =\n  \"inline-flex h-9 shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-lg border border-border bg-secondary px-4 text-sm font-medium text-secondary-foreground outline-none transition-colors hover:bg-[color-mix(in_oklab,var(--color-foreground)_6%,transparent)] focus-visible:ring-[3px] focus-visible:ring-ring/50 [&>svg]:size-4 [&>svg]:shrink-0 [&>i]:text-base [&>i]:leading-none\"\n\nconst panelBase =\n  \"origin-top rounded-xl border border-border bg-popover p-1.5 text-sm text-popover-foreground shadow-lg outline-none\"\n\nconst itemBase =\n  \"flex w-full cursor-default select-none items-center gap-2.5 rounded-md px-3 text-left text-sm text-popover-foreground outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:bg-accent focus-visible:text-accent-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 [&_svg]:size-4 [&_svg]:shrink-0 [&_i]:text-base [&_i]:leading-none\"\n\nconst kbdChip =\n  \"inline-flex h-5 min-w-5 select-none items-center justify-center rounded border border-border bg-[color-mix(in_oklab,var(--color-foreground)_5%,transparent)] px-1.5 font-mono text-[0.6875rem] font-medium text-muted-foreground\"\n\nconst menuMotion = {\n  initial: { opacity: 0, scale: 0.96, y: -6 },\n  animate: { opacity: 1, scale: 1, y: 0 },\n  exit: { opacity: 0, scale: 0.96, y: -6 },\n  transition: { type: \"spring\" as const, stiffness: 340, damping: 26 },\n}\n\nconst fade = { initial: { opacity: 0 }, animate: { opacity: 1 }, exit: { opacity: 0 } }\n\nfunction useMenuState() {\n  const [open, setOpen] = React.useState(false)\n  const wrapperRef = React.useRef<HTMLDivElement>(null)\n  const triggerRef = React.useRef<HTMLButtonElement>(null)\n\n  React.useEffect(() => {\n    if (!open) return\n    const onKey = (e: KeyboardEvent) => {\n      if (e.key === \"Escape\") {\n        setOpen(false)\n        triggerRef.current?.focus()\n      }\n    }\n    const onPointer = (e: PointerEvent) => {\n      const node = wrapperRef.current\n      if (node && e.target instanceof Node && !node.contains(e.target)) {\n        setOpen(false)\n      }\n    }\n    window.addEventListener(\"keydown\", onKey)\n    window.addEventListener(\"pointerdown\", onPointer)\n    return () => {\n      window.removeEventListener(\"keydown\", onKey)\n      window.removeEventListener(\"pointerdown\", onPointer)\n    }\n  }, [open])\n\n  return { open, setOpen, wrapperRef, triggerRef }\n}\n\nfunction useArrowNav(panelRef: React.RefObject<HTMLDivElement | null>) {\n  return React.useCallback(\n    (e: React.KeyboardEvent<HTMLDivElement>) => {\n      if (e.key !== \"ArrowDown\" && e.key !== \"ArrowUp\") return\n      const panel = panelRef.current\n      if (!panel) return\n      const nodes = Array.from(\n        panel.querySelectorAll<HTMLButtonElement>(\n          '[role=\"menuitem\"],[role=\"menuitemcheckbox\"]'\n        )\n      )\n      if (nodes.length === 0) return\n      e.preventDefault()\n      const current = nodes.indexOf(document.activeElement as HTMLButtonElement)\n      const next =\n        e.key === \"ArrowDown\"\n          ? (current + 1) % nodes.length\n          : current <= 0\n            ? nodes.length - 1\n            : current - 1\n      nodes[next]?.focus()\n    },\n    [panelRef]\n  )\n}\n\nfunction MenuShell({\n  size = \"md\",\n  label = \"Options\",\n  className,\n  children,\n}: {\n  size?: StyledSize\n  label?: React.ReactNode\n  className?: string\n  children: (close: () => void) => React.ReactNode\n}) {\n  const { open, setOpen, wrapperRef, triggerRef } = useMenuState()\n  const panelRef = React.useRef<HTMLDivElement>(null)\n  const onArrow = useArrowNav(panelRef)\n  const reduce = useReducedMotion()\n  const id = React.useId()\n  const panelId = `${id}-menu`\n\n  const active = reduce ? fade : menuMotion\n\n  return (\n    <div\n      ref={wrapperRef}\n      data-slot=\"styled-dropdown-menu\"\n      className={cn(\"relative inline-flex\", className)}\n    >\n      <button\n        ref={triggerRef}\n        type=\"button\"\n        data-slot=\"styled-dropdown-menu-trigger\"\n        aria-haspopup=\"menu\"\n        aria-expanded={open}\n        aria-controls={open ? panelId : undefined}\n        className={triggerBtn}\n        onClick={() => setOpen(!open)}\n      >\n        {label}\n        <ChevronDown\n          className={cn(\"transition-transform duration-200\", open && \"rotate-180\")}\n        />\n      </button>\n\n      <AnimatePresence>\n        {open ? (\n          <div className={cn(\"absolute left-0 top-full z-50 mt-1\", panelWidth[size])}>\n            <motion.div\n              ref={panelRef}\n              id={panelId}\n              role=\"menu\"\n              data-slot=\"styled-dropdown-menu-content\"\n              className={panelBase}\n              initial={active.initial}\n              animate={active.animate}\n              exit={active.exit}\n              transition={reduce ? { duration: 0.16 } : menuMotion.transition}\n              onKeyDown={onArrow}\n            >\n              {children(() => setOpen(false))}\n            </motion.div>\n          </div>\n        ) : null}\n      </AnimatePresence>\n    </div>\n  )\n}\n\n/* Splits a shortcut like \"Ctrl+K\" into individual keys. */\nfunction keysOf(shortcut: string): string[] {\n  return shortcut.split(/[+\\s]+/).filter(Boolean)\n}\n\nconst editItems: StyledMenuItem[] = [\n  { label: \"Undo\", icon: <Undo2 />, shortcut: \"Ctrl+Z\" },\n  { label: \"Redo\", icon: <Redo2 />, shortcut: \"Ctrl+Y\" },\n  { label: \"Cut\", icon: <Scissors />, shortcut: \"Ctrl+X\" },\n  { label: \"Copy\", icon: <Copy />, shortcut: \"Ctrl+C\" },\n  { label: \"Paste\", icon: <Clipboard />, shortcut: \"Ctrl+V\" },\n]\n\nconst fileItems: StyledMenuItem[] = [\n  { label: \"New file\", icon: <FilePlus />, shortcut: \"Ctrl+N\" },\n  { label: \"Open\", icon: <FolderOpen />, shortcut: \"Ctrl+O\" },\n  { label: \"Save\", icon: <Save />, shortcut: \"Ctrl+S\" },\n  { label: \"Search\", icon: <Search />, shortcut: \"Ctrl+F\" },\n]\n\n/* Kbd: kisayol gercek kbd tuslari olarak, sagda hizali cipler halinde. */\nexport function KbdMenu({ className, size = \"md\", label = \"Edit\", items }: StyledMenuProps) {\n  const list = items ?? editItems\n  return (\n    <MenuShell size={size} label={label} className={className}>\n      {(close) => (\n        <div className=\"flex flex-col\">\n          {list.map((item, i) => (\n            <button\n              key={`${i}`}\n              type=\"button\"\n              role=\"menuitem\"\n              tabIndex={0}\n              className={cn(itemBase, itemHeight[size])}\n              onClick={() => {\n                item.onSelect?.()\n                close()\n              }}\n            >\n              {item.icon ? (\n                <span className=\"flex shrink-0 items-center text-muted-foreground\">\n                  {item.icon}\n                </span>\n              ) : null}\n              <span className=\"flex-1\">{item.label}</span>\n              {item.shortcut ? (\n                <span className=\"flex shrink-0 items-center gap-1\">\n                  {keysOf(item.shortcut).map((key, k) => (\n                    <kbd key={`${k}`} className={kbdChip}>\n                      {key}\n                    </kbd>\n                  ))}\n                </span>\n              ) : null}\n            </button>\n          ))}\n        </div>\n      )}\n    </MenuShell>\n  )\n}\n\n/* Inline: the shortcut sits right after the label as muted inline text. */\nexport function InlineMenu({ className, size = \"md\", label = \"File\", items }: StyledMenuProps) {\n  const list = items ?? fileItems\n  return (\n    <MenuShell size={size} label={label} className={className}>\n      {(close) => (\n        <div className=\"flex flex-col\">\n          {list.map((item, i) => (\n            <button\n              key={`${i}`}\n              type=\"button\"\n              role=\"menuitem\"\n              tabIndex={0}\n              className={cn(itemBase, itemHeight[size])}\n              onClick={() => {\n                item.onSelect?.()\n                close()\n              }}\n            >\n              {item.icon ? (\n                <span className=\"flex shrink-0 items-center text-muted-foreground\">\n                  {item.icon}\n                </span>\n              ) : null}\n              <span className=\"truncate\">{item.label}</span>\n              {item.shortcut ? (\n                <span className=\"shrink-0 font-mono text-xs text-muted-foreground\">\n                  {item.shortcut}\n                </span>\n              ) : null}\n            </button>\n          ))}\n        </div>\n      )}\n    </MenuShell>\n  )\n}\n\n/* Trailing: the shortcut aligns to a single monospace track on the right, giving a fixed-width column feel. */\nexport function TrailingMenu({ className, size = \"md\", label = \"Actions\", items }: StyledMenuProps) {\n  const list = items ?? editItems\n  return (\n    <MenuShell size={size} label={label} className={className}>\n      {(close) => (\n        <div className=\"flex flex-col\">\n          {list.map((item, i) => (\n            <button\n              key={`${i}`}\n              type=\"button\"\n              role=\"menuitem\"\n              tabIndex={0}\n              className={cn(itemBase, itemHeight[size])}\n              onClick={() => {\n                item.onSelect?.()\n                close()\n              }}\n            >\n              {item.icon ? (\n                <span className=\"flex shrink-0 items-center text-muted-foreground\">\n                  {item.icon}\n                </span>\n              ) : null}\n              <span className=\"flex-1 truncate\">{item.label}</span>\n              {item.shortcut ? (\n                <span className=\"w-16 shrink-0 text-right font-mono text-xs tracking-tight text-muted-foreground\">\n                  {item.shortcut}\n                </span>\n              ) : null}\n            </button>\n          ))}\n        </div>\n      )}\n    </MenuShell>\n  )\n}\n\ninterface GroupedMenuProps extends StyledMenuProps {\n  groups?: { heading: React.ReactNode; items: StyledMenuItem[] }[]\n}\n\nconst groupData: { heading: React.ReactNode; items: StyledMenuItem[] }[] = [\n  {\n    heading: \"File\",\n    items: [\n      { label: \"New file\", icon: <FilePlus />, shortcut: \"Ctrl+N\" },\n      { label: \"Save\", icon: <Save />, shortcut: \"Ctrl+S\" },\n    ],\n  },\n  {\n    heading: \"Edit\",\n    items: [\n      { label: \"Copy\", icon: <Copy />, shortcut: \"Ctrl+C\" },\n      { label: \"Paste\", icon: <Clipboard />, shortcut: \"Ctrl+V\" },\n    ],\n  },\n]\n\n/* Grouped: kisayollu itemlar baslikli bolumlere ayrilir, aralarinda ayirici. */\nexport function GroupedMenu({ className, size = \"md\", label = \"Commands\", groups }: GroupedMenuProps) {\n  const data = groups ?? groupData\n  return (\n    <MenuShell size={size} label={label} className={className}>\n      {(close) => (\n        <div className=\"flex flex-col\">\n          {data.map((group, gi) => (\n            <div key={`${gi}`} className=\"flex flex-col\">\n              {gi > 0 ? (\n                <div\n                  role=\"separator\"\n                  className=\"my-1.5 h-px bg-[color-mix(in_oklab,var(--color-border)_100%,transparent)]\"\n                />\n              ) : null}\n              <div className=\"px-3 py-1.5 text-xs font-medium text-muted-foreground\">\n                {group.heading}\n              </div>\n              {group.items.map((item, i) => (\n                <button\n                  key={`${gi}-${i}`}\n                  type=\"button\"\n                  role=\"menuitem\"\n                  tabIndex={0}\n                  className={cn(itemBase, itemHeight[size])}\n                  onClick={() => {\n                    item.onSelect?.()\n                    close()\n                  }}\n                >\n                  {item.icon ? (\n                    <span className=\"flex shrink-0 items-center text-muted-foreground\">\n                      {item.icon}\n                    </span>\n                  ) : null}\n                  <span className=\"flex-1 truncate\">{item.label}</span>\n                  {item.shortcut ? (\n                    <span className=\"flex shrink-0 items-center gap-1\">\n                      {keysOf(item.shortcut).map((key, k) => (\n                        <kbd key={`${k}`} className={kbdChip}>\n                          {key}\n                        </kbd>\n                      ))}\n                    </span>\n                  ) : null}\n                </button>\n              ))}\n            </div>\n          ))}\n        </div>\n      )}\n    </MenuShell>\n  )\n}\n\nconst compactItems: StyledMenuItem[] = [\n  { label: \"New file\", shortcut: \"Ctrl+N\" },\n  { label: \"Save\", shortcut: \"Ctrl+S\" },\n  { label: \"Search\", shortcut: \"Ctrl+F\" },\n  { label: \"Settings\", shortcut: \"Ctrl+,\" },\n  { label: \"Delete\", shortcut: \"Del\" },\n]\n\n/* Compact: no icons, tight rows - for a shortcut-heavy command list. */\nexport function CompactMenu({ className, size = \"md\", label = \"Quick\", items }: StyledMenuProps) {\n  const list = items ?? compactItems\n  return (\n    <MenuShell size={size} label={label} className={className}>\n      {(close) => (\n        <div className=\"flex flex-col\">\n          {list.map((item, i) => (\n            <button\n              key={`${i}`}\n              type=\"button\"\n              role=\"menuitem\"\n              tabIndex={0}\n              className={cn(\n                itemBase,\n                compactHeight[size],\n                \"gap-2 px-2 text-xs text-muted-foreground\"\n              )}\n              onClick={() => {\n                item.onSelect?.()\n                close()\n              }}\n            >\n              <span className=\"flex-1 truncate\">{item.label}</span>\n              {item.shortcut ? (\n                <span className=\"shrink-0 font-mono text-[0.6875rem]\">{item.shortcut}</span>\n              ) : null}\n            </button>\n          ))}\n        </div>\n      )}\n    </MenuShell>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/ui/dropdown-menu-shortcut.tsx"
    }
  ],
  "categories": [
    "styled",
    "dropdown"
  ],
  "type": "registry:component"
}