{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "morphing-dialog",
  "type": "registry:ui",
  "title": "Morphing Dialog",
  "description": "Dialog with morphing animation using native View Transitions API. Supports click-outside and Escape key.",
  "dependencies": [
    "lucide-react"
  ],
  "files": [
    {
      "path": "registry/new-york/ui/morphing-dialog.tsx",
      "content": "\"use client\"\n\nimport { X } from \"lucide-react\"\nimport * as React from \"react\"\nimport { flushSync } from \"react-dom\"\nimport { cn } from \"@/lib/utils\"\n\n// ============================================================================\n// Types & Context\n// ============================================================================\n\ninterface MorphingDialogContextValue {\n  isOpen: boolean\n  uniqueId: string\n  openDialog: (triggerEl: HTMLElement) => void\n  closeDialog: () => void\n  dialogRef: React.RefObject<HTMLDialogElement | null>\n  sourceRef: React.MutableRefObject<HTMLElement | null>\n}\n\nconst MorphingDialogContext =\n  React.createContext<MorphingDialogContextValue | null>(null)\n\nfunction useMorphingDialog() {\n  const context = React.useContext(MorphingDialogContext)\n  if (!context) {\n    throw new Error(\n      \"MorphingDialog components must be used within MorphingDialog\",\n    )\n  }\n  return context\n}\n\n// ============================================================================\n// Utilities\n// ============================================================================\n\nconst prefersReducedMotion = () => {\n  if (typeof window === \"undefined\") return false\n  return window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n}\n\n// Fallback for browsers without View Transitions\nconst transition = (callback: () => void) => {\n  if (\n    typeof document === \"undefined\" ||\n    !document.startViewTransition ||\n    prefersReducedMotion()\n  ) {\n    callback()\n    return { finished: Promise.resolve() }\n  }\n  return document.startViewTransition(callback)\n}\n\n// Helper to set/clear view transition names\nconst setTransitionNames = (\n  element: HTMLElement | null,\n  name: string,\n  active: boolean,\n) => {\n  if (!element || prefersReducedMotion()) return\n  element.style.viewTransitionName = active ? name : \"\"\n}\n\n// ============================================================================\n// MorphingDialog (Root)\n// ============================================================================\n\ninterface MorphingDialogProps {\n  children: React.ReactNode\n}\n\nexport function MorphingDialog({ children }: MorphingDialogProps) {\n  const [isOpen, setIsOpen] = React.useState(false)\n  const uniqueId = React.useId().replace(/:/g, \"-\")\n  const dialogRef = React.useRef<HTMLDialogElement | null>(null)\n  const sourceRef = React.useRef<HTMLElement | null>(null)\n\n  const openDialog = React.useCallback(\n    (triggerEl: HTMLElement) => {\n      sourceRef.current = triggerEl\n      const dialog = dialogRef.current\n      if (!dialog) return\n\n      // Set view transition name on source before transition\n      setTransitionNames(triggerEl, `morph-${uniqueId}`, true)\n\n      transition(() => {\n        // Mark source as expanded (hidden) and remove name\n        triggerEl.setAttribute(\"data-expanded\", \"true\")\n        setTransitionNames(triggerEl, `morph-${uniqueId}`, false)\n        // Render content first\n        flushSync(() => {\n          setIsOpen(true)\n        })\n        // Open dialog (makes it visible)\n        dialog.showModal()\n        // Now set name on visible dialog\n        setTransitionNames(dialog, `morph-${uniqueId}`, true)\n      }).finished.then(() => {\n        // Keep name on dialog for closing transition\n      })\n    },\n    [uniqueId],\n  )\n\n  const closeDialog = React.useCallback(() => {\n    const dialog = dialogRef.current\n    const source = sourceRef.current\n    if (!dialog || !source) return\n\n    // Dialog already has view-transition-name from opening\n    transition(() => {\n      // Hand-off: remove from dialog, add to source\n      setTransitionNames(dialog, `morph-${uniqueId}`, false)\n      setTransitionNames(source, `morph-${uniqueId}`, true)\n      // Remove expanded state\n      source.removeAttribute(\"data-expanded\")\n      // Close dialog\n      flushSync(() => {\n        setIsOpen(false)\n      })\n      dialog.close()\n    }).finished.then(() => {\n      // Clean up after transition\n      if (source) {\n        setTransitionNames(source, `morph-${uniqueId}`, false)\n      }\n    })\n  }, [uniqueId])\n\n  return (\n    <MorphingDialogContext.Provider\n      value={{\n        isOpen,\n        uniqueId,\n        openDialog,\n        closeDialog,\n        dialogRef,\n        sourceRef,\n      }}\n    >\n      {children}\n    </MorphingDialogContext.Provider>\n  )\n}\n\n// ============================================================================\n// MorphingDialogTrigger\n// ============================================================================\n\ninterface MorphingDialogTriggerProps {\n  children: React.ReactNode\n  className?: string\n  style?: React.CSSProperties\n}\n\nexport function MorphingDialogTrigger({\n  children,\n  className,\n  style,\n}: MorphingDialogTriggerProps) {\n  const { openDialog } = useMorphingDialog()\n  const triggerRef = React.useRef<HTMLDivElement>(null)\n\n  const handleClick = React.useCallback(() => {\n    if (triggerRef.current) {\n      openDialog(triggerRef.current)\n    }\n  }, [openDialog])\n\n  return (\n    <div\n      ref={triggerRef}\n      onClick={handleClick}\n      className={cn(\n        \"cursor-pointer [&[data-expanded=true]]:invisible\",\n        className,\n      )}\n      style={style}\n      role=\"button\"\n      tabIndex={0}\n      onKeyDown={(e) => {\n        if (e.key === \"Enter\" || e.key === \" \") {\n          e.preventDefault()\n          handleClick()\n        }\n      }}\n    >\n      {children}\n    </div>\n  )\n}\n\n// ============================================================================\n// MorphingDialogContainer (Native Dialog)\n// ============================================================================\n\ninterface MorphingDialogContainerProps {\n  children: React.ReactNode\n}\n\nexport function MorphingDialogContainer({\n  children,\n}: MorphingDialogContainerProps) {\n  const { isOpen, closeDialog, dialogRef } = useMorphingDialog()\n\n  // Handle cancel event (Escape key, click outside with closedby=\"any\")\n  const handleCancel = React.useCallback(\n    (e: React.SyntheticEvent) => {\n      e.preventDefault()\n      closeDialog()\n    },\n    [closeDialog],\n  )\n\n  // Handle backdrop click\n  const handleBackdropClick = React.useCallback(\n    (e: React.MouseEvent<HTMLDialogElement>) => {\n      // Only close if clicking directly on the dialog (backdrop)\n      if (e.target === e.currentTarget) {\n        closeDialog()\n      }\n    },\n    [closeDialog],\n  )\n\n  return (\n    <dialog\n      ref={dialogRef}\n      onCancel={handleCancel}\n      onClick={handleBackdropClick}\n      className={cn(\n        \"m-auto border-none bg-transparent p-0\",\n        \"w-full max-w-lg max-h-[calc(100vh-2rem)]\",\n        \"backdrop:bg-black/50 backdrop:backdrop-blur-sm\",\n      )}\n    >\n      {isOpen && children}\n    </dialog>\n  )\n}\n\n// ============================================================================\n// MorphingDialogContent\n// ============================================================================\n\ninterface MorphingDialogContentProps {\n  children: React.ReactNode\n  className?: string\n  style?: React.CSSProperties\n}\n\nexport function MorphingDialogContent({\n  children,\n  className,\n  style,\n}: MorphingDialogContentProps) {\n  const contentRef = React.useRef<HTMLDivElement>(null)\n\n  // Focus content when mounted\n  React.useEffect(() => {\n    contentRef.current?.focus()\n  }, [])\n\n  return (\n    <div\n      ref={contentRef}\n      className={cn(\n        \"relative w-full rounded-xl bg-background p-6 shadow-2xl outline-none\",\n        className,\n      )}\n      style={style}\n      tabIndex={-1}\n    >\n      {children}\n    </div>\n  )\n}\n\n// ============================================================================\n// MorphingDialogImage\n// ============================================================================\n\ninterface MorphingDialogImageProps {\n  src: string\n  alt: string\n  className?: string\n  style?: React.CSSProperties\n}\n\nexport function MorphingDialogImage({\n  src,\n  alt,\n  className,\n  style,\n}: MorphingDialogImageProps) {\n  return (\n    <img\n      src={src}\n      alt={alt}\n      className={cn(\"object-cover\", className)}\n      style={style}\n    />\n  )\n}\n\n// ============================================================================\n// MorphingDialogTitle\n// ============================================================================\n\ninterface MorphingDialogTitleProps {\n  children: React.ReactNode\n  className?: string\n}\n\nexport function MorphingDialogTitle({\n  children,\n  className,\n}: MorphingDialogTitleProps) {\n  return <h2 className={cn(\"text-lg font-semibold\", className)}>{children}</h2>\n}\n\n// ============================================================================\n// MorphingDialogSubtitle\n// ============================================================================\n\ninterface MorphingDialogSubtitleProps {\n  children: React.ReactNode\n  className?: string\n}\n\nexport function MorphingDialogSubtitle({\n  children,\n  className,\n}: MorphingDialogSubtitleProps) {\n  return (\n    <p className={cn(\"text-sm text-muted-foreground\", className)}>{children}</p>\n  )\n}\n\n// ============================================================================\n// MorphingDialogDescription\n// ============================================================================\n\ninterface MorphingDialogDescriptionProps {\n  children: React.ReactNode\n  className?: string\n}\n\nexport function MorphingDialogDescription({\n  children,\n  className,\n}: MorphingDialogDescriptionProps) {\n  return (\n    <div className={cn(\"mt-4 text-sm text-muted-foreground\", className)}>\n      {children}\n    </div>\n  )\n}\n\n// ============================================================================\n// MorphingDialogClose\n// ============================================================================\n\ninterface MorphingDialogCloseProps {\n  children?: React.ReactNode\n  className?: string\n}\n\nexport function MorphingDialogClose({\n  children,\n  className,\n}: MorphingDialogCloseProps) {\n  const { closeDialog } = useMorphingDialog()\n\n  return (\n    <button\n      onClick={closeDialog}\n      className={cn(\n        \"absolute right-2 top-2 rounded-full p-1 transition-colors cursor-pointer\",\n        \"bg-background/80 text-foreground backdrop-blur-sm\",\n        \"hover:bg-background\",\n        // Invisible touch target extension for mobile (44x44px minimum)\n        \"after:absolute after:inset-0 after:-m-2 after:content-['']\",\n        className,\n      )}\n      aria-label=\"Close dialog\"\n    >\n      {children || <X className=\"h-3.5 w-3.5\" />}\n    </button>\n  )\n}\n",
      "type": "registry:ui"
    }
  ]
}