{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "dropdown-menu-motion",
  "title": "Menu-motion dropdown",
  "description": "Styled dropdown: the Menu-motion set. 5 decorative dropdown variations (SlideMenu, PopMenu, FadeMenu, FlipMenu, SpringMenu) 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-motion.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/* Motion dropdown family: 5 open/close characters. The panel stays the same clear\n   popover surface in every variant - the whole difference is in the ENTER/EXIT\n   animation: slide, pop, fade, flip and spring. Each export is a complete dropdown:\n   a relative wrapper + a trigger + a panel absolutely positioned below the trigger.\n   NO radix or portal. It closes on an outside click, on Escape (focus returns to\n   the trigger) and on an item selection. The arrow keys move between items. No\n   transform under reduced motion, only a fade. Color comes ONLY from tokens, via\n   alpha 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 MotionSpec {\n  initial: Record<string, number>\n  animate: Record<string, number>\n  exit: Record<string, number>\n  transition: Record<string, unknown>\n  /* The panel's transform origin, meaningful for flip and pop. */\n  origin?: string\n}\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: motionSpec comes from outside, the rest is the same in every variant. */\nfunction MenuShell({\n  size = \"md\",\n  label = \"Options\",\n  className,\n  spec,\n  children,\n}: {\n  size?: StyledSize\n  label?: React.ReactNode\n  className?: string\n  spec: MotionSpec\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 : spec\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\n            className={cn(\"absolute left-0 top-full z-50 mt-1\", panelWidth[size])}\n            style={{ perspective: 800 }}\n          >\n            <motion.div\n              ref={panelRef}\n              id={panelId}\n              role=\"menu\"\n              data-slot=\"styled-dropdown-menu-content\"\n              className={cn(panelBase, spec.origin ?? \"origin-top\")}\n              initial={active.initial}\n              animate={active.animate}\n              exit={active.exit}\n              transition={reduce ? { duration: 0.16 } : spec.transition}\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/* Slide: panel yukaridan asagi kayarak girer, cikista geri kayar. */\nconst slideSpec: MotionSpec = {\n  initial: { opacity: 0, y: -10 },\n  animate: { opacity: 1, y: 0 },\n  exit: { opacity: 0, y: -10 },\n  transition: { duration: 0.18, ease: \"easeOut\" },\n}\n\nexport function SlideMenu({ className, size = \"md\", label = \"Slide\", items }: StyledMenuProps) {\n  return (\n    <MenuShell size={size} label={label} className={className} spec={slideSpec}>\n      {(close) => <MenuList items={items ?? accountItems} size={size} close={close} />}\n    </MenuShell>\n  )\n}\n\n/* Pop: bursts out of the trigger growing from small. */\nconst popSpec: MotionSpec = {\n  initial: { opacity: 0, scale: 0.82 },\n  animate: { opacity: 1, scale: 1 },\n  exit: { opacity: 0, scale: 0.82 },\n  transition: { type: \"spring\" as const, stiffness: 520, damping: 24 },\n  origin: \"origin-top-left\",\n}\n\nexport function PopMenu({ className, size = \"md\", label = \"Pop\", items }: StyledMenuProps) {\n  return (\n    <MenuShell size={size} label={label} className={className} spec={popSpec}>\n      {(close) => <MenuList items={items ?? workspaceItems} size={size} close={close} />}\n    </MenuShell>\n  )\n}\n\n/* Fade: no transform, opacity only. The calmest variant. */\nconst fadeSpec: MotionSpec = {\n  initial: { opacity: 0 },\n  animate: { opacity: 1 },\n  exit: { opacity: 0 },\n  transition: { duration: 0.2, ease: \"easeOut\" },\n}\n\nexport function FadeMenu({ className, size = \"md\", label = \"Fade\", items }: StyledMenuProps) {\n  return (\n    <MenuShell size={size} label={label} className={className} spec={fadeSpec}>\n      {(close) => <MenuList items={items ?? accountItems} size={size} close={close} />}\n    </MenuShell>\n  )\n}\n\n/* Flip: opens by rotating on the X axis from the top edge; the perspective comes from the wrapper. */\nconst flipSpec: MotionSpec = {\n  initial: { opacity: 0, rotateX: -70 },\n  animate: { opacity: 1, rotateX: 0 },\n  exit: { opacity: 0, rotateX: -70 },\n  transition: { duration: 0.24, ease: \"easeOut\" },\n}\n\nexport function FlipMenu({ className, size = \"md\", label = \"Flip\", items }: StyledMenuProps) {\n  return (\n    <MenuShell size={size} label={label} className={className} spec={flipSpec}>\n      {(close) => <MenuList items={items ?? workspaceItems} size={size} close={close} />}\n    </MenuShell>\n  )\n}\n\n/* Spring: a soft bouncy spring - it falls down and settles into place. */\nconst springSpec: MotionSpec = {\n  initial: { opacity: 0, y: -18, scale: 0.94 },\n  animate: { opacity: 1, y: 0, scale: 1 },\n  exit: { opacity: 0, y: -12, scale: 0.94 },\n  transition: { type: \"spring\" as const, stiffness: 260, damping: 14 },\n}\n\nexport function SpringMenu({ className, size = \"md\", label = \"Spring\", items }: StyledMenuProps) {\n  return (\n    <MenuShell size={size} label={label} className={className} spec={springSpec}>\n      {(close) => <MenuList items={items ?? accountItems} size={size} close={close} />}\n    </MenuShell>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/ui/dropdown-menu-motion.tsx"
    }
  ],
  "categories": [
    "styled",
    "dropdown"
  ],
  "type": "registry:component"
}