{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "animated-dropdown",
  "type": "registry:ui",
  "title": "Animated Dropdown",
  "description": "A composable, accessible dropdown menu built on Base UI with controlled state, interruptible hover highlighting, and origin-aware enter motion.",
  "files": [
    {
      "path": "registry/sonaui/animated-dropdown/animated-dropdown.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport { Menu } from \"@base-ui/react/menu\";\nimport {\n  AnimatePresence,\n  LayoutGroup,\n  motion,\n  useReducedMotion,\n} from \"motion/react\";\nimport {\n  createContext,\n  type ReactNode,\n  useContext,\n  useId,\n  useState,\n} from \"react\";\nimport { motionTransition } from \"@/lib/sona-motion\";\nimport { cn } from \"@/lib/sona-utils\";\n\n// ─── Context ─────────────────────────────────────────────────────────────────\n\ninterface DropdownContextValue {\n  /** Unique layoutId prefix for the hover highlight — scoped per Root instance. */\n  layoutId: string;\n  /** Currently highlighted item id (for keyboard + mouse parity). */\n  activeId: string | null;\n  setActiveId: (id: string | null) => void;\n}\n\nconst DropdownContext = createContext<DropdownContextValue | null>(null);\n\nfunction useDropdownContext() {\n  const ctx = useContext(DropdownContext);\n  if (!ctx)\n    throw new Error(\n      \"AnimatedDropdown subcomponents must be used within <AnimatedDropdown>\",\n    );\n  return ctx;\n}\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport interface AnimatedDropdownProps {\n  children: ReactNode;\n  /** Controlled open state. */\n  open?: boolean;\n  /** Initial open state for uncontrolled usage. @default false */\n  defaultOpen?: boolean;\n  /** Callback when open state changes. */\n  onOpenChange?: (open: boolean) => void;\n  /** Whether the menu ignores user interaction. @default false */\n  disabled?: boolean;\n  /** Whether the open menu limits interaction to the menu. @default true */\n  modal?: boolean;\n}\n\nexport interface AnimatedDropdownContentProps {\n  children: ReactNode;\n  className?: string;\n  /**\n   * Side the menu opens on.\n   * @default \"bottom\"\n   */\n  side?: \"bottom\" | \"top\" | \"left\" | \"right\";\n  /**\n   * Alignment along the side.\n   * @default \"center\"\n   */\n  align?: \"start\" | \"center\" | \"end\";\n  /**\n   * Gap between trigger and popup in px.\n   * @default 6\n   */\n  sideOffset?: number;\n}\n\nexport interface AnimatedDropdownItemProps {\n  children: ReactNode;\n  /** Icon to display before the label (any ReactNode — no HugeIcons dependency). */\n  icon?: ReactNode;\n  /**\n   * Visual variant.\n   * @default \"default\"\n   */\n  variant?: \"default\" | \"danger\";\n  disabled?: boolean;\n  onClick?: () => void;\n  className?: string;\n}\n\nexport interface AnimatedDropdownTriggerProps {\n  children: ReactNode;\n  className?: string;\n  /** Associates the trigger with an external visible label. */\n  \"aria-labelledby\"?: string;\n}\n\nexport interface AnimatedDropdownTriggerIndicatorProps {\n  className?: string;\n}\n\n// ─── Root ─────────────────────────────────────────────────────────────────────\n\n/**\n * Root dropdown — owns open state and the hover-highlight layout group.\n * Wrap all other AnimatedDropdown subcomponents inside this.\n */\nexport function AnimatedDropdown({\n  children,\n  open,\n  defaultOpen = false,\n  onOpenChange,\n  disabled = false,\n  modal = true,\n}: AnimatedDropdownProps) {\n  const layoutId = useId();\n  const [activeId, setActiveId] = useState<string | null>(null);\n\n  const handleOpenChange = (nextOpen: boolean) => {\n    if (!nextOpen) setActiveId(null);\n    onOpenChange?.(nextOpen);\n  };\n\n  return (\n    <DropdownContext.Provider value={{ layoutId, activeId, setActiveId }}>\n      <LayoutGroup id={layoutId}>\n        <Menu.Root\n          open={open}\n          defaultOpen={defaultOpen}\n          disabled={disabled}\n          modal={modal}\n          onOpenChange={handleOpenChange}\n        >\n          {children}\n        </Menu.Root>\n      </LayoutGroup>\n    </DropdownContext.Provider>\n  );\n}\n\n// ─── Trigger ──────────────────────────────────────────────────────────────────\n\n/**\n * The element that opens the dropdown when clicked.\n * Renders as a `<button>` by default via Base UI.\n */\nexport function AnimatedDropdownTrigger({\n  children,\n  className,\n  \"aria-labelledby\": ariaLabelledBy,\n}: AnimatedDropdownTriggerProps) {\n  return (\n    <Menu.Trigger\n      aria-labelledby={ariaLabelledBy}\n      className={cn(\n        \"group inline-flex items-center gap-1.5 rounded-lg px-3 py-1.5\",\n        \"bg-secondary text-secondary-foreground text-sm font-medium\",\n        \"hover:cursor-pointer hover:bg-popover transition-colors duration-150\",\n        \"focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none\",\n        className,\n      )}\n    >\n      {children}\n    </Menu.Trigger>\n  );\n}\n\n/**\n * A state-aware chevron for the dropdown trigger.\n * Rotates when Base UI marks the parent trigger as open.\n */\nexport function AnimatedDropdownTriggerIndicator({\n  className,\n}: AnimatedDropdownTriggerIndicatorProps) {\n  return (\n    <svg\n      aria-hidden=\"true\"\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth=\"2\"\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      className={cn(\n        \"size-4 shrink-0 text-secondary-foreground\",\n        \"transition-transform duration-150 ease-[cubic-bezier(0.16,1,0.3,1)]\",\n        \"group-data-[popup-open]:rotate-180 motion-reduce:transition-none\",\n        className,\n      )}\n    >\n      <path d=\"m6 9 6 6 6-6\" />\n    </svg>\n  );\n}\n\n// ─── Content ──────────────────────────────────────────────────────────────────\n\n/**\n * The animated popup panel containing menu items.\n * Scales in from its trigger origin using Base UI's `--transform-origin` variable.\n */\nexport function AnimatedDropdownContent({\n  children,\n  className,\n  side = \"bottom\",\n  align = \"center\",\n  sideOffset = 6,\n}: AnimatedDropdownContentProps) {\n  const shouldReduceMotion = useReducedMotion();\n\n  return (\n    <Menu.Portal>\n      <Menu.Positioner\n        side={side}\n        align={align}\n        sideOffset={sideOffset}\n        className=\"z-50\"\n      >\n        <Menu.Popup\n          className={cn(\n            // Layout\n            \"z-50 min-w-[160px] rounded-xl p-1\",\n            // Surface\n            \"bg-popover text-popover-foreground shadow-lg\",\n            \"border border-border/60\",\n            // Origin-aware transform — Base UI injects --transform-origin\n            \"origin-[var(--transform-origin)]\",\n            // Enter animation (CSS @starting-style + transition)\n            \"transition-[opacity,transform]\",\n            \"starting:scale-95 starting:opacity-0\",\n            shouldReduceMotion\n              ? \"duration-0 starting:scale-100 starting:opacity-100\"\n              : \"duration-150\",\n            className,\n          )}\n        >\n          {children}\n        </Menu.Popup>\n      </Menu.Positioner>\n    </Menu.Portal>\n  );\n}\n\n// ─── Item ─────────────────────────────────────────────────────────────────────\n\n/**\n * A single menu item with an animated hover-highlight background.\n * The highlight uses a shared `layoutId` so it glides between items on mouse-over.\n */\nexport function AnimatedDropdownItem({\n  children,\n  icon,\n  variant = \"default\",\n  disabled,\n  onClick,\n  className,\n}: AnimatedDropdownItemProps) {\n  const { layoutId, activeId, setActiveId } = useDropdownContext();\n  const itemId = useId();\n  const shouldReduceMotion = useReducedMotion();\n\n  const isActive = activeId === itemId;\n\n  return (\n    <Menu.Item\n      disabled={disabled}\n      onClick={onClick}\n      className={cn(\n        \"group relative flex cursor-pointer select-none items-center gap-2.5\",\n        \"rounded-lg px-2.5 py-2 text-sm outline-none\",\n        \"transition-colors duration-75\",\n        variant === \"danger\"\n          ? \"text-destructive focus:text-destructive-foreground\"\n          : \"text-popover-foreground\",\n        disabled && \"cursor-not-allowed opacity-50\",\n        className,\n      )}\n      onMouseEnter={() => setActiveId(itemId)}\n      onFocus={() => setActiveId(itemId)}\n      onMouseLeave={() => setActiveId(null)}\n      onBlur={() => setActiveId(null)}\n    >\n      {/* Animated highlight — shared across all items in this dropdown instance */}\n      <AnimatePresence>\n        {isActive && (\n          <motion.span\n            layoutId={shouldReduceMotion ? undefined : `${layoutId}-highlight`}\n            className={cn(\n              \"absolute inset-0 rounded-lg\",\n              variant === \"danger\" ? \"bg-destructive\" : \"bg-accent\",\n            )}\n            initial={shouldReduceMotion ? false : { opacity: 0 }}\n            animate={{ opacity: 1 }}\n            exit={{ opacity: 0 }}\n            transition={\n              shouldReduceMotion\n                ? motionTransition.reduced\n                : motionTransition.spatial\n            }\n          />\n        )}\n      </AnimatePresence>\n\n      {/* Icon */}\n      {icon && (\n        <span\n          className={cn(\n            \"relative z-10 shrink-0 [&_svg]:size-4 text-muted-foreground\",\n            variant === \"danger\"\n              ? \"text-destructive group-focus:text-destructive-foreground\"\n              : \"text-popover-foreground\",\n          )}\n        >\n          {icon}\n        </span>\n      )}\n\n      {/* Label */}\n      <span className=\"relative z-10 flex-1\">{children}</span>\n    </Menu.Item>\n  );\n}\n\n// ─── Separator ────────────────────────────────────────────────────────────────\n\n/** A thin visual divider between groups of items. */\nexport function AnimatedDropdownSeparator({\n  className,\n}: {\n  className?: string;\n}) {\n  return (\n    <Menu.Separator className={cn(\"my-1 h-px bg-border/60 mx-1\", className)} />\n  );\n}\n",
      "target": "components/ui/animated-dropdown/animated-dropdown.tsx"
    }
  ],
  "dependencies": [
    "@base-ui/react",
    "motion"
  ],
  "registryDependencies": [
    "@sona-ui/sona-utils",
    "@sona-ui/sona-motion"
  ]
}