{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"motion-popover","type":"registry:component","title":"Popover (Motion)","description":"Popover with motion: it springs out of its trigger from the side it opens on, and glides to its new height when what's inside changes.","dependencies":["motion","radix-ui"],"registryDependencies":["https://dashral.com/r/utils.json","https://dashral.com/r/lib-portal.json","https://dashral.com/r/motion-reduced.json","https://dashral.com/r/motion-tokens.json","https://dashral.com/r/popover.json"],"files":[{"path":"registry/dashral/motion/popover.tsx","type":"registry:file","target":"components/dashral/motion/popover.tsx","content":"\"use client\"\n\nimport * as React from \"react\"\nimport { Popover as PopoverPrimitive } from \"radix-ui\"\nimport { AnimatePresence, animate, motion, useMotionValue } from \"motion/react\"\nimport type { PopoverProps } from \"../popover\"\nimport { cn } from \"../lib/utils\"\nimport { usePortalContainer } from \"../lib/portal\"\n\nimport { useReducedMotion } from \"../lib/motion-reduced\"\nimport { duration, ease, spring } from \"../lib/motion-tokens\"\n\nexport type { PopoverProps } from \"../popover\"\n\n// How far it starts back toward its trigger, by side.\nconst offsets = { top: { x: 0, y: 6 }, bottom: { x: 0, y: -6 }, left: { x: 6, y: 0 }, right: { x: -6, y: 0 } } as const\n\n/**\n * The kit's Popover with motion: the same trigger, side, align and props. It springs out of its trigger from the side\n * it opens on and leaves quickly; when what's inside changes (a filter added, a form's next step, a message after\n * saving), it glides to its new height instead of jumping. With reduced motion it only fades.\n */\nexport function Popover({ trigger, children, side = \"bottom\", align = \"center\", className, open, defaultOpen, onOpenChange, ...props }: PopoverProps) {\n  const portalContainer = usePortalContainer()\n  const reduced = useReducedMotion()\n  // Kept here, so the panel can stay on screen while it leaves.\n  const [own, setOwn] = React.useState(defaultOpen ?? false)\n  const isOpen = open ?? own\n  const change = (next: boolean) => {\n    if (open === undefined) setOwn(next)\n    onOpenChange?.(next)\n  }\n\n  // The panel's height follows what's inside on a glide; before it's measured (and with reduced motion) it's natural.\n  const inside = React.useRef<HTMLDivElement>(null)\n  const height = useMotionValue<number | \"auto\">(\"auto\")\n  const [gliding, setGliding] = React.useState(false)\n  React.useEffect(() => {\n    const node = inside.current\n    if (!isOpen || !node) return\n    let first = true\n    const observer = new ResizeObserver(([entry]) => {\n      const next = entry?.borderBoxSize[0]?.blockSize ?? node.offsetHeight\n      if (first || reduced) height.jump(next)\n      else {\n        setGliding(true)\n        animate(height, next, { ...spring.glide, onComplete: () => setGliding(false) })\n      }\n      first = false\n    })\n    observer.observe(node)\n    return () => {\n      observer.disconnect()\n      height.jump(\"auto\")\n    }\n  }, [isOpen, height, reduced])\n\n  const from = reduced ? { opacity: 0 } : { opacity: 0, scale: 0.94, ...offsets[side] }\n\n  return (\n    <PopoverPrimitive.Root data-slot=\"popover\" open={isOpen} onOpenChange={change} {...props}>\n      <PopoverPrimitive.Trigger asChild>{trigger}</PopoverPrimitive.Trigger>\n      <AnimatePresence>\n        {isOpen && (\n          <PopoverPrimitive.Portal forceMount container={portalContainer}>\n            {/* Kept clear of the screen's edges, so on a phone it never touches them. */}\n            <PopoverPrimitive.Content forceMount asChild side={side} align={align} sideOffset={4} collisionPadding={8}>\n              <motion.div\n                data-slot=\"popover-content\"\n                className={cn(\"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-hidden\", className)}\n                // It grows from the point facing its trigger (Radix works it out).\n                style={{ transformOrigin: \"var(--radix-popover-content-transform-origin)\" }}\n                initial={from}\n                animate={{ opacity: 1, scale: 1, x: 0, y: 0 }}\n                exit={{ ...from, transition: { duration: duration.fast, ease: ease.exit } }}\n                transition={reduced ? { duration: duration.fast } : { ...spring.appear, opacity: { duration: duration.fast, ease: ease.enter } }}\n              >\n                {/* Clipped only while it glides, so a focus ring at the edge isn't cut off the rest of the time. */}\n                <motion.div style={{ height }} className={cn(gliding && \"overflow-hidden\")}>\n                  <div ref={inside}>{children}</div>\n                </motion.div>\n              </motion.div>\n            </PopoverPrimitive.Content>\n          </PopoverPrimitive.Portal>\n        )}\n      </AnimatePresence>\n    </PopoverPrimitive.Root>\n  )\n}\n"}]}