{
  "name": "popover",
  "dependencies": [],
  "registryDependencies": [],
  "files": [
    {
      "path": "popover.tsx",
      "content": "\"use client\";\r\n\r\nimport { computePosition, css, scaleIn, scaleOut, themeVars as theme, type Placement } from \"@yugnex/core\";\r\nimport { useClickOutside, useEscapeKey, usePortal, usePresence } from \"@yugnex/core/client\";\r\nimport {\r\n  cloneElement,\r\n  createContext,\r\n  useContext,\r\n  useEffect,\r\n  useRef,\r\n  useState,\r\n  type HTMLAttributes,\r\n  type MutableRefObject,\r\n  type ReactElement,\r\n  type ReactNode,\r\n  type Ref,\r\n} from \"react\";\r\nimport { createPortal } from \"react-dom\";\r\n\r\ninterface PopoverContextValue {\r\n  open: boolean;\r\n  setOpen: (open: boolean) => void;\r\n  anchorRef: MutableRefObject<HTMLElement | null>;\r\n}\r\n\r\nconst PopoverContext = createContext<PopoverContextValue | null>(null);\r\n\r\nfunction usePopoverContext(component: string): PopoverContextValue {\r\n  const ctx = useContext(PopoverContext);\r\n  if (!ctx) throw new Error(`<${component}> must be used within <Popover>`);\r\n  return ctx;\r\n}\r\n\r\nfunction mergeRefs<T>(...refs: Array<Ref<T> | undefined>): (node: T | null) => void {\r\n  return (node) => {\r\n    for (const ref of refs) {\r\n      if (typeof ref === \"function\") ref(node);\r\n      else if (ref) (ref as { current: T | null }).current = node;\r\n    }\r\n  };\r\n}\r\n\r\nconst contentClass = css({\r\n  position: \"fixed\",\r\n  top: 0,\r\n  left: 0,\r\n  zIndex: theme.zIndex.popover,\r\n  minWidth: \"12rem\",\r\n  padding: theme.space[4],\r\n  borderRadius: theme.radius.md,\r\n  border: `1px solid ${theme.color.border}`,\r\n  backgroundColor: theme.color.popover,\r\n  color: theme.color.popoverForeground,\r\n  boxShadow: theme.shadow.lg,\r\n  fontFamily: theme.fontFamily.sans,\r\n  fontSize: theme.fontSize.sm,\r\n  '&[data-state=\"open\"]': { animation: `${scaleIn} ${theme.duration.fast} ${theme.easing.decelerate}` },\r\n  '&[data-state=\"closed\"]': { animation: `${scaleOut} ${theme.duration.fast} ${theme.easing.accelerate}` },\r\n  \"&:focus-visible\": { outline: \"none\" },\r\n});\r\n\r\nexport interface PopoverProps {\r\n  open: boolean;\r\n  onOpenChange: (open: boolean) => void;\r\n  children: ReactNode;\r\n}\r\n\r\n/** A floating panel anchored to a trigger. The generic base other floating UI is built from. */\r\nexport function Popover({ open, onOpenChange, children }: PopoverProps) {\r\n  const anchorRef = useRef<HTMLElement | null>(null);\r\n  return (\r\n    <PopoverContext.Provider value={{ open, setOpen: onOpenChange, anchorRef }}>{children}</PopoverContext.Provider>\r\n  );\r\n}\r\n\r\nexport function PopoverTrigger({ children }: { children: ReactElement }) {\r\n  const { open, setOpen, anchorRef } = usePopoverContext(\"PopoverTrigger\");\r\n  const element = children as ReactElement<Record<string, unknown>>;\r\n  const childRef = (element as { ref?: Ref<HTMLElement> }).ref;\r\n\r\n  return cloneElement(element, {\r\n    ref: mergeRefs(anchorRef, childRef),\r\n    \"aria-haspopup\": \"dialog\",\r\n    \"aria-expanded\": open,\r\n    onClick: () => setOpen(!open),\r\n  });\r\n}\r\n\r\nexport interface PopoverContentProps extends HTMLAttributes<HTMLDivElement> {\r\n  placement?: Placement;\r\n  align?: \"start\" | \"center\" | \"end\";\r\n  /** Match the trigger's width — useful when the popover acts as a dropdown surface. */\r\n  matchTriggerWidth?: boolean;\r\n}\r\n\r\nexport function PopoverContent({\r\n  placement = \"bottom\",\r\n  align = \"start\",\r\n  matchTriggerWidth = false,\r\n  className,\r\n  children,\r\n  ...props\r\n}: PopoverContentProps) {\r\n  const { open, setOpen, anchorRef } = usePopoverContext(\"PopoverContent\");\r\n  const { mounted, dataState } = usePresence(open, { exitDuration: 150 });\r\n  const portalNode = usePortal();\r\n  const contentRef = useRef<HTMLDivElement | null>(null);\r\n  const [pos, setPos] = useState({ x: 0, y: 0 });\r\n  const [width, setWidth] = useState<number | undefined>(undefined);\r\n\r\n  useEscapeKey(() => setOpen(false), open);\r\n  useClickOutside(contentRef, () => setOpen(false), open);\r\n\r\n  useEffect(() => {\r\n    if (!mounted) return;\r\n    const update = () => {\r\n      if (!anchorRef.current || !contentRef.current) return;\r\n      if (matchTriggerWidth) setWidth(anchorRef.current.getBoundingClientRect().width);\r\n      const result = computePosition(anchorRef.current, contentRef.current, { placement, align });\r\n      setPos({ x: result.x, y: result.y });\r\n    };\r\n    update();\r\n    window.addEventListener(\"scroll\", update, true);\r\n    window.addEventListener(\"resize\", update);\r\n    return () => {\r\n      window.removeEventListener(\"scroll\", update, true);\r\n      window.removeEventListener(\"resize\", update);\r\n    };\r\n  }, [mounted, placement, align, matchTriggerWidth, anchorRef]);\r\n\r\n  // Move focus into the panel once it exists, and hand it back on close.\r\n  // Keyed on `mounted` rather than `open`: the portaled node is not rendered\r\n  // until a tick after `open` flips, so a ref read on `open` would be null.\r\n  useEffect(() => {\r\n    if (!mounted) return;\r\n    const previous = document.activeElement as HTMLElement | null;\r\n    const raf = requestAnimationFrame(() => contentRef.current?.focus());\r\n    return () => {\r\n      cancelAnimationFrame(raf);\r\n      previous?.focus?.();\r\n    };\r\n  }, [mounted]);\r\n\r\n  if (!mounted || !portalNode) return null;\r\n\r\n  return createPortal(\r\n    <div\r\n      ref={contentRef}\r\n      role=\"dialog\"\r\n      tabIndex={-1}\r\n      data-state={dataState}\r\n      className={className ? `${contentClass} ${className}` : contentClass}\r\n      style={{ transform: `translate(${pos.x}px, ${pos.y}px)`, width }}\r\n      {...props}\r\n    >\r\n      {children}\r\n    </div>,\r\n    portalNode,\r\n  );\r\n}\r\n",
      "type": "registry:component"
    }
  ]
}