{
  "name": "morphing-dialog-image",
  "type": "registry:ui",
  "componentName": "morphing-dialog",
  "description": "Morphing dialog that transitions from an image to a full dialog.",
  "files": [
    {
      "path": "morphing-dialog-image.tsx",
      "content": "import {\n  MorphingDialog,\n  MorphingDialogTrigger,\n  MorphingDialogContent,\n  MorphingDialogClose,\n  MorphingDialogImage,\n  MorphingDialogContainer,\n} from '@/components/core/morphing-dialog';\nimport { XIcon } from 'lucide-react';\n\nexport function MorphingDialogBasicImage() {\n  return (\n    <MorphingDialog\n      transition={{\n        duration: 0.3,\n        ease: 'easeInOut',\n      }}\n    >\n      <MorphingDialogTrigger>\n        <MorphingDialogImage\n          src='https://64.media.tumblr.com/a56381366ceaab29b760aabe5e88df07/f3b5e9af4d3103f3-97/s2048x3072/caec6beeee2cbeb80b05e88c917b187926749703.jpg'\n          alt='Sony Style Store in the Sony Center complex - Berlin, Germany (2000)'\n          className='max-w-xs rounded-[4px]'\n        />\n      </MorphingDialogTrigger>\n      <MorphingDialogContainer>\n        <MorphingDialogContent className='relative'>\n          <MorphingDialogImage\n            src='https://64.media.tumblr.com/a56381366ceaab29b760aabe5e88df07/f3b5e9af4d3103f3-97/s2048x3072/caec6beeee2cbeb80b05e88c917b187926749703.jpg'\n            alt='Sony Style Store in the Sony Center complex - Berlin, Germany (2000)'\n            className='h-auto w-full max-w-[90vw] rounded-[4px] object-cover lg:h-[90vh]'\n          />\n        </MorphingDialogContent>\n        <MorphingDialogClose\n          className='fixed right-6 top-6 h-fit w-fit rounded-full bg-white p-1'\n          variants={{\n            initial: { opacity: 0 },\n            animate: {\n              opacity: 1,\n              transition: { delay: 0.3, duration: 0.1 },\n            },\n            exit: { opacity: 0, transition: { duration: 0 } },\n          }}\n        >\n          <XIcon className='h-5 w-5 text-zinc-500' />\n        </MorphingDialogClose>\n      </MorphingDialogContainer>\n    </MorphingDialog>\n  );\n}\n",
      "type": "registry:component"
    },
    {
      "path": "components/core/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<React.SetStateAction<boolean>>;\n  uniqueId: string;\n  triggerRef: React.RefObject<HTMLButtonElement | null>;\n};\n\nconst MorphingDialogContext =\n  React.createContext<MorphingDialogContextType | null>(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<HTMLButtonElement>(null!);\n\n  const contextValue = useMemo(\n    () => ({\n      isOpen,\n      setIsOpen,\n      uniqueId,\n      triggerRef,\n    }),\n    [isOpen, uniqueId]\n  );\n\n  return (\n    <MorphingDialogContext.Provider value={contextValue}>\n      <MotionConfig transition={transition}>{children}</MotionConfig>\n    </MorphingDialogContext.Provider>\n  );\n}\n\nexport type MorphingDialogProps = {\n  children: React.ReactNode;\n  transition?: Transition;\n};\n\nfunction MorphingDialog({ children, transition }: MorphingDialogProps) {\n  return (\n    <MorphingDialogProvider>\n      <MotionConfig transition={transition}>{children}</MotionConfig>\n    </MorphingDialogProvider>\n  );\n}\n\nexport type MorphingDialogTriggerProps = {\n  children: React.ReactNode;\n  className?: string;\n  style?: React.CSSProperties;\n  triggerRef?: React.RefObject<HTMLButtonElement>;\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    <motion.button\n      ref={triggerRef}\n      layoutId={`dialog-${uniqueId}`}\n      className={cn('relative cursor-pointer', className)}\n      onClick={handleClick}\n      onKeyDown={handleKeyDown}\n      style={style}\n      aria-haspopup='dialog'\n      aria-expanded={isOpen}\n      aria-controls={`motion-ui-morphing-dialog-content-${uniqueId}`}\n      aria-label={`Open dialog ${uniqueId}`}\n    >\n      {children}\n    </motion.button>\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<HTMLDivElement>(null!);\n  const [firstFocusableElement, setFirstFocusableElement] =\n    useState<HTMLElement | null>(null);\n  const [lastFocusableElement, setLastFocusableElement] =\n    useState<HTMLElement | null>(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    <motion.div\n      ref={containerRef}\n      layoutId={`dialog-${uniqueId}`}\n      className={cn('overflow-hidden', className)}\n      style={style}\n      role='dialog'\n      aria-modal='true'\n      aria-labelledby={`motion-ui-morphing-dialog-title-${uniqueId}`}\n      aria-describedby={`motion-ui-morphing-dialog-description-${uniqueId}`}\n    >\n      {children}\n    </motion.div>\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    <AnimatePresence initial={false} mode='sync'>\n      {isOpen && (\n        <>\n          <motion.div\n            key={`backdrop-${uniqueId}`}\n            className='fixed inset-0 h-full w-full bg-white/40 backdrop-blur-xs dark:bg-black/40'\n            initial={{ opacity: 0 }}\n            animate={{ opacity: 1 }}\n            exit={{ opacity: 0 }}\n          />\n          <div className='fixed inset-0 z-50 flex items-center justify-center'>\n            {children}\n          </div>\n        </>\n      )}\n    </AnimatePresence>,\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    <motion.div\n      layoutId={`dialog-title-container-${uniqueId}`}\n      className={className}\n      style={style}\n      layout\n    >\n      {children}\n    </motion.div>\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    <motion.div\n      layoutId={`dialog-subtitle-container-${uniqueId}`}\n      className={className}\n      style={style}\n    >\n      {children}\n    </motion.div>\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    <motion.div\n      key={`dialog-description-${uniqueId}`}\n      layoutId={\n        disableLayoutAnimation\n          ? undefined\n          : `dialog-description-content-${uniqueId}`\n      }\n      variants={variants}\n      className={className}\n      initial='initial'\n      animate='animate'\n      exit='exit'\n      id={`dialog-description-${uniqueId}`}\n    >\n      {children}\n    </motion.div>\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    <motion.img\n      src={src}\n      alt={alt}\n      className={cn(className)}\n      layoutId={`dialog-img-${uniqueId}`}\n      style={style}\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    <motion.button\n      onClick={handleClose}\n      type='button'\n      aria-label='Close dialog'\n      key={`dialog-close-${uniqueId}`}\n      className={cn('absolute top-6 right-6', className)}\n      initial='initial'\n      animate='animate'\n      exit='exit'\n      variants={variants}\n    >\n      {children || <XIcon size={24} />}\n    </motion.button>\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": "components/core/hooks/useClickOutside.tsx",
      "content": "import { RefObject, useEffect } from 'react';\n\nfunction useClickOutside<T extends HTMLElement>(\n  ref: RefObject<T | null>,\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"
    }
  ]
}