{ "name": "morphing-dialog", "type": "registry:ui", "registryDependencies": [], "dependencies": [ "motion" ], "devDependencies": [], "tailwind": {}, "cssVars": { "light": {}, "dark": {} }, "files": [ { "path": "morphing-dialog.tsx", "content": "'use client';\n\nimport React, {\n useCallback,\n useContext,\n useEffect,\n useId,\n useMemo,\n useRef,\n useState,\n} from 'react';\nimport {\n motion,\n AnimatePresence,\n MotionConfig,\n Transition,\n Variant,\n} from 'motion/react';\nimport { createPortal } from 'react-dom';\nimport { cn } from '@/lib/utils';\nimport { XIcon } from 'lucide-react';\nimport useClickOutside from '@/hooks/useClickOutside';\n\nexport type MorphingDialogContextType = {\n isOpen: boolean;\n setIsOpen: React.Dispatch>;\n uniqueId: string;\n triggerRef: React.RefObject;\n};\n\nconst MorphingDialogContext =\n React.createContext(null);\n\nfunction useMorphingDialog() {\n const context = useContext(MorphingDialogContext);\n if (!context) {\n throw new Error(\n 'useMorphingDialog must be used within a MorphingDialogProvider'\n );\n }\n return context;\n}\n\nexport type MorphingDialogProviderProps = {\n children: React.ReactNode;\n transition?: Transition;\n};\n\nfunction MorphingDialogProvider({\n children,\n transition,\n}: MorphingDialogProviderProps) {\n const [isOpen, setIsOpen] = useState(false);\n const uniqueId = useId();\n const triggerRef = useRef(null!);\n\n const contextValue = useMemo(\n () => ({\n isOpen,\n setIsOpen,\n uniqueId,\n triggerRef,\n }),\n [isOpen, uniqueId]\n );\n\n return (\n \n {children}\n \n );\n}\n\nexport type MorphingDialogProps = {\n children: React.ReactNode;\n transition?: Transition;\n};\n\nfunction MorphingDialog({ children, transition }: MorphingDialogProps) {\n return (\n \n {children}\n \n );\n}\n\nexport type MorphingDialogTriggerProps = {\n children: React.ReactNode;\n className?: string;\n style?: React.CSSProperties;\n triggerRef?: React.RefObject;\n};\n\nfunction MorphingDialogTrigger({\n children,\n className,\n style,\n triggerRef,\n}: MorphingDialogTriggerProps) {\n const { setIsOpen, isOpen, uniqueId } = useMorphingDialog();\n\n const handleClick = useCallback(() => {\n setIsOpen(!isOpen);\n }, [isOpen, setIsOpen]);\n\n const handleKeyDown = useCallback(\n (event: React.KeyboardEvent) => {\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault();\n setIsOpen(!isOpen);\n }\n },\n [isOpen, setIsOpen]\n );\n\n return (\n \n {children}\n \n );\n}\n\nexport type MorphingDialogContentProps = {\n children: React.ReactNode;\n className?: string;\n style?: React.CSSProperties;\n};\n\nfunction MorphingDialogContent({\n children,\n className,\n style,\n}: MorphingDialogContentProps) {\n const { setIsOpen, isOpen, uniqueId, triggerRef } = useMorphingDialog();\n const containerRef = useRef(null!);\n const [firstFocusableElement, setFirstFocusableElement] =\n useState(null);\n const [lastFocusableElement, setLastFocusableElement] =\n useState(null);\n\n useEffect(() => {\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.key === 'Escape') {\n setIsOpen(false);\n }\n if (event.key === 'Tab') {\n if (!firstFocusableElement || !lastFocusableElement) return;\n\n if (event.shiftKey) {\n if (document.activeElement === firstFocusableElement) {\n event.preventDefault();\n lastFocusableElement.focus();\n }\n } else {\n if (document.activeElement === lastFocusableElement) {\n event.preventDefault();\n firstFocusableElement.focus();\n }\n }\n }\n };\n\n document.addEventListener('keydown', handleKeyDown);\n\n return () => {\n document.removeEventListener('keydown', handleKeyDown);\n };\n }, [setIsOpen, firstFocusableElement, lastFocusableElement]);\n\n useEffect(() => {\n if (isOpen) {\n document.body.classList.add('overflow-hidden');\n const focusableElements = containerRef.current?.querySelectorAll(\n 'button, [href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])'\n );\n if (focusableElements && focusableElements.length > 0) {\n setFirstFocusableElement(focusableElements[0] as HTMLElement);\n setLastFocusableElement(\n focusableElements[focusableElements.length - 1] as HTMLElement\n );\n (focusableElements[0] as HTMLElement).focus();\n }\n } else {\n document.body.classList.remove('overflow-hidden');\n triggerRef.current?.focus();\n }\n }, [isOpen, triggerRef]);\n\n useClickOutside(containerRef, () => {\n if (isOpen) {\n setIsOpen(false);\n }\n });\n\n return (\n \n {children}\n \n );\n}\n\nexport type MorphingDialogContainerProps = {\n children: React.ReactNode;\n className?: string;\n style?: React.CSSProperties;\n};\n\nfunction MorphingDialogContainer({ children }: MorphingDialogContainerProps) {\n const { isOpen, uniqueId } = useMorphingDialog();\n const [mounted, setMounted] = useState(false);\n\n useEffect(() => {\n setMounted(true);\n return () => setMounted(false);\n }, []);\n\n if (!mounted) return null;\n\n return createPortal(\n \n {isOpen && (\n <>\n \n
\n {children}\n
\n \n )}\n
,\n document.body\n );\n}\n\nexport type MorphingDialogTitleProps = {\n children: React.ReactNode;\n className?: string;\n style?: React.CSSProperties;\n};\n\nfunction MorphingDialogTitle({\n children,\n className,\n style,\n}: MorphingDialogTitleProps) {\n const { uniqueId } = useMorphingDialog();\n\n return (\n \n {children}\n \n );\n}\n\nexport type MorphingDialogSubtitleProps = {\n children: React.ReactNode;\n className?: string;\n style?: React.CSSProperties;\n};\n\nfunction MorphingDialogSubtitle({\n children,\n className,\n style,\n}: MorphingDialogSubtitleProps) {\n const { uniqueId } = useMorphingDialog();\n\n return (\n \n {children}\n \n );\n}\n\nexport type MorphingDialogDescriptionProps = {\n children: React.ReactNode;\n className?: string;\n disableLayoutAnimation?: boolean;\n variants?: {\n initial: Variant;\n animate: Variant;\n exit: Variant;\n };\n};\n\nfunction MorphingDialogDescription({\n children,\n className,\n variants,\n disableLayoutAnimation,\n}: MorphingDialogDescriptionProps) {\n const { uniqueId } = useMorphingDialog();\n\n return (\n \n {children}\n \n );\n}\n\nexport type MorphingDialogImageProps = {\n src: string;\n alt: string;\n className?: string;\n style?: React.CSSProperties;\n};\n\nfunction MorphingDialogImage({\n src,\n alt,\n className,\n style,\n}: MorphingDialogImageProps) {\n const { uniqueId } = useMorphingDialog();\n\n return (\n \n );\n}\n\nexport type MorphingDialogCloseProps = {\n children?: React.ReactNode;\n className?: string;\n variants?: {\n initial: Variant;\n animate: Variant;\n exit: Variant;\n };\n};\n\nfunction MorphingDialogClose({\n children,\n className,\n variants,\n}: MorphingDialogCloseProps) {\n const { setIsOpen, uniqueId } = useMorphingDialog();\n\n const handleClose = useCallback(() => {\n setIsOpen(false);\n }, [setIsOpen]);\n\n return (\n \n {children || }\n \n );\n}\n\nexport {\n MorphingDialog,\n MorphingDialogTrigger,\n MorphingDialogContainer,\n MorphingDialogContent,\n MorphingDialogClose,\n MorphingDialogTitle,\n MorphingDialogSubtitle,\n MorphingDialogDescription,\n MorphingDialogImage,\n};\n", "type": "registry:ui" }, { "path": "hooks/useClickOutside.tsx", "content": "import { RefObject, useEffect } from 'react';\n\nfunction useClickOutside(\n ref: RefObject,\n handler: (event: MouseEvent | TouchEvent) => void\n): void {\n useEffect(() => {\n const handleClickOutside = (event: MouseEvent | TouchEvent) => {\n if (!ref || !ref.current || ref.current.contains(event.target as Node)) {\n return;\n }\n\n handler(event);\n };\n\n document.addEventListener('mousedown', handleClickOutside);\n document.addEventListener('touchstart', handleClickOutside);\n\n return () => {\n document.removeEventListener('mousedown', handleClickOutside);\n document.removeEventListener('touchstart', handleClickOutside);\n };\n }, [ref, handler]);\n}\n\nexport default useClickOutside;\n", "type": "registry:hook" } ] }