{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "drawer-motion",
  "title": "Motion drawer",
  "description": "Styled drawer: the Motion set. 5 decorative drawer variations (SlideDrawer, SpringDrawer, FadeDrawer, ScaleDrawer, OvershootDrawer) on the ai2 token system, powered by framer-motion, reduced-motion aware and sized sm to xl. Part of the free styled layer.",
  "dependencies": [
    "motion@^12.42.2",
    "lucide-react@^1.23.0"
  ],
  "registryDependencies": [
    "@ai2/tokens"
  ],
  "files": [
    {
      "path": "registry/ai2/styled/drawer-motion.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { motion, AnimatePresence, useReducedMotion } from \"motion/react\"\nimport { X } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\n/* Motion drawer family: 5 showy bottom drawers. They all open upward from the\n   BOTTOM edge; the panel itself is plain (bg-card) and the DIFFERENCE is the\n   character of the enter/exit animation. Each export is a complete drawer: internal\n   open/closed state (uncontrolled defaultOpen or controlled open/onOpenChange), NO\n   radix/vaul/portal - a self-contained fixed backdrop + a fixed panel pinned to the\n   edge. It closes on a backdrop click, Escape or the close button. The panel takes\n   focus on open. AnimatePresence handles enter/exit; under reduced motion ALL\n   transforms drop away and only an opacity fade remains. Color comes ONLY from\n   tokens, via alpha color-mix. The essentials mechanism is preserved: sliding from\n   the bottom edge via data-slot + slideOffset. */\n\nexport type StyledSize = \"sm\" | \"md\" | \"lg\" | \"xl\"\n\ntype Edge = \"bottom\"\n\nconst panelHeight: Record<StyledSize, string> = {\n  sm: \"h-[35vh]\",\n  md: \"h-[50vh]\",\n  lg: \"h-[68vh]\",\n  xl: \"h-[82vh]\",\n}\n\nconst edgeAnchor: Record<Edge, string> = {\n  bottom: \"inset-x-0 bottom-0\",\n}\n\n/* The offset the panel is pushed past the edge by when closed. +y for the bottom edge. */\nfunction slideOffset(edge: Edge) {\n  switch (edge) {\n    case \"bottom\":\n      return { y: \"100%\" }\n  }\n}\n\n/* The enter/exit definition each export carries. Combined with slideOffset. */\ninterface DrawerMotion {\n  initial: Record<string, number | string>\n  animate: Record<string, number | string>\n  exit: Record<string, number | string>\n  transition: Record<string, unknown>\n}\n\ninterface DrawerProps {\n  size?: StyledSize\n  trigger?: React.ReactNode\n  title?: React.ReactNode\n  children?: React.ReactNode\n  className?: string\n  open?: boolean\n  defaultOpen?: boolean\n  onOpenChange?: (o: boolean) => void\n}\n\n/* Controlled/uncontrolled acik durum yonetimi. */\nfunction useControllableOpen(\n  open: boolean | undefined,\n  defaultOpen: boolean | undefined,\n  onOpenChange: ((o: boolean) => void) | undefined\n) {\n  const [uncontrolled, setUncontrolled] = React.useState(defaultOpen ?? false)\n  const isControlled = open !== undefined\n  const value = isControlled ? open : uncontrolled\n  const setValue = React.useCallback(\n    (next: boolean) => {\n      if (!isControlled) setUncontrolled(next)\n      onOpenChange?.(next)\n    },\n    [isControlled, onOpenChange]\n  )\n  return [value, setValue] as const\n}\n\ninterface ShellProps extends DrawerProps {\n  edge: Edge\n  slot: string\n  panelClassName: string\n  backdropClassName: string\n  drawerMotion: DrawerMotion\n}\n\n/* Shared shell: trigger plus an AnimatePresence backdrop plus panel. drawerMotion carries the enter/exit animation supplied from outside; it differs in every export. The Essentials slideOffset mechanism is preserved: the closed position is pushed past the edge. */\nfunction DrawerShell({\n  edge,\n  slot,\n  panelClassName,\n  backdropClassName,\n  drawerMotion,\n  size = \"md\",\n  trigger,\n  title,\n  children,\n  className,\n  open,\n  defaultOpen,\n  onOpenChange,\n}: ShellProps) {\n  const reduce = useReducedMotion()\n  const [isOpen, setOpen] = useControllableOpen(open, defaultOpen, onOpenChange)\n  const panelRef = React.useRef<HTMLDivElement>(null)\n\n  React.useEffect(() => {\n    if (!isOpen) return\n    const onKey = (e: KeyboardEvent) => {\n      if (e.key === \"Escape\") setOpen(false)\n    }\n    window.addEventListener(\"keydown\", onKey)\n    return () => window.removeEventListener(\"keydown\", onKey)\n  }, [isOpen, setOpen])\n\n  const sizeClass = cn(\"w-full\", panelHeight[size])\n  const offset = slideOffset(edge)\n\n  /* Reduced motion: every transform is dropped, only the opacity fade remains. */\n  const fade = {\n    initial: { opacity: 0 },\n    animate: { opacity: 1 },\n    exit: { opacity: 0 },\n    transition: { duration: 0 },\n  }\n  const active = reduce\n    ? fade\n    : {\n        initial: { ...offset, ...drawerMotion.initial },\n        animate: { x: 0, y: 0, ...drawerMotion.animate },\n        exit: { ...offset, ...drawerMotion.exit },\n        transition: drawerMotion.transition,\n      }\n\n  return (\n    <div data-slot={slot} className=\"inline-flex\">\n      {/* The ARIA state lives on the REAL trigger. There used to be a\n          `<span role=\"button\" tabIndex={-1}>` wrapper, which produced two\n          buttons in the AX tree: one carrying the state (not focusable) and\n          one that took focus (stateless). A screen-reader user never heard\n          \"has dialog\" or \"expanded\" - measured with the CDP AX tree. */}\n      {React.isValidElement<React.ButtonHTMLAttributes<HTMLButtonElement>>(trigger) ? (\n        React.cloneElement(trigger, {\n          \"aria-haspopup\": \"dialog\",\n          \"aria-expanded\": isOpen,\n          onClick: (event: React.MouseEvent<HTMLButtonElement>) => {\n            trigger.props.onClick?.(event)\n            setOpen(true)\n          },\n        })\n      ) : (\n        <button\n          type=\"button\"\n          aria-haspopup=\"dialog\"\n          aria-expanded={isOpen}\n          onClick={() => setOpen(true)}\n          className=\"inline-flex h-9 shrink-0 select-none items-center justify-center gap-2 rounded-lg border border-border bg-secondary px-4 text-sm font-medium text-secondary-foreground outline-none whitespace-nowrap transition-[color,background-color] duration-(--motion-fast) ease-(--motion-ease) hover:bg-surface-3 focus-visible:ring-[3px] focus-visible:ring-ring/50 [&_i]:text-base [&_i]:leading-none [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0\"\n        >\n          {trigger ?? \"Open\"}\n        </button>\n      )}\n\n      <AnimatePresence>\n        {isOpen ? (\n          <div className=\"fixed inset-0 z-50\">\n            <motion.div\n              data-slot=\"styled-drawer-backdrop\"\n              onClick={() => setOpen(false)}\n              initial={{ opacity: 0 }}\n              animate={{ opacity: 1 }}\n              exit={{ opacity: 0 }}\n              transition={{ duration: reduce ? 0 : 0.2, ease: [0.4, 0, 0.2, 1] }}\n              className={cn(\"absolute inset-0\", backdropClassName)}\n            />\n            <motion.div\n              ref={panelRef}\n              data-slot=\"styled-drawer-panel\"\n              role=\"dialog\"\n              aria-modal=\"true\"\n              tabIndex={-1}\n              onAnimationComplete={() => panelRef.current?.focus()}\n              initial={active.initial}\n              animate={active.animate}\n              exit={active.exit}\n              transition={active.transition}\n              className={cn(\n                \"fixed flex flex-col shadow-lg outline-none\",\n                edgeAnchor[edge],\n                sizeClass,\n                panelClassName,\n                className\n              )}\n            >\n              {/* Tutamac: alt cekmecenin okunabilir gorsel imzasi. */}\n              <div className=\"flex shrink-0 justify-center pt-3 pb-1\">\n                <span className=\"h-1.5 w-12 rounded-full bg-[color-mix(in_oklab,var(--color-foreground)_25%,transparent)]\" />\n              </div>\n              <div className=\"flex shrink-0 items-start justify-between gap-4 px-6 pt-1\">\n                {title ? (\n                  <h2 data-slot=\"styled-drawer-title\" className=\"text-base font-semibold text-foreground\">\n                    {title}\n                  </h2>\n                ) : (\n                  <span />\n                )}\n                <button\n                  type=\"button\"\n                  onClick={() => setOpen(false)}\n                  aria-label=\"Close\"\n                  className=\"relative -m-1 inline-flex size-7 shrink-0 items-center justify-center rounded-md text-muted-foreground outline-none transition-[color,background-color] duration-(--motion-fast) ease-(--motion-ease) hover:bg-surface-3 hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 after:absolute after:-inset-2 [&_i]:text-base [&_i]:leading-none [&_svg]:size-4\"\n                >\n                  <X />\n                </button>\n              </div>\n              <div className=\"min-h-0 flex-1 overflow-auto px-6 py-4 text-sm text-muted-foreground\">\n                {children}\n              </div>\n            </motion.div>\n          </div>\n        ) : null}\n      </AnimatePresence>\n    </div>\n  )\n}\n\nconst solidBackdrop = \"bg-[color-mix(in_oklab,var(--color-foreground)_50%,transparent)]\"\nconst panelLook = \"rounded-t-2xl border-t border-border bg-card text-card-foreground\"\n\n/* Slide: duz kayma, yumusak ease. Overshoot yok, sabit sureli tween. */\nexport function SlideDrawer(props: DrawerProps) {\n  return (\n    <DrawerShell\n      {...props}\n      edge=\"bottom\"\n      slot=\"styled-drawer\"\n      backdropClassName={solidBackdrop}\n      panelClassName={panelLook}\n      drawerMotion={{\n        initial: {},\n        animate: {},\n        exit: {},\n        transition: { type: \"tween\", duration: 0.32, ease: [0.4, 0, 0.2, 1] },\n      }}\n    />\n  )\n}\n\n/* Spring: a springy entrance that settles with a slight overshoot. */\nexport function SpringDrawer(props: DrawerProps) {\n  return (\n    <DrawerShell\n      {...props}\n      edge=\"bottom\"\n      slot=\"styled-drawer\"\n      backdropClassName={solidBackdrop}\n      panelClassName={panelLook}\n      drawerMotion={{\n        initial: {},\n        animate: {},\n        exit: {},\n        transition: { type: \"spring\", stiffness: 300, damping: 26 },\n      }}\n    />\n  )\n}\n\n/* Fade: kayma ile opacity fade birlikte; panel kayarken belirir. */\nexport function FadeDrawer(props: DrawerProps) {\n  return (\n    <DrawerShell\n      {...props}\n      edge=\"bottom\"\n      slot=\"styled-drawer\"\n      backdropClassName={solidBackdrop}\n      panelClassName={panelLook}\n      drawerMotion={{\n        initial: { opacity: 0 },\n        animate: { opacity: 1 },\n        exit: { opacity: 0 },\n        transition: { type: \"tween\", duration: 0.36, ease: [0.16, 1, 0.3, 1] },\n      }}\n    />\n  )\n}\n\n/* Scale: kayarken hafif buyume; kapalikken az kuculmus gelir. */\nexport function ScaleDrawer(props: DrawerProps) {\n  return (\n    <DrawerShell\n      {...props}\n      edge=\"bottom\"\n      slot=\"styled-drawer\"\n      backdropClassName={solidBackdrop}\n      panelClassName={panelLook}\n      drawerMotion={{\n        initial: { scale: 0.94 },\n        animate: { scale: 1 },\n        exit: { scale: 0.94 },\n        transition: { type: \"spring\", stiffness: 260, damping: 28 },\n      }}\n    />\n  )\n}\n\n/* Overshoot: agresif yay, dusuk damping ile kenardan gecip geri seker. */\nexport function OvershootDrawer(props: DrawerProps) {\n  return (\n    <DrawerShell\n      {...props}\n      edge=\"bottom\"\n      slot=\"styled-drawer\"\n      backdropClassName={solidBackdrop}\n      panelClassName={panelLook}\n      drawerMotion={{\n        initial: {},\n        animate: {},\n        exit: {},\n        transition: { type: \"spring\", stiffness: 520, damping: 13 },\n      }}\n    />\n  )\n}\n",
      "type": "registry:component",
      "target": "components/ui/drawer-motion.tsx"
    }
  ],
  "categories": [
    "styled",
    "drawer"
  ],
  "type": "registry:component"
}