{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "popover",
  "type": "registry:component",
  "title": "Popover",
  "description": "Places non-modal content above the page with the native Popover API and light dismiss.",
  "registryDependencies": [
    "https://vlak.dev/r/vlak-base.json",
    "https://vlak.dev/r/inter.json",
    "https://vlak.dev/r/button.json",
    "https://vlak.dev/r/vlak-lib.json"
  ],
  "dependencies": [
    "@stylexjs/stylex"
  ],
  "devDependencies": [
    "@stylexjs/babel-plugin"
  ],
  "docs": "Vlak leaves are StyleX. Compile them with @stylexjs/babel-plugin (Vite: @stylexjs/unplugin, Next: @stylexjs/nextjs-plugin). If you would rather not run a compiler, import @noorddev/vlak-react instead: it ships precompiled with one stylesheet.",
  "files": [
    {
      "path": "vlak/popover.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as stylex from \"@stylexjs/stylex\";\nimport { vlak, mq } from \"./tokens.stylex\";\nimport { rs } from \"./rs\";\n\nimport { Button } from \"./button\";\nimport { popoverTargetAttrs } from \"./compat\";\n\nexport interface PopoverProps extends React.HTMLAttributes<HTMLDivElement> {\n  /** Trigger content, rendered in a ghost button. */\n  trigger: React.ReactNode;\n  /** Where the panel sits relative to the trigger. */\n  align?: \"start\" | \"end\";\n}\n\nconst GAP = 6;\nconst EDGE = 8;\n\nconst styles = stylex.create({\n  panel: {\n    boxSizing: \"border-box\",\n    position: \"fixed\",\n    inset: \"auto\",\n    margin: 0,\n    borderWidth: vlak.hairline,\n    borderStyle: \"solid\",\n    borderColor: vlak.divider,\n    borderRadius: {\n      default: vlak.radius,\n      [mq.phone]: vlak.radius,\n    },\n    backgroundColor: vlak.paper,\n    color: vlak.ink,\n    boxShadow: {\n      default: \"0 8px 24px rgba(0,0,0,0.06)\",\n      [mq.phone]: \"none\",\n    },\n    padding: {\n      default: \"0.875rem\",\n      [mq.phone]: \"1rem\",\n    },\n    width: {\n      default: \"15rem\",\n      [mq.phone]: \"min(320px, calc(100vw - 32px))\",\n    },\n    fontSize: {\n      default: \"0.8125rem\",\n      [mq.phone]: \"1rem\",\n    },\n    lineHeight: 1.45,\n    letterSpacing: \"-0.01em\",\n  },\n  title: {\n    display: \"block\",\n    fontSize: {\n      default: \"0.8125rem\",\n      [mq.phone]: \"1rem\",\n    },\n    fontWeight: 600,\n    letterSpacing: \"-0.01em\",\n    color: vlak.ink,\n    marginBottom: \"0.25rem\",\n  },\n  body: {\n    color: vlak.gray,\n    margin: 0,\n  },\n});\n\n/** CSS Anchor Positioning: the platform places, flips, and follows scroll. */\nfunction supportsAnchor(): boolean {\n  return typeof CSS !== \"undefined\" && typeof CSS.supports === \"function\" && CSS.supports(\"anchor-name\", \"none\");\n}\n\n/**\n * The native Popover API. The panel lives in the top layer; light\n * dismiss comes from the platform. Anchor positioning places it where\n * supported; otherwise it is measured, follows scroll and resize, and\n * flips above the trigger when there is no room below.\n */\nexport function Popover({ trigger, align = \"start\", className, style, children, onToggle, ...props }: PopoverProps) {\n  const id = React.useId();\n  const anchorName = `--rs-anchor-${id.replace(/[^\\w-]/g, \"\")}`;\n  const triggerRef = React.useRef<HTMLButtonElement>(null);\n  const panelRef = React.useRef<HTMLDivElement>(null);\n  const [open, setOpen] = React.useState(false);\n  const [anchored, setAnchored] = React.useState(false);\n  const [coords, setCoords] = React.useState<{ top: number; left: number } | null>(null);\n\n  React.useEffect(() => {\n    setAnchored(supportsAnchor());\n  }, []);\n\n  const position = React.useCallback(() => {\n    const anchor = triggerRef.current;\n    const panel = panelRef.current;\n    if (!anchor || !panel) return;\n    const rect = anchor.getBoundingClientRect();\n    const height = panel.offsetHeight;\n    const width = panel.offsetWidth;\n    const below = rect.bottom + GAP;\n    const above = rect.top - GAP - height;\n    const fitsBelow = below + height <= window.innerHeight - EDGE;\n    setCoords({\n      top: fitsBelow || above < EDGE ? below : above,\n      left: align === \"end\" ? Math.max(EDGE, rect.right - width) : Math.min(rect.left, window.innerWidth - width - EDGE),\n    });\n  }, [align]);\n\n  React.useEffect(() => {\n    if (!open || anchored) return;\n    position();\n    window.addEventListener(\"scroll\", position, true);\n    window.addEventListener(\"resize\", position);\n    return () => {\n      window.removeEventListener(\"scroll\", position, true);\n      window.removeEventListener(\"resize\", position);\n    };\n  }, [open, anchored, position]);\n\n  const placement: React.CSSProperties = anchored\n    ? ({\n        positionAnchor: anchorName,\n        positionArea: align === \"end\" ? \"bottom span-left\" : \"bottom span-right\",\n        positionTryFallbacks: \"flip-block\",\n        margin: `${GAP}px 0`,\n      } as React.CSSProperties)\n    : coords\n      ? { top: coords.top, left: coords.left }\n      : {};\n\n  const sx = rs([\"rs-popover\", className], styles.panel);\n  return (\n    <>\n      <Button\n        ref={triggerRef}\n        type=\"button\"\n        variant=\"ghost\"\n        size=\"sm\"\n        {...popoverTargetAttrs(id)}\n        style={anchored ? ({ anchorName } as React.CSSProperties) : undefined}\n      >\n        {trigger}\n      </Button>\n      <div\n        ref={panelRef}\n        id={id}\n        popover=\"auto\"\n        {...props}\n        className={sx.className}\n        style={{ ...sx.style, ...style, ...placement }}\n        onToggle={(e) => {\n          onToggle?.(e);\n          setOpen((e.nativeEvent as ToggleEvent).newState === \"open\");\n        }}\n      >\n        {children}\n      </div>\n    </>\n  );\n}\n\nexport const PopoverTitle = React.forwardRef<HTMLSpanElement, React.HTMLAttributes<HTMLSpanElement>>(function PopoverTitle(\n  { className, style, ...props },\n  ref,\n) {\n  const sx = rs([\"rs-popover-title\", className], styles.title);\n  return <span ref={ref} {...props} className={sx.className} style={{ ...sx.style, ...style }} />;\n});\n\nexport const PopoverBody = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(function PopoverBody(\n  { className, style, ...props },\n  ref,\n) {\n  const sx = rs([\"rs-popover-body\", className], styles.body);\n  return <p ref={ref} {...props} className={sx.className} style={{ ...sx.style, ...style }} />;\n});\n",
      "type": "registry:component",
      "target": "components/vlak/popover.tsx"
    },
    {
      "path": "vlak/compat.ts",
      "content": "import * as React from \"react\";\n\n/* React 18 and 19 spell newer DOM attributes differently: 19 knows\n   popoverTarget and boolean inert, 18 only passes lowercase unknown\n   attributes through as strings. These helpers emit the right shape. */\nconst major = Number.parseInt(React.version.split(\".\")[0] ?? \"19\", 10);\n\n/** Attributes that point a button at a popover element. */\nexport function popoverTargetAttrs(id: string): Record<string, string> {\n  return major >= 19 ? { popoverTarget: id } : { popovertarget: id };\n}\n\n/** The inert attribute, on when `on` is true. */\nexport function inertAttrs(on: boolean): Record<string, boolean | string | undefined> {\n  if (major >= 19) return { inert: on };\n  return { inert: on ? \"\" : undefined };\n}\n",
      "type": "registry:file",
      "target": "components/vlak/compat.ts"
    },
    {
      "path": "vlak/styles/popover.css",
      "content": "/* ── popover: generated from packages/react/src/components/popover.tsx ── */\n.rs-popover{box-sizing:border-box;position:fixed;inset:auto;margin:0;border-width:1px;border-style:solid;border-color:var(--divider);border-radius:var(--radius);background-color:var(--bg);color:var(--text);box-shadow:0 8px 24px rgba(0,0,0,0.06);padding:0.875rem;width:15rem;font-size:0.8125rem;line-height:1.45;letter-spacing:-0.01em}\n@media (max-width: 640px){.rs-popover{border-radius:var(--radius);box-shadow:none;padding:1rem;width:min(320px, calc(100vw - 32px));font-size:1rem}}\n.rs-popover-title{display:block;font-size:0.8125rem;font-weight:600;letter-spacing:-0.01em;color:var(--text);margin-bottom:0.25rem}\n@media (max-width: 640px){.rs-popover-title{font-size:1rem}}\n.rs-popover-body{color:var(--text-secondary);margin:0}\n",
      "type": "registry:file",
      "target": "styles/vlak/popover.css"
    }
  ],
  "meta": {
    "vlak": {
      "category": "surfaces",
      "classes": [
        "rs-popover",
        "rs-popover-title",
        "rs-popover-body"
      ],
      "snippet": "<button class=\"rs-btn-ghost rs-btn-sm\" popovertarget=\"info\">Details</button>\n<div id=\"info\" popover=\"auto\" class=\"rs-popover\"><span class=\"rs-popover-title\">Module grid</span><p class=\"rs-popover-body\">204px modules: a 184px column and a 20px gutter.</p></div>",
      "cssOnly": false,
      "registryDependencies": [
        "button",
        "vlak-lib"
      ],
      "aliases": [
        "Popover",
        "Popup",
        "Flyout"
      ],
      "example": "import { Popover, PopoverBody, PopoverTitle } from \"@noorddev/vlak-react\";\n\n<Popover trigger=\"Details\" align=\"start\" aria-label=\"Module grid\">\n  <PopoverTitle>Module grid</PopoverTitle>\n  <PopoverBody>204px modules: a 184px column and a 20px gutter.</PopoverBody>\n</Popover>",
      "usage": {
        "use": [
          "Secondary detail the user asks for and dismisses: a definition, a small form, a legend.",
          "align=\"end\" when the trigger sits at the right edge."
        ],
        "avoid": [
          "Menus; use DropdownMenu.",
          "Anything that must block the page; use Dialog."
        ]
      },
      "keyboard": [
        {
          "keys": "Enter, Space",
          "does": "Opens or closes from the trigger"
        },
        {
          "keys": "Escape",
          "does": "Closes (platform light dismiss)"
        },
        {
          "keys": "Tab",
          "does": "Moves into the panel"
        }
      ],
      "a11y": [
        "The native Popover API: the panel is popover=\"auto\" in the top layer; the trigger is a ghost Button with popovertarget, so the platform manages aria-expanded and light dismiss.",
        "Name the panel with aria-label or aria-labelledby; PopoverTitle is visible text only.",
        "Placed by CSS anchor positioning where supported, else measured and flipped above when there is no room below."
      ]
    }
  }
}
