{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "animated-modal",
  "title": "Animated Modal",
  "description": "Native dialog whose panel springs in with a tilt over a blurred backdrop.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/components/velora/animated-modal.tsx",
      "content": "\"use client\";\n\nimport {\n  createContext,\n  useContext,\n  useEffect,\n  useId,\n  useLayoutEffect,\n  useRef,\n  useState,\n  useSyncExternalStore,\n} from \"react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst noopSubscribe = () => () => {};\n\ninterface ModalState {\n  open: boolean;\n  setOpen: (open: boolean) => void;\n  id: string;\n  triggerRef: React.RefObject<HTMLButtonElement | null>;\n}\n\nconst ModalContext = createContext<ModalState | null>(null);\n\nfunction useModal() {\n  const ctx = useContext(ModalContext);\n  if (!ctx) throw new Error(\"Modal parts must be inside <Modal>\");\n  return ctx;\n}\n\ninterface ModalProps {\n  /** Controlled open state */\n  open?: boolean;\n  /** Initial open state when uncontrolled */\n  defaultOpen?: boolean;\n  /** Called whenever the modal asks to open or close */\n  onOpenChange?: (open: boolean) => void;\n  /** ModalTrigger, ModalContent and anything else */\n  children: React.ReactNode;\n}\n\n/**\n * Dialog built on the native `<dialog>` element: the panel springs in with a\n * slight tilt over a blurred backdrop, and plays its exit before closing.\n * Compose `ModalTrigger`, `ModalContent`, `ModalFooter` and `ModalClose`.\n */\nexport function Modal({\n  open: openProp,\n  defaultOpen = false,\n  onOpenChange,\n  children,\n}: ModalProps) {\n  const [inner, setInner] = useState(defaultOpen);\n  const open = openProp ?? inner;\n  const triggerRef = useRef<HTMLButtonElement>(null);\n  const setOpen = (next: boolean) => {\n    if (openProp === undefined) setInner(next);\n    onOpenChange?.(next);\n  };\n\n  return (\n    <ModalContext.Provider value={{ open, setOpen, id: useId(), triggerRef }}>\n      {children}\n    </ModalContext.Provider>\n  );\n}\n\ntype ModalTriggerProps = React.ButtonHTMLAttributes<HTMLButtonElement>;\n\n/** Button that opens the modal. */\nexport function ModalTrigger({ className, onClick, ...props }: ModalTriggerProps) {\n  const { open, setOpen, id, triggerRef } = useModal();\n  return (\n    <button\n      type=\"button\"\n      {...props}\n      ref={triggerRef}\n      aria-haspopup=\"dialog\"\n      aria-expanded={open}\n      aria-controls={id}\n      onClick={(event) => {\n        onClick?.(event);\n        if (!event.defaultPrevented) setOpen(true);\n      }}\n      className={cn(\n        \"inline-flex h-10 cursor-pointer items-center justify-center gap-2 rounded-lg bg-primary px-4 text-sm font-medium text-primary-foreground transition-opacity hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n        className\n      )}\n    />\n  );\n}\n\ntype MotionConflicts = \"onAnimationStart\" | \"onDrag\" | \"onDragStart\" | \"onDragEnd\";\n\ninterface ModalContentProps\n  extends Omit<React.HTMLAttributes<HTMLDivElement>, \"title\" | MotionConflicts> {\n  /** Heading rendered at the top; it becomes the dialog's accessible name */\n  title?: React.ReactNode;\n  /** Supporting text under the title, announced as the dialog's description */\n  description?: React.ReactNode;\n  /** Show the × button in the top-right corner */\n  showCloseButton?: boolean;\n}\n\n/** The dialog panel. Only mounted while open (or animating out). */\nexport function ModalContent({\n  title,\n  description,\n  showCloseButton = true,\n  className,\n  children,\n  \"aria-labelledby\": labelledBy,\n  ...props\n}: ModalContentProps) {\n  const { open, setOpen, id, triggerRef } = useModal();\n  const dialogRef = useRef<HTMLDialogElement>(null);\n  const panelRef = useRef<HTMLDivElement>(null);\n  const returnFocus = useRef<HTMLElement | null>(null);\n  const downOutside = useRef(false);\n  const hydrated = useSyncExternalStore(noopSubscribe, () => true, () => false);\n  const reduce = useReducedMotion() === true && hydrated;\n\n  // Open before paint so the enter animation starts on a visible dialog.\n  useLayoutEffect(() => {\n    const dialog = dialogRef.current;\n    if (!open || !dialog || dialog.open) return;\n    returnFocus.current = document.activeElement as HTMLElement | null;\n    document.documentElement.style.overflow = \"hidden\";\n    dialog.showModal();\n  }, [open]);\n\n  // However it closes (animation done, double Escape), unlock and hand focus back.\n  useEffect(() => {\n    const dialog = dialogRef.current;\n    if (!dialog) return;\n    const onClose = () => {\n      document.documentElement.style.overflow = \"\";\n      const back = returnFocus.current;\n      (back && back !== document.body ? back : triggerRef.current)?.focus({\n        preventScroll: true,\n      });\n      if (open) setOpen(false);\n    };\n    dialog.addEventListener(\"close\", onClose);\n    return () => dialog.removeEventListener(\"close\", onClose);\n  });\n\n  useEffect(\n    () => () => {\n      document.documentElement.style.overflow = \"\";\n    },\n    []\n  );\n\n  const outside = (target: EventTarget) =>\n    !panelRef.current?.contains(target as Node);\n  const spring = reduce\n    ? { duration: 0.15 }\n    : { type: \"spring\" as const, stiffness: 380, damping: 30 };\n  const hidden = reduce\n    ? { opacity: 0 }\n    : { opacity: 0, scale: 0.9, rotateX: 14, y: 24 };\n\n  return (\n    <dialog\n      ref={dialogRef}\n      id={id}\n      aria-labelledby={labelledBy ?? (title ? `${id}-title` : undefined)}\n      aria-describedby={description ? `${id}-desc` : undefined}\n      onCancel={(event) => {\n        // Escape: run the exit animation instead of closing instantly.\n        event.preventDefault();\n        setOpen(false);\n      }}\n      onPointerDown={(event) => (downOutside.current = outside(event.target))}\n      onClick={(event) => {\n        if (downOutside.current && outside(event.target)) setOpen(false);\n      }}\n      className=\"fixed inset-0 m-0 size-full max-h-none max-w-none overflow-y-auto border-0 bg-transparent p-0 text-foreground outline-none backdrop:bg-transparent\"\n    >\n      <AnimatePresence onExitComplete={() => dialogRef.current?.close()}>\n        {open && (\n          <div className=\"grid min-h-full place-items-center p-4 perspective-distant\">\n            <motion.div\n              aria-hidden\n              initial={{ opacity: 0 }}\n              animate={{ opacity: 1 }}\n              exit={{ opacity: 0 }}\n              transition={{ duration: 0.2 }}\n              className=\"fixed inset-0 bg-background/60 backdrop-blur-md\"\n            />\n            <motion.div\n              {...props}\n              ref={panelRef}\n              initial={hidden}\n              animate={{ opacity: 1, scale: 1, rotateX: 0, y: 0 }}\n              exit={hidden}\n              transition={spring}\n              data-slot=\"modal-content\"\n              className={cn(\n                \"relative w-full max-w-lg overflow-hidden rounded-2xl border bg-card text-card-foreground shadow-2xl\",\n                className\n              )}\n            >\n              {(title || description) && (\n                <div className=\"space-y-1.5 px-6 pt-6 pr-14\">\n                  {title && (\n                    <h2 id={`${id}-title`} className=\"text-lg font-semibold tracking-tight\">\n                      {title}\n                    </h2>\n                  )}\n                  {description && (\n                    <p id={`${id}-desc`} className=\"text-sm text-muted-foreground\">\n                      {description}\n                    </p>\n                  )}\n                </div>\n              )}\n              {children}\n              {showCloseButton && (\n                <ModalClose\n                  aria-label=\"Close\"\n                  className=\"absolute top-4 right-4 size-8 border-0 bg-transparent p-0 text-muted-foreground\"\n                >\n                  <svg viewBox=\"0 0 24 24\" className=\"size-4\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" aria-hidden>\n                    <path d=\"M18 6 6 18M6 6l12 12\" />\n                  </svg>\n                </ModalClose>\n              )}\n            </motion.div>\n          </div>\n        )}\n      </AnimatePresence>\n    </dialog>\n  );\n}\n\n/** Action row pinned to the bottom of the panel. */\nexport function ModalFooter({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {\n  return (\n    <div\n      {...props}\n      data-slot=\"modal-footer\"\n      className={cn(\n        \"flex flex-wrap items-center justify-end gap-2 border-t bg-muted/40 px-6 py-4\",\n        className\n      )}\n    />\n  );\n}\n\n/** Button that closes the modal (with its exit animation). */\nexport function ModalClose({\n  className,\n  onClick,\n  ...props\n}: React.ButtonHTMLAttributes<HTMLButtonElement>) {\n  const { setOpen } = useModal();\n  return (\n    <button\n      type=\"button\"\n      {...props}\n      onClick={(event) => {\n        onClick?.(event);\n        if (!event.defaultPrevented) setOpen(false);\n      }}\n      className={cn(\n        \"inline-flex h-9 cursor-pointer items-center justify-center rounded-lg border bg-background px-3.5 text-sm font-medium transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n        className\n      )}\n    />\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/velora/animated-modal.tsx"
    }
  ],
  "type": "registry:ui"
}