{
  "name": "morphing-dialog-basic-1",
  "type": "registry:ui",
  "componentName": "morphing-dialog-basic-1",
  "description": "Basic implementation of the morphing dialog effect.",
  "files": [
    {
      "path": "morphing-dialog-basic-1.tsx",
      "content": "import {\n  MorphingDialog,\n  MorphingDialogTrigger,\n  MorphingDialogContent,\n  MorphingDialogTitle,\n  MorphingDialogImage,\n  MorphingDialogSubtitle,\n  MorphingDialogClose,\n  MorphingDialogDescription,\n  MorphingDialogContainer,\n} from '@/components/core/morphing-dialog';\nimport { PlusIcon } from 'lucide-react';\n\nexport function MorphingDialogBasicOne() {\n  return (\n    <MorphingDialog\n      transition={{\n        type: 'spring',\n        bounce: 0.05,\n        duration: 0.25,\n      }}\n    >\n      <MorphingDialogTrigger\n        style={{\n          borderRadius: '12px',\n        }}\n        className='flex max-w-[270px] flex-col overflow-hidden border border-zinc-950/10 bg-white dark:border-zinc-50/10 dark:bg-zinc-900'\n      >\n        <MorphingDialogImage\n          src='/eb-27-lamp-edouard-wilfrid-buquet.jpg'\n          alt='A desk lamp designed by Edouard Wilfrid Buquet in 1925. It features a double-arm design and is made from nickel-plated brass, aluminium and varnished wood.'\n          className='h-48 w-full object-cover'\n        />\n        <div className='flex grow flex-row items-end justify-between px-3 py-2'>\n          <div>\n            <MorphingDialogTitle className='text-zinc-950 dark:text-zinc-50'>\n              EB27\n            </MorphingDialogTitle>\n            <MorphingDialogSubtitle className='text-zinc-700 dark:text-zinc-400'>\n              Edouard Wilfrid Buquet\n            </MorphingDialogSubtitle>\n          </div>\n          <span\n            className='relative ml-1 flex h-6 w-6 shrink-0 scale-100 select-none appearance-none items-center justify-center rounded-lg border border-zinc-950/10 text-zinc-500 transition-colors hover:bg-zinc-100 hover:text-zinc-800 focus-visible:ring-2 active:scale-[0.98] dark:border-zinc-50/10 dark:bg-zinc-900 dark:text-zinc-500 dark:hover:bg-zinc-800 dark:hover:text-zinc-50 dark:focus-visible:ring-zinc-500'\n            aria-hidden='true'\n          >\n            <PlusIcon size={12} />\n          </span>\n        </div>\n      </MorphingDialogTrigger>\n      <MorphingDialogContainer>\n        <MorphingDialogContent\n          style={{\n            borderRadius: '24px',\n          }}\n          className='pointer-events-auto relative flex h-auto w-full flex-col overflow-hidden border border-zinc-950/10 bg-white dark:border-zinc-50/10 dark:bg-zinc-900 sm:w-[500px]'\n        >\n          <MorphingDialogImage\n            src='/eb-27-lamp-edouard-wilfrid-buquet.jpg'\n            alt='A desk lamp designed by Edouard Wilfrid Buquet in 1925. It features a double-arm design and is made from nickel-plated brass, aluminium and varnished wood.'\n            className='h-full w-full'\n          />\n          <div className='p-6'>\n            <MorphingDialogTitle className='text-2xl text-zinc-950 dark:text-zinc-50'>\n              EB27\n            </MorphingDialogTitle>\n            <MorphingDialogSubtitle className='text-zinc-700 dark:text-zinc-400'>\n              Edouard Wilfrid Buquet\n            </MorphingDialogSubtitle>\n            <MorphingDialogDescription\n              disableLayoutAnimation\n              variants={{\n                initial: { opacity: 0, scale: 0.8, y: 100 },\n                animate: { opacity: 1, scale: 1, y: 0 },\n                exit: { opacity: 0, scale: 0.8, y: 100 },\n              }}\n            >\n              <p className='mt-2 text-zinc-500 dark:text-zinc-500'>\n                Little is known about the life of Édouard-Wilfrid Buquet. He was\n                born in France in 1866, but the time and place of his death is\n                unfortunately a mystery.\n              </p>\n              <p className='text-zinc-500'>\n                Research conducted in the 1970s revealed that he’d designed the\n                “EB 27” double-arm desk lamp in 1925, handcrafting it from\n                nickel-plated brass, aluminium and varnished wood.\n              </p>\n              <a\n                className='mt-2 inline-flex text-zinc-500 underline'\n                href='https://www.are.na/block/12759029'\n                target='_blank'\n                rel='noopener noreferrer'\n              >\n                Are.na block\n              </a>\n            </MorphingDialogDescription>\n          </div>\n          <MorphingDialogClose className='text-zinc-50' />\n        </MorphingDialogContent>\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"
    }
  ]
}