{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "specials-magnetic-drawer",
  "type": "registry:component",
  "title": "Magnetic Drawer",
  "description": "A physics-based drawer with magnetic snap points",
  "dependencies": [
    "motion",
    "lucide-react"
  ],
  "files": [
    {
      "path": "registry/new-york/animations/specials/magnetic-drawer.tsx",
      "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport {\n    motion,\n    useMotionValue,\n    useTransform,\n    PanInfo,\n    animate,\n    AnimatePresence,\n} from \"motion/react\";\nimport { useEffect, useRef, useState } from \"react\";\nimport { X } from \"lucide-react\";\nimport { createPortal } from \"react-dom\";\n\ninterface MagneticDrawerProps {\n    children: React.ReactNode;\n    open: boolean;\n    onOpenChange: (open: boolean) => void;\n    snapPoints?: number[]; // Values between 0 and 1, e.g., [0.5, 1]\n    className?: string;\n}\n\nexport function MagneticDrawer({\n    children,\n    open,\n    onOpenChange,\n    snapPoints = [0.5, 0.9],\n    className,\n    portal = true,\n}: MagneticDrawerProps & { portal?: boolean }) {\n    const [mounted, setMounted] = useState(false);\n    const [drawerHeight, setDrawerHeight] = useState(0);\n    const containerRef = useRef<HTMLDivElement>(null);\n    const parentRef = useRef<HTMLDivElement>(null);\n    const y = useMotionValue(0);\n\n    useEffect(() => {\n        setMounted(true);\n        if (typeof window !== \"undefined\") {\n            if (portal) {\n                setDrawerHeight(window.innerHeight);\n                const handleResize = () => setDrawerHeight(window.innerHeight);\n                window.addEventListener(\"resize\", handleResize);\n                return () => window.removeEventListener(\"resize\", handleResize);\n            } else if (parentRef.current) {\n                // Initial height measurement for inline mode\n                setDrawerHeight(parentRef.current.offsetHeight);\n                // Optional: Resize observer for parent\n                const observer = new ResizeObserver((entries) => {\n                    for (const entry of entries) {\n                        setDrawerHeight(entry.contentRect.height);\n                    }\n                });\n                observer.observe(parentRef.current);\n                return () => observer.disconnect();\n            }\n        }\n    }, [portal]);\n\n    useEffect(() => {\n        if (open) {\n            const initialSnap = snapPoints[0];\n            const targetY = drawerHeight * (1 - initialSnap);\n            animate(y, targetY, { type: \"spring\", damping: 30, stiffness: 300 });\n            document.body.style.overflow = \"hidden\";\n        } else {\n            animate(y, drawerHeight, { type: \"spring\", damping: 30, stiffness: 300 });\n            document.body.style.overflow = \"\";\n        }\n        return () => { document.body.style.overflow = \"\"; };\n    }, [open, drawerHeight, snapPoints]);\n\n    const handleDragEnd = (_: any, info: PanInfo) => {\n        const velocity = info.velocity.y;\n        const currentY = y.get();\n        const currentOpenedHeight = drawerHeight - currentY;\n        const currentRatio = currentOpenedHeight / drawerHeight;\n\n        let bestSnap = snapPoints[0];\n        let minDiff = Infinity;\n\n        if (velocity > 500 || currentRatio < 0.1) {\n            onOpenChange(false);\n            return;\n        }\n\n        for (const point of snapPoints) {\n            const diff = Math.abs(currentRatio - point);\n            if (diff < minDiff) {\n                minDiff = diff;\n                bestSnap = point;\n            }\n        }\n\n        if (velocity < -500) {\n            const nextSnap = snapPoints.find(p => p > currentRatio);\n            if (nextSnap) bestSnap = nextSnap;\n        } else if (velocity > 500) {\n            const prevSnap = [...snapPoints].reverse().find(p => p < currentRatio);\n            if (prevSnap) bestSnap = prevSnap;\n        }\n\n        const targetY = drawerHeight * (1 - bestSnap);\n        animate(y, targetY, { type: \"spring\", damping: 30, stiffness: 300 });\n    };\n\n    const backdropOpacity = useTransform(y, [drawerHeight, 0], [0, 0.5]);\n    const pointerEvents = useTransform(y, (latest) => latest >= drawerHeight ? \"none\" : \"auto\");\n\n    if (!mounted) return null;\n\n    const drawerContent = (\n        <AnimatePresence>\n            {open && (\n                <>\n                    <motion.div\n                        className={cn(portal ? \"fixed\" : \"absolute\", \"inset-0 z-[99]\")}\n                        style={{ backgroundColor: \"black\", opacity: backdropOpacity, pointerEvents }}\n                        onClick={() => onOpenChange(false)}\n                        initial={{ opacity: 0 }}\n                        animate={{ opacity: 0.5 }}\n                        exit={{ opacity: 0 }}\n                        transition={{ duration: 0.3 }}\n                    />\n                    <motion.div\n                        ref={containerRef}\n                        className={cn(\n                            portal ? \"fixed\" : \"absolute\",\n                            \"bottom-0 left-0 right-0 z-[100] flex flex-col rounded-t-[20px] bg-white shadow-xl outline-none dark:bg-neutral-900\",\n                            className\n                        )}\n                        style={{ y, height: drawerHeight, touchAction: \"none\" }}\n                        initial={{ y: drawerHeight }}\n                        drag=\"y\"\n                        dragConstraints={{ top: 0, bottom: drawerHeight }}\n                        dragElastic={0.05}\n                        dragMomentum={false}\n                        onDragEnd={handleDragEnd}\n                    >\n                        <div className=\"flex w-full items-center justify-center p-4\">\n                            <div className=\"h-1.5 w-12 rounded-full bg-neutral-300 dark:bg-neutral-700\" />\n                        </div>\n                        <button\n                            onClick={() => onOpenChange(false)}\n                            className=\"absolute right-4 top-4 rounded-full p-2 text-neutral-500 hover:bg-neutral-100 dark:hover:bg-neutral-800\"\n                        >\n                            <X className=\"h-5 w-5\" />\n                        </button>\n                        <div className=\"flex-1 overflow-y-auto px-4 pb-10\">\n                            {children}\n                        </div>\n                    </motion.div>\n                </>\n            )}\n        </AnimatePresence>\n    );\n\n    if (portal) {\n        return createPortal(drawerContent, document.body);\n    }\n\n    return (\n        <div ref={parentRef} className=\"relative w-full h-full overflow-hidden\">\n            {drawerContent}\n        </div>\n    );\n}\n",
      "type": "registry:component"
    }
  ]
}