{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion","lucide-react","usehooks-ts"],"description":"A BasicModal component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { X } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { useEffect, useRef, useState } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { useOnClickOutside } from \"usehooks-ts\";\n\nexport interface BasicModalProps {\n  children: React.ReactNode;\n  isOpen: boolean;\n  onClose: () => void;\n  size?: \"sm\" | \"md\" | \"lg\" | \"xl\" | \"full\";\n  title?: string;\n}\n\nconst modalSizes = {\n  full: \"max-w-4xl\",\n  lg: \"max-w-lg\",\n  md: \"max-w-md\",\n  sm: \"max-w-sm\",\n  xl: \"max-w-xl\",\n};\n\nexport default function BasicModal({\n  isOpen,\n  onClose,\n  title,\n  children,\n  size = \"md\",\n}: BasicModalProps) {\n  const overlayRef = useRef<HTMLDivElement>(null);\n  const modalRef = useRef<HTMLDivElement>(\n    null\n  ) as React.RefObject<HTMLDivElement>;\n  const closeButtonRef = useRef<HTMLButtonElement>(null);\n  const previousActiveElementRef = useRef<HTMLElement | null>(null);\n  useOnClickOutside(modalRef, () => onClose());\n  const [mounted, setMounted] = useState(false);\n  const shouldReduceMotion = useReducedMotion();\n\n  const titleId = title\n    ? `modal-title-${Math.random().toString(36).substring(2, 9)}`\n    : undefined;\n\n  useEffect(() => {\n    setMounted(true);\n  }, []);\n\n  // Focus management: Save previous focus and restore on close\n  useEffect(() => {\n    if (isOpen) {\n      previousActiveElementRef.current = document.activeElement as HTMLElement;\n      // Focus the close button or first focusable element when modal opens\n      setTimeout(() => {\n        closeButtonRef.current?.focus();\n      }, 100);\n    } else if (previousActiveElementRef.current) {\n      // Restore focus when modal closes\n      previousActiveElementRef.current.focus();\n    }\n  }, [isOpen]);\n\n  // Close on Escape key press and focus trap\n  useEffect(() => {\n    if (!isOpen) {\n      return;\n    }\n\n    const handleKeyDown = (e: KeyboardEvent) => {\n      if (e.key === \"Escape\") {\n        onClose();\n        return;\n      }\n\n      // Focus trap: keep focus within modal\n      if (e.key === \"Tab\" && modalRef.current) {\n        const focusableElements = Array.from(\n          modalRef.current.querySelectorAll<HTMLElement>(\n            'button, [href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])'\n          )\n        );\n        const [firstElement] = focusableElements;\n        const lastElement = focusableElements.at(-1);\n\n        if (e.shiftKey) {\n          // Shift + Tab\n          if (document.activeElement === firstElement) {\n            e.preventDefault();\n            lastElement?.focus();\n          }\n        } else if (document.activeElement === lastElement) {\n          // Tab\n          e.preventDefault();\n          firstElement?.focus();\n        }\n      }\n    };\n\n    document.addEventListener(\"keydown\", handleKeyDown);\n    return () => document.removeEventListener(\"keydown\", handleKeyDown);\n  }, [isOpen, onClose]);\n\n  // Note: Body scroll locking is handled by the overlay and modal positioning\n  // No need to manually set body overflow as it can conflict with other components\n\n  const modalContent = (\n    <AnimatePresence>\n      {isOpen ? (\n        <>\n          {/* Backdrop */}\n          <motion.div\n            animate={{ opacity: 1 }}\n            className=\"fixed inset-0 z-[80] bg-background/70 backdrop-blur-sm\"\n            exit={{ opacity: 0 }}\n            initial={shouldReduceMotion ? { opacity: 1 } : { opacity: 0 }}\n            onClick={(e) => {\n              if (e.target === overlayRef.current) {\n                onClose();\n              }\n            }}\n            ref={overlayRef}\n            transition={{ duration: shouldReduceMotion ? 0 : 0.2 }}\n          />\n\n          {/* Modal */}\n          <motion.div\n            animate={{ opacity: 1 }}\n            className=\"fixed inset-0 z-[90] flex items-center justify-center overflow-y-auto px-4 py-6 sm:p-0\"\n            exit={{ opacity: 0 }}\n            initial={shouldReduceMotion ? { opacity: 1 } : { opacity: 0 }}\n            transition={{ duration: shouldReduceMotion ? 0 : 0.2 }}\n          >\n            <motion.div\n              animate={shouldReduceMotion ? {} : { opacity: 1, scale: 1, y: 0 }}\n              aria-labelledby={titleId}\n              aria-modal=\"true\"\n              className={`${modalSizes[size]} relative mx-auto w-full rounded-xl border bg-primary p-4 shadow-xl sm:p-6`}\n              exit={\n                shouldReduceMotion\n                  ? { opacity: 0, transition: { duration: 0 } }\n                  : {\n                      opacity: 0,\n                      scale: 0.95,\n                      transition: { duration: 0.15 },\n                      y: 10,\n                    }\n              }\n              initial={\n                shouldReduceMotion\n                  ? { opacity: 1 }\n                  : { opacity: 0, scale: 0.95, y: 10 }\n              }\n              ref={modalRef}\n              role=\"dialog\"\n              transition={\n                shouldReduceMotion\n                  ? { duration: 0 }\n                  : {\n                      damping: 25,\n                      duration: 0.25,\n                      stiffness: 300,\n                      type: \"spring\" as const,\n                    }\n              }\n            >\n              {/* Header */}\n              <div className=\"mb-4 flex items-center justify-between\">\n                {title ? (\n                  <h3 className=\"font-medium text-xl leading-6\" id={titleId}>\n                    {title}\n                  </h3>\n                ) : null}\n                <motion.button\n                  aria-label=\"Close modal\"\n                  className=\"ml-auto min-h-[44px] min-w-[44px] cursor-pointer rounded-full p-2 transition-colors hover:bg-secondary focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\"\n                  onClick={onClose}\n                  ref={closeButtonRef}\n                  transition={{ duration: shouldReduceMotion ? 0 : 0.2 }}\n                  type=\"button\"\n                  whileHover={shouldReduceMotion ? {} : { rotate: 90 }}\n                >\n                  <X aria-hidden=\"true\" className=\"h-5 w-5\" />\n                </motion.button>\n              </div>\n\n              {/* Content */}\n              <div className=\"relative\">{children}</div>\n            </motion.div>\n          </motion.div>\n        </>\n      ) : null}\n    </AnimatePresence>\n  );\n\n  if (!mounted) {\n    return null;\n  }\n\n  return createPortal(modalContent, document.body);\n}\n","path":"index.tsx","target":"components/smoothui/basic-modal/index.tsx","type":"registry:ui"}],"name":"basic-modal","registryDependencies":[],"title":"Basic Modal","type":"registry:ui"}