{"$schema":"https:\/\/ui.particle.academy\/schema\/registry-item.json","name":"dropdown","type":"registry:ui","title":"Dropdown","description":"Dropdown from react-fancy","package":"react-fancy","dependencies":[],"registryDependencies":["portal"],"files":[{"path":"components\/fancy\/dropdown\/Dropdown.context.ts","content":"import { createContext, useContext } from \"react\";\r\nimport type { DropdownContextValue } from \".\/Dropdown.types\";\r\n\r\nexport const DropdownContext = createContext<DropdownContextValue | null>(null);\r\n\r\nexport function useDropdown(): DropdownContextValue {\r\n  const ctx = useContext(DropdownContext);\r\n  if (!ctx) {\r\n    throw new Error(\r\n      \"Dropdown compound components must be used within <Dropdown>\",\r\n    );\r\n  }\r\n  return ctx;\r\n}\r\n","type":"registry:ui","target":"components\/fancy\/dropdown\/Dropdown.context.ts"},{"path":"components\/fancy\/dropdown\/Dropdown.tsx","content":"import { useMemo, useRef, useState } from \"react\";\r\nimport { DropdownContext } from \".\/Dropdown.context\";\r\nimport { DropdownTrigger } from \".\/DropdownTrigger\";\r\nimport { DropdownItems } from \".\/DropdownItems\";\r\nimport { DropdownItem } from \".\/DropdownItem\";\r\nimport { DropdownSeparator } from \".\/DropdownSeparator\";\r\nimport type { DropdownProps } from \".\/Dropdown.types\";\r\n\r\nfunction DropdownRoot({\r\n  children,\r\n  placement = \"bottom-start\",\r\n  offset = 4,\r\n}: DropdownProps) {\r\n  const [open, setOpen] = useState(false);\r\n  const [activeIndex, setActiveIndex] = useState(-1);\r\n  const anchorRef = useRef<HTMLElement>(null);\r\n\r\n  const ctx = useMemo(\r\n    () => ({ open, setOpen, anchorRef, activeIndex, setActiveIndex, placement, offset }),\r\n    [open, anchorRef, activeIndex, placement, offset],\r\n  );\r\n\r\n  return (\r\n    <DropdownContext.Provider value={ctx}>{children}<\/DropdownContext.Provider>\r\n  );\r\n}\r\n\r\nexport const Dropdown = Object.assign(DropdownRoot, {\r\n  Trigger: DropdownTrigger,\r\n  Items: DropdownItems,\r\n  Item: DropdownItem,\r\n  Separator: DropdownSeparator,\r\n});\r\n","type":"registry:ui","target":"components\/fancy\/dropdown\/Dropdown.tsx"},{"path":"components\/fancy\/dropdown\/Dropdown.types.ts","content":"import type { ReactNode } from \"react\";\r\nimport type { Placement } from \"..\/..\/utils\/types\";\r\n\r\nexport interface DropdownContextValue {\r\n  open: boolean;\r\n  setOpen: (open: boolean) => void;\r\n  anchorRef: React.RefObject<HTMLElement | null>;\r\n  activeIndex: number;\r\n  setActiveIndex: (index: number) => void;\r\n  placement: Placement;\r\n  offset: number;\r\n}\r\n\r\nexport interface DropdownProps {\r\n  children: ReactNode;\r\n  placement?: Placement;\r\n  offset?: number;\r\n}\r\n\r\nexport interface DropdownTriggerProps {\r\n  children: ReactNode;\r\n}\r\n\r\nexport interface DropdownItemsProps {\r\n  children: ReactNode;\r\n  className?: string;\r\n}\r\n\r\nexport interface DropdownItemProps {\r\n  children: ReactNode;\r\n  onClick?: () => void;\r\n  disabled?: boolean;\r\n  danger?: boolean;\r\n  className?: string;\r\n}\r\n\r\nexport interface DropdownSeparatorProps {\r\n  className?: string;\r\n}\r\n","type":"registry:ui","target":"components\/fancy\/dropdown\/Dropdown.types.ts"},{"path":"components\/fancy\/dropdown\/DropdownItem.tsx","content":"import { cn } from \"..\/..\/utils\/cn\";\r\nimport { useDropdown } from \".\/Dropdown.context\";\r\nimport type { DropdownItemProps } from \".\/Dropdown.types\";\r\n\r\nexport function DropdownItem({\r\n  children,\r\n  onClick,\r\n  disabled = false,\r\n  danger = false,\r\n  className,\r\n}: DropdownItemProps) {\r\n  const { setOpen } = useDropdown();\r\n\r\n  return (\r\n    <button\r\n      data-react-fancy-dropdown-item=\"\"\r\n      type=\"button\"\r\n      role=\"menuitem\"\r\n      disabled={disabled}\r\n      aria-disabled={disabled}\r\n      className={cn(\r\n        \"flex w-full items-center rounded-lg px-3 py-2 text-left text-sm transition-colors\",\r\n        danger\r\n          ? \"text-red-600 hover:bg-red-50 dark:text-red-400 dark:hover:bg-red-950\"\r\n          : \"text-zinc-700 hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-zinc-800\",\r\n        disabled && \"cursor-not-allowed opacity-50\",\r\n        className,\r\n      )}\r\n      onClick={() => {\r\n        if (disabled) return;\r\n        onClick?.();\r\n        setOpen(false);\r\n      }}\r\n    >\r\n      {children}\r\n    <\/button>\r\n  );\r\n}\r\n\r\nDropdownItem.displayName = \"DropdownItem\";\r\n","type":"registry:ui","target":"components\/fancy\/dropdown\/DropdownItem.tsx"},{"path":"components\/fancy\/dropdown\/DropdownItems.tsx","content":"import { useCallback, useEffect, useRef } from \"react\";\r\nimport { cn } from \"..\/..\/utils\/cn\";\r\nimport { Portal } from \"..\/Portal\";\r\nimport { useDropdown } from \".\/Dropdown.context\";\r\nimport { useFloatingPosition } from \"..\/..\/hooks\/use-floating-position\";\r\nimport { useOutsideClick } from \"..\/..\/hooks\/use-outside-click\";\r\nimport { useEscapeKey } from \"..\/..\/hooks\/use-escape-key\";\r\nimport { useAnimation } from \"..\/..\/hooks\/use-animation\";\r\nimport type { DropdownItemsProps } from \".\/Dropdown.types\";\r\n\r\nexport function DropdownItems({ children, className }: DropdownItemsProps) {\r\n  const { open, setOpen, anchorRef, activeIndex, setActiveIndex, placement, offset } =\r\n    useDropdown();\r\n  const floatingRef = useRef<HTMLDivElement>(null);\r\n  const outsideRef = useRef<HTMLDivElement>(null);\r\n\r\n  const position = useFloatingPosition(anchorRef, floatingRef, {\r\n    placement,\r\n    offset,\r\n    enabled: open,\r\n  });\r\n\r\n  const close = useCallback(() => {\r\n    setOpen(false);\r\n    setActiveIndex(-1);\r\n  }, [setOpen, setActiveIndex]);\r\n\r\n  useOutsideClick(outsideRef, close, open, anchorRef);\r\n  useEscapeKey(close, open);\r\n\r\n  const { mounted, className: animClass, ref: animRef } = useAnimation({\r\n    open,\r\n    enterClass: \"fancy-scale-in\",\r\n    exitClass: \"fancy-fade-out\",\r\n  });\r\n\r\n  useEffect(() => {\r\n    if (!open || !floatingRef.current) return;\r\n\r\n    const handleKeyDown = (e: KeyboardEvent) => {\r\n      const items = floatingRef.current?.querySelectorAll<HTMLElement>(\r\n        '[role=\"menuitem\"]:not([aria-disabled=\"true\"])',\r\n      );\r\n      if (!items?.length) return;\r\n\r\n      if (e.key === \"ArrowDown\") {\r\n        e.preventDefault();\r\n        const next = activeIndex < items.length - 1 ? activeIndex + 1 : 0;\r\n        setActiveIndex(next);\r\n        items[next]?.focus();\r\n      } else if (e.key === \"ArrowUp\") {\r\n        e.preventDefault();\r\n        const prev = activeIndex > 0 ? activeIndex - 1 : items.length - 1;\r\n        setActiveIndex(prev);\r\n        items[prev]?.focus();\r\n      }\r\n    };\r\n\r\n    document.addEventListener(\"keydown\", handleKeyDown);\r\n    return () => document.removeEventListener(\"keydown\", handleKeyDown);\r\n  }, [open, activeIndex, setActiveIndex]);\r\n\r\n  if (!mounted) return null;\r\n\r\n  return (\r\n    <Portal>\r\n      <div\r\n        ref={(node) => {\r\n          outsideRef.current = node;\r\n          floatingRef.current = node;\r\n          (animRef as React.MutableRefObject<HTMLElement | null>).current = node;\r\n        }}\r\n        data-react-fancy-dropdown=\"\"\r\n        role=\"menu\"\r\n        className={cn(\r\n          \"fixed z-50 min-w-[8rem] rounded-xl border border-zinc-200 bg-white p-1 shadow-lg dark:border-zinc-700 dark:bg-zinc-900\",\r\n          animClass,\r\n          className,\r\n        )}\r\n        style={{ left: position.x, top: position.y }}\r\n      >\r\n        {children}\r\n      <\/div>\r\n    <\/Portal>\r\n  );\r\n}\r\n\r\nDropdownItems.displayName = \"DropdownItems\";\r\n","type":"registry:ui","target":"components\/fancy\/dropdown\/DropdownItems.tsx"},{"path":"components\/fancy\/dropdown\/DropdownSeparator.tsx","content":"import { cn } from \"..\/..\/utils\/cn\";\r\nimport type { DropdownSeparatorProps } from \".\/Dropdown.types\";\r\n\r\nexport function DropdownSeparator({ className }: DropdownSeparatorProps) {\r\n  return (\r\n    <div\r\n      data-react-fancy-dropdown-separator=\"\"\r\n      role=\"separator\"\r\n      className={cn(\r\n        \"my-1 h-px bg-zinc-200 dark:bg-zinc-700\",\r\n        className,\r\n      )}\r\n    \/>\r\n  );\r\n}\r\n\r\nDropdownSeparator.displayName = \"DropdownSeparator\";\r\n","type":"registry:ui","target":"components\/fancy\/dropdown\/DropdownSeparator.tsx"},{"path":"components\/fancy\/dropdown\/DropdownTrigger.tsx","content":"import { useDropdown } from \".\/Dropdown.context\";\r\nimport type { DropdownTriggerProps } from \".\/Dropdown.types\";\r\n\r\nexport function DropdownTrigger({ children }: DropdownTriggerProps) {\r\n  const { setOpen, open, anchorRef } = useDropdown();\r\n\r\n  return (\r\n    <span\r\n      ref={anchorRef as React.RefObject<HTMLSpanElement>}\r\n      data-react-fancy-dropdown-trigger=\"\"\r\n      className=\"inline-flex\"\r\n      onClick={() => setOpen(!open)}\r\n      aria-expanded={open}\r\n      aria-haspopup=\"menu\"\r\n    >\r\n      {children}\r\n    <\/span>\r\n  );\r\n}\r\n\r\nDropdownTrigger.displayName = \"DropdownTrigger\";\r\n","type":"registry:ui","target":"components\/fancy\/dropdown\/DropdownTrigger.tsx"},{"path":"components\/fancy\/dropdown\/index.ts","content":"export { Dropdown } from \".\/Dropdown\";\r\nexport { useDropdown } from \".\/Dropdown.context\";\r\nexport type {\r\n  DropdownProps,\r\n  DropdownTriggerProps,\r\n  DropdownItemsProps,\r\n  DropdownItemProps,\r\n  DropdownSeparatorProps,\r\n  DropdownContextValue,\r\n} from \".\/Dropdown.types\";\r\n","type":"registry:ui","target":"components\/fancy\/dropdown\/index.ts"}]}