{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"morphing-modal","type":"registry:component","title":"Morphing Modal","description":"Family-app-style modal. A single panel that morphs its height as you navigate between inner views, with blur cross-fade on content.","author":"Saurabh <saurabh10102@gmail.com>","dependencies":["clsx","motion","tailwind-merge"],"registryDependencies":[],"files":[{"path":"components/motion/morphing-modal.tsx","type":"registry:component","target":"@components/motion/morphing-modal.tsx","content":"\"use client\";\n\nimport {\n  AnimatePresence,\n  motion,\n  useReducedMotion,\n} from \"motion/react\";\nimport { type ReactNode, useEffect } from \"react\";\nimport { EASE_OUT, SPRING_PANEL } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface MorphingModalProps {\n  /** Which view is currently shown. `null` closes the modal. */\n  viewId: string | null;\n  onClose: () => void;\n  children: ReactNode;\n  /** \"bottom\" anchors to the viewport bottom (mobile-like). \"center\" centers vertically. */\n  placement?: \"bottom\" | \"center\";\n  className?: string;\n}\n\nexport function MorphingModal({\n  viewId,\n  onClose,\n  children,\n  placement = \"bottom\",\n  className,\n}: MorphingModalProps) {\n  const open = viewId !== null;\n  const reduce = useReducedMotion();\n  const enterY = reduce ? 0 : placement === \"bottom\" ? 40 : 20;\n  const enterScale = reduce ? 1 : 0.97;\n\n  useEffect(() => {\n    if (!open) return;\n    const prev = document.body.style.overflow;\n    document.body.style.overflow = \"hidden\";\n    return () => {\n      document.body.style.overflow = prev;\n    };\n  }, [open]);\n\n  return (\n    <div\n      aria-hidden={!open}\n      className={cn(\n        \"fixed inset-0 z-[80]\",\n        open ? \"pointer-events-auto\" : \"pointer-events-none\",\n      )}\n    >\n      <motion.div\n        initial={false}\n        animate={{ opacity: open ? 1 : 0 }}\n        transition={{ duration: 0.2, ease: EASE_OUT }}\n        onClick={onClose}\n        className={cn(\n          \"absolute inset-0 bg-background/5 [backdrop-filter:blur(14px)_saturate(140%)] [-webkit-backdrop-filter:blur(14px)_saturate(140%)]\",\n          open ? \"pointer-events-auto\" : \"pointer-events-none\",\n        )}\n      />\n\n      <div\n        className={cn(\n          \"pointer-events-none absolute inset-0 flex justify-center px-4\",\n          placement === \"bottom\" ? \"items-end pb-8\" : \"items-center\",\n        )}\n      >\n        <AnimatePresence initial={false}>\n          {open ? (\n            <motion.div\n              key=\"panel\"\n              layout\n              initial={{ opacity: 0, y: enterY, scale: enterScale }}\n              animate={{ opacity: 1, y: 0, scale: 1 }}\n              exit={{\n                opacity: 0,\n                y: enterY,\n                scale: reduce ? 1 : 0.98,\n                transition: { duration: 0.18, ease: EASE_OUT },\n              }}\n              transition={SPRING_PANEL}\n              className={cn(\n                \"pointer-events-auto relative w-full max-w-sm overflow-hidden rounded-3xl border border-border bg-card shadow-2xl will-change-transform\",\n                className,\n              )}\n            >\n              <motion.div layout=\"position\" className=\"p-5\">\n                <AnimatePresence mode=\"popLayout\" initial={false}>\n                  <motion.div\n                    key={viewId}\n                    initial={\n                      reduce\n                        ? { opacity: 0 }\n                        : { opacity: 0, y: 8, filter: \"blur(4px)\" }\n                    }\n                    animate={\n                      reduce\n                        ? {\n                            opacity: 1,\n                            transition: {\n                              duration: 0.18,\n                              ease: EASE_OUT,\n                            },\n                          }\n                        : {\n                            opacity: 1,\n                            y: 0,\n                            filter: \"blur(0px)\",\n                            transition: {\n                              duration: 0.24,\n                              ease: EASE_OUT,\n                            },\n                          }\n                    }\n                    exit={\n                      reduce\n                        ? {\n                            opacity: 0,\n                            transition: {\n                              duration: 0.14,\n                              ease: EASE_OUT,\n                            },\n                          }\n                        : {\n                            opacity: 0,\n                            y: -8,\n                            filter: \"blur(4px)\",\n                            transition: {\n                              duration: 0.16,\n                              ease: EASE_OUT,\n                            },\n                          }\n                    }\n                  >\n                    {children}\n                  </motion.div>\n                </AnimatePresence>\n              </motion.div>\n            </motion.div>\n          ) : null}\n        </AnimatePresence>\n      </div>\n    </div>\n  );\n}\n"},{"path":"lib/ease.ts","type":"registry:lib","target":"@lib/ease.ts","content":"// Shared motion tokens. Easing curves mirror the CSS custom properties in\n// globals.css; springs are the canonical physics used across components.\n// Strong custom variants — defaults like `ease-in`/`ease-out` feel weak.\n\nexport const EASE_OUT = [0.16, 1, 0.3, 1] as const;\nexport const EASE_IN_OUT = [0.77, 0, 0.175, 1] as const;\nexport const EASE_DRAWER = [0.32, 0.72, 0, 1] as const;\n\n/** CSS string form of EASE_OUT for inline style transitions. */\nexport const EASE_OUT_CSS = \"cubic-bezier(0.16, 1, 0.3, 1)\";\n\n/** Press feedback on buttons and other tappable surfaces. */\nexport const SPRING_PRESS = {\n  type: \"spring\",\n  stiffness: 500,\n  damping: 30,\n  mass: 0.6,\n} as const;\n\n/** Content swaps — label/icon slots trading places inside a control. */\nexport const SPRING_SWAP = {\n  type: \"spring\",\n  stiffness: 460,\n  damping: 30,\n  mass: 0.55,\n} as const;\n\n/** Overlay panel entrances — modals and sheets summoned by pointer. */\nexport const SPRING_PANEL = {\n  type: \"spring\",\n  stiffness: 420,\n  damping: 40,\n  mass: 0.5,\n} as const;\n\n/** Shared-layout glides — pills, indicators and panels morphing between positions. */\nexport const SPRING_LAYOUT = {\n  type: \"spring\",\n  stiffness: 360,\n  damping: 32,\n  mass: 0.6,\n} as const;\n\n/** Cursor-follow physics for decorative mouse tracking (magnetic, tilt, dock). */\nexport const SPRING_MOUSE = {\n  stiffness: 200,\n  damping: 15,\n  mass: 0.3,\n} as const;\n"},{"path":"lib/utils.ts","type":"registry:lib","target":"@lib/utils.ts","content":"import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n"}]}