{
  "name": "modal",
  "dependencies": [],
  "registryDependencies": [],
  "files": [
    {
      "path": "modal.tsx",
      "content": "\"use client\";\r\n\r\nimport { css, overlayHide, overlayShow, scaleIn, scaleOut, themeVars as theme } from \"@yugnex/core\";\r\nimport { useClickOutside, useEscapeKey, useFocusTrap, usePortal, usePresence } from \"@yugnex/core/client\";\r\nimport { createContext, useContext, useEffect, type HTMLAttributes, type ReactNode } from \"react\";\r\nimport { createPortal } from \"react-dom\";\r\n\r\ninterface ModalContextValue {\r\n  open: boolean;\r\n  onOpenChange: (open: boolean) => void;\r\n}\r\n\r\nconst ModalContext = createContext<ModalContextValue | null>(null);\r\n\r\nfunction useModalContext(component: string): ModalContextValue {\r\n  const ctx = useContext(ModalContext);\r\n  if (!ctx) throw new Error(`<${component}> must be used within <Modal>`);\r\n  return ctx;\r\n}\r\n\r\n/** Read the open state / close handler from inside <ModalContent> — e.g. to build a close button. */\r\nexport function useModal(): ModalContextValue {\r\n  return useModalContext(\"useModal\");\r\n}\r\n\r\nconst overlayClass = css({\r\n  position: \"fixed\",\r\n  inset: 0,\r\n  zIndex: theme.zIndex.overlay,\r\n  backgroundColor: theme.color.overlay,\r\n  '&[data-state=\"open\"]': { animation: `${overlayShow} ${theme.duration.base} ${theme.easing.standard}` },\r\n  '&[data-state=\"closed\"]': { animation: `${overlayHide} ${theme.duration.base} ${theme.easing.standard}` },\r\n});\r\n\r\nconst contentClass = css({\r\n  position: \"fixed\",\r\n  top: \"50%\",\r\n  left: \"50%\",\r\n  transform: \"translate(-50%, -50%)\",\r\n  zIndex: theme.zIndex.modal,\r\n  width: \"min(28rem, calc(100vw - 2rem))\",\r\n  maxHeight: \"calc(100vh - 4rem)\",\r\n  overflowY: \"auto\",\r\n  borderRadius: theme.radius.lg,\r\n  border: `1px solid ${theme.color.border}`,\r\n  backgroundColor: theme.color.card,\r\n  color: theme.color.cardForeground,\r\n  boxShadow: theme.shadow.overlay,\r\n  padding: theme.space[6],\r\n  \"&:focus-visible\": { outline: \"none\" },\r\n  '&[data-state=\"open\"]': { animation: `${scaleIn} ${theme.duration.base} ${theme.easing.decelerate}` },\r\n  '&[data-state=\"closed\"]': { animation: `${scaleOut} ${theme.duration.fast} ${theme.easing.accelerate}` },\r\n});\r\n\r\nconst headerClass = css({\r\n  display: \"flex\",\r\n  flexDirection: \"column\",\r\n  gap: theme.space[1.5],\r\n  marginBottom: theme.space[4],\r\n});\r\nconst titleClass = css({ fontSize: theme.fontSize.lg, fontWeight: theme.fontWeight.semibold, margin: 0 });\r\nconst descriptionClass = css({ fontSize: theme.fontSize.sm, color: theme.color.mutedForeground, margin: 0 });\r\nconst footerClass = css({\r\n  display: \"flex\",\r\n  justifyContent: \"flex-end\",\r\n  gap: theme.space[2],\r\n  marginTop: theme.space[6],\r\n});\r\n\r\nexport interface ModalProps {\r\n  open: boolean;\r\n  onOpenChange: (open: boolean) => void;\r\n  children: ReactNode;\r\n}\r\n\r\n/** Root: owns open state via context. Compose with <ModalContent> (and Header/Title/Description/Footer). */\r\nexport function Modal({ open, onOpenChange, children }: ModalProps) {\r\n  return <ModalContext.Provider value={{ open, onOpenChange }}>{children}</ModalContext.Provider>;\r\n}\r\n\r\nexport interface ModalContentProps extends HTMLAttributes<HTMLDivElement> {\r\n  children: ReactNode;\r\n}\r\n\r\nexport function ModalContent({ children, className, ...props }: ModalContentProps) {\r\n  const { open, onOpenChange } = useModalContext(\"ModalContent\");\r\n  const { mounted, dataState } = usePresence(open, { exitDuration: 200 });\r\n  const portalNode = usePortal();\r\n  // Keyed on `mounted`, not `open`: the content node (and its ref) only\r\n  // exists once `mounted` flips true one tick after `open` does, so trapping\r\n  // focus on `open` would fire before there's anything to trap focus in.\r\n  const contentRef = useFocusTrap<HTMLDivElement>(mounted);\r\n\r\n  useEscapeKey(() => onOpenChange(false), open);\r\n  useClickOutside(contentRef, () => onOpenChange(false), open);\r\n\r\n  useEffect(() => {\r\n    if (!mounted) return;\r\n    const original = document.body.style.overflow;\r\n    document.body.style.overflow = \"hidden\";\r\n    return () => {\r\n      document.body.style.overflow = original;\r\n    };\r\n  }, [mounted]);\r\n\r\n  if (!mounted || !portalNode) return null;\r\n\r\n  return createPortal(\r\n    <>\r\n      <div className={overlayClass} data-state={dataState} aria-hidden=\"true\" />\r\n      <div\r\n        ref={contentRef}\r\n        role=\"dialog\"\r\n        aria-modal=\"true\"\r\n        data-state={dataState}\r\n        tabIndex={-1}\r\n        className={className ? `${contentClass} ${className}` : contentClass}\r\n        {...props}\r\n      >\r\n        {children}\r\n      </div>\r\n    </>,\r\n    portalNode,\r\n  );\r\n}\r\n\r\nexport function ModalHeader({ className, ...props }: HTMLAttributes<HTMLDivElement>) {\r\n  return <div className={className ? `${headerClass} ${className}` : headerClass} {...props} />;\r\n}\r\n\r\nexport function ModalTitle({ className, ...props }: HTMLAttributes<HTMLHeadingElement>) {\r\n  return <h2 className={className ? `${titleClass} ${className}` : titleClass} {...props} />;\r\n}\r\n\r\nexport function ModalDescription({ className, ...props }: HTMLAttributes<HTMLParagraphElement>) {\r\n  return <p className={className ? `${descriptionClass} ${className}` : descriptionClass} {...props} />;\r\n}\r\n\r\nexport function ModalFooter({ className, ...props }: HTMLAttributes<HTMLDivElement>) {\r\n  return <div className={className ? `${footerClass} ${className}` : footerClass} {...props} />;\r\n}\r\n",
      "type": "registry:component"
    }
  ]
}