{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "morphing-modal",
  "type": "registry:ui",
  "title": "Modal",
  "description": "A Modal component for displaying a modal dialog.",
  "files": [
    {
      "path": "src/registry/ui/morphing-modal.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport { useId, useState } from \"react\";\nimport * as DialogPrimitive from \"@radix-ui/react-dialog\";\nimport * as TabsPrimitive from \"@radix-ui/react-tabs\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { X } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\nexport type MorphingModalTab = {\n  value: string;\n  label: React.ReactNode;\n  icon?: React.ComponentType<{ className?: string }>;\n  content: React.ReactNode;\n};\n\nexport type MorphingModalProps = Omit<React.ComponentProps<\"button\">, \"title\" | \"children\"> & {\n  /** Content of the button that opens the modal. */\n  trigger: React.ReactNode;\n  title: React.ReactNode;\n  description?: React.ReactNode;\n  tabs: MorphingModalTab[];\n  tab?: string;\n  defaultTab?: string;\n  onTabChange?: (tab: string) => void;\n  open?: boolean;\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  /** Rendered below the tab panels, e.g. cancel / submit buttons. */\n  footer?: React.ReactNode;\n  /** Accessible label for the close button. */\n  closeLabel?: string;\n  contentClassName?: string;\n};\n\nconst spring = { type: \"spring\", bounce: 0.15, duration: 0.45 } as const;\n// Indicators move on every arrow key, so they settle fast with barely any overshoot.\nconst INDICATOR_SPRING = { type: \"spring\", duration: 0.3, bounce: 0.1 } as const;\n\nexport function MorphingModal({\n  trigger,\n  title,\n  description,\n  tabs,\n  tab,\n  defaultTab,\n  onTabChange,\n  open,\n  defaultOpen = false,\n  onOpenChange,\n  footer,\n  closeLabel = \"Close\",\n  className,\n  contentClassName,\n  ...props\n}: MorphingModalProps) {\n  const id = useId();\n  const reduceMotion = useReducedMotion();\n\n  const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);\n  const isOpen = open ?? uncontrolledOpen;\n  const setOpen = (next: boolean) => {\n    if (open === undefined) setUncontrolledOpen(next);\n    onOpenChange?.(next);\n  };\n\n  const [uncontrolledTab, setUncontrolledTab] = useState(defaultTab ?? tabs[0]?.value);\n  const activeTab = tab ?? uncontrolledTab;\n  const setTab = (next: string) => {\n    if (tab === undefined) setUncontrolledTab(next);\n    onTabChange?.(next);\n  };\n\n  const surfaceId = reduceMotion ? undefined : `${id}-surface`;\n  const transition = reduceMotion ? { duration: 0.15 } : spring;\n\n  return (\n    <DialogPrimitive.Root open={isOpen} onOpenChange={setOpen}>\n      <DialogPrimitive.Trigger asChild>\n        <button\n          type=\"button\"\n          data-slot=\"morphing-modal\"\n          className={cn(\n            \"relative inline-flex h-9 items-center justify-center gap-2 rounded-lg px-4 text-sm font-medium text-foreground outline-none\",\n            \"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n            \"disabled:pointer-events-none disabled:opacity-50\",\n            className\n          )}\n          {...props}\n        >\n          {/* The button's surface is the element that morphs into the dialog. */}\n          {!isOpen && (\n            <motion.span\n              aria-hidden\n              layoutId={surfaceId}\n              transition={transition}\n              className=\"absolute inset-0 rounded-lg border bg-background transition-colors hover:bg-muted\"\n            />\n          )}\n          <span\n            className={cn(\n              \"pointer-events-none relative inline-flex items-center gap-2 transition-opacity\",\n              isOpen && \"opacity-0\"\n            )}\n          >\n            {trigger}\n          </span>\n        </button>\n      </DialogPrimitive.Trigger>\n\n      <AnimatePresence>\n        {isOpen && (\n          <DialogPrimitive.Portal forceMount>\n            <DialogPrimitive.Overlay asChild forceMount>\n              <motion.div\n                className=\"fixed inset-0 z-50 bg-black/50\"\n                initial={{ opacity: 0 }}\n                animate={{ opacity: 1 }}\n                exit={{ opacity: 0 }}\n                transition={{ duration: 0.2 }}\n              />\n            </DialogPrimitive.Overlay>\n            <div className=\"pointer-events-none fixed inset-0 z-50 grid place-items-center p-4\">\n              <DialogPrimitive.Content\n                asChild\n                forceMount\n                {...(description ? {} : { \"aria-describedby\": undefined })}\n              >\n                <motion.div\n                  layoutId={surfaceId}\n                  transition={transition}\n                  initial={reduceMotion ? { opacity: 0 } : undefined}\n                  animate={reduceMotion ? { opacity: 1 } : undefined}\n                  exit={reduceMotion ? { opacity: 0 } : undefined}\n                  className={cn(\n                    \"pointer-events-auto relative flex max-h-[calc(100dvh-2rem)] w-full max-w-lg flex-col overflow-hidden rounded-xl border bg-background text-foreground outline-none\",\n                    contentClassName\n                  )}\n                >\n                  <motion.div\n                    className=\"flex min-h-0 flex-1 flex-col\"\n                    initial={{ opacity: 0 }}\n                    animate={{ opacity: 1, transition: { delay: reduceMotion ? 0 : 0.12 } }}\n                    exit={{ opacity: 0, transition: { duration: 0.08 } }}\n                  >\n                    <div className=\"space-y-1.5 p-6 pr-14 pb-4\">\n                      <DialogPrimitive.Title className=\"text-lg font-semibold\">\n                        {title}\n                      </DialogPrimitive.Title>\n                      {description && (\n                        <DialogPrimitive.Description className=\"text-sm text-muted-foreground\">\n                          {description}\n                        </DialogPrimitive.Description>\n                      )}\n                    </div>\n\n                    <TabsPrimitive.Root\n                      value={activeTab}\n                      onValueChange={setTab}\n                      className=\"flex min-h-0 flex-1 flex-col\"\n                    >\n                      <TabsPrimitive.List className=\"mx-6 flex gap-1 overflow-x-auto rounded-lg bg-muted p-1\">\n                        {tabs.map((item) => {\n                          const Icon = item.icon;\n                          return (\n                            <TabsPrimitive.Trigger\n                              key={item.value}\n                              value={item.value}\n                              className={cn(\n                                \"relative flex flex-1 items-center justify-center gap-2 rounded-md px-3 py-1.5 text-sm font-medium whitespace-nowrap outline-none transition-colors\",\n                                \"text-muted-foreground hover:text-foreground data-[state=active]:text-foreground\",\n                                \"focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50\"\n                              )}\n                            >\n                              {item.value === activeTab && (\n                                <motion.span\n                                  aria-hidden\n                                  layoutId={`${id}-tab-indicator`}\n                                  transition={reduceMotion ? { duration: 0 } : INDICATOR_SPRING}\n                                  className=\"absolute inset-0 rounded-md border bg-background shadow-sm\"\n                                />\n                              )}\n                              {Icon && <Icon className=\"relative size-4 shrink-0\" />}\n                              <span className=\"relative\">{item.label}</span>\n                            </TabsPrimitive.Trigger>\n                          );\n                        })}\n                      </TabsPrimitive.List>\n\n                      <div className=\"min-h-0 flex-1 overflow-y-auto p-6\">\n                        {tabs.map((item) => (\n                          <TabsPrimitive.Content\n                            key={item.value}\n                            value={item.value}\n                            className=\"rounded-md outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n                          >\n                            <motion.div\n                              initial={reduceMotion ? false : { opacity: 0, y: 4 }}\n                              animate={{ opacity: 1, y: 0 }}\n                              transition={{ duration: 0.2 }}\n                            >\n                              {item.content}\n                            </motion.div>\n                          </TabsPrimitive.Content>\n                        ))}\n                      </div>\n                    </TabsPrimitive.Root>\n\n                    {footer && (\n                      <div className=\"flex justify-end gap-2 border-t bg-muted/50 px-6 py-4\">\n                        {footer}\n                      </div>\n                    )}\n                  </motion.div>\n\n                  <DialogPrimitive.Close\n                    aria-label={closeLabel}\n                    className={cn(\n                      \"absolute top-4 right-4 flex size-8 items-center justify-center rounded-md text-muted-foreground outline-none transition-colors\",\n                      \"hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring\"\n                    )}\n                  >\n                    <X className=\"size-4\" aria-hidden />\n                  </DialogPrimitive.Close>\n                </motion.div>\n              </DialogPrimitive.Content>\n            </div>\n          </DialogPrimitive.Portal>\n        )}\n      </AnimatePresence>\n    </DialogPrimitive.Root>\n  );\n}\n"
    }
  ],
  "dependencies": [
    "@radix-ui/react-dialog",
    "@radix-ui/react-tabs",
    "lucide-react",
    "motion"
  ]
}
