{"$schema":"https:\/\/ui.particle.academy\/schema\/registry-item.json","name":"modal","type":"registry:ui","title":"Modal","description":"Modal from react-fancy","package":"react-fancy","dependencies":["lucide-react"],"registryDependencies":["portal"],"files":[{"path":"components\/fancy\/modal\/Modal.context.ts","content":"import { createContext, useContext } from \"react\";\r\nimport type { ModalContextValue } from \".\/Modal.types\";\r\n\r\nexport const ModalContext = createContext<ModalContextValue | null>(null);\r\n\r\nexport function useModal(): ModalContextValue {\r\n  const ctx = useContext(ModalContext);\r\n  if (!ctx) {\r\n    throw new Error(\"Modal compound components must be used within <Modal>\");\r\n  }\r\n  return ctx;\r\n}\r\n","type":"registry:ui","target":"components\/fancy\/modal\/Modal.context.ts"},{"path":"components\/fancy\/modal\/Modal.tsx","content":"import { useCallback, useEffect, useMemo, useRef } from \"react\";\r\nimport { cn } from \"..\/..\/utils\/cn\";\r\nimport { Portal } from \"..\/Portal\";\r\nimport { ModalContext } from \".\/Modal.context\";\r\nimport { ModalHeader } from \".\/ModalHeader\";\r\nimport { ModalBody } from \".\/ModalBody\";\r\nimport { ModalFooter } from \".\/ModalFooter\";\r\nimport { useFocusTrap } from \"..\/..\/hooks\/use-focus-trap\";\r\nimport { useEscapeKey } from \"..\/..\/hooks\/use-escape-key\";\r\nimport { useAnimation } from \"..\/..\/hooks\/use-animation\";\r\nimport type { ModalProps } from \".\/Modal.types\";\r\n\r\nconst SIZE_MAP = {\r\n  sm: \"max-w-sm\",\r\n  md: \"max-w-lg\",\r\n  lg: \"max-w-2xl\",\r\n  xl: \"max-w-4xl\",\r\n  full: \"max-w-[calc(100vw-2rem)] max-h-[calc(100vh-2rem)]\",\r\n} as const;\r\n\r\nfunction ModalRoot({\r\n  children,\r\n  open,\r\n  onClose,\r\n  size = \"md\",\r\n  className,\r\n  ...rest\r\n}: ModalProps) {\r\n  const panelRef = useRef<HTMLDivElement>(null);\r\n\r\n  const close = useCallback(() => onClose(), [onClose]);\r\n  useEscapeKey(close, open);\r\n  useFocusTrap(panelRef, open);\r\n\r\n  \/\/ Body scroll lock\r\n  useEffect(() => {\r\n    if (open) {\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    }\r\n  }, [open]);\r\n\r\n  const { mounted, className: animClass, ref: animRef } = useAnimation({\r\n    open,\r\n    enterClass: \"fancy-slide-up\",\r\n    exitClass: \"fancy-fade-out\",\r\n  });\r\n\r\n  const ctx = useMemo(() => ({ open, close }), [open, close]);\r\n\r\n  if (!mounted) return null;\r\n\r\n  return (\r\n    <ModalContext.Provider value={ctx}>\r\n      <Portal>\r\n        <div className=\"fixed inset-0 z-50 flex items-center justify-center p-4\">\r\n          {\/* Backdrop *\/}\r\n          <div\r\n            className=\"absolute inset-0 bg-black\/50 backdrop-blur-sm\"\r\n            onClick={close}\r\n            aria-hidden=\"true\"\r\n          \/>\r\n          {\/* Panel *\/}\r\n          <div\r\n            ref={(node) => {\r\n              panelRef.current = node;\r\n              (animRef as React.MutableRefObject<HTMLElement | null>).current =\r\n                node;\r\n            }}\r\n            data-react-fancy-modal=\"\"\r\n            role=\"dialog\"\r\n            aria-modal=\"true\"\r\n            {...rest}\r\n            className={cn(\r\n              \"relative flex w-full flex-col overflow-hidden rounded-2xl border border-zinc-200 bg-white shadow-xl dark:border-zinc-700 dark:bg-zinc-900\",\r\n              SIZE_MAP[size],\r\n              animClass,\r\n              className,\r\n            )}\r\n          >\r\n            {children}\r\n          <\/div>\r\n        <\/div>\r\n      <\/Portal>\r\n    <\/ModalContext.Provider>\r\n  );\r\n}\r\n\r\nexport const Modal = Object.assign(ModalRoot, {\r\n  Header: ModalHeader,\r\n  Body: ModalBody,\r\n  Footer: ModalFooter,\r\n});\r\n","type":"registry:ui","target":"components\/fancy\/modal\/Modal.tsx"},{"path":"components\/fancy\/modal\/Modal.types.ts","content":"import type { HTMLAttributes, ReactNode } from \"react\";\r\n\r\nexport interface ModalContextValue {\r\n  open: boolean;\r\n  close: () => void;\r\n}\r\n\r\nexport interface ModalProps extends Omit<HTMLAttributes<HTMLDivElement>, \"children\"> {\r\n  children: ReactNode;\r\n  open: boolean;\r\n  onClose: () => void;\r\n  size?: \"sm\" | \"md\" | \"lg\" | \"xl\" | \"full\";\r\n  className?: string;\r\n}\r\n\r\nexport interface ModalHeaderProps {\r\n  children: ReactNode;\r\n  className?: string;\r\n}\r\n\r\nexport interface ModalBodyProps {\r\n  children: ReactNode;\r\n  className?: string;\r\n}\r\n\r\nexport interface ModalFooterProps {\r\n  children: ReactNode;\r\n  className?: string;\r\n}\r\n","type":"registry:ui","target":"components\/fancy\/modal\/Modal.types.ts"},{"path":"components\/fancy\/modal\/ModalBody.tsx","content":"import { cn } from \"..\/..\/utils\/cn\";\r\nimport type { ModalBodyProps } from \".\/Modal.types\";\r\n\r\nexport function ModalBody({ children, className }: ModalBodyProps) {\r\n  return (\r\n    <div data-react-fancy-modal-body=\"\" className={cn(\"flex-1 overflow-y-auto px-6 py-4 text-zinc-700 dark:text-zinc-300\", className)}>\r\n      {children}\r\n    <\/div>\r\n  );\r\n}\r\n\r\nModalBody.displayName = \"ModalBody\";\r\n","type":"registry:ui","target":"components\/fancy\/modal\/ModalBody.tsx"},{"path":"components\/fancy\/modal\/ModalFooter.tsx","content":"import { cn } from \"..\/..\/utils\/cn\";\r\nimport type { ModalFooterProps } from \".\/Modal.types\";\r\n\r\nexport function ModalFooter({ children, className }: ModalFooterProps) {\r\n  return (\r\n    <div\r\n      data-react-fancy-modal-footer=\"\"\r\n      className={cn(\r\n        \"flex items-center justify-end gap-3 border-t border-zinc-200 px-6 py-4 dark:border-zinc-700\",\r\n        className,\r\n      )}\r\n    >\r\n      {children}\r\n    <\/div>\r\n  );\r\n}\r\n\r\nModalFooter.displayName = \"ModalFooter\";\r\n","type":"registry:ui","target":"components\/fancy\/modal\/ModalFooter.tsx"},{"path":"components\/fancy\/modal\/ModalHeader.tsx","content":"import { X } from \"lucide-react\";\r\nimport { cn } from \"..\/..\/utils\/cn\";\r\nimport { useModal } from \".\/Modal.context\";\r\nimport type { ModalHeaderProps } from \".\/Modal.types\";\r\n\r\nexport function ModalHeader({ children, className }: ModalHeaderProps) {\r\n  const { close } = useModal();\r\n\r\n  return (\r\n    <div\r\n      data-react-fancy-modal-header=\"\"\r\n      className={cn(\r\n        \"flex items-center justify-between border-b border-zinc-200 px-6 py-4 dark:border-zinc-700\",\r\n        className,\r\n      )}\r\n    >\r\n      <div className=\"text-lg font-semibold text-zinc-900 dark:text-zinc-100\">{children}<\/div>\r\n      <button\r\n        type=\"button\"\r\n        onClick={close}\r\n        className=\"rounded-lg p-1 text-zinc-400 transition-colors hover:bg-zinc-100 hover:text-zinc-600 dark:hover:bg-zinc-800 dark:hover:text-zinc-300\"\r\n        aria-label=\"Close\"\r\n      >\r\n        <X size={20} \/>\r\n      <\/button>\r\n    <\/div>\r\n  );\r\n}\r\n\r\nModalHeader.displayName = \"ModalHeader\";\r\n","type":"registry:ui","target":"components\/fancy\/modal\/ModalHeader.tsx"},{"path":"components\/fancy\/modal\/index.ts","content":"export { Modal } from \".\/Modal\";\r\nexport { useModal } from \".\/Modal.context\";\r\nexport type {\r\n  ModalProps,\r\n  ModalHeaderProps,\r\n  ModalBodyProps,\r\n  ModalFooterProps,\r\n  ModalContextValue,\r\n} from \".\/Modal.types\";\r\n","type":"registry:ui","target":"components\/fancy\/modal\/index.ts"}]}