{
  "name": "popover",
  "type": "registry:ui",
  "title": "Popover",
  "description": "Contextual information, compact actions and lightweight forms anchored to a trigger.",
  "registryDependencies": [
    "@siteplane/base"
  ],
  "dependencies": [
    "@base-ui/react@1.5.0"
  ],
  "files": [
    {
      "path": "src/components/ui/popover-motion.ts",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport {\n  useCallback,\n  useRef,\n  type MutableRefObject,\n  type RefCallback,\n} from \"react\";\n\ntype PopoverSide = \"top\" | \"right\" | \"bottom\" | \"left\";\n\nconst flipMotionCleanupBuffer = 40;\nconst popoverFlipSettleTransition =\n  \"transform var(--motion-duration-route) var(--motion-ease-apple-out)\";\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\nexport function usePopoverFlipMotion(): RefCallback<HTMLDivElement> {\n  const elementRef = useRef<HTMLDivElement | null>(null);\n  const previousRectRef = useRef<DOMRect | null>(null);\n  const previousSideRef = useRef<PopoverSide | 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  const measurePositioner = useCallback((element: HTMLDivElement) => {\n    if (isAnimatingRef.current) {\n      return;\n    }\n\n    if (isPrePositioningPopoverPositioner(element)) {\n      return;\n    }\n\n    const currentSide = readPopoverSide(element);\n    const currentRect = element.getBoundingClientRect();\n    const previousRect = previousRectRef.current;\n    const previousSide = previousSideRef.current;\n\n    if (\n      !previousRect ||\n      !isVerticalPopoverSideFlip(previousSide, currentSide) ||\n      shouldReducePopoverFlipMotion()\n    ) {\n      previousRectRef.current = currentRect;\n      previousSideRef.current = currentSide;\n\n      return;\n    }\n\n    const popup = element.querySelector<HTMLElement>('[data-slot=\"popover-popup\"]');\n\n    if (!popup) {\n      previousRectRef.current = currentRect;\n      previousSideRef.current = currentSide;\n\n      return;\n    }\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(previousRect, currentRect, popup, currentSide);\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        previousRectRef.current = element.getBoundingClientRect();\n        previousSideRef.current = readPopoverSide(element);\n        isAnimatingRef.current = false;\n      }, getPopoverFlipCleanupDelay(element));\n    });\n  }, []);\n\n  return useCallback((element) => {\n    observerRef.current?.disconnect();\n    observerRef.current = null;\n    elementRef.current = element;\n\n    if (!element) {\n      previousRectRef.current = null;\n      previousSideRef.current = null;\n      isAnimatingRef.current = false;\n      cancelPendingPopoverFlipMotion(animationFrameRef, cleanupTimeoutRef);\n\n      return;\n    }\n\n    previousRectRef.current = null;\n    previousSideRef.current = null;\n\n    if (typeof MutationObserver === \"undefined\") {\n      return;\n    }\n\n    observerRef.current = new MutationObserver(() => measurePositioner(element));\n    observerRef.current.observe(element, {\n      attributeFilter: [\"data-side\", \"style\"],\n      attributes: true,\n    });\n  }, [measurePositioner]);\n}\n\nfunction isPrePositioningPopoverPositioner(element: HTMLElement): boolean {\n  return element.style.opacity === \"0\" || getComputedStyle(element).opacity === \"0\";\n}\n\nfunction readPopoverSide(element: HTMLElement): PopoverSide | null {\n  const side = element.getAttribute(\"data-side\");\n\n  if (side === \"top\" || side === \"right\" || side === \"bottom\" || side === \"left\") {\n    return side;\n  }\n\n  return null;\n}\n\nfunction shouldReducePopoverFlipMotion(): boolean {\n  if (typeof window === \"undefined\") {\n    return true;\n  }\n\n  if (document.documentElement.dataset.motionReduce === \"true\") {\n    return true;\n  }\n\n  return window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n}\n\nfunction getPopoverFlipCleanupDelay(element: HTMLElement): number {\n  const duration = getCssDurationMs(element, \"--motion-duration-route\");\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"
    },
    {
      "path": "src/components/ui/popover.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport { Popover as PopoverPrimitive } from \"@base-ui/react/popover\";\nimport type React from \"react\";\nimport { usePopoverFlipMotion } from \"@/components/ui/popover-motion\";\nimport { SiteplaneUIPortalScope } from \"@/components/ui/siteplane-provider\";\nimport { surfaceClassNames } from \"@/components/ui/surface\";\nimport { cn } from \"@/lib/utils\";\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}: PopoverPrimitive.Trigger.Props): React.ReactElement {\n  return (\n    <PopoverPrimitive.Trigger\n      className={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  anchor,\n  portalProps,\n  ...props\n}: PopoverPrimitive.Popup.Props & {\n  portalProps?: 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  anchor?: PopoverPrimitive.Positioner.Props[\"anchor\"];\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=\"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              surfaceClassNames.floatingOverlay,\n              \"flex h-(--popup-height,auto) w-(--popup-width,auto) origin-(--transform-origin) rounded-lg outline-none before:rounded-[inherit] has-data-[slot=calendar]:rounded-xl has-data-[slot=calendar]:before:rounded-[inherit]\",\n              tooltipStyle &&\n                \"w-fit text-balance rounded-md text-xs shadow-md/5 before:rounded-[inherit]\",\n              className,\n            )}\n            data-slot=\"popover-popup\"\n            {...props}\n          >\n            <PopoverPrimitive.Viewport\n              className={cn(\n                \"relative size-full max-h-(--available-height) overflow-clip px-(--viewport-inline-padding) py-4 [--viewport-inline-padding:--spacing(4)] has-data-[slot=calendar]:p-2 data-instant:transition-none **:data-current:data-ending-style:opacity-0 **:data-current:data-starting-style:opacity-0 **:data-previous:data-ending-style:opacity-0 **:data-previous:data-starting-style:opacity-0 **:data-current:w-[calc(var(--popup-width)-2*var(--viewport-inline-padding)-2px)] **:data-previous:w-[calc(var(--popup-width)-2*var(--viewport-inline-padding)-2px)] **:data-current:opacity-100 **:data-previous:opacity-100 **:data-current:transition-opacity **:data-previous:transition-opacity\",\n                tooltipStyle\n                  ? \"py-1 [--viewport-inline-padding:--spacing(2)]\"\n                  : \"not-data-transitioning:overflow-y-auto\",\n              )}\n              data-slot=\"popover-viewport\"\n            >\n              {children}\n            </PopoverPrimitive.Viewport>\n          </PopoverPrimitive.Popup>\n        </PopoverPrimitive.Positioner>\n      </SiteplaneUIPortalScope>\n    </PopoverPrimitive.Portal>\n  );\n}\n\nexport function PopoverClose({\n  ...props\n}: PopoverPrimitive.Close.Props): React.ReactElement {\n  return <PopoverPrimitive.Close data-slot=\"popover-close\" {...props} />;\n}\n\nexport function PopoverTitle({\n  className,\n  ...props\n}: PopoverPrimitive.Title.Props): React.ReactElement {\n  return (\n    <PopoverPrimitive.Title\n      className={cn(\"font-semibold text-sm\", className)}\n      data-slot=\"popover-title\"\n      {...props}\n    />\n  );\n}\n\nexport function PopoverDescription({\n  className,\n  ...props\n}: PopoverPrimitive.Description.Props): React.ReactElement {\n  return (\n    <PopoverPrimitive.Description\n      className={cn(\n        \"text-muted-foreground text-sm [[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\nexport { PopoverPrimitive, PopoverPopup as PopoverContent };\n"
    }
  ],
  "categories": [
    "overlays"
  ],
  "docs": "# Popover\n\n> Siteplane UI release `0.1.1`.\n\n## Public Purpose\n\nUse Popover for short contextual interactions, supporting information or compact forms anchored to a trigger without leaving the current workflow.\n\n## Import\n\n`import { Popover } from \"@/components/ui/popover\";`\n\n`import { PopoverClose, PopoverContent, PopoverCreateHandle, PopoverDescription, PopoverPopup, PopoverTitle, PopoverTrigger } from \"@/components/ui/popover\";`\n\n`import { Field } from \"@/components/ui/field\";`\n\n`import { Form } from \"@/components/ui/form\";`\n\n`import { Textarea } from \"@/components/ui/textarea\";`\n\n`import { InputGroup, InputGroupAddon, InputGroupInput } from \"@/components/ui/input-group\";`\n\n## Public API\n\n- Compose `Popover`, `PopoverTrigger` and `PopoverPopup`; the popup already owns its portal and positioner.\n- `PopoverPopup` accepts `side`, `align`, `sideOffset`, `alignOffset`, `anchor`, `portalProps` and the popup content props.\n- Use `PopoverTitle`, `PopoverDescription` and `PopoverClose` for structured, dismissible content.\n- `PopoverCreateHandle` supports detached triggers or externally controlled popup access when the normal trigger relationship is not sufficient.\n\n## Public Motion\n\n- `PopoverPopup` uses the shared floating-panel fade, scale and directional offset tokens.\n- Collision handling, transform origin and reduced-motion behavior remain part of the native Siteplane floating-panel contract.\n\n## Public Accessibility\n\n- Escape, outside click and focus management are provided by Base UI and must remain intact.\n- Give icon-only triggers an accessible name and use `PopoverTitle` and `PopoverDescription` for structured content.\n- Use Tooltip for a non-interactive hint and Dialog or Alert Dialog for long or critical tasks.\n\n## Public Agent Guidance\n\n- Keep portal, positioning, focus and dismissal logic inside the primitive.\n- Prefer the regular trigger relationship; use `anchor` or `PopoverCreateHandle` only for a genuine detached-anchor requirement.\n- Do not use Popover for irreversible decisions or long forms.\n\n## Public Links\n\n- [Documentation and preview](https://ui.siteplane.io/docs/components/overlays/popover)\n\n- [Registry source](https://ui.siteplane.io/r/popover.json)\n",
  "meta": {
    "siteplane:agentContractVersion": 1,
    "siteplane:docs": "https://ui.siteplane.io/docs/components/overlays/popover",
    "siteplane:releaseVersion": "0.1.1",
    "siteplane:sourceOwned": true
  },
  "devDependencies": []
}
