{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "dropdown-menu-placement",
  "title": "Menu-placement dropdown",
  "description": "Styled dropdown: the Menu-placement set. 5 decorative dropdown variations (BottomMenu, TopMenu, LeftMenu, RightMenu, CenterMenu) 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-placement.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport {\n  Bell,\n  ChevronDown,\n  CreditCard,\n  Layers,\n  LogOut,\n  Settings,\n  User,\n} from \"lucide-react\"\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\"\n\nimport { cn } from \"@/lib/utils\"\n\n/* Placement dropdown family: 5 placement directions. The panel is the same clear\n   popover surface in every variant - the whole difference is WHERE it attaches\n   relative to the trigger: bottom, top, left, right and bottom-center. The panel is\n   always positioned relative to ITS OWN trigger (absolute inside a relative\n   wrapper), never pinned to the viewport; that is what lets dozens of examples work\n   correctly side by side on one page. NO radix or portal. It closes on an outside\n   click, on Escape (focus returns to the trigger) and on an item selection. The\n   arrow keys move between items. Color comes ONLY from tokens, via alpha\n   color-mix. */\n\nexport type StyledSize = \"sm\" | \"md\" | \"lg\" | \"xl\"\n\nconst panelWidth: Record<StyledSize, string> = {\n  sm: \"w-48\",\n  md: \"w-56\",\n  lg: \"w-64\",\n  xl: \"w-72\",\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\nexport interface StyledMenuItem {\n  label: React.ReactNode\n  icon?: React.ReactNode\n  description?: React.ReactNode\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  \"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 fade = { initial: { opacity: 0 }, animate: { opacity: 1 }, exit: { opacity: 0 } }\n\ninterface Placement {\n  /* Konumlandirici wrapper sinifi - panelin trigger'a gore yeri. */\n  anchor: string\n  /* The panel's transform origin. */\n  origin: string\n  /* Giris/cikis kaymasinin yonu. */\n  offset: { x?: number; y?: number }\n  /* Trigger ikonunun acikken donme yonu. */\n  chevron: string\n}\n\nconst spring = { type: \"spring\" as const, stiffness: 340, damping: 26 }\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\n/* Shared shell: the positioner (absolute) wrapper is built from the placement supplied from outside; the animation is carried by the inner motion panel, so placement and transform never get tangled. */\nfunction MenuShell({\n  size = \"md\",\n  label = \"Options\",\n  className,\n  placement,\n  children,\n}: {\n  size?: StyledSize\n  label?: React.ReactNode\n  className?: string\n  placement: Placement\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 spec = reduce\n    ? fade\n    : {\n        initial: { opacity: 0, scale: 0.96, ...placement.offset },\n        animate: { opacity: 1, scale: 1, x: 0, y: 0 },\n        exit: { opacity: 0, scale: 0.96, ...placement.offset },\n      }\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 && placement.chevron)}\n        />\n      </button>\n\n      <AnimatePresence>\n        {open ? (\n          <div className={cn(\"absolute z-50\", placement.anchor, panelWidth[size])}>\n            <motion.div\n              ref={panelRef}\n              id={panelId}\n              role=\"menu\"\n              data-slot=\"styled-dropdown-menu-content\"\n              className={cn(panelBase, placement.origin)}\n              initial={spec.initial}\n              animate={spec.animate}\n              exit={spec.exit}\n              transition={reduce ? { duration: 0.16 } : spring}\n              onKeyDown={onArrow}\n            >\n              {children(() => setOpen(false))}\n            </motion.div>\n          </div>\n        ) : null}\n      </AnimatePresence>\n    </div>\n  )\n}\n\nfunction MenuList({\n  items,\n  size,\n  close,\n}: {\n  items: StyledMenuItem[]\n  size: StyledSize\n  close: () => void\n}) {\n  return (\n    <div className=\"flex flex-col\">\n      {items.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          {item.label}\n        </button>\n      ))}\n    </div>\n  )\n}\n\nconst accountItems: StyledMenuItem[] = [\n  { label: \"Profile\", icon: <User /> },\n  { label: \"Billing\", icon: <CreditCard /> },\n  { label: \"Settings\", icon: <Settings /> },\n  { label: \"Sign out\", icon: <LogOut /> },\n]\n\nconst workspaceItems: StyledMenuItem[] = [\n  { label: \"Overview\", icon: <Layers /> },\n  { label: \"Members\", icon: <User /> },\n  { label: \"Notifications\", icon: <Bell /> },\n  { label: \"Settings\", icon: <Settings /> },\n]\n\n/* Bottom: the default direction - the panel hangs below the trigger, aligned to its left edge. */\nexport function BottomMenu({ className, size = \"md\", label = \"Bottom\", items }: StyledMenuProps) {\n  return (\n    <MenuShell\n      size={size}\n      label={label}\n      className={className}\n      placement={{\n        anchor: \"left-0 top-full mt-1\",\n        origin: \"origin-top-left\",\n        offset: { y: -6 },\n        chevron: \"rotate-180\",\n      }}\n    >\n      {(close) => <MenuList items={items ?? accountItems} size={size} close={close} />}\n    </MenuShell>\n  )\n}\n\n/* Top: the panel opens ABOVE the trigger - for triggers near the bottom of the page. */\nexport function TopMenu({ className, size = \"md\", label = \"Top\", items }: StyledMenuProps) {\n  return (\n    <MenuShell\n      size={size}\n      label={label}\n      className={className}\n      placement={{\n        anchor: \"bottom-full left-0 mb-1\",\n        origin: \"origin-bottom-left\",\n        offset: { y: 6 },\n        chevron: \"rotate-180\",\n      }}\n    >\n      {(close) => <MenuList items={items ?? accountItems} size={size} close={close} />}\n    </MenuShell>\n  )\n}\n\n/* Left: the panel opens to the LEFT of the trigger, top edges aligned. */\nexport function LeftMenu({ className, size = \"md\", label = \"Left\", items }: StyledMenuProps) {\n  return (\n    <MenuShell\n      size={size}\n      label={label}\n      className={className}\n      placement={{\n        anchor: \"right-full top-0 mr-1\",\n        origin: \"origin-top-right\",\n        offset: { x: 6 },\n        chevron: \"rotate-90\",\n      }}\n    >\n      {(close) => <MenuList items={items ?? workspaceItems} size={size} close={close} />}\n    </MenuShell>\n  )\n}\n\n/* Right: the panel opens to the RIGHT of the trigger, top edges aligned. */\nexport function RightMenu({ className, size = \"md\", label = \"Right\", items }: StyledMenuProps) {\n  return (\n    <MenuShell\n      size={size}\n      label={label}\n      className={className}\n      placement={{\n        anchor: \"left-full top-0 ml-1\",\n        origin: \"origin-top-left\",\n        offset: { x: -6 },\n        chevron: \"-rotate-90\",\n      }}\n    >\n      {(close) => <MenuList items={items ?? workspaceItems} size={size} close={close} />}\n    </MenuShell>\n  )\n}\n\n/* Center: the panel is CENTRED HORIZONTALLY under the trigger (left-1/2 plus -translate-x-1/2 live on the positioner, so they do not clash with the motion transform). */\nexport function CenterMenu({ className, size = \"md\", label = \"Center\", items }: StyledMenuProps) {\n  return (\n    <MenuShell\n      size={size}\n      label={label}\n      className={className}\n      placement={{\n        anchor: \"left-1/2 top-full mt-1 -translate-x-1/2\",\n        origin: \"origin-top\",\n        offset: { y: -6 },\n        chevron: \"rotate-180\",\n      }}\n    >\n      {(close) => <MenuList items={items ?? accountItems} size={size} close={close} />}\n    </MenuShell>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/ui/dropdown-menu-placement.tsx"
    }
  ],
  "categories": [
    "styled",
    "dropdown"
  ],
  "type": "registry:component"
}