{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"popover","title":"Popover","description":"Anchored non-modal surface for contextual content.","dependencies":["@base-ui/react@1.7.0","lucide-react@1.18.0"],"registryDependencies":["@siteplane/base","@siteplane/button"],"files":[{"path":"packages/ui/src/components/popover/popover.tsx","content":"\"use client\";\n\nimport { Popover as PopoverPrimitive } from \"@base-ui/react/popover\";\nimport { XIcon } from \"lucide-react\";\nimport type React from \"react\";\nimport { usePopoverFlipMotion } from \"./popover-motion\";\nimport { SiteplaneUIPortalScope } from \"../../system/provider/siteplane-ui-provider\";\n\nimport { surfaceClassNames } from \"../../system/styles/surface\";\nimport { cn, type WithStringClassName } from \"../../system/utils/cn\";\nimport { baseClass, slotClass } from \"../../system/utils/slot-class\";\nimport { KitButton } from \"../button/button-internal\";\n\nexport const PopoverCreateHandle: typeof PopoverPrimitive.createHandle =\n  PopoverPrimitive.createHandle;\n\nexport const Popover: typeof PopoverPrimitive.Root = PopoverPrimitive.Root;\n\nexport function PopoverTrigger({\n  className,\n  children,\n  ...props\n}: WithStringClassName<PopoverPrimitive.Trigger.Props>): React.ReactElement {\n  return (\n    <PopoverPrimitive.Trigger\n      className={cn(slotClass(\"popover-trigger\", props), className)}\n      data-slot=\"popover-trigger\"\n      {...props}\n    >\n      {children}\n    </PopoverPrimitive.Trigger>\n  );\n}\n\nexport function PopoverPopup({\n  children,\n  className,\n  side = \"bottom\",\n  align = \"start\",\n  sideOffset = 4,\n  alignOffset = 0,\n  tooltipStyle = false,\n  viewportPadding = \"default\",\n  anchor,\n  portalProps,\n  showCloseButton = false,\n  closeProps,\n  variant = \"default\",\n  ...props\n}: WithStringClassName<PopoverPrimitive.Popup.Props> & {\n  portalProps?: WithStringClassName<PopoverPrimitive.Portal.Props>;\n  side?: PopoverPrimitive.Positioner.Props[\"side\"];\n  align?: PopoverPrimitive.Positioner.Props[\"align\"];\n  sideOffset?: PopoverPrimitive.Positioner.Props[\"sideOffset\"];\n  alignOffset?: PopoverPrimitive.Positioner.Props[\"alignOffset\"];\n  tooltipStyle?: boolean;\n  viewportPadding?: \"compact\" | \"default\";\n  anchor?: PopoverPrimitive.Positioner.Props[\"anchor\"];\n  /**\n   * Shows a close button in the top-end corner, as Dialog, Sheet and Drawer\n   * do: the 24 px overlay-dismiss Button 8 px inside the corner. The first\n   * block of the content keeps 8 px clear of it.\n   */\n  showCloseButton?: boolean;\n  closeProps?: WithStringClassName<PopoverPrimitive.Close.Props>;\n  /** `glass` lays the shared glass surface over whatever sits behind the popover. */\n  variant?: \"default\" | \"glass\";\n}): React.ReactElement {\n  const positionerRef = usePopoverFlipMotion();\n\n  return (\n    <PopoverPrimitive.Portal {...portalProps}>\n      <SiteplaneUIPortalScope>\n        <PopoverPrimitive.Positioner\n          align={align}\n          alignOffset={alignOffset}\n          anchor={anchor}\n          className=\"sp-popover-positioner z-50 h-(--positioner-height) w-(--positioner-width) max-w-(--available-width)\"\n          data-slot=\"popover-positioner\"\n          ref={positionerRef}\n          side={side}\n          sideOffset={sideOffset}\n        >\n          <PopoverPrimitive.Popup\n            className={cn(\n              // Glass keeps the frame and takes the glass surface in place of\n              // the opaque one, whose fill, edge and shadow the glass rule draws.\n              slotClass(\"popover-popup\", props),\n              baseClass(\"popover\", props),\n              surfaceClassNames.floatingOverlayFrame,\n              variant === \"glass\"\n                ? surfaceClassNames.glass\n                : surfaceClassNames.floatingOverlaySurface,\n              \"flex h-(--popup-height,auto) outline-none before:rounded-[inherit] has-data-[slot=calendar]:before:rounded-[inherit]\",\n              tooltipStyle && \"text-balance before:rounded-[inherit]\",\n              className,\n            )}\n            data-foundation=\"popover\"\n            data-slot=\"popover-popup\"\n            data-tooltip-style={tooltipStyle ? \"\" : undefined}\n            data-variant={variant === \"glass\" ? variant : undefined}\n            {...props}\n          >\n            <PopoverPrimitive.Viewport\n              className={cn(\n                // The cross-fade of the current and the previous content is\n                // the popover viewport rule in design-rework.css.\n                \"sp-popover-viewport\",\n                \"relative size-full max-h-(--available-height) overflow-clip px-(--viewport-inline-padding) data-instant:transition-none\",\n                tooltipStyle ? \"\" : \"not-data-transitioning:overflow-y-auto\",\n                // The first block sits beside the corner close button and ends\n                // 8 px before it, 40 px from the edge, the viewport's own\n                // padding included.\n                showCloseButton &&\n                  \"[&>:is([data-current],[data-previous])>:first-child]:pe-[calc(--spacing(10)-var(--viewport-inline-padding))]\",\n              )}\n              data-padding={viewportPadding}\n              data-slot=\"popover-viewport\"\n            >\n              {children}\n            </PopoverPrimitive.Viewport>\n            {showCloseButton && (\n              <PopoverClose\n                aria-label=\"Close\"\n                className=\"absolute end-2 top-2\"\n                render={\n                  <KitButton\n                    profile=\"overlay-dismiss\"\n                    shape=\"square\"\n                    variant=\"ghost\"\n                  />\n                }\n                {...closeProps}\n              >\n                <XIcon />\n              </PopoverClose>\n            )}\n          </PopoverPrimitive.Popup>\n        </PopoverPrimitive.Positioner>\n      </SiteplaneUIPortalScope>\n    </PopoverPrimitive.Portal>\n  );\n}\n\nexport function PopoverClose({\n  ...props\n}: WithStringClassName<PopoverPrimitive.Close.Props>): React.ReactElement {\n  return (\n    <PopoverPrimitive.Close\n      data-slot=\"popover-close\"\n      {...props}\n      className={cn(slotClass(\"popover-close\", props), props.className)}\n    />\n  );\n}\n\nexport function PopoverTitle({\n  className,\n  ...props\n}: WithStringClassName<PopoverPrimitive.Title.Props>): React.ReactElement {\n  return (\n    <PopoverPrimitive.Title\n      className={cn(slotClass(\"popover-title\", props), className)}\n      data-slot=\"popover-title\"\n      {...props}\n    />\n  );\n}\n\nexport function PopoverDescription({\n  className,\n  ...props\n}: WithStringClassName<PopoverPrimitive.Description.Props>): React.ReactElement {\n  return (\n    <PopoverPrimitive.Description\n      className={cn(\n        slotClass(\"popover-description\", props),\n        \"wrap-anywhere [[data-slot=popover-viewport]>[data-current]>[data-slot=popover-title]+&]:mt-1\",\n        className,\n      )}\n      data-slot=\"popover-description\"\n      {...props}\n    />\n  );\n}\n\n// PopoverContent is the shadcn name of PopoverPopup.\nexport { PopoverPrimitive, PopoverPopup as PopoverContent };\n","type":"registry:ui","target":"~/src/siteplane/components/popover/popover.tsx"},{"path":"packages/ui/src/components/popover/popover-motion.ts","content":"\"use client\";\n\nimport {\n  useCallback,\n  useRef,\n  type MutableRefObject,\n  type RefCallback,\n} from \"react\";\n\nimport { readReducedMotion } from \"../../system/motion/use-reduced-motion-preference\";\n\ntype PopoverSide = \"top\" | \"right\" | \"bottom\" | \"left\";\n\nconst flipMotionCleanupBuffer = 40;\nconst popoverFlipSettleTransition =\n  \"transform var(--motion-duration-moderate) var(--motion-ease-standard)\";\n\nexport function isVerticalPopoverSideFlip(\n  previousSide: string | null,\n  currentSide: string | null,\n): boolean {\n  return (\n    (previousSide === \"bottom\" && currentSide === \"top\") ||\n    (previousSide === \"top\" && currentSide === \"bottom\")\n  );\n}\n\n/**\n * Base UI hides a positioner with an inline `opacity: 0` until it has a\n * position; a batch that starts from that style places the popover for the\n * first time, which is no flip.\n */\nconst unplacedStyle = /(?:^|;)\\s*opacity:\\s*0\\s*(?:;|$)/;\n\n/**\n * Where the positioner sat before a batch of attribute writes: the batch's\n * first `style` record holds the style it had, which is restored for one\n * measurement. Without a style record nothing moved.\n */\nfunction rectBeforeWrites(\n  element: HTMLElement,\n  moved: MutationRecord | undefined,\n  current: DOMRect,\n): DOMRect {\n  if (!moved) return current;\n  const style = element.getAttribute(\"style\");\n  setStyleAttribute(element, moved.oldValue);\n  const rect = element.getBoundingClientRect();\n  setStyleAttribute(element, style);\n  return rect;\n}\n\nfunction setStyleAttribute(element: HTMLElement, value: string | null) {\n  if (value === null) element.removeAttribute(\"style\");\n  else element.setAttribute(\"style\", value);\n}\n\nexport function usePopoverFlipMotion(): RefCallback<HTMLDivElement> {\n  const elementRef = useRef<HTMLDivElement | null>(null);\n  const observerRef = useRef<MutationObserver | null>(null);\n  const animationFrameRef = useRef<number | null>(null);\n  const cleanupTimeoutRef = useRef<number | null>(null);\n  const isAnimatingRef = useRef(false);\n\n  // Floating UI writes the positioner's style on every position update while\n  // the popover is open (scroll, resize). Those writes are left alone: the\n  // positioner is measured only in the batch that flips its side, and the\n  // place it had comes from the style the batch replaced (RW-11).\n  const measurePositioner = useCallback(\n    (element: HTMLDivElement, records: readonly MutationRecord[]) => {\n      if (isAnimatingRef.current) {\n        return;\n      }\n\n      const sideChange = records.find(\n        (record) => record.attributeName === \"data-side\",\n      );\n\n      if (!sideChange) {\n        return;\n      }\n\n      const previousSide = toPopoverSide(sideChange.oldValue);\n      const currentSide = readPopoverSide(element);\n      const moved = records.find((record) => record.attributeName === \"style\");\n\n      if (\n        !isVerticalPopoverSideFlip(previousSide, currentSide) ||\n        // The flip settles at once with reduced motion (M5): the system\n        // setting, the document mode or the provider preset that the\n        // popover's portal scope carries.\n        readReducedMotion(element) ||\n        unplacedStyle.test(moved?.oldValue ?? \"\") ||\n        isPrePositioningPopoverPositioner(element)\n      ) {\n        return;\n      }\n\n      const popup = element.querySelector<HTMLElement>(\n        '[data-slot=\"popover-popup\"]',\n      );\n\n      if (!popup) {\n        return;\n      }\n\n      const currentRect = element.getBoundingClientRect();\n      const previousRect = rectBeforeWrites(element, moved, currentRect);\n\n      cancelPendingPopoverFlipMotion(animationFrameRef, cleanupTimeoutRef);\n      isAnimatingRef.current = true;\n\n      const previousInlineTransition = popup.style.transition;\n      const previousInlineTransform = popup.style.transform;\n      const offset = getPopoverFlipOffset(\n        previousRect,\n        currentRect,\n        popup,\n        currentSide,\n      );\n\n      popup.style.transition = \"none\";\n      popup.style.transform = `translate3d(0, ${offset}px, 0) scale(var(--motion-overlay-scale))`;\n      popup.getBoundingClientRect();\n\n      animationFrameRef.current = window.requestAnimationFrame(() => {\n        animationFrameRef.current = null;\n        popup.style.transition = popoverFlipSettleTransition;\n        popup.style.transform = previousInlineTransform;\n\n        cleanupTimeoutRef.current = window.setTimeout(() => {\n          cleanupTimeoutRef.current = null;\n\n          if (elementRef.current !== element || !element.contains(popup)) {\n            isAnimatingRef.current = false;\n\n            return;\n          }\n\n          popup.style.transition = previousInlineTransition;\n          popup.style.transform = previousInlineTransform;\n          isAnimatingRef.current = false;\n        }, getPopoverFlipCleanupDelay(element));\n      });\n    },\n    [],\n  );\n\n  return useCallback(\n    (element) => {\n      observerRef.current?.disconnect();\n      observerRef.current = null;\n      elementRef.current = element;\n\n      if (!element) {\n        isAnimatingRef.current = false;\n        cancelPendingPopoverFlipMotion(animationFrameRef, cleanupTimeoutRef);\n\n        return;\n      }\n\n      if (typeof MutationObserver === \"undefined\") {\n        return;\n      }\n\n      observerRef.current = new MutationObserver((records) =>\n        measurePositioner(element, records),\n      );\n      observerRef.current.observe(element, {\n        attributeFilter: [\"data-side\", \"style\"],\n        attributeOldValue: true,\n        attributes: true,\n      });\n    },\n    [measurePositioner],\n  );\n}\n\nfunction isPrePositioningPopoverPositioner(element: HTMLElement): boolean {\n  return (\n    element.style.opacity === \"0\" || getComputedStyle(element).opacity === \"0\"\n  );\n}\n\nfunction readPopoverSide(element: HTMLElement): PopoverSide | null {\n  return toPopoverSide(element.getAttribute(\"data-side\"));\n}\n\nfunction toPopoverSide(side: string | null): PopoverSide | null {\n  if (\n    side === \"top\" ||\n    side === \"right\" ||\n    side === \"bottom\" ||\n    side === \"left\"\n  ) {\n    return side;\n  }\n\n  return null;\n}\n\nfunction getPopoverFlipCleanupDelay(element: HTMLElement): number {\n  const duration = getCssDurationMs(element, \"--motion-duration-moderate\");\n\n  return duration + flipMotionCleanupBuffer;\n}\n\nfunction getPopoverFlipOffset(\n  previousRect: DOMRect,\n  currentRect: DOMRect,\n  element: HTMLElement,\n  currentSide: PopoverSide | null,\n): number {\n  const offset = previousRect.top - currentRect.top;\n\n  if (Math.abs(offset) > 0.5) {\n    return offset;\n  }\n\n  return getPopoverFlipSettleOffset(element, currentSide);\n}\n\nfunction getPopoverFlipSettleOffset(\n  element: HTMLElement,\n  currentSide: PopoverSide | null,\n): number {\n  const distance = getCssDistancePx(element, \"--motion-overlay-y\");\n\n  return currentSide === \"top\" ? distance : distance * -1;\n}\n\nfunction getCssDurationMs(element: HTMLElement, propertyName: string): number {\n  const value = getComputedStyle(element).getPropertyValue(propertyName).trim();\n\n  if (value.endsWith(\"ms\")) {\n    return Number.parseFloat(value);\n  }\n\n  if (value.endsWith(\"s\")) {\n    return Number.parseFloat(value) * 1000;\n  }\n\n  return 0;\n}\n\nfunction getCssDistancePx(element: HTMLElement, propertyName: string): number {\n  const value = getComputedStyle(element).getPropertyValue(propertyName).trim();\n\n  if (value.endsWith(\"px\")) {\n    return Number.parseFloat(value);\n  }\n\n  return 0;\n}\n\nfunction cancelPendingPopoverFlipMotion(\n  animationFrameRef: MutableRefObject<number | null>,\n  cleanupTimeoutRef: MutableRefObject<number | null>,\n) {\n  if (typeof window === \"undefined\") {\n    return;\n  }\n\n  if (animationFrameRef.current !== null) {\n    window.cancelAnimationFrame(animationFrameRef.current);\n    animationFrameRef.current = null;\n  }\n\n  if (cleanupTimeoutRef.current !== null) {\n    window.clearTimeout(cleanupTimeoutRef.current);\n    cleanupTimeoutRef.current = null;\n  }\n}\n","type":"registry:ui","target":"~/src/siteplane/components/popover/popover-motion.ts"},{"path":"packages/ui/src/components/popover/index.ts","content":"export * from \"./popover\";\n","type":"registry:ui","target":"~/src/siteplane/components/popover/index.ts"}],"type":"registry:ui","docs":"https://ui.siteplane.io/docs-markdown/components/popover.md","categories":["Overlays & disclosure"],"meta":{"avoidWhen":["Avoid for a short label on hover or focus; use Tooltip.","Avoid for a task or a decision that needs the whole focus of the screen; use Dialog."],"description":"Anchored non-modal surface for contextual content.","knownLimitations":[],"providerNotes":[],"releaseStatus":"stable","searchTerms":["popover"],"useWhen":["Use for contextual content beside a trigger that people can interact with, such as fields, buttons or a short form."]}}