{
  "name": "dialog-custom-backdrop",
  "type": "registry:ui",
  "componentName": "dialog",
  "description": "Dialog implementation with a customized backdrop and transition.",
  "files": [
    {
      "path": "dialog-custom-backdrop.tsx",
      "content": "import {\n  Dialog,\n  DialogDescription,\n  DialogContent,\n  DialogHeader,\n  DialogTitle,\n  DialogClose,\n  DialogTrigger,\n} from '@/components/core/dialog';\n\nexport function DialogCustomBackdrop() {\n  return (\n    <Dialog>\n      <DialogTrigger className='bg-zinc-950 px-4 py-2 text-sm text-white hover:bg-zinc-900 dark:bg-white dark:text-zinc-900 dark:hover:bg-zinc-100'>\n        Join the waitlist\n      </DialogTrigger>\n      <DialogContent className='w-full max-w-md p-6 shadow-[0_4px_12px_#0000001a] backdrop:bg-white/80 backdrop:backdrop-blur-xs'>\n        <DialogHeader>\n          <DialogTitle className='text-zinc-900 dark:text-white'>\n            Join the waitlist\n          </DialogTitle>\n          <DialogDescription className='text-zinc-600 dark:text-zinc-400'>\n            Enter your email address to receive updates when we launch.\n          </DialogDescription>\n        </DialogHeader>\n        <div className='mt-6 flex flex-col space-y-4'>\n          <label htmlFor='name' className='sr-only'>\n            Email\n          </label>\n          <input\n            id='name'\n            type='email'\n            className='h-9 w-full rounded-lg border border-zinc-200 bg-white px-3 text-base text-zinc-900 outline-hidden focus:ring-2 focus:ring-black/5 dark:border-zinc-700 dark:bg-zinc-800 dark:text-white dark:focus:ring-white/5 sm:text-sm'\n            placeholder='Enter your email'\n          />\n          <button\n            className='inline-flex items-center justify-center self-end rounded-lg bg-black px-4 py-2 text-sm font-medium text-zinc-50 dark:bg-white dark:text-zinc-900'\n            type='submit'\n          >\n            Join now\n          </button>\n        </div>\n        <DialogClose />\n      </DialogContent>\n    </Dialog>\n  );\n}\n",
      "type": "registry:component"
    },
    {
      "path": "components/core/dialog.tsx",
      "content": "'use client';\nimport { AnimatePresence, motion, Transition, Variants } from 'motion/react';\nimport React, { createContext, useContext, useEffect, useRef } from 'react';\nimport { cn } from '@/lib/utils';\nimport { useId } from 'react';\nimport { createPortal } from 'react-dom';\nimport { X } from 'lucide-react';\nimport { usePreventScroll } from '@/hooks/usePreventScroll';\n\nconst DialogContext = createContext<{\n  isOpen: boolean;\n  setIsOpen: (open: boolean) => void;\n  dialogRef: React.RefObject<HTMLDialogElement | null>;\n  variants: Variants;\n  transition?: Transition;\n  ids: {\n    dialog: string;\n    title: string;\n    description: string;\n  };\n  onAnimationComplete: (definition: string) => void;\n  handleTrigger: () => void;\n} | null>(null);\n\nconst defaultVariants: Variants = {\n  initial: {\n    opacity: 0,\n    scale: 0.9,\n  },\n  animate: {\n    opacity: 1,\n    scale: 1,\n  },\n};\n\nconst defaultTransition: Transition = {\n  ease: 'easeOut',\n  duration: 0.2,\n};\n\nexport type DialogProps = {\n  children: React.ReactNode;\n  variants?: Variants;\n  transition?: Transition;\n  className?: string;\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  open?: boolean;\n};\n\nfunction Dialog({\n  children,\n  variants = defaultVariants,\n  transition = defaultTransition,\n  defaultOpen,\n  onOpenChange,\n  open,\n}: DialogProps) {\n  const [uncontrolledOpen, setUncontrolledOpen] = React.useState(\n    defaultOpen || false\n  );\n  const dialogRef = useRef<HTMLDialogElement>(null);\n  const isOpen = open !== undefined ? open : uncontrolledOpen;\n\n  // prevent scroll when dialog is open on iOS\n  usePreventScroll({\n    isDisabled: !isOpen,\n  });\n\n  const setIsOpen = React.useCallback(\n    (value: boolean) => {\n      setUncontrolledOpen(value);\n      onOpenChange?.(value);\n    },\n    [onOpenChange]\n  );\n\n  useEffect(() => {\n    const dialog = dialogRef.current;\n    if (!dialog) return;\n\n    if (isOpen) {\n      document.body.classList.add('overflow-hidden');\n    } else {\n      document.body.classList.remove('overflow-hidden');\n    }\n\n    const handleCancel = (e: Event) => {\n      e.preventDefault();\n      if (isOpen) {\n        setIsOpen(false);\n      }\n    };\n\n    dialog.addEventListener('cancel', handleCancel);\n    return () => {\n      dialog.removeEventListener('cancel', handleCancel);\n      document.body.classList.remove('overflow-hidden');\n    };\n  }, [dialogRef, isOpen, setIsOpen]);\n\n  useEffect(() => {\n    if (isOpen && dialogRef.current) {\n      dialogRef.current.showModal();\n    }\n  }, [isOpen]);\n\n  const handleTrigger = () => {\n    setIsOpen(true);\n  };\n\n  const onAnimationComplete = (definition: string) => {\n    if (definition === 'exit' && !isOpen) {\n      dialogRef.current?.close();\n    }\n  };\n\n  const baseId = useId();\n  const ids = {\n    dialog: `motion-ui-dialog-${baseId}`,\n    title: `motion-ui-dialog-title-${baseId}`,\n    description: `motion-ui-dialog-description-${baseId}`,\n  };\n\n  return (\n    <DialogContext.Provider\n      value={{\n        isOpen,\n        setIsOpen,\n        dialogRef,\n        variants,\n        transition,\n        ids,\n        onAnimationComplete,\n        handleTrigger,\n      }}\n    >\n      {children}\n    </DialogContext.Provider>\n  );\n}\n\nexport type DialogTriggerProps = {\n  children: React.ReactNode;\n  className?: string;\n};\n\nfunction DialogTrigger({ children, className }: DialogTriggerProps) {\n  const context = useContext(DialogContext);\n  if (!context) throw new Error('DialogTrigger must be used within Dialog');\n\n  return (\n    <button\n      onClick={context.handleTrigger}\n      className={cn(\n        'inline-flex items-center justify-center rounded-md text-sm font-medium',\n        'transition-colors focus-visible:ring-2 focus-visible:outline-hidden',\n        'focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',\n        className\n      )}\n    >\n      {children}\n    </button>\n  );\n}\n\nexport type DialogPortalProps = {\n  children: React.ReactNode;\n  container?: HTMLElement | null;\n};\n\nfunction DialogPortal({\n  children,\n  container = typeof window !== 'undefined' ? document.body : null,\n}: DialogPortalProps) {\n  const [mounted, setMounted] = React.useState(false);\n  const [portalContainer, setPortalContainer] =\n    React.useState<HTMLElement | null>(null);\n\n  useEffect(() => {\n    setMounted(true);\n    setPortalContainer(container || document.body);\n    return () => setMounted(false);\n  }, [container]);\n\n  if (!mounted || !portalContainer) {\n    return null;\n  }\n\n  return createPortal(children, portalContainer);\n}\nexport type DialogContentProps = {\n  children: React.ReactNode;\n  className?: string;\n  container?: HTMLElement;\n};\n\nfunction DialogContent({ children, className, container }: DialogContentProps) {\n  const context = useContext(DialogContext);\n  if (!context) throw new Error('DialogContent must be used within Dialog');\n  const {\n    isOpen,\n    setIsOpen,\n    dialogRef,\n    variants,\n    transition,\n    ids,\n    onAnimationComplete,\n  } = context;\n\n  const content = (\n    <AnimatePresence mode='wait'>\n      {isOpen && (\n        <motion.dialog\n          key={ids.dialog}\n          ref={dialogRef as React.RefObject<HTMLDialogElement>}\n          id={ids.dialog}\n          aria-labelledby={ids.title}\n          aria-describedby={ids.description}\n          aria-modal='true'\n          role='dialog'\n          onClick={(e) => {\n            if (e.target === dialogRef.current) {\n              setIsOpen(false);\n            }\n          }}\n          initial='initial'\n          animate='animate'\n          exit='exit'\n          variants={variants}\n          transition={transition}\n          onAnimationComplete={onAnimationComplete}\n          className={cn(\n            'fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 transform rounded-lg border border-zinc-200 p-0 shadow-lg dark:border dark:border-zinc-700',\n            'backdrop:bg-black/50 backdrop:backdrop-blur-xs',\n            'open:flex open:flex-col',\n            className\n          )}\n        >\n          <div className='w-full'>{children}</div>\n        </motion.dialog>\n      )}\n    </AnimatePresence>\n  );\n\n  return <DialogPortal container={container}>{content}</DialogPortal>;\n}\n\nexport type DialogHeaderProps = {\n  children: React.ReactNode;\n  className?: string;\n};\n\nfunction DialogHeader({ children, className }: DialogHeaderProps) {\n  return (\n    <div className={cn('flex flex-col space-y-1.5', className)}>{children}</div>\n  );\n}\n\nexport type DialogTitleProps = {\n  children: React.ReactNode;\n  className?: string;\n};\n\nfunction DialogTitle({ children, className }: DialogTitleProps) {\n  const context = useContext(DialogContext);\n  if (!context) throw new Error('DialogTitle must be used within Dialog');\n\n  return (\n    <h2\n      id={context.ids.title}\n      className={cn('text-base font-medium', className)}\n    >\n      {children}\n    </h2>\n  );\n}\n\nexport type DialogDescriptionProps = {\n  children: React.ReactNode;\n  className?: string;\n};\n\nfunction DialogDescription({ children, className }: DialogDescriptionProps) {\n  const context = useContext(DialogContext);\n  if (!context) throw new Error('DialogDescription must be used within Dialog');\n\n  return (\n    <p\n      id={context.ids.description}\n      className={cn('text-base text-zinc-500', className)}\n    >\n      {children}\n    </p>\n  );\n}\n\nexport type DialogCloseProps = {\n  className?: string;\n  children?: React.ReactNode;\n  disabled?: boolean;\n};\n\nfunction DialogClose({ className, children, disabled }: DialogCloseProps) {\n  const context = useContext(DialogContext);\n  if (!context) throw new Error('DialogClose must be used within Dialog');\n\n  return (\n    <button\n      onClick={() => context.setIsOpen(false)}\n      type='button'\n      aria-label='Close dialog'\n      className={cn(\n        'absolute top-4 right-4 rounded-xs opacity-70 transition-opacity',\n        'hover:opacity-100 focus:ring-2 focus:outline-hidden',\n        'focus:ring-zinc-500 focus:ring-offset-2 disabled:pointer-events-none',\n        className\n      )}\n      disabled={disabled}\n    >\n      {children || <X className='h-4 w-4' />}\n      <span className='sr-only'>Close</span>\n    </button>\n  );\n}\n\nexport {\n  Dialog,\n  DialogTrigger,\n  DialogContent,\n  DialogHeader,\n  DialogTitle,\n  DialogDescription,\n  DialogClose,\n};\n",
      "type": "registry:ui"
    },
    {
      "path": "components/core/hooks/usePreventScroll.tsx",
      "content": "// This code comes from https://github.com/adobe/react-spectrum/blob/main/packages/%40react-aria/overlays/src/usePreventScroll.ts\n\nimport { useEffect, useLayoutEffect } from 'react';\n\nfunction isMac(): boolean | undefined {\n  return testPlatform(/^Mac/);\n}\n\nfunction isIPhone(): boolean | undefined {\n  return testPlatform(/^iPhone/);\n}\n\nfunction isIPad(): boolean | undefined {\n  return (\n    testPlatform(/^iPad/) ||\n    // iPadOS 13 lies and says it's a Mac, but we can distinguish by detecting touch support.\n    (isMac() && navigator.maxTouchPoints > 1)\n  );\n}\n\nfunction isIOS(): boolean | undefined {\n  return isIPhone() || isIPad();\n}\n\nfunction testPlatform(re: RegExp): boolean | undefined {\n  return typeof window !== 'undefined' && window.navigator != null\n    ? re.test(window.navigator.platform)\n    : undefined;\n}\n\nconst KEYBOARD_BUFFER = 24;\n\nexport const useIsomorphicLayoutEffect =\n  typeof window !== 'undefined' ? useLayoutEffect : useEffect;\n\ninterface PreventScrollOptions {\n  /** Whether the scroll lock is disabled. */\n  isDisabled?: boolean;\n  focusCallback?: () => void;\n}\n\nfunction chain(...callbacks: any[]): (...args: any[]) => void {\n  return (...args: any[]) => {\n    for (let callback of callbacks) {\n      if (typeof callback === 'function') {\n        callback(...args);\n      }\n    }\n  };\n}\n\n// @ts-ignore\nconst visualViewport = typeof document !== 'undefined' && window.visualViewport;\n\nexport function isScrollable(node: Element): boolean {\n  let style = window.getComputedStyle(node);\n  return /(auto|scroll)/.test(\n    style.overflow + style.overflowX + style.overflowY\n  );\n}\n\nexport function getScrollParent(node: Element): Element {\n  if (isScrollable(node)) {\n    node = node.parentElement as HTMLElement;\n  }\n\n  while (node && !isScrollable(node)) {\n    node = node.parentElement as HTMLElement;\n  }\n\n  return node || document.scrollingElement || document.documentElement;\n}\n\n// HTML input types that do not cause the software keyboard to appear.\nconst nonTextInputTypes = new Set([\n  'checkbox',\n  'radio',\n  'range',\n  'color',\n  'file',\n  'image',\n  'button',\n  'submit',\n  'reset',\n]);\n\n// The number of active usePreventScroll calls. Used to determine whether to revert back to the original page style/scroll position\nlet preventScrollCount = 0;\nlet restore: () => void;\n\n/**\n * Prevents scrolling on the document body on mount, and\n * restores it on unmount. Also ensures that content does not\n * shift due to the scrollbars disappearing.\n */\nexport function usePreventScroll(options: PreventScrollOptions = {}) {\n  let { isDisabled } = options;\n\n  useIsomorphicLayoutEffect(() => {\n    if (isDisabled) {\n      return;\n    }\n\n    preventScrollCount++;\n    if (preventScrollCount === 1) {\n      if (isIOS()) {\n        restore = preventScrollMobileSafari();\n      }\n    }\n\n    return () => {\n      preventScrollCount--;\n      if (preventScrollCount === 0) {\n        restore?.();\n      }\n    };\n  }, [isDisabled]);\n}\n\n// Mobile Safari is a whole different beast. Even with overflow: hidden,\n// it still scrolls the page in many situations:\n//\n// 1. When the bottom toolbar and address bar are collapsed, page scrolling is always allowed.\n// 2. When the keyboard is visible, the viewport does not resize. Instead, the keyboard covers part of\n//    it, so it becomes scrollable.\n// 3. When tapping on an input, the page always scrolls so that the input is centered in the visual viewport.\n//    This may cause even fixed position elements to scroll off the screen.\n// 4. When using the next/previous buttons in the keyboard to navigate between inputs, the whole page always\n//    scrolls, even if the input is inside a nested scrollable element that could be scrolled instead.\n//\n// In order to work around these cases, and prevent scrolling without jankiness, we do a few things:\n//\n// 1. Prevent default on `touchmove` events that are not in a scrollable element. This prevents touch scrolling\n//    on the window.\n// 2. Prevent default on `touchmove` events inside a scrollable element when the scroll position is at the\n//    top or bottom. This avoids the whole page scrolling instead, but does prevent overscrolling.\n// 3. Prevent default on `touchend` events on input elements and handle focusing the element ourselves.\n// 4. When focusing an input, apply a transform to trick Safari into thinking the input is at the top\n//    of the page, which prevents it from scrolling the page. After the input is focused, scroll the element\n//    into view ourselves, without scrolling the whole page.\n// 5. Offset the body by the scroll position using a negative margin and scroll to the top. This should appear the\n//    same visually, but makes the actual scroll position always zero. This is required to make all of the\n//    above work or Safari will still try to scroll the page when focusing an input.\n// 6. As a last resort, handle window scroll events, and scroll back to the top. This can happen when attempting\n//    to navigate to an input with the next/previous buttons that's outside a modal.\nfunction preventScrollMobileSafari() {\n  let scrollable: Element;\n  let lastY = 0;\n  let onTouchStart = (e: TouchEvent) => {\n    // Store the nearest scrollable parent element from the element that the user touched.\n    scrollable = getScrollParent(e.target as Element);\n    if (\n      scrollable === document.documentElement &&\n      scrollable === document.body\n    ) {\n      return;\n    }\n\n    lastY = e.changedTouches[0].pageY;\n  };\n\n  let onTouchMove = (e: TouchEvent) => {\n    // Prevent scrolling the window.\n    if (\n      !scrollable ||\n      scrollable === document.documentElement ||\n      scrollable === document.body\n    ) {\n      e.preventDefault();\n      return;\n    }\n\n    // Prevent scrolling up when at the top and scrolling down when at the bottom\n    // of a nested scrollable area, otherwise mobile Safari will start scrolling\n    // the window instead. Unfortunately, this disables bounce scrolling when at\n    // the top but it's the best we can do.\n    let y = e.changedTouches[0].pageY;\n    let scrollTop = scrollable.scrollTop;\n    let bottom = scrollable.scrollHeight - scrollable.clientHeight;\n\n    if (bottom === 0) {\n      return;\n    }\n\n    if ((scrollTop <= 0 && y > lastY) || (scrollTop >= bottom && y < lastY)) {\n      e.preventDefault();\n    }\n\n    lastY = y;\n  };\n\n  let onTouchEnd = (e: TouchEvent) => {\n    let target = e.target as HTMLElement;\n\n    // Apply this change if we're not already focused on the target element\n    if (isInput(target) && target !== document.activeElement) {\n      e.preventDefault();\n\n      // Apply a transform to trick Safari into thinking the input is at the top of the page\n      // so it doesn't try to scroll it into view. When tapping on an input, this needs to\n      // be done before the \"focus\" event, so we have to focus the element ourselves.\n      target.style.transform = 'translateY(-2000px)';\n      target.focus();\n      requestAnimationFrame(() => {\n        target.style.transform = '';\n      });\n    }\n  };\n\n  let onFocus = (e: FocusEvent) => {\n    let target = e.target as HTMLElement;\n    if (isInput(target)) {\n      // Transform also needs to be applied in the focus event in cases where focus moves\n      // other than tapping on an input directly, e.g. the next/previous buttons in the\n      // software keyboard. In these cases, it seems applying the transform in the focus event\n      // is good enough, whereas when tapping an input, it must be done before the focus event. 🤷‍♂️\n      target.style.transform = 'translateY(-2000px)';\n      requestAnimationFrame(() => {\n        target.style.transform = '';\n\n        // This will have prevented the browser from scrolling the focused element into view,\n        // so we need to do this ourselves in a way that doesn't cause the whole page to scroll.\n        if (visualViewport) {\n          if (visualViewport.height < window.innerHeight) {\n            // If the keyboard is already visible, do this after one additional frame\n            // to wait for the transform to be removed.\n            requestAnimationFrame(() => {\n              scrollIntoView(target);\n            });\n          } else {\n            // Otherwise, wait for the visual viewport to resize before scrolling so we can\n            // measure the correct position to scroll to.\n            visualViewport.addEventListener(\n              'resize',\n              () => scrollIntoView(target),\n              { once: true }\n            );\n          }\n        }\n      });\n    }\n  };\n\n  let onWindowScroll = () => {\n    // Last resort. If the window scrolled, scroll it back to the top.\n    // It should always be at the top because the body will have a negative margin (see below).\n    window.scrollTo(0, 0);\n  };\n\n  // Record the original scroll position so we can restore it.\n  // Then apply a negative margin to the body to offset it by the scroll position. This will\n  // enable us to scroll the window to the top, which is required for the rest of this to work.\n  let scrollX = window.pageXOffset;\n  let scrollY = window.pageYOffset;\n\n  let restoreStyles = chain(\n    setStyle(\n      document.documentElement,\n      'paddingRight',\n      `${window.innerWidth - document.documentElement.clientWidth}px`\n    )\n    // setStyle(document.documentElement, 'overflow', 'hidden'),\n    // setStyle(document.body, 'marginTop', `-${scrollY}px`),\n  );\n\n  // Scroll to the top. The negative margin on the body will make this appear the same.\n  window.scrollTo(0, 0);\n\n  let removeEvents = chain(\n    addEvent(document, 'touchstart', onTouchStart, {\n      passive: false,\n      capture: true,\n    }),\n    addEvent(document, 'touchmove', onTouchMove, {\n      passive: false,\n      capture: true,\n    }),\n    addEvent(document, 'touchend', onTouchEnd, {\n      passive: false,\n      capture: true,\n    }),\n    addEvent(document, 'focus', onFocus, true),\n    addEvent(window, 'scroll', onWindowScroll)\n  );\n\n  return () => {\n    // Restore styles and scroll the page back to where it was.\n    restoreStyles();\n    removeEvents();\n    window.scrollTo(scrollX, scrollY);\n  };\n}\n\n// Sets a CSS property on an element, and returns a function to revert it to the previous value.\nfunction setStyle(\n  element: HTMLElement,\n  style: keyof React.CSSProperties,\n  value: string\n) {\n  // https://github.com/microsoft/TypeScript/issues/17827#issuecomment-391663310\n  // @ts-ignore\n  let cur = element.style[style];\n  // @ts-ignore\n  element.style[style] = value;\n\n  return () => {\n    // @ts-ignore\n    element.style[style] = cur;\n  };\n}\n\n// Adds an event listener to an element, and returns a function to remove it.\nfunction addEvent<K extends keyof GlobalEventHandlersEventMap>(\n  target: EventTarget,\n  event: K,\n  handler: (this: Document, ev: GlobalEventHandlersEventMap[K]) => any,\n  options?: boolean | AddEventListenerOptions\n) {\n  // @ts-ignore\n  target.addEventListener(event, handler, options);\n\n  return () => {\n    // @ts-ignore\n    target.removeEventListener(event, handler, options);\n  };\n}\n\nfunction scrollIntoView(target: Element) {\n  let root = document.scrollingElement || document.documentElement;\n  while (target && target !== root) {\n    // Find the parent scrollable element and adjust the scroll position if the target is not already in view.\n    let scrollable = getScrollParent(target);\n    if (\n      scrollable !== document.documentElement &&\n      scrollable !== document.body &&\n      scrollable !== target\n    ) {\n      let scrollableTop = scrollable.getBoundingClientRect().top;\n      let targetTop = target.getBoundingClientRect().top;\n      let targetBottom = target.getBoundingClientRect().bottom;\n      // Buffer is needed for some edge cases\n      const keyboardHeight =\n        scrollable.getBoundingClientRect().bottom + KEYBOARD_BUFFER;\n\n      if (targetBottom > keyboardHeight) {\n        scrollable.scrollTop += targetTop - scrollableTop;\n      }\n    }\n\n    // @ts-ignore\n    target = scrollable.parentElement;\n  }\n}\n\nexport function isInput(target: Element) {\n  return (\n    (target instanceof HTMLInputElement &&\n      !nonTextInputTypes.has(target.type)) ||\n    target instanceof HTMLTextAreaElement ||\n    (target instanceof HTMLElement && target.isContentEditable)\n  );\n}\n",
      "type": "registry:hook"
    }
  ]
}