{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "adaptive-drawer",
  "title": "Adaptive Drawer",
  "description": "A shadcn Drawer that measures panel content and animates height changes.",
  "dependencies": [
    "motion",
    "lucide-react"
  ],
  "registryDependencies": [
    "button",
    "drawer"
  ],
  "files": [
    {
      "path": "registry/base/ui/adaptive-drawer.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { X } from \"lucide-react\";\nimport {\n  AnimatePresence,\n  motion,\n  MotionConfig,\n  useReducedMotion,\n  type HTMLMotionProps,\n} from \"motion/react\";\n\nimport { buttonVariants } from \"@/components/ui/button\";\nimport {\n  Drawer,\n  DrawerClose,\n  DrawerContent,\n  DrawerDescription,\n  DrawerTitle,\n  DrawerTrigger,\n} from \"@/components/ui/drawer\";\nimport { cn } from \"@/lib/utils\";\n\ntype MotionTransition = NonNullable<HTMLMotionProps<\"div\">[\"transition\"]>;\n\nconst minPanelDuration = 0.15;\nconst maxPanelDuration = 0.27;\nconst drawerExitResetDelay = 550;\nconst heightChangeDurationDivisor = 500;\nconst heightChangeThreshold = 0.5;\n\nexport type AdaptiveDrawerControls = {\n  activePanel: string;\n  setPanel: (panel: string) => void;\n  close: () => void;\n};\n\nexport type AdaptiveDrawerPanel = {\n  id: string;\n  title: React.ReactNode;\n  description?: React.ReactNode;\n  content:\n    | React.ReactNode\n    | ((controls: AdaptiveDrawerControls) => React.ReactNode);\n};\n\nexport type AdaptiveDrawerProps = {\n  panels: AdaptiveDrawerPanel[];\n  panel?: string;\n  defaultPanel?: string;\n  onPanelChange?: (panel: string) => void;\n  resetOnClose?: boolean;\n  title?: React.ReactNode;\n  description?: React.ReactNode;\n  /**\n   * Custom trigger. An element is rendered as the trigger itself (no wrapper\n   * button, so `<Button>` works without nesting). Pass `null` to render no\n   * trigger at all, e.g. when the drawer is opened from elsewhere via `open`.\n   */\n  trigger?: React.ReactNode;\n  triggerLabel?: React.ReactNode;\n  closeLabel?: string;\n  open?: boolean;\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  /** Classes for the drawer card. */\n  className?: string;\n  drawerClassName?: string;\n  contentClassName?: string;\n  heightTransition?: MotionTransition;\n  panelTransition?: MotionTransition;\n};\n\nexport function AdaptiveDrawer({\n  panels,\n  panel,\n  defaultPanel,\n  onPanelChange,\n  resetOnClose = true,\n  title = \"Adaptive drawer\",\n  description,\n  trigger,\n  triggerLabel = \"Open drawer\",\n  closeLabel = \"Close drawer\",\n  open,\n  defaultOpen = false,\n  onOpenChange,\n  className,\n  drawerClassName,\n  contentClassName,\n  heightTransition,\n  panelTransition,\n}: AdaptiveDrawerProps) {\n  const fallbackPanel = defaultPanel ?? panels[0]?.id ?? \"\";\n  const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen);\n  const [uncontrolledPanel, setUncontrolledPanel] =\n    React.useState(fallbackPanel);\n  const [height, setHeight] = React.useState<number | null>(null);\n  const [panelDuration, setPanelDuration] =\n    React.useState(minPanelDuration);\n  const [contentElement, setContentElement] =\n    React.useState<HTMLDivElement | null>(null);\n  const frameRef = React.useRef<number | null>(null);\n  const previousHeightRef = React.useRef<number | null>(null);\n  const shouldReduceMotion = useReducedMotion();\n  const isControlledOpen = open !== undefined;\n  const isControlledPanel = panel !== undefined;\n  const isOpen = isControlledOpen ? open : uncontrolledOpen;\n  const activePanelId = isControlledPanel ? panel : uncontrolledPanel;\n  const activePanel =\n    panels.find((item) => item.id === activePanelId) ?? panels[0];\n\n  const setOpen = React.useCallback(\n    (nextOpen: boolean) => {\n      if (!isControlledOpen) {\n        setUncontrolledOpen(nextOpen);\n      }\n\n      onOpenChange?.(nextOpen);\n    },\n    [isControlledOpen, onOpenChange],\n  );\n\n  const setPanel = React.useCallback(\n    (nextPanel: string) => {\n      if (!panels.some((item) => item.id === nextPanel)) return;\n\n      if (!isControlledPanel) {\n        setUncontrolledPanel(nextPanel);\n      }\n\n      onPanelChange?.(nextPanel);\n    },\n    [isControlledPanel, onPanelChange, panels],\n  );\n\n  const controls = React.useMemo<AdaptiveDrawerControls>(\n    () => ({\n      activePanel: activePanel?.id ?? \"\",\n      setPanel,\n      close: () => setOpen(false),\n    }),\n    [activePanel?.id, setOpen, setPanel],\n  );\n\n  React.useEffect(() => {\n    if (!isOpen) {\n      const timer = window.setTimeout(() => {\n        setHeight(null);\n        setPanelDuration(shouldReduceMotion ? 0 : minPanelDuration);\n        previousHeightRef.current = null;\n\n        if (resetOnClose && !isControlledPanel) {\n          setUncontrolledPanel(fallbackPanel);\n        }\n      }, drawerExitResetDelay);\n\n      return () => {\n        window.clearTimeout(timer);\n      };\n    }\n  }, [\n    fallbackPanel,\n    isControlledPanel,\n    isOpen,\n    resetOnClose,\n    shouldReduceMotion,\n  ]);\n\n  React.useEffect(() => {\n    if (!isOpen) return;\n\n    const element = contentElement;\n\n    if (!element) return;\n\n    const measure = () => {\n      const nextHeight = element.getBoundingClientRect().height;\n      const previousHeight = previousHeightRef.current;\n\n      if (nextHeight <= 0) return;\n\n      setHeight((currentHeight) =>\n        currentHeight !== null &&\n        Math.abs(currentHeight - nextHeight) < heightChangeThreshold\n          ? currentHeight\n          : nextHeight,\n      );\n\n      if (shouldReduceMotion || !previousHeight || !nextHeight) {\n        setPanelDuration(shouldReduceMotion ? 0 : minPanelDuration);\n        previousHeightRef.current = nextHeight;\n        return;\n      }\n\n      const heightDifference = Math.abs(nextHeight - previousHeight);\n      const nextDuration = Math.min(\n        Math.max(heightDifference / heightChangeDurationDivisor, minPanelDuration),\n        maxPanelDuration,\n      );\n\n      setPanelDuration((currentDuration) =>\n        Math.abs(currentDuration - nextDuration) < 0.01\n          ? currentDuration\n          : nextDuration,\n      );\n      previousHeightRef.current = nextHeight;\n    };\n\n    if (frameRef.current !== null) {\n      window.cancelAnimationFrame(frameRef.current);\n    }\n\n    frameRef.current = window.requestAnimationFrame(measure);\n\n    if (typeof ResizeObserver === \"undefined\") {\n      return () => {\n        if (frameRef.current !== null) {\n          window.cancelAnimationFrame(frameRef.current);\n        }\n      };\n    }\n\n    const observer = new ResizeObserver(measure);\n    observer.observe(element);\n\n    return () => {\n      if (frameRef.current !== null) {\n        window.cancelAnimationFrame(frameRef.current);\n      }\n\n      observer.disconnect();\n    };\n  }, [activePanel?.id, contentElement, isOpen, shouldReduceMotion]);\n\n  const resolvedHeightTransition: MotionTransition = shouldReduceMotion\n    ? { duration: 0 }\n    : heightTransition ?? { duration: 0.27, ease: [0.25, 1, 0.5, 1] };\n  const resolvedPanelTransition: MotionTransition = shouldReduceMotion\n    ? { duration: 0 }\n    : panelTransition ?? {\n        duration: panelDuration,\n        ease: [0.26, 0.08, 0.25, 1],\n      };\n\n  if (!activePanel) {\n    return null;\n  }\n\n  return (\n    <Drawer open={isOpen} onOpenChange={setOpen}>\n      {trigger === null ? null : React.isValidElement(trigger) ? (\n        <DrawerTrigger asChild>{trigger}</DrawerTrigger>\n      ) : (\n        <DrawerTrigger className={buttonVariants({ variant: \"outline\" })}>\n          {trigger ?? triggerLabel}\n        </DrawerTrigger>\n      )}\n      <DrawerContent\n        onPointerDown={(event) => {\n          if (event.target === event.currentTarget) {\n            setOpen(false);\n          }\n        }}\n        className={cn(\n          \"inset-x-0 bottom-0 mx-auto max-w-none overflow-visible border-0 bg-transparent p-4 shadow-none after:hidden [&>div:first-child]:hidden\",\n          \"data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-0 data-[vaul-drawer-direction=bottom]:max-h-none data-[vaul-drawer-direction=bottom]:rounded-none data-[vaul-drawer-direction=bottom]:border-0\",\n        )}\n      >\n        <div\n          className={cn(\n            \"mx-auto max-w-sm overflow-hidden rounded-2xl border bg-background\",\n            className,\n            drawerClassName,\n          )}\n        >\n          <MotionConfig reducedMotion=\"user\">\n            <motion.div\n              initial={false}\n              animate={\n                shouldReduceMotion\n                  ? { height: \"auto\" }\n                  : { height: height ?? 0 }\n              }\n              transition={resolvedHeightTransition}\n              className=\"overflow-hidden\"\n            >\n              <div\n                ref={setContentElement}\n                className={cn(\"px-6 pb-6 pt-5\", contentClassName)}\n              >\n                <div className=\"grid grid-cols-[1fr_2rem] items-start gap-4\">\n                  <div className=\"min-w-0\">\n                    <DrawerTitle className=\"text-base font-semibold\">\n                      {title}\n                    </DrawerTitle>\n                    {description ? (\n                      <DrawerDescription className=\"mt-1\">\n                        {description}\n                      </DrawerDescription>\n                    ) : null}\n                  </div>\n                  <DrawerClose\n                    aria-label={closeLabel}\n                    className={buttonVariants({\n                      variant: \"ghost\",\n                      size: \"icon\",\n                      className: \"rounded-full\",\n                    })}\n                  >\n                    <X aria-hidden />\n                  </DrawerClose>\n                </div>\n\n                <div className=\"relative mt-6 overflow-hidden\">\n                  <AnimatePresence initial={false} mode=\"popLayout\">\n                    <motion.div\n                      key={activePanel.id}\n                      initial={{ opacity: 0, scale: 0.96 }}\n                      animate={{ opacity: 1, scale: 1 }}\n                      exit={{ opacity: 0, scale: 0.96 }}\n                      transition={resolvedPanelTransition}\n                    >\n                      <div>\n                        <h3 className=\"text-lg font-semibold tracking-tight\">\n                          {activePanel.title}\n                        </h3>\n                        {activePanel.description ? (\n                          <p className=\"mt-1 text-sm leading-6 text-muted-foreground\">\n                            {activePanel.description}\n                          </p>\n                        ) : null}\n                      </div>\n                      <div className=\"mt-5\">\n                        {typeof activePanel.content === \"function\"\n                          ? activePanel.content(controls)\n                          : activePanel.content}\n                      </div>\n                    </motion.div>\n                  </AnimatePresence>\n                </div>\n              </div>\n            </motion.div>\n          </MotionConfig>\n        </div>\n      </DrawerContent>\n    </Drawer>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ui/adaptive-drawer.tsx"
    }
  ],
  "meta": {
    "tags": [
      "drawer",
      "adaptive-height",
      "bottom-sheet",
      "content-transition",
      "layout"
    ],
    "effects": [
      "height-animation",
      "fade",
      "scale"
    ]
  },
  "categories": [
    "overlay"
  ],
  "type": "registry:ui"
}