{
  "$schema": "https://blode.co/ui/schema/registry-item.json",
  "name": "responsive-dialog",
  "title": "Responsive Dialog",
  "author": "Matthew Blode",
  "description": "A dialog on desktop that becomes a drawer on small screens.",
  "registryDependencies": ["dialog", "drawer", "@blode/use-mobile"],
  "files": [
    {
      "path": "ui/responsive-dialog.tsx",
      "content": "\"use client\";\n\nimport type * as React from \"react\";\nimport { createContext, useContext, useState } from \"react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { useIsMobile } from \"@/hooks/use-mobile\";\nimport {\n  Dialog,\n  DialogClose,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n  DialogTrigger,\n} from \"@/components/ui/dialog\";\nimport {\n  Drawer,\n  DrawerClose,\n  DrawerContent,\n  DrawerDescription,\n  DrawerFooter,\n  DrawerHeader,\n  DrawerTitle,\n  DrawerTrigger,\n} from \"@/components/ui/drawer\";\n\nconst ResponsiveDialogContext = createContext<boolean | null>(null);\n\n/**\n * Reads the layout decided by the nearest `ResponsiveDialog`, so every part\n * renders against the same primitive without calling `useIsMobile` again.\n */\nconst useResponsiveDialogLayout = () => {\n  const isMobile = useContext(ResponsiveDialogContext);\n\n  if (isMobile === null) {\n    throw new Error(\"ResponsiveDialog parts must be rendered inside <ResponsiveDialog>.\");\n  }\n\n  return isMobile;\n};\n\nexport interface ResponsiveDialogProps {\n  /** The dialog parts. */\n  children?: React.ReactNode;\n  /** Whether the dialog is open on first render, for uncontrolled usage. */\n  defaultOpen?: boolean;\n  /** Whether the dialog traps focus and locks page scroll while open. */\n  modal?: boolean | \"trap-focus\";\n  /** Called when the dialog opens or closes. */\n  onOpenChange?: (open: boolean) => void;\n  /** Whether the dialog is open, for controlled usage. */\n  open?: boolean;\n}\n\nconst ResponsiveDialog = ({\n  children,\n  defaultOpen,\n  onOpenChange,\n  open,\n  ...props\n}: ResponsiveDialogProps) => {\n  // The layout always tracks the viewport, including while the dialog is open.\n  // Crossing 768px unmounts one tree and mounts the other, so focus and any\n  // uncontrolled input state inside are lost — see the demo, which lifts its\n  // field state above this component. Holding the primitive until close would\n  // avoid that, at the cost of rendering a drawer at desktop widths, where\n  // every `sm:`/`md:` class a caller wrote for the dialog now applies to the\n  // drawer instead. That mismatch is visible on every resize; the state loss\n  // is not, and callers can prevent it.\n  const isMobile = useIsMobile();\n  const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen ?? false);\n\n  const handleOpenChange = (next: boolean) => {\n    setUncontrolledOpen(next);\n    onOpenChange?.(next);\n  };\n\n  // The swapped-in root has no memory of the other tree's open state, so an\n  // uncontrolled dialog has to be told it is open across the boundary.\n  const isOpen = open ?? uncontrolledOpen;\n  const Root = isMobile ? Drawer : Dialog;\n\n  return (\n    <ResponsiveDialogContext.Provider value={isMobile}>\n      <Root onOpenChange={handleOpenChange} open={isOpen} {...props}>\n        {children}\n      </Root>\n    </ResponsiveDialogContext.Provider>\n  );\n};\n\nexport interface ResponsiveDialogTriggerProps extends React.ComponentProps<\"button\"> {\n  /** Render the single child element as the trigger instead of a button. */\n  asChild?: boolean;\n}\n\nconst ResponsiveDialogTrigger = ({ asChild, ...props }: ResponsiveDialogTriggerProps) => {\n  const isMobile = useResponsiveDialogLayout();\n  const Trigger = isMobile ? DrawerTrigger : DialogTrigger;\n\n  return <Trigger asChild={asChild} data-slot=\"responsive-dialog-trigger\" {...props} />;\n};\n\nexport interface ResponsiveDialogContentProps extends React.ComponentProps<\"div\"> {\n  /** Show the corner close button. Desktop only — the drawer closes by swipe. */\n  showCloseButton?: boolean;\n}\n\nconst ResponsiveDialogContent = ({\n  children,\n  className,\n  showCloseButton = true,\n  ...props\n}: ResponsiveDialogContentProps) => {\n  const isMobile = useResponsiveDialogLayout();\n\n  // `data-layout` is the styling hook, not a breakpoint. Which primitive is\n  // mounted is the thing that varies, and a viewport query only approximates\n  // it: `md:` happens to line up with the 768px swap, `sm:` does not, so\n  // `sm:max-w-*` written for the dialog also clamps the drawer between 640 and\n  // 767px. Style the content root with `data-[layout=dialog]:*` and anything\n  // below it with `group-data-[layout=drawer]/responsive-dialog:*`, and the\n  // condition is the rendered primitive exactly.\n  if (isMobile) {\n    return (\n      <DrawerContent\n        className={cn(\"group/responsive-dialog\", className)}\n        data-layout=\"drawer\"\n        data-slot=\"responsive-dialog-content\"\n        {...props}\n      >\n        {children}\n      </DrawerContent>\n    );\n  }\n\n  return (\n    <DialogContent\n      className={cn(\"group/responsive-dialog\", className)}\n      data-layout=\"dialog\"\n      data-slot=\"responsive-dialog-content\"\n      showCloseButton={showCloseButton}\n      {...props}\n    >\n      {children}\n    </DialogContent>\n  );\n};\n\nconst ResponsiveDialogHeader = ({ className, ...props }: React.ComponentProps<\"div\">) => {\n  const isMobile = useResponsiveDialogLayout();\n  const Header = isMobile ? DrawerHeader : DialogHeader;\n\n  return <Header className={className} data-slot=\"responsive-dialog-header\" {...props} />;\n};\n\nconst ResponsiveDialogFooter = ({ className, ...props }: React.ComponentProps<\"div\">) => {\n  const isMobile = useResponsiveDialogLayout();\n\n  if (isMobile) {\n    return <DrawerFooter className={className} data-slot=\"responsive-dialog-footer\" {...props} />;\n  }\n\n  return <DialogFooter className={className} data-slot=\"responsive-dialog-footer\" {...props} />;\n};\n\nconst ResponsiveDialogTitle = ({ className, ...props }: React.ComponentProps<\"h2\">) => {\n  const isMobile = useResponsiveDialogLayout();\n\n  if (isMobile) {\n    return (\n      <DrawerTitle\n        className={cn(\"text-lg leading-none\", className)}\n        data-slot=\"responsive-dialog-title\"\n        {...props}\n      />\n    );\n  }\n\n  return <DialogTitle className={className} data-slot=\"responsive-dialog-title\" {...props} />;\n};\n\nconst ResponsiveDialogDescription = ({ className, ...props }: React.ComponentProps<\"p\">) => {\n  const isMobile = useResponsiveDialogLayout();\n  const Description = isMobile ? DrawerDescription : DialogDescription;\n\n  return <Description className={className} data-slot=\"responsive-dialog-description\" {...props} />;\n};\n\nexport interface ResponsiveDialogCloseProps extends React.ComponentProps<\"button\"> {\n  /** Render the single child element as the close control instead of a button. */\n  asChild?: boolean;\n}\n\nconst ResponsiveDialogClose = ({ asChild, ...props }: ResponsiveDialogCloseProps) => {\n  const isMobile = useResponsiveDialogLayout();\n  const Close = isMobile ? DrawerClose : DialogClose;\n\n  return <Close asChild={asChild} data-slot=\"responsive-dialog-close\" {...props} />;\n};\n\nexport {\n  ResponsiveDialog,\n  ResponsiveDialogClose,\n  ResponsiveDialogContent,\n  ResponsiveDialogDescription,\n  ResponsiveDialogFooter,\n  ResponsiveDialogHeader,\n  ResponsiveDialogTitle,\n  ResponsiveDialogTrigger,\n};\n",
      "type": "registry:ui",
      "target": ""
    }
  ],
  "type": "registry:ui"
}
