# Popover

> Anchored panel that flips to stay on screen, with arrow and focus handling.

Category: Overlay. Docs: https://neonkit.heyaadi.com/components/popover

## Installation

```bash
npx shadcn@latest add https://neonkit.heyaadi.com/r/popover.json
```

With the `@neonkit` namespace configured: `npx shadcn@latest add @neonkit/popover`.

npm dependencies: `@floating-ui/react-dom`, `motion`. Also needs `lib/utils.ts` (`cn`) and `lib/neon.ts` (`neonVars`), which the CLI adds.

## Usage

```tsx
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"

<Popover>
  <PopoverTrigger asChild>
    <Button>Share</Button>
  </PopoverTrigger>
  <PopoverContent>…</PopoverContent>
</Popover>
```

## Examples

### Share

Focus lands on the link field, which selects itself, so ⌘C copies straight away.

```tsx
"use client";

import * as React from "react";
import { Check, Copy, Link2, Share2 } from "@/components/ui/icons";
import { Avatar } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Switch } from "@/components/ui/switch";

const LINK = "https://acme.dev/d/q4-launch-plan";

const people = [
  { name: "Ana Lima", email: "ana@acme.dev", access: "Owner" },
  { name: "Kenji Watanabe", email: "kenji@acme.dev", access: "Can edit" },
  { name: "Priya Raman", email: "priya@acme.dev", access: "Can comment" },
];

export default function PopoverDemo() {
  const [copied, setCopied] = React.useState(false);
  const [linkAccess, setLinkAccess] = React.useState(true);
  const timer = React.useRef<number | undefined>(undefined);

  React.useEffect(() => () => window.clearTimeout(timer.current), []);

  async function copy() {
    try {
      await navigator.clipboard.writeText(LINK);
    } catch {
      return;
    }
    setCopied(true);
    window.clearTimeout(timer.current);
    timer.current = window.setTimeout(() => setCopied(false), 1600);
  }

  return (
    <Popover>
      <PopoverTrigger asChild>
        <Button>
          <Share2 /> Share
        </Button>
      </PopoverTrigger>
      <PopoverContent arrow className="w-[340px] p-0">
        <div className="px-4 pb-3 pt-4">
          <p className="text-sm font-medium text-white/90">Share “Q4 launch plan”</p>
          <p className="mt-0.5 text-xs leading-5 text-white/50">
            {linkAccess ? "Anyone at Acme with the link can view." : "Only people below can open it."}
          </p>
        </div>

        <div className="px-4">
          <Input
            readOnly
            aria-label="Share link"
            value={LINK}
            onFocus={(event) => event.currentTarget.select()}
            leading={<Link2 />}
            disabled={!linkAccess}
            className="font-mono text-xs"
            trailing={
              <Button
                size="sm"
                variant="soft"
                onClick={copy}
                disabled={!linkAccess}
                className="-mr-[9px] w-[84px]"
              >
                {copied ? <Check /> : <Copy />}
                {copied ? "Copied" : "Copy"}
              </Button>
            }
          />
        </div>

        <ul className="mt-3 grid gap-0.5 px-2">
          {people.map((person) => (
            <li key={person.email} className="flex items-center gap-3 rounded-lg px-2 py-1.5">
              <Avatar size="sm" alt={person.name} />
              <div className="min-w-0 flex-1">
                <p className="truncate text-[13px] text-white/85">{person.name}</p>
                <p className="truncate text-xs text-white/40">{person.email}</p>
              </div>
              <span className="shrink-0 text-xs text-white/50">{person.access}</span>
            </li>
          ))}
        </ul>

        <div className="mt-2 flex items-center justify-between gap-4 border-t border-white/[0.08] px-4 py-3">
          <span id="link-access" className="text-[13px] text-white/70">
            Link sharing
          </span>
          <Switch
            size="sm"
            aria-labelledby="link-access"
            checked={linkAccess}
            onCheckedChange={setLinkAccess}
          />
        </div>
      </PopoverContent>
    </Popover>
  );
}
```

### Filters

Control open to apply a draft on submit and throw it away on Escape or Cancel.

```tsx
"use client";

import * as React from "react";
import { ListFilter } from "@/components/ui/icons";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Popover, PopoverClose, PopoverContent, PopoverTrigger } from "@/components/ui/popover";

const statuses = [
  { value: "ready", label: "Ready", count: 128 },
  { value: "building", label: "Building", count: 3 },
  { value: "error", label: "Error", count: 7 },
  { value: "canceled", label: "Canceled", count: 12 },
];

const environments = [
  { value: "production", label: "Production" },
  { value: "preview", label: "Preview" },
];

// Floated so the legend sits inside the fieldset's padding like any other block.
const legend = "float-left mb-2.5 w-full text-xs font-medium text-white/50";

const DEFAULTS = { status: ["ready", "building", "error"], env: ["production", "preview"] };

export default function PopoverFilters() {
  const [open, setOpen] = React.useState(false);
  const [applied, setApplied] = React.useState(DEFAULTS);
  const [draft, setDraft] = React.useState(DEFAULTS);
  const hidden = statuses.length - applied.status.length + environments.length - applied.env.length;

  function toggle(group: "status" | "env", value: string, checked: boolean) {
    setDraft((current) => ({
      ...current,
      [group]: checked ? [...current[group], value] : current[group].filter((item) => item !== value),
    }));
  }

  return (
    <Popover
      open={open}
      onOpenChange={(next) => {
        // Reopening starts from what's applied, not an abandoned draft.
        if (next) setDraft(applied);
        setOpen(next);
      }}
    >
      <PopoverTrigger asChild>
        <Button variant="ghost" className="border border-white/10">
          <ListFilter /> Filters
          {hidden > 0 && (
            <span className="ml-0.5 grid h-5 min-w-5 place-items-center rounded-full bg-[rgb(var(--neon)/0.15)] px-1.5 text-[11px] tabular-nums text-[rgb(var(--neon))]">
              <span aria-hidden>{hidden}</span>
              <span className="sr-only">, {hidden} active</span>
            </span>
          )}
        </Button>
      </PopoverTrigger>
      <PopoverContent align="start" className="w-64 p-0">
        <form
          onSubmit={(event) => {
            event.preventDefault();
            setApplied(draft);
            setOpen(false);
          }}
        >
          <fieldset className="px-4 pt-4">
            <legend className={legend}>Status</legend>
            <div className="clear-left grid gap-2.5">
              {statuses.map((status) => (
                <div key={status.value} className="flex items-center justify-between gap-3">
                  <Checkbox
                    size="sm"
                    label={status.label}
                    checked={draft.status.includes(status.value)}
                    onCheckedChange={(checked) => toggle("status", status.value, checked)}
                  />
                  <span className="font-mono text-[11px] tabular-nums text-white/35">{status.count}</span>
                </div>
              ))}
            </div>
          </fieldset>

          <fieldset className="mt-4 border-t border-white/[0.08] px-4 pt-4">
            <legend className={legend}>Environment</legend>
            <div className="clear-left grid gap-2.5">
              {environments.map((env) => (
                <Checkbox
                  key={env.value}
                  size="sm"
                  label={env.label}
                  checked={draft.env.includes(env.value)}
                  onCheckedChange={(checked) => toggle("env", env.value, checked)}
                />
              ))}
            </div>
          </fieldset>

          <div className="mt-4 flex items-center justify-between gap-2 border-t border-white/[0.08] px-3 py-3">
            <Button type="button" size="sm" variant="ghost" onClick={() => setDraft(DEFAULTS)}>
              Reset
            </Button>
            <div className="flex gap-2">
              <PopoverClose asChild>
                <Button size="sm" variant="ghost">
                  Cancel
                </Button>
              </PopoverClose>
              <Button type="submit" size="sm" variant="solid">
                Apply
              </Button>
            </div>
          </div>
        </form>
      </PopoverContent>
    </Popover>
  );
}
```

### Inline confirm

Without a color, the panel takes the trigger's. Here it's red, and so is the Delete button inside.

```tsx
"use client";

import * as React from "react";
import { Trash2 } from "@/components/ui/icons";
import { Button } from "@/components/ui/button";
import { Popover, PopoverClose, PopoverContent, PopoverTrigger } from "@/components/ui/popover";

export default function PopoverConfirm() {
  const [open, setOpen] = React.useState(false);
  const [deleted, setDeleted] = React.useState(false);

  if (deleted) {
    return (
      <p className="text-sm text-white/55">
        3 previews deleted.{" "}
        <button
          type="button"
          onClick={() => setDeleted(false)}
          className="cursor-pointer text-white/80 underline decoration-white/30 underline-offset-4 hover:decoration-white/70"
        >
          Undo
        </button>
      </p>
    );
  }

  return (
    <Popover open={open} onOpenChange={setOpen}>
      <PopoverTrigger asChild>
        <Button color="red" variant="soft">
          <Trash2 /> Delete previews
        </Button>
      </PopoverTrigger>
      {/* No color here: the panel picks up red from the trigger. */}
      <PopoverContent side="top" arrow className="w-64">
        <p className="font-medium text-white/90">Delete 3 preview deployments?</p>
        <p className="mt-1 text-xs leading-5 text-white/50">
          Their URLs stop working right away. Production isn&apos;t affected.
        </p>
        <div className="mt-4 flex justify-end gap-2">
          <PopoverClose asChild>
            <Button size="sm" variant="ghost" color="white">
              Keep
            </Button>
          </PopoverClose>
          <Button
            size="sm"
            variant="solid"
            onClick={() => {
              setOpen(false);
              setDeleted(true);
            }}
          >
            Delete
          </Button>
        </div>
      </PopoverContent>
    </Popover>
  );
}
```

### Placement

side and align set the preferred position. It flips and shifts to stay on screen.

```tsx
"use client";

import { Button } from "@/components/ui/button";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";

const placements = [
  { side: "top", label: "Top", hint: "Flips below when it runs out of room above." },
  { side: "right", label: "Right", hint: "Slides along the edge to stay on screen." },
  { side: "bottom", label: "Bottom", hint: "The default. Light leaks in from the trigger." },
  { side: "left", label: "Left", hint: "The arrow tracks the trigger as it shifts." },
] as const;

export default function PopoverPlacement() {
  return (
    <div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
      {placements.map((placement) => (
        <Popover key={placement.side}>
          <PopoverTrigger asChild>
            <Button size="sm" variant="ghost" className="border border-white/10">
              {placement.label}
            </Button>
          </PopoverTrigger>
          <PopoverContent side={placement.side} arrow initialFocus="content" className="w-52 px-3 py-2.5">
            <p className="text-xs leading-5 text-white/70">{placement.hint}</p>
          </PopoverContent>
        </Popover>
      ))}
    </div>
  );
}
```

## API reference

### Popover

Holds the open state. Renders no element of its own.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `open` | `boolean` | — | Controlled open state. |
| `defaultOpen` | `boolean` | `false` | Initial state when uncontrolled. |
| `onOpenChange` | `(open: boolean) => void` | — | Called when the trigger, a close button, Escape, Tab or a press outside asks to change state. |

### PopoverTrigger

Renders a button that toggles the popover. Also accepts every native button attribute.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` | `false` | Merges trigger behaviour, ARIA and ref onto its single child element. The child's own handlers run first. |

### PopoverContent

The panel, rendered in a portal with role="dialog". Other props go to the panel. It's w-72 p-4 by default; override with className.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `side` | `"top" \| "right" \| "bottom" \| "left"` | `"bottom"` | Preferred side. Flips when there isn't room. |
| `align` | `"start" \| "center" \| "end"` | `"center"` | Alignment along the trigger. |
| `sideOffset` | `number` | `8` | Gap from the trigger in pixels. |
| `arrow` | `boolean` | `false` | Shows an arrow that tracks the trigger, lit where the light leaks in. |
| `initialFocus` | `"first" \| "content"` | `"first"` | On open, focus the first focusable element (or one marked data-autofocus), or the panel itself. |
| `color` | `NeonColor` | `trigger's` | Palette name or hex. Falls back to the trigger's --neon, then --neon-accent, then cyan. |

### PopoverClose

Renders a button that closes the popover. Also accepts every native button attribute.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` | `false` | Merges close behaviour onto its single child element. |

### PopoverAnchor

Optional. Positions the panel against this element instead of the trigger, e.g. a whole field.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` | `false` | Merges onto its single child element instead of rendering a div. |

### CSS variables

Set on the positioned wrapper, so the panel and its children can size against the space available.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `--popover-trigger-width` | `px` | — | Width of the trigger (or anchor), e.g. w-[var(--popover-trigger-width)]. |
| `--popover-available-height` | `px` | — | Room left on the chosen side. Cap tall panels with max-h-[var(--popover-available-height)]. |
| `--popover-available-width` | `px` | — | Room left across. The panel's max width already uses it. |

## Accessibility

- The panel has role="dialog" and is labelled by its trigger, which gets aria-haspopup, aria-expanded and aria-controls.
- Opening moves focus into the panel. Escape, a press outside or a PopoverClose closes it and puts focus back on the trigger.
- Tab past the last element closes the panel and continues to whatever follows the trigger; Shift+Tab from the first returns to the trigger.
- It's non-modal: the rest of the page stays interactive, and moving focus elsewhere closes it.
- Inside a Dialog or Sheet the panel renders into that <dialog>, so it stays on the top layer and clickable.
- Without a visible trigger label, pass aria-label to PopoverContent.

## Source

`components/ui/popover.tsx`

```tsx
"use client";

import * as React from "react";
import { createPortal } from "react-dom";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import {
  arrow as arrowMiddleware,
  autoUpdate,
  flip,
  offset,
  shift,
  size,
  useFloating,
  type Middleware,
} from "@floating-ui/react-dom";

import { cn } from "@/lib/utils";
import { neonVars, type NeonColor } from "@/lib/neon";

/** The single element in `node`. Server Components can pass it as a lazy reference, which Children resolves. */
function resolveElement<P = Record<string, unknown>>(node: React.ReactNode): React.ReactElement<P> | null {
  const [child] = React.Children.toArray(node);
  return React.isValidElement<P>(child) ? child : null;
}

type RenderProp = { asChild?: boolean; render?: React.ReactElement; nativeButton?: boolean; children?: React.ReactNode };

/**
 * Base UI spells asChild as `render={<Link />}` plus `nativeButton`, and the shadcn CLI rewrites
 * asChild into that form in base-* projects. Folds it back into asChild and children.
 */
function fromRenderProp<P extends RenderProp>({ render, nativeButton, ...props }: P): Omit<P, "render" | "nativeButton"> {
  void nativeButton; // Only meaningful to Base UI.
  if (!render) return props;
  const children = props.children === undefined ? render : React.cloneElement(render, undefined, props.children);
  return { ...props, asChild: true, children };
}

type Side = "top" | "right" | "bottom" | "left";
type Align = "start" | "center" | "end";

const EASE = [0.23, 1, 0.32, 1] as const;

const TABBABLE = [
  "a[href]",
  "button:not([disabled])",
  "input:not([disabled]):not([type='hidden'])",
  "select:not([disabled])",
  "textarea:not([disabled])",
  "summary",
  "iframe",
  "[contenteditable='true']",
  "[tabindex]",
].join(",");

function getTabbables(root: ParentNode) {
  return Array.from(root.querySelectorAll<HTMLElement>(TABBABLE)).filter(
    (element) => element.tabIndex >= 0 && !element.closest("[hidden], [inert]"),
  );
}

type PossibleRef<T> = React.Ref<T> | undefined;

function setRef<T>(ref: PossibleRef<T>, value: T | null) {
  if (typeof ref === "function") ref(value);
  else if (ref) (ref as React.MutableRefObject<T | null>).current = value;
}

function useComposedRefs<T>(a: PossibleRef<T>, b: PossibleRef<T>, c?: PossibleRef<T>) {
  return React.useCallback(
    (node: T | null) => {
      setRef(a, node);
      setRef(b, node);
      setRef(c, node);
    },
    [a, b, c],
  );
}

// React 19 reads `ref` from props; React 18 keeps it on the element.
function getElementRef(element: React.ReactElement) {
  if (Number.parseInt(React.version, 10) >= 19) {
    return (element.props as { ref?: React.Ref<HTMLElement> }).ref;
  }
  return (element as unknown as { ref?: React.Ref<HTMLElement> }).ref;
}

type AnyProps = Record<string, unknown>;

function mergeSlotProps(slotProps: AnyProps, childProps: AnyProps) {
  const merged: AnyProps = { ...slotProps };
  for (const key of Object.keys(childProps)) {
    const ours = slotProps[key];
    const theirs = childProps[key];
    if (/^on[A-Z]/.test(key) && typeof ours === "function" && typeof theirs === "function") {
      merged[key] = (event: { defaultPrevented?: boolean }) => {
        (theirs as (event: unknown) => void)(event);
        if (!event?.defaultPrevented) (ours as (event: unknown) => void)(event);
      };
    } else if (key === "className") {
      merged.className = cn(theirs as string, ours as string);
    } else if (key === "style") {
      merged.style = { ...(theirs as object), ...(ours as object) };
    } else if (ours === undefined) {
      merged[key] = theirs;
    }
  }
  return merged;
}

/** Merges props and refs onto its single child element. The child's handlers run first. */
const Slot = React.forwardRef<HTMLElement, AnyProps & { children?: React.ReactNode }>(
  ({ children, ...props }, forwardedRef) => {
    const child = resolveElement<AnyProps>(children as React.ReactNode);
    const ref = useComposedRefs(forwardedRef, child ? getElementRef(child) : undefined);
    if (!child) return null;
    return React.cloneElement(child, { ...mergeSlotProps(props, child.props), ref } as AnyProps);
  },
);
Slot.displayName = "PopoverSlot";

const subscribe = () => () => {};
const useIsClient = () =>
  React.useSyncExternalStore(
    subscribe,
    () => true,
    () => false,
  );

interface PopoverContextValue {
  open: boolean;
  setOpen: (open: boolean) => void;
  trigger: HTMLElement | null;
  setTrigger: (node: HTMLElement | null) => void;
  anchor: HTMLElement | null;
  setAnchor: (node: HTMLElement | null) => void;
  triggerId: string;
  contentId: string;
}

const PopoverContext = React.createContext<PopoverContextValue | null>(null);

function usePopover(component: string) {
  const context = React.useContext(PopoverContext);
  if (!context) throw new Error(`<${component}> must be rendered inside <Popover>.`);
  return context;
}

export interface PopoverProps {
  open?: boolean;
  defaultOpen?: boolean;
  onOpenChange?: (open: boolean) => void;
  children?: React.ReactNode;
}

function Popover({ open: openProp, defaultOpen = false, onOpenChange, children }: PopoverProps) {
  const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen);
  const [trigger, setTrigger] = React.useState<HTMLElement | null>(null);
  const [anchor, setAnchor] = React.useState<HTMLElement | null>(null);
  const id = React.useId();
  const isControlled = openProp !== undefined;
  const open = isControlled ? openProp : uncontrolledOpen;
  // A close can be requested twice in one event (Tab moves focus, which also reads as focus-outside).
  const pending = React.useRef<boolean | null>(null);

  const setOpen = React.useCallback(
    (next: boolean) => {
      if (next === open || pending.current === next) return;
      pending.current = next;
      queueMicrotask(() => {
        pending.current = null;
      });
      if (!isControlled) setUncontrolledOpen(next);
      onOpenChange?.(next);
    },
    [open, isControlled, onOpenChange],
  );

  const value = React.useMemo<PopoverContextValue>(
    () => ({
      open,
      setOpen,
      trigger,
      setTrigger,
      anchor,
      setAnchor,
      triggerId: `${id}-trigger`,
      contentId: `${id}-content`,
    }),
    [open, setOpen, trigger, anchor, id],
  );

  return <PopoverContext.Provider value={value}>{children}</PopoverContext.Provider>;
}
Popover.displayName = "Popover";

export interface PopoverTriggerProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  /** Merge trigger behaviour onto the single child element instead of rendering a button. */
  asChild?: boolean;
  /** Base UI-style asChild: `render={<Link href="/" />}`. */
  render?: React.ReactElement;
  /** Accepted for Base UI compatibility; no effect. */
  nativeButton?: boolean;
}

const PopoverTrigger = React.forwardRef<HTMLButtonElement, PopoverTriggerProps>(
  (raw, forwardedRef) => {
    const { asChild = false, id, onClick, onKeyDown, children, ...props } = fromRenderProp(raw);
    const { open, setOpen, setTrigger, triggerId, contentId } = usePopover("PopoverTrigger");
    const ref = useComposedRefs<HTMLElement>(forwardedRef as PossibleRef<HTMLElement>, setTrigger);
    const childId =
      asChild ? resolveElement<{ id?: string }>(children)?.props.id : undefined;

    const triggerProps = {
      ...props,
      // The content is labelled by the trigger, so it always needs an id.
      id: id ?? childId ?? triggerId,
      "aria-haspopup": "dialog" as const,
      "aria-expanded": open,
      "aria-controls": open ? contentId : undefined,
      "data-state": open ? "open" : "closed",
      onClick: (event: React.MouseEvent<HTMLButtonElement>) => {
        onClick?.(event);
        if (!event.defaultPrevented) setOpen(!open);
      },
      onKeyDown: (event: React.KeyboardEvent<HTMLButtonElement>) => {
        onKeyDown?.(event);
        if (event.defaultPrevented || !open || event.key !== "Escape") return;
        event.preventDefault();
        setOpen(false);
      },
    };

    if (asChild) {
      return (
        <Slot ref={ref} {...triggerProps}>
          {children}
        </Slot>
      );
    }

    return (
      <button ref={ref} type="button" {...triggerProps}>
        {children}
      </button>
    );
  },
);
PopoverTrigger.displayName = "PopoverTrigger";

export interface PopoverAnchorProps extends React.HTMLAttributes<HTMLDivElement> {
  /** Merge anchor behaviour onto the single child element instead of rendering a div. */
  asChild?: boolean;
  /** Base UI-style asChild: `render={<Link href="/" />}`. */
  render?: React.ReactElement;
  /** Accepted for Base UI compatibility; no effect. */
  nativeButton?: boolean;
}

/** Optional. Positions the content against this element instead of the trigger. */
const PopoverAnchor = React.forwardRef<HTMLDivElement, PopoverAnchorProps>(
  (raw, forwardedRef) => {
    const { asChild = false, children, ...props } = fromRenderProp(raw);
    const { setAnchor } = usePopover("PopoverAnchor");
    const ref = useComposedRefs<HTMLElement>(forwardedRef as PossibleRef<HTMLElement>, setAnchor);

    if (asChild) {
      return (
        <Slot ref={ref} {...props}>
          {children}
        </Slot>
      );
    }

    return (
      <div ref={ref} {...props}>
        {children}
      </div>
    );
  },
);
PopoverAnchor.displayName = "PopoverAnchor";

// Where the trigger's centre falls on the panel, so motion and light come from that point.
const anchorPoint: Middleware = {
  name: "anchorPoint",
  fn({ x, y, rects }) {
    const clamp = (value: number, max: number) => Math.min(Math.max(value, 16), Math.max(max - 16, 16));
    return {
      data: {
        x: clamp(rects.reference.x + rects.reference.width / 2 - x, rects.floating.width),
        y: clamp(rects.reference.y + rects.reference.height / 2 - y, rects.floating.height),
      },
    };
  },
};

// The panel starts nudged toward the trigger and settles outward.
const nudges: Record<Side, { x?: number; y?: number }> = {
  top: { y: 4 },
  bottom: { y: -4 },
  left: { x: 4 },
  right: { x: -4 },
};

// Light leaks in along the edge that faces the trigger, centred on it.
const leakLines: Record<Side, string> = {
  bottom:
    "left-[var(--popover-anchor-x,50%)] top-0 h-px w-40 -translate-x-1/2 bg-[linear-gradient(90deg,transparent,rgb(var(--neon)/0.7),transparent)]",
  top: "bottom-0 left-[var(--popover-anchor-x,50%)] h-px w-40 -translate-x-1/2 bg-[linear-gradient(90deg,transparent,rgb(var(--neon)/0.7),transparent)]",
  right:
    "left-0 top-[var(--popover-anchor-y,50%)] h-24 w-px -translate-y-1/2 bg-[linear-gradient(180deg,transparent,rgb(var(--neon)/0.7),transparent)]",
  left: "right-0 top-[var(--popover-anchor-y,50%)] h-24 w-px -translate-y-1/2 bg-[linear-gradient(180deg,transparent,rgb(var(--neon)/0.7),transparent)]",
};

const leakGlows: Record<Side, string> = {
  bottom: "left-[var(--popover-anchor-x,50%)] top-0 h-12 w-56 -translate-x-1/2 -translate-y-1/2",
  top: "bottom-0 left-[var(--popover-anchor-x,50%)] h-12 w-56 -translate-x-1/2 translate-y-1/2",
  right: "left-0 top-[var(--popover-anchor-y,50%)] h-32 w-12 -translate-x-1/2 -translate-y-1/2",
  left: "right-0 top-[var(--popover-anchor-y,50%)] h-32 w-12 -translate-y-1/2 translate-x-1/2",
};

const arrowBorders: Record<Side, string> = {
  top: "border-b border-r",
  bottom: "border-l border-t",
  left: "border-r border-t",
  right: "border-b border-l",
};

const staticSides: Record<Side, Side> = { top: "bottom", right: "left", bottom: "top", left: "right" };

function readNeon(element: HTMLElement | null) {
  if (!element) return undefined;
  const styles = window.getComputedStyle(element);
  const rgb =
    styles.getPropertyValue("--neon").trim() || styles.getPropertyValue("--neon-accent").trim();
  return rgb ? ({ "--neon": rgb, "--neon-accent": rgb } as React.CSSProperties) : undefined;
}

export interface PopoverContentProps
  extends Omit<
    React.HTMLAttributes<HTMLDivElement>,
    "color" | "onAnimationStart" | "onDrag" | "onDragStart" | "onDragEnd"
  > {
  /** Preferred side. Flips to the opposite one when there isn't room. */
  side?: Side;
  align?: Align;
  /** Gap between the trigger and the panel, in pixels. */
  sideOffset?: number;
  /** Shows a small arrow pointing at the trigger. */
  arrow?: boolean;
  /** Palette name or hex. Inherits the trigger's color when omitted. */
  color?: NeonColor;
  /** What gets focus on open: the first focusable element inside, or the panel itself. */
  initialFocus?: "content" | "first";
}

const PopoverPanel = React.forwardRef<HTMLDivElement, PopoverContentProps>(
  (
    {
      side = "bottom",
      align = "center",
      sideOffset = 8,
      arrow = false,
      color,
      initialFocus = "first",
      className,
      style,
      children,
      onKeyDown,
      onPointerDownCapture,
      onFocusCapture,
      ...props
    },
    forwardedRef,
  ) => {
    const { open, setOpen, trigger, anchor, contentId } = usePopover("PopoverContent");
    const reduceMotion = useReducedMotion();
    const [surface, setSurface] = React.useState<HTMLDivElement | null>(null);
    const [floating, setFloating] = React.useState<HTMLDivElement | null>(null);
    const [arrowElement, setArrowElement] = React.useState<HTMLSpanElement | null>(null);
    const ref = useComposedRefs(forwardedRef, setSurface);
    // A portal leaves the trigger's subtree, so carry its color across.
    const [inherited] = React.useState(() => readNeon(trigger));
    // Events from portaled children (a nested popover) bubble here through React, not the DOM.
    const insideTree = React.useRef({ pointer: false, focus: false });

    const { floatingStyles, placement, middlewareData, isPositioned } = useFloating({
      placement: align === "center" ? side : `${side}-${align}`,
      elements: { reference: anchor ?? trigger, floating },
      whileElementsMounted: autoUpdate,
      middleware: [
        offset(sideOffset + (arrow ? 6 : 0)),
        flip({ padding: 8 }),
        shift({ padding: 8 }),
        size({
          padding: 8,
          apply({ availableWidth, availableHeight, rects, elements: { floating } }) {
            floating.style.setProperty("--popover-available-width", `${Math.floor(availableWidth)}px`);
            floating.style.setProperty("--popover-available-height", `${Math.floor(availableHeight)}px`);
            floating.style.setProperty("--popover-trigger-width", `${rects.reference.width}px`);
          },
        }),
        arrow && arrowMiddleware({ element: arrowElement, padding: 12 }),
        anchorPoint,
      ],
    });

    const placedSide = placement.split("-")[0] as Side;
    const point = middlewareData.anchorPoint as { x: number; y: number } | undefined;
    const pointX = point ? `${point.x}px` : "50%";
    const pointY = point ? `${point.y}px` : "50%";
    const transformOrigin = {
      top: `${pointX} 100%`,
      bottom: `${pointX} 0%`,
      left: `100% ${pointY}`,
      right: `0% ${pointY}`,
    }[placedSide];

    // Declared before the focus effect below, so these listeners see the initial focus land inside.
    React.useEffect(() => {
      if (!open || !floating) return;
      insideTree.current = { pointer: false, focus: false };
      const isInside = (target: EventTarget | null) =>
        target instanceof Node && (floating.contains(target) || Boolean(trigger?.contains(target)));

      const onPointerDown = (event: PointerEvent) => {
        const fromTree = insideTree.current.pointer;
        insideTree.current.pointer = false;
        if (fromTree || isInside(event.target)) return;
        setOpen(false);
        // If the press didn't land on something focusable, focus would drop to the page.
        window.setTimeout(() => {
          const active = document.activeElement;
          if (!active || (trigger && active.contains(trigger))) trigger?.focus({ preventScroll: true });
        });
      };

      const onFocusIn = (event: FocusEvent) => {
        const fromTree = insideTree.current.focus;
        insideTree.current.focus = false;
        if (fromTree || isInside(event.target)) return;
        setOpen(false);
      };

      document.addEventListener("pointerdown", onPointerDown);
      document.addEventListener("focusin", onFocusIn);
      return () => {
        document.removeEventListener("pointerdown", onPointerDown);
        document.removeEventListener("focusin", onFocusIn);
      };
    }, [open, floating, trigger, setOpen]);

    // Keyed on open too: reopening mid-exit reuses this instance.
    React.useEffect(() => {
      if (!open || !surface) return;
      const target =
        initialFocus === "first"
          ? (surface.querySelector<HTMLElement>("[autofocus], [data-autofocus]") ??
            getTabbables(surface)[0] ??
            surface)
          : surface;
      target.focus({ preventScroll: true });
    }, [open, surface, initialFocus]);

    // However it closed (Escape, a close button, a parent's state), focus inside goes back to the trigger.
    React.useEffect(() => {
      if (open || !floating) return;
      const active = document.activeElement;
      if (active && floating.contains(active)) trigger?.focus({ preventScroll: true });
    }, [open, floating, trigger]);

    function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
      onKeyDown?.(event);
      if (event.defaultPrevented) return;

      if (event.key === "Escape") {
        // Stop here so a dialog or parent popover behind this one stays open.
        event.preventDefault();
        event.stopPropagation();
        setOpen(false);
        return;
      }

      if (event.key !== "Tab") return;
      const panel = event.currentTarget;
      const tabbables = getTabbables(panel);
      const active = document.activeElement;

      if (event.shiftKey) {
        if (!tabbables.length || active === panel || active === tabbables[0]) {
          event.preventDefault();
          setOpen(false);
          trigger?.focus({ preventScroll: true });
        }
        return;
      }

      if (tabbables.length && active !== tabbables[tabbables.length - 1]) return;
      // The panel lives in a portal, so continue the tab order from the trigger instead.
      event.preventDefault();
      const scope = trigger?.closest("dialog") ?? document.body;
      const next = trigger
        ? getTabbables(scope).find(
            (element) =>
              !floating?.contains(element) &&
              !trigger.contains(element) &&
              trigger.compareDocumentPosition(element) & Node.DOCUMENT_POSITION_FOLLOWING,
          )
        : undefined;
      (next ?? trigger)?.focus({ preventScroll: true });
      setOpen(false);
    }

    const hidden = reduceMotion
      ? { opacity: 0 }
      : { opacity: 0, scale: 0.96, x: 0, y: 0, ...nudges[placedSide] };
    const arrowData = middlewareData.arrow;

    return (
      <div
        ref={setFloating}
        style={floatingStyles}
        className="z-50 w-max max-w-[var(--popover-available-width,calc(100vw-16px))]"
      >
        <motion.div
          {...props}
          ref={ref}
          id={contentId}
          role="dialog"
          tabIndex={-1}
          aria-labelledby={
            props["aria-labelledby"] ?? (props["aria-label"] ? undefined : trigger?.id || undefined)
          }
          data-state={open ? "open" : "closed"}
          data-side={placedSide}
          data-align={align}
          initial={hidden}
          // Hold still until floating-ui has placed it, so it never flashes at the corner.
          animate={isPositioned ? { opacity: 1, scale: 1, x: 0, y: 0 } : hidden}
          exit={{ ...hidden, transition: { duration: reduceMotion ? 0.1 : 0.12, ease: EASE } }}
          transition={{ duration: reduceMotion ? 0.12 : 0.16, ease: EASE }}
          onKeyDown={handleKeyDown}
          onPointerDownCapture={(event) => {
            onPointerDownCapture?.(event);
            insideTree.current.pointer = true;
          }}
          onFocusCapture={(event) => {
            onFocusCapture?.(event);
            insideTree.current.focus = true;
          }}
          style={
            {
              ...inherited,
              ...neonVars(color, style),
              transformOrigin,
              "--popover-anchor-x": pointX,
              "--popover-anchor-y": pointY,
            } as React.CSSProperties
          }
          className={cn(
            "[--neon:var(--neon-accent,34_211_238)]",
            "relative w-72 max-w-full rounded-xl border border-white/10 bg-[#0a0b11] p-4 text-sm text-white/90 outline-none",
            "shadow-[0_18px_48px_-12px_rgb(0_0_0/0.85),0_0_0_1px_rgb(var(--neon)/0.06),0_0_28px_-10px_rgb(var(--neon)/CALC(0.45*var(--neon-glow,1)))]",
            !open && "pointer-events-none",
            className,
          )}
        >
          <span aria-hidden className="pointer-events-none absolute -inset-px overflow-hidden rounded-[inherit]">
            <motion.span
              className="absolute inset-0"
              initial={{ opacity: 0 }}
              animate={{ opacity: isPositioned ? 1 : 0 }}
              // The leak warms up a beat after the panel lands, like a tube striking.
              transition={reduceMotion ? { duration: 0 } : { duration: 0.36, delay: 0.06, ease: EASE }}
            >
              <span className={cn("absolute", leakLines[placedSide])} />
              <span
                className={cn(
                  "absolute bg-[radial-gradient(closest-side,rgb(var(--neon)/CALC(0.13*var(--neon-glow,1))),transparent)]",
                  leakGlows[placedSide],
                )}
              />
            </motion.span>
          </span>
          {children}
          {arrow && (
            <span
              ref={setArrowElement}
              aria-hidden
              style={{
                // Coordinates are for the border box; the arrow sits inside the 1px border.
                left: arrowData?.x != null ? arrowData.x - 1 : undefined,
                top: arrowData?.y != null ? arrowData.y - 1 : undefined,
                [staticSides[placedSide]]: -6,
              }}
              className={cn(
                "pointer-events-none absolute size-2.5 rotate-45 border-[rgb(var(--neon)/0.35)] bg-[#0a0b11]",
                arrowBorders[placedSide],
              )}
            />
          )}
        </motion.div>
      </div>
    );
  },
);
PopoverPanel.displayName = "PopoverPanel";

const PopoverContent = React.forwardRef<HTMLDivElement, PopoverContentProps>((props, ref) => {
  const { open, trigger } = usePopover("PopoverContent");
  const isClient = useIsClient();
  if (!isClient) return null;

  // A modal <dialog> makes everything outside it inert, so render inside it when the trigger is in one.
  const container = trigger?.closest("dialog") ?? document.body;

  return createPortal(
    <AnimatePresence>{open && <PopoverPanel key="popover" ref={ref} {...props} />}</AnimatePresence>,
    container,
  );
});
PopoverContent.displayName = "PopoverContent";

export interface PopoverCloseProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  /** Merge close behaviour onto the single child element instead of rendering a button. */
  asChild?: boolean;
  /** Base UI-style asChild: `render={<Link href="/" />}`. */
  render?: React.ReactElement;
  /** Accepted for Base UI compatibility; no effect. */
  nativeButton?: boolean;
}

const PopoverClose = React.forwardRef<HTMLButtonElement, PopoverCloseProps>(
  (raw, ref) => {
    const { asChild = false, onClick, children, ...props } = fromRenderProp(raw);
    const { setOpen } = usePopover("PopoverClose");

    const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
      onClick?.(event);
      if (!event.defaultPrevented) setOpen(false);
    };

    if (asChild) {
      return (
        <Slot ref={ref} {...props} onClick={handleClick}>
          {children}
        </Slot>
      );
    }

    return (
      <button ref={ref} type="button" {...props} onClick={handleClick}>
        {children}
      </button>
    );
  },
);
PopoverClose.displayName = "PopoverClose";

export { Popover, PopoverTrigger, PopoverAnchor, PopoverContent, PopoverClose };
```
