{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"dropdown-menu","type":"registry:ui","title":"Dropdown Menu","description":"A frosted menu that scales out of its trigger and animates out when it closes.","dependencies":["@radix-ui/react-dropdown-menu@^2.1.24","@untitledui/icons@^0.0.23","motion@^14.0.0"],"files":[{"path":"components/ui/dropdown-menu.tsx","type":"registry:ui","content":"\"use client\";\n\nimport * as DropdownMenuPrimitive from \"@radix-ui/react-dropdown-menu\";\nimport { Check, ChevronRight, Circle } from \"@untitledui/icons\";\nimport { AnimatePresence, motion } from \"motion/react\";\nimport * as React from \"react\";\nimport { createAnimatedRoot, popMotion } from \"@/lib/motion\";\nimport { cn } from \"@/lib/utils\";\n\nconst [DropdownMenu, useDropdownMenuOpenState] = createAnimatedRoot(DropdownMenuPrimitive.Root, \"dropdown-menu\");\nconst [DropdownMenuSub, useDropdownMenuSubOpenState] = createAnimatedRoot(\n  DropdownMenuPrimitive.Sub,\n  \"dropdown-menu-sub\",\n);\n\nfunction DropdownMenuPortal({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {\n  return <DropdownMenuPrimitive.Portal data-slot=\"dropdown-menu-portal\" {...props} />;\n}\n\nfunction DropdownMenuTrigger({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {\n  return <DropdownMenuPrimitive.Trigger data-slot=\"dropdown-menu-trigger\" {...props} />;\n}\n\nfunction DropdownMenuContent({\n  className,\n  sideOffset = 4,\n  children,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {\n  const { open, onExitComplete } = useDropdownMenuOpenState();\n\n  return (\n    <AnimatePresence onExitComplete={onExitComplete}>\n      {open && (\n        <DropdownMenuPrimitive.Portal forceMount>\n          <DropdownMenuPrimitive.Content\n            data-slot=\"dropdown-menu-content\"\n            sideOffset={sideOffset}\n            forceMount\n            className=\"z-50 outline-hidden\"\n            {...props}\n          >\n            <motion.div\n              {...popMotion}\n              className={cn(\n                \"frosted text-popover-foreground max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-float\",\n                className,\n              )}\n            >\n              {children}\n            </motion.div>\n          </DropdownMenuPrimitive.Content>\n        </DropdownMenuPrimitive.Portal>\n      )}\n    </AnimatePresence>\n  );\n}\n\nfunction DropdownMenuGroup({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {\n  return <DropdownMenuPrimitive.Group data-slot=\"dropdown-menu-group\" {...props} />;\n}\n\nfunction DropdownMenuItem({\n  className,\n  inset,\n  variant = \"default\",\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {\n  inset?: boolean;\n  variant?: \"default\" | \"destructive\";\n}) {\n  return (\n    <DropdownMenuPrimitive.Item\n      data-slot=\"dropdown-menu-item\"\n      data-inset={inset}\n      data-variant={variant}\n      className={cn(\n        \"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&>svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction DropdownMenuCheckboxItem({\n  className,\n  children,\n  checked,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {\n  return (\n    <DropdownMenuPrimitive.CheckboxItem\n      data-slot=\"dropdown-menu-checkbox-item\"\n      className={cn(\n        \"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n        className,\n      )}\n      checked={checked}\n      {...props}\n    >\n      <span className=\"pointer-events-none absolute left-2 flex size-3.5 items-center justify-center\">\n        <DropdownMenuPrimitive.ItemIndicator>\n          <Check className=\"size-4\" />\n        </DropdownMenuPrimitive.ItemIndicator>\n      </span>\n      {children}\n    </DropdownMenuPrimitive.CheckboxItem>\n  );\n}\n\nfunction DropdownMenuRadioGroup({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {\n  return <DropdownMenuPrimitive.RadioGroup data-slot=\"dropdown-menu-radio-group\" {...props} />;\n}\n\nfunction DropdownMenuRadioItem({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {\n  return (\n    <DropdownMenuPrimitive.RadioItem\n      data-slot=\"dropdown-menu-radio-item\"\n      className={cn(\n        \"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n        className,\n      )}\n      {...props}\n    >\n      <span className=\"pointer-events-none absolute left-2 flex size-3.5 items-center justify-center\">\n        <DropdownMenuPrimitive.ItemIndicator forceMount className=\"data-[state=unchecked]:opacity-10\">\n          <Circle className=\"size-2 fill-current\" />\n        </DropdownMenuPrimitive.ItemIndicator>\n      </span>\n      {children}\n    </DropdownMenuPrimitive.RadioItem>\n  );\n}\n\nfunction DropdownMenuLabel({\n  className,\n  inset,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {\n  inset?: boolean;\n}) {\n  return (\n    <DropdownMenuPrimitive.Label\n      data-slot=\"dropdown-menu-label\"\n      data-inset={inset}\n      className={cn(\"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction DropdownMenuSeparator({ className, ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {\n  return (\n    <DropdownMenuPrimitive.Separator\n      data-slot=\"dropdown-menu-separator\"\n      className={cn(\"bg-border -mx-1 my-1 h-px\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction DropdownMenuShortcut({ className, ...props }: React.ComponentProps<\"span\">) {\n  return (\n    <span\n      data-slot=\"dropdown-menu-shortcut\"\n      className={cn(\"text-muted-foreground ml-auto text-xs tracking-widest\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction DropdownMenuSubTrigger({\n  className,\n  inset,\n  children,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {\n  inset?: boolean;\n}) {\n  return (\n    <DropdownMenuPrimitive.SubTrigger\n      data-slot=\"dropdown-menu-sub-trigger\"\n      data-inset={inset}\n      className={cn(\n        \"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&>svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n      <ChevronRight className=\"ml-auto size-4\" />\n    </DropdownMenuPrimitive.SubTrigger>\n  );\n}\n\nfunction DropdownMenuSubContent({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {\n  const { open, onExitComplete } = useDropdownMenuSubOpenState();\n\n  return (\n    <AnimatePresence onExitComplete={onExitComplete}>\n      {open && (\n        <DropdownMenuPrimitive.Portal forceMount>\n          <DropdownMenuPrimitive.SubContent\n            data-slot=\"dropdown-menu-sub-content\"\n            forceMount\n            className=\"z-50 outline-hidden\"\n            {...props}\n          >\n            <motion.div\n              {...popMotion}\n              className={cn(\n                \"frosted text-popover-foreground min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-float\",\n                className,\n              )}\n            >\n              {children}\n            </motion.div>\n          </DropdownMenuPrimitive.SubContent>\n        </DropdownMenuPrimitive.Portal>\n      )}\n    </AnimatePresence>\n  );\n}\n\nexport {\n  DropdownMenu,\n  DropdownMenuCheckboxItem,\n  DropdownMenuContent,\n  DropdownMenuGroup,\n  DropdownMenuItem,\n  DropdownMenuLabel,\n  DropdownMenuPortal,\n  DropdownMenuRadioGroup,\n  DropdownMenuRadioItem,\n  DropdownMenuSeparator,\n  DropdownMenuShortcut,\n  DropdownMenuSub,\n  DropdownMenuSubContent,\n  DropdownMenuSubTrigger,\n  DropdownMenuTrigger,\n};\n"},{"path":"lib/motion.ts","type":"registry:lib","content":"\"use client\";\n\nimport * as React from \"react\";\nimport { useControllableState } from \"@/hooks/use-controllable-state\";\nimport type { MotionProps, Transition } from \"motion/react\";\n\nexport const chewyEase = [0.32, 0.72, 0, 1] as const;\n\nexport const growTransition = { duration: 0.6, ease: chewyEase };\n\nconst springIn: Transition = { type: \"spring\", bounce: 0.2, duration: 0.3 };\nconst quickOut: Transition = { duration: 0.12, ease: \"easeIn\" };\n\nexport const popMotion = {\n  initial: { opacity: 0, scale: 0.96 },\n  animate: { opacity: 1, scale: 1, transition: springIn },\n  exit: { opacity: 0, scale: 0.96, transition: quickOut },\n};\n\nexport const tooltipMotion = {\n  initial: { opacity: 0, scale: 0.94 },\n  animate: { opacity: 1, scale: 1, transition: { ...springIn, duration: 0.2 } },\n  exit: { opacity: 0, scale: 0.94, transition: { duration: 0.08 } },\n};\n\nexport const dialogMotion = {\n  initial: { opacity: 0, scale: 0.95, y: 8 },\n  animate: { opacity: 1, scale: 1, y: 0, transition: { type: \"spring\", bounce: 0.25, duration: 0.4 } as Transition },\n  exit: { opacity: 0, scale: 0.97, y: 4, transition: { duration: 0.15, ease: \"easeIn\" } as Transition },\n};\n\nexport const overlayMotion = {\n  initial: { opacity: 0 },\n  animate: { opacity: 1, transition: { duration: 0.2 } },\n  exit: { opacity: 0, transition: { duration: 0.15 } },\n};\n\nexport type PanelSide = \"top\" | \"right\" | \"bottom\" | \"left\";\n\nexport function panelMotion(side: PanelSide): Pick<MotionProps, \"initial\" | \"animate\" | \"exit\"> {\n  const offscreen = side === \"left\" || side === \"top\" ? \"calc(-100% - 1rem)\" : \"calc(100% + 1rem)\";\n  const horizontal = side === \"left\" || side === \"right\";\n  const hidden = horizontal ? { x: offscreen } : { y: offscreen };\n  const shown = horizontal ? { x: 0 } : { y: 0 };\n\n  return {\n    initial: hidden,\n    animate: { ...shown, transition: { duration: 0.45, ease: chewyEase } },\n    exit: { ...hidden, transition: { duration: 0.3, ease: chewyEase } },\n  };\n}\n\nexport function useAnimatedOpen(\n  openProp: boolean | undefined,\n  defaultOpen: boolean | undefined,\n  onOpenChange: ((open: boolean) => void) | undefined,\n) {\n  const [open, setOpen] = useControllableState(openProp, defaultOpen ?? false, onOpenChange);\n  const [exiting, setExiting] = React.useState(false);\n  const [wasOpen, setWasOpen] = React.useState(open);\n\n  if (open !== wasOpen) {\n    setWasOpen(open);\n    setExiting(!open);\n  }\n\n  const onExitComplete = React.useCallback(() => setExiting(false), []);\n\n  return { open, setOpen, radixOpen: open || exiting, onExitComplete };\n}\n\ntype AnimatedOpenState = { open: boolean; onExitComplete: () => void };\n\ntype OpenProps = { open?: boolean; defaultOpen?: boolean; onOpenChange?: (open: boolean) => void };\n\nexport function createAnimatedRoot<P extends OpenProps>(Primitive: React.ComponentType<P>, slot: string) {\n  const Context = React.createContext<AnimatedOpenState>({ open: false, onExitComplete: () => {} });\n\n  function AnimatedRoot({ open: openProp, defaultOpen, onOpenChange, ...props }: P) {\n    const { open, setOpen, radixOpen, onExitComplete } = useAnimatedOpen(openProp, defaultOpen, onOpenChange);\n    const value = React.useMemo(() => ({ open, onExitComplete }), [open, onExitComplete]);\n    const primitiveProps = { \"data-slot\": slot, ...props, open: radixOpen, onOpenChange: setOpen } as unknown as P;\n\n    return React.createElement(Context.Provider, { value }, React.createElement(Primitive, primitiveProps));\n  }\n\n  return [AnimatedRoot, () => React.useContext(Context)] as const;\n}\n"},{"path":"hooks/use-controllable-state.ts","type":"registry:hook","content":"import * as React from \"react\";\n\nexport function useControllableState<T>(prop: T | undefined, defaultProp: T, onChange?: (value: T) => void) {\n  const [uncontrolled, setUncontrolled] = React.useState(defaultProp);\n  const isControlled = prop !== undefined;\n  const value = isControlled ? prop : uncontrolled;\n\n  const setValue = React.useCallback(\n    (next: T) => {\n      if (!isControlled) setUncontrolled(next);\n      onChange?.(next);\n    },\n    [isControlled, onChange],\n  );\n\n  return [value, setValue] as const;\n}\n"}],"cssVars":{"theme":{"shadow-float":"0 16px 36px -12px oklch(0 0 0 / 0.2), 0 2px 6px -2px oklch(0 0 0 / 0.08)"}},"css":{"@utility frosted":{"background-color":"color-mix(in oklab, var(--popover) 65%, transparent)","backdrop-filter":"blur(20px) saturate(1.6)"}}}