{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"hover-card","type":"registry:component","title":"Hover Card","description":"Hover card that springs out of its trigger after an open delay and stays open while the pointer travels into it; also opens on keyboard focus, closes on Escape or blur, and flips and aligns on any side to stay in the viewport, while touch taps simply follow the link.","author":"kara <ztzyrant@gmail.com>","dependencies":["clsx","motion","tailwind-merge"],"registryDependencies":[],"files":[{"path":"components/motion/hover-card.tsx","type":"registry:component","target":"@components/motion/hover-card.tsx","content":"\"use client\";\n// karaui.kamisama-v0.my.id/components/motion/hover-card\n\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport {\n  cloneElement,\n  createContext,\n  isValidElement,\n  type MutableRefObject,\n  type ReactElement,\n  type FocusEvent as ReactFocusEvent,\n  type ReactNode,\n  type PointerEvent as ReactPointerEvent,\n  type Ref,\n  useCallback,\n  useContext,\n  useEffect,\n  useId,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\nimport {\n  type PlacementAlign,\n  type PlacementSide,\n  resolvePlacement,\n} from \"@/components/motion/anchored-placement\";\nimport { usePopoverPortalPosition } from \"@/components/motion/popover-position\";\nimport { EASE_OUT, SPRING_PANEL } from \"@/lib/ease\";\nimport { useHoverGesture } from \"@/lib/hooks/use-hover-gesture\";\nimport { cn } from \"@/lib/utils\";\n\ninterface HoverCardContextValue {\n  open: boolean;\n  contentId: string;\n  triggerRef: MutableRefObject<HTMLElement | null>;\n  contentRef: MutableRefObject<HTMLDivElement | null>;\n  /** Open after `openDelay` (or at once when `immediate`). */\n  scheduleOpen: (immediate?: boolean) => void;\n  /** Close after `closeDelay` — cancelled if the pointer comes back. */\n  scheduleClose: () => void;\n  cancel: () => void;\n  close: () => void;\n  reduce: boolean;\n}\n\nconst HoverCardContext = createContext<HoverCardContextValue | null>(null);\n\nfunction useHoverCardContext(component: string) {\n  const ctx = useContext(HoverCardContext);\n  if (!ctx) throw new Error(`${component} must be used within <HoverCard>`);\n  return ctx;\n}\n\nexport interface HoverCardProps {\n  children: ReactNode;\n  /** Controlled open state. */\n  open?: boolean;\n  /** Uncontrolled initial open state. */\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  /** Hover time before the card opens, in ms. Default 500. */\n  openDelay?: number;\n  /** Time after the pointer leaves before the card closes, in ms. Default 300. */\n  closeDelay?: number;\n  className?: string;\n}\n\n/**\n * A rich preview shown while the pointer rests on a trigger (usually a link).\n *\n * - Opens after `openDelay`, closes `closeDelay` after the pointer leaves both\n *   the trigger and the card, so the pointer can travel into the card.\n * - Opens when the trigger receives keyboard focus; closes on Escape or blur.\n * - Touch has no hover: a tap is just a tap and the trigger behaves as the\n *   link it is. The card is supplementary — never put the only path to an\n *   action inside it.\n */\nexport function HoverCard({\n  children,\n  open: controlledOpen,\n  defaultOpen = false,\n  onOpenChange,\n  openDelay = 500,\n  closeDelay = 300,\n  className,\n}: HoverCardProps) {\n  const [internalOpen, setInternalOpen] = useState(defaultOpen);\n  const controlled = controlledOpen !== undefined;\n  const open = controlled ? controlledOpen : internalOpen;\n  const triggerRef = useRef<HTMLElement | null>(null);\n  const contentRef = useRef<HTMLDivElement | null>(null);\n  const timer = useRef<ReturnType<typeof setTimeout> | null>(null);\n  const contentId = useId();\n  const reduce = useReducedMotion() ?? false;\n\n  const setOpen = useCallback(\n    (next: boolean) => {\n      if (!controlled) setInternalOpen(next);\n      onOpenChange?.(next);\n    },\n    [controlled, onOpenChange],\n  );\n\n  const cancel = useCallback(() => {\n    if (timer.current) clearTimeout(timer.current);\n    timer.current = null;\n  }, []);\n\n  const scheduleOpen = useCallback(\n    (immediate = false) => {\n      cancel();\n      if (open) return;\n      if (immediate) {\n        setOpen(true);\n        return;\n      }\n      timer.current = setTimeout(() => {\n        timer.current = null;\n        setOpen(true);\n      }, openDelay);\n    },\n    [cancel, open, openDelay, setOpen],\n  );\n\n  const scheduleClose = useCallback(() => {\n    cancel();\n    if (!open) return;\n    timer.current = setTimeout(() => {\n      timer.current = null;\n      setOpen(false);\n    }, closeDelay);\n  }, [cancel, closeDelay, open, setOpen]);\n\n  const close = useCallback(() => {\n    cancel();\n    setOpen(false);\n  }, [cancel, setOpen]);\n\n  useEffect(() => cancel, [cancel]);\n\n  useEffect(() => {\n    if (!open) return;\n    const onKey = (event: KeyboardEvent) => {\n      if (event.key === \"Escape\") close();\n    };\n    window.addEventListener(\"keydown\", onKey);\n    return () => window.removeEventListener(\"keydown\", onKey);\n  }, [open, close]);\n\n  const ctx = useMemo<HoverCardContextValue>(\n    () => ({\n      open,\n      contentId,\n      triggerRef,\n      contentRef,\n      scheduleOpen,\n      scheduleClose,\n      cancel,\n      close,\n      reduce,\n    }),\n    [open, contentId, scheduleOpen, scheduleClose, cancel, close, reduce],\n  );\n\n  return (\n    <HoverCardContext.Provider value={ctx}>\n      <span className={cn(\"inline-flex\", className)}>{children}</span>\n    </HoverCardContext.Provider>\n  );\n}\n\ntype TriggerChildProps = React.HTMLAttributes<HTMLElement> & {\n  ref?: Ref<HTMLElement>;\n  [key: `data-${string}`]: string | undefined;\n};\n\nexport interface HoverCardTriggerProps {\n  /** A single focusable element, usually an `<a>`. Its own handlers still run. */\n  children: ReactElement<TriggerChildProps>;\n  className?: string;\n}\n\nfunction assignRef<T>(ref: Ref<T> | undefined, value: T | null) {\n  if (typeof ref === \"function\") ref(value);\n  else if (ref) (ref as MutableRefObject<T | null>).current = value;\n}\n\nexport function HoverCardTrigger({ children, className }: HoverCardTriggerProps) {\n  const ctx = useHoverCardContext(\"HoverCardTrigger\");\n  const hover = useHoverGesture();\n  if (!isValidElement(children)) {\n    throw new Error(\"<HoverCardTrigger> requires a single React element\");\n  }\n  const childProps = children.props;\n  const childRef = childProps.ref;\n\n  return cloneElement(children, {\n    ref: (node: HTMLElement | null) => {\n      ctx.triggerRef.current = node;\n      assignRef(childRef, node);\n    },\n    \"data-state\": ctx.open ? \"open\" : \"closed\",\n    className: cn(childProps.className, className),\n    // Pointer events, filtered through useHoverGesture: a finger never rests,\n    // so touch never schedules an open and the tap reaches the link intact.\n    onPointerEnter: (event: ReactPointerEvent<HTMLElement>) => {\n      childProps.onPointerEnter?.(event);\n      if (hover.enter(event)) ctx.scheduleOpen();\n    },\n    onPointerLeave: (event: ReactPointerEvent<HTMLElement>) => {\n      childProps.onPointerLeave?.(event);\n      if (hover.leave(event)) ctx.scheduleClose();\n    },\n    onFocus: (event: ReactFocusEvent<HTMLElement>) => {\n      childProps.onFocus?.(event);\n      // Only keyboard focus opens it; a click's focus is already a hover.\n      if (event.currentTarget.matches(\":focus-visible\")) ctx.scheduleOpen();\n    },\n    onBlur: (event: ReactFocusEvent<HTMLElement>) => {\n      childProps.onBlur?.(event);\n      const next = event.relatedTarget as Node | null;\n      if (next && ctx.contentRef.current?.contains(next)) return;\n      ctx.close();\n    },\n  });\n}\n\nexport interface HoverCardContentProps {\n  children: ReactNode;\n  /** Preferred side; flips when the viewport lacks room. Default \"bottom\". */\n  side?: PlacementSide;\n  /** Default \"center\". */\n  align?: PlacementAlign;\n  /** Gap between trigger and card, in px. Default 8. */\n  sideOffset?: number;\n  alignOffset?: number;\n  className?: string;\n}\n\n// Distance the card travels in from the trigger side on entrance.\nconst ENTER_SHIFT = 6;\n\nfunction shiftFor(side: PlacementSide) {\n  if (side === \"bottom\") return { x: 0, y: -ENTER_SHIFT };\n  if (side === \"top\") return { x: 0, y: ENTER_SHIFT };\n  if (side === \"right\") return { x: -ENTER_SHIFT, y: 0 };\n  return { x: ENTER_SHIFT, y: 0 };\n}\n\nexport function HoverCardContent({\n  children,\n  side = \"bottom\",\n  align = \"center\",\n  sideOffset = 8,\n  alignOffset = 0,\n  className,\n}: HoverCardContentProps) {\n  const ctx = useHoverCardContext(\"HoverCardContent\");\n  const hover = useHoverGesture();\n  const [portalReady, setPortalReady] = useState(false);\n  const layout = usePopoverPortalPosition(\n    ctx.triggerRef,\n    ctx.contentRef,\n    portalReady && ctx.open,\n  );\n\n  useEffect(() => setPortalReady(true), []);\n\n  if (!portalReady) return null;\n\n  const placement = layout\n    ? resolvePlacement(layout, { side, align, sideOffset, alignOffset })\n    : null;\n  // The requested side seeds the entrance offset: the first frame mounts\n  // before measurement, and a flip is the rare case.\n  const shift = shiftFor(side);\n\n  return createPortal(\n    <AnimatePresence>\n      {ctx.open ? (\n        <motion.div\n          ref={ctx.contentRef}\n          id={ctx.contentId}\n          data-side={placement?.side ?? side}\n          initial={\n            ctx.reduce\n              ? { opacity: 0 }\n              : { opacity: 0, scale: 0.92, x: shift.x, y: shift.y }\n          }\n          animate={{ opacity: 1, scale: 1, x: 0, y: 0 }}\n          exit={\n            ctx.reduce\n              ? { opacity: 0, transition: { duration: 0.1 } }\n              : {\n                  opacity: 0,\n                  scale: 0.96,\n                  transition: { duration: 0.14, ease: EASE_OUT },\n                }\n          }\n          transition={\n            ctx.reduce\n              ? { duration: 0.14, ease: EASE_OUT }\n              : { ...SPRING_PANEL, opacity: { duration: 0.18, ease: EASE_OUT } }\n          }\n          style={{\n            left: placement?.left ?? 0,\n            top: placement?.top ?? 0,\n            transformOrigin: placement?.origin ?? \"50% 0%\",\n            visibility: placement ? \"visible\" : \"hidden\",\n          }}\n          onPointerEnter={(event) => {\n            if (hover.enter(event)) ctx.cancel();\n          }}\n          onPointerLeave={(event) => {\n            if (hover.leave(event)) ctx.scheduleClose();\n          }}\n          onBlur={(event) => {\n            const next = event.relatedTarget as Node | null;\n            if (\n              next &&\n              (event.currentTarget.contains(next) ||\n                ctx.triggerRef.current?.contains(next))\n            )\n              return;\n            ctx.close();\n          }}\n          className={cn(\n            \"fixed z-[100] w-72 rounded-2xl border border-border bg-card p-4 text-card-foreground shadow-[0_20px_44px_-14px_rgba(0,0,0,0.3)]\",\n            className,\n          )}\n        >\n          {children}\n        </motion.div>\n      ) : null}\n    </AnimatePresence>,\n    document.body,\n  );\n}\n"},{"path":"components/motion/anchored-placement.ts","type":"registry:component","target":"@components/motion/anchored-placement.ts","content":"import type { PortalLayout } from \"@/components/motion/popover-position\";\n\nexport type PlacementSide = \"top\" | \"right\" | \"bottom\" | \"left\";\nexport type PlacementAlign = \"start\" | \"center\" | \"end\";\n\nexport interface PlacementOptions {\n  side: PlacementSide;\n  align: PlacementAlign;\n  /** Gap between trigger and panel along the side axis, in px. */\n  sideOffset?: number;\n  /** Shift along the align axis, in px. */\n  alignOffset?: number;\n  /** Minimum distance kept from the viewport edges, in px. */\n  padding?: number;\n}\n\nexport interface Placement {\n  left: number;\n  top: number;\n  /** The side actually used — differs from the requested one after a flip. */\n  side: PlacementSide;\n  /** Trigger-facing point of the panel, as a CSS transform-origin (px). */\n  origin: string;\n}\n\nconst OPPOSITE: Record<PlacementSide, PlacementSide> = {\n  top: \"bottom\",\n  bottom: \"top\",\n  left: \"right\",\n  right: \"left\",\n};\n\nfunction clamp(value: number, min: number, max: number) {\n  return Math.min(Math.max(value, min), Math.max(min, max));\n}\n\n/**\n * Places a portalled panel against its trigger in viewport coordinates\n * (pair with `usePopoverPortalPosition`). Flips to the opposite side when the\n * requested one lacks room and the other has more, then clamps the panel into\n * the viewport on both axes. The returned origin is the trigger's center\n * projected onto the panel's facing edge, so a scale entrance grows out of the\n * trigger even after a flip or a clamp.\n */\nexport function resolvePlacement(\n  layout: PortalLayout,\n  {\n    side: requested,\n    align,\n    sideOffset = 6,\n    alignOffset = 0,\n    padding = 8,\n  }: PlacementOptions,\n): Placement {\n  const { trigger, content } = layout;\n  const vw = window.innerWidth;\n  const vh = window.innerHeight;\n  const space: Record<PlacementSide, number> = {\n    top: trigger.top - sideOffset - padding,\n    bottom: vh - (trigger.top + trigger.height) - sideOffset - padding,\n    left: trigger.left - sideOffset - padding,\n    right: vw - (trigger.left + trigger.width) - sideOffset - padding,\n  };\n  const vertical = requested === \"top\" || requested === \"bottom\";\n  const need = vertical ? content.height : content.width;\n  const opposite = OPPOSITE[requested];\n  const side =\n    space[requested] < need && space[opposite] > space[requested]\n      ? opposite\n      : requested;\n\n  let left: number;\n  let top: number;\n  if (vertical) {\n    top =\n      side === \"bottom\"\n        ? trigger.top + trigger.height + sideOffset\n        : trigger.top - content.height - sideOffset;\n    left =\n      align === \"start\"\n        ? trigger.left\n        : align === \"end\"\n          ? trigger.left + trigger.width - content.width\n          : trigger.left + trigger.width / 2 - content.width / 2;\n    left = clamp(left + alignOffset, padding, vw - content.width - padding);\n    top = clamp(top, padding, vh - content.height - padding);\n  } else {\n    left =\n      side === \"right\"\n        ? trigger.left + trigger.width + sideOffset\n        : trigger.left - content.width - sideOffset;\n    top =\n      align === \"start\"\n        ? trigger.top\n        : align === \"end\"\n          ? trigger.top + trigger.height - content.height\n          : trigger.top + trigger.height / 2 - content.height / 2;\n    top = clamp(top + alignOffset, padding, vh - content.height - padding);\n    left = clamp(left, padding, vw - content.width - padding);\n  }\n\n  const cx = clamp(trigger.left + trigger.width / 2 - left, 0, content.width);\n  const cy = clamp(trigger.top + trigger.height / 2 - top, 0, content.height);\n  const origin =\n    side === \"bottom\"\n      ? `${cx}px 0px`\n      : side === \"top\"\n        ? `${cx}px ${content.height}px`\n        : side === \"right\"\n          ? `0px ${cy}px`\n          : `${content.width}px ${cy}px`;\n\n  return { left, top, side, origin };\n}\n"},{"path":"components/motion/popover-position.ts","type":"registry:component","target":"@components/motion/popover-position.ts","content":"\"use client\";\n\nimport {\n  type MutableRefObject,\n  useCallback,\n  useLayoutEffect,\n  useState,\n} from \"react\";\n\nexport type PortalLayout = {\n  trigger: {\n    left: number;\n    top: number;\n    width: number;\n    height: number;\n  };\n  content: {\n    width: number;\n    height: number;\n  };\n};\n\nfunction sameLayout(a: PortalLayout | null, b: PortalLayout) {\n  return (\n    a?.trigger.left === b.trigger.left &&\n    a.trigger.top === b.trigger.top &&\n    a.trigger.width === b.trigger.width &&\n    a.trigger.height === b.trigger.height &&\n    a.content.width === b.content.width &&\n    a.content.height === b.content.height\n  );\n}\n\n/** Measures a trigger and portalled panel in viewport coordinates. */\nexport function usePopoverPortalPosition<\n  TriggerElement extends HTMLElement,\n  ContentElement extends HTMLElement,\n>(\n  triggerRef: MutableRefObject<TriggerElement | null>,\n  contentRef: MutableRefObject<ContentElement | null>,\n  active: boolean,\n) {\n  const [layout, setLayout] = useState<PortalLayout | null>(null);\n\n  const update = useCallback(() => {\n    const trigger = triggerRef.current;\n    const content = contentRef.current;\n    if (!trigger || !content) return;\n\n    const rect = trigger.getBoundingClientRect();\n    const next: PortalLayout = {\n      trigger: {\n        left: rect.left,\n        top: rect.top,\n        width: rect.width,\n        height: rect.height,\n      },\n      content: {\n        width: content.offsetWidth,\n        height: content.offsetHeight,\n      },\n    };\n    setLayout((current) => (sameLayout(current, next) ? current : next));\n  }, [contentRef, triggerRef]);\n\n  useLayoutEffect(() => {\n    update();\n    if (!active) return;\n\n    const trigger = triggerRef.current;\n    const content = contentRef.current;\n    const observer = new ResizeObserver(update);\n    if (trigger) observer.observe(trigger);\n    if (content) observer.observe(content);\n\n    window.addEventListener(\"scroll\", update, true);\n    window.addEventListener(\"resize\", update);\n    return () => {\n      observer.disconnect();\n      window.removeEventListener(\"scroll\", update, true);\n      window.removeEventListener(\"resize\", update);\n    };\n  }, [active, contentRef, triggerRef, update]);\n\n  return layout;\n}\n"},{"path":"lib/ease.ts","type":"registry:lib","target":"@lib/ease.ts","content":"// Shared motion tokens. Easing curves mirror the CSS custom properties in\n// globals.css; springs are the canonical physics used across components.\n// Strong custom variants — defaults like `ease-in`/`ease-out` feel weak.\n\nexport const EASE_OUT = [0.16, 1, 0.3, 1] as const;\nexport const EASE_IN_OUT = [0.77, 0, 0.175, 1] as const;\nexport const EASE_DRAWER = [0.32, 0.72, 0, 1] as const;\n\n/** CSS string form of EASE_OUT for inline style transitions. */\nexport const EASE_OUT_CSS = \"cubic-bezier(0.16, 1, 0.3, 1)\";\n\n/** Press feedback on buttons and other tappable surfaces. */\nexport const SPRING_PRESS = {\n  type: \"spring\",\n  stiffness: 500,\n  damping: 30,\n  mass: 0.6,\n} as const;\n\n/** Content swaps — label/icon slots trading places inside a control. */\nexport const SPRING_SWAP = {\n  type: \"spring\",\n  stiffness: 460,\n  damping: 30,\n  mass: 0.55,\n} as const;\n\n/** Overlay panel entrances — modals and sheets summoned by pointer. */\nexport const SPRING_PANEL = {\n  type: \"spring\",\n  stiffness: 420,\n  damping: 40,\n  mass: 0.5,\n} as const;\n\n/** Shared-layout glides — pills, indicators and panels morphing between positions. */\nexport const SPRING_LAYOUT = {\n  type: \"spring\",\n  stiffness: 360,\n  damping: 32,\n  mass: 0.6,\n} as const;\n\n/** Cursor-follow physics for decorative mouse tracking (magnetic, tilt, dock). */\nexport const SPRING_MOUSE = {\n  stiffness: 200,\n  damping: 15,\n  mass: 0.3,\n} as const;\n\n/** Dragged handles and fills (sliders) — critically damped `useSpring` config,\n * so the value follows the pointer butterily and never rebounds off an end. */\nexport const SPRING_GLIDE = {\n  stiffness: 700,\n  damping: 50,\n  mass: 0.5,\n} as const;\n"},{"path":"lib/hooks/use-hover-gesture.ts","type":"registry:hook","target":"@lib/hooks/use-hover-gesture.ts","content":"\"use client\";\n\nimport { useMemo, useRef } from \"react\";\nimport { isHoveringPointer } from \"@/lib/touch\";\n\ninterface BoundaryEvent {\n  pointerId: number;\n  pointerType: string;\n  buttons: number;\n}\n\nexport interface HoverGesture {\n  /** True when this enter starts a hover: the pointer arrived resting, not pressing. */\n  enter: (event: BoundaryEvent) => boolean;\n  /** True when this leave ends a hover that entered as one. */\n  leave: (event: BoundaryEvent) => boolean;\n}\n\n/**\n * Pairs a surface's enter with its leave, per pointer.\n *\n * `isHoveringPointer` answers the question the *enter* asks — is this pointer\n * resting on the surface or pressing it — and both boundary cases go wrong if\n * the leave is asked the same question again:\n *\n * - A pen with no hover never rests. It arrives in contact, taps, and the spec\n *   then requires its boundary events after `pointerup`, so the leave carries\n *   `buttons: 0` and reads as a mouse gliding off. Hover teardown then undid\n *   the tap — the panel the pen had just opened closed under it.\n * - A mouse pressed on the surface and dragged off leaves with `buttons: 1`.\n *   Skipping teardown there strands the surface open: the release happens\n *   outside, and no second leave ever comes.\n *\n * So the state a hover holds is released by the pointer that took it, whatever\n * the buttons say at the boundary, and a pointer that arrived in contact never\n * took it in the first place. Contact is the exception tracked here, not\n * hover: a leave from a pointer this surface never saw enter — mounted under\n * the cursor, say — still counts, since the alternative is state with no way\n * out.\n */\nexport function useHoverGesture(): HoverGesture {\n  const contact = useRef(new Set<number>());\n\n  return useMemo(\n    () => ({\n      enter: (event) => {\n        if (isHoveringPointer(event)) {\n          contact.current.delete(event.pointerId);\n          return true;\n        }\n        contact.current.add(event.pointerId);\n        return false;\n      },\n      leave: (event) => {\n        const arrivedInContact = contact.current.delete(event.pointerId);\n        return !arrivedInContact && event.pointerType !== \"touch\";\n      },\n    }),\n    [],\n  );\n}\n"},{"path":"lib/utils.ts","type":"registry:lib","target":"@lib/utils.ts","content":"import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n"},{"path":"lib/touch.ts","type":"registry:lib","target":"@lib/touch.ts","content":"// Shared touch primitives. iOS and iPadOS run their own gestures on top of the\n// page — the long-press selection callout and the selection it drags in with\n// it — and they win: once the platform claims a touch it cancels ours\n// mid-gesture, so a press-and-hold or a drag simply dies. Surfaces that own\n// their gesture have to opt out.\n//\n// What the two classes below cover, precisely:\n// - `-webkit-touch-callout: none` stops iOS's long-press callout. WebKit-only:\n//   it is not a property other engines have, so it is inert everywhere else.\n// - `user-select: none` stops the long-press selection on every engine,\n//   Android included, and stops a drag from painting a selection under the\n//   cursor. It is inherited, so it reaches every descendant — which is why the\n//   two classes differ only in whether they apply it unconditionally.\n// What neither covers:\n// - Chrome for Android's long-press menu on a link or an image. No CSS\n//   suppresses it; a gesture surface that wraps one needs its own\n//   `onContextMenu` with `preventDefault()`.\n// - The native drag of an `<img>` or `<a>` descendant. `-webkit-user-drag` is\n//   not inherited and plain divs and buttons are not drag sources, so setting\n//   it on the surface does nothing — the child itself needs `draggable={false}`.\n\n/**\n * Classes for a surface that *is* the control: a thumb, a drum, a stage, a\n * handle, a hold button. Selection is suppressed on every input, because a\n * drag that highlights the control's own label is wrong on a mouse too.\n * Compose with `touch-none` when the surface also owns the scroll axis — leave\n * it off when the page must still scroll from there.\n */\nexport const TOUCH_GESTURE_CLASS = \"select-none [-webkit-touch-callout:none]\";\n\n/**\n * The same opt-out for a gesture surface that wraps content the consumer owns:\n * a scroller, a context-menu trigger, a sheet header, a list row. Selection is\n * suppressed only where the platform runs its own press gestures — a coarse\n * pointer — so a mouse user can still select and copy that content. If the\n * gesture itself would paint a selection under the cursor, add `select-none`\n * for the duration of the gesture rather than reaching for\n * `TOUCH_GESTURE_CLASS`.\n *\n * `pointer: coarse` describes the *primary* pointer and nothing else, so a\n * hybrid machine reads it wrong in both directions: a tablet with a mouse\n * plugged in keeps touch as primary and loses mouse selection, and a laptop\n * with a touchscreen keeps the mouse as primary and leaves selection live\n * under a finger. No media query can answer per interaction — the query is\n * about the device, and the question is about the gesture in progress. The\n * default stays here because it is right on the machines that are one thing or\n * the other, and losing a selection is a nuisance; where the miss costs a\n * *gesture* instead, the surface pairs it with `holdSelection` on the press.\n */\nexport const TOUCH_GESTURE_CONTENT_CLASS =\n  \"[-webkit-touch-callout:none] pointer-coarse:select-none\";\n\n/**\n * Grow a small control's *hit area* to at least 44×44px (WCAG 2.5.5, Apple\n * HIG) on a coarse pointer, without changing what it looks like or the space\n * it takes in layout. An invisible `::after` box, centered on the control and\n * `max(100%, 2.75rem)` on each axis, catches the taps: a 32px icon button\n * reaches 6px past each edge, a wide 40px button only grows vertically, and a\n * control already 44px or larger is left as it is. Hit-testing a pseudo\n * element lands on its owner, so handlers, focus and `:active` all behave.\n *\n * Mouse and trackpad rendering is untouched — nothing here applies outside\n * `pointer: coarse`. Requirements on the element it goes on:\n * - It must be the containing block: add `relative` (and keep it mergeable, so\n *   a consumer's `absolute` still wins through `cn`).\n * - It must not clip itself (`overflow-hidden`), or the overhang is clipped\n *   away along with its hit area.\n * - It must render children: `<input>` and other void elements have no\n *   `::after`. Put it on the wrapper or label instead.\n * - It must not already use `::after` for something visible.\n *\n * Centered 44px means two 32px buttons with a 4px gap overlap by 8px; the\n * later one wins that strip, which still ends well short of the earlier\n * button's visual center. Below a ~32px pitch the later neighbour starts\n * eating into that center — use `TOUCH_TARGET_Y_CLASS` for such dense rows.\n *\n * The overhang is real geometry: inside a scroll container it counts as\n * scrollable overflow, so a control flush against a scroller's edge can add a\n * few pixels of scroll on touch screens. Give such a scroller a little padding\n * on that side.\n */\nexport const TOUCH_TARGET_CLASS =\n  \"pointer-coarse:after:absolute pointer-coarse:after:top-1/2 pointer-coarse:after:left-1/2 pointer-coarse:after:size-[max(100%,2.75rem)] pointer-coarse:after:-translate-x-1/2 pointer-coarse:after:-translate-y-1/2 pointer-coarse:after:content-['']\";\n\n/**\n * `TOUCH_TARGET_CLASS` on the vertical axis only, for controls packed tighter\n * than ~32px apart in a row (pagination dots, dense swatches), where growing\n * sideways would hand most of each control to its neighbour. Same rules.\n */\nexport const TOUCH_TARGET_Y_CLASS =\n  \"pointer-coarse:after:absolute pointer-coarse:after:inset-x-0 pointer-coarse:after:top-1/2 pointer-coarse:after:h-[max(100%,2.75rem)] pointer-coarse:after:-translate-y-1/2 pointer-coarse:after:content-['']\";\n\n/**\n * Suppress selection on `element` for as long as a gesture is running on it,\n * whatever the primary pointer of the machine happens to be. Returns the\n * release. Inline, so it wins over the class above and is gone again the\n * moment the gesture ends.\n *\n * For the press gestures a native selection would otherwise steal — a\n * long-press that opens a menu. Elsewhere prefer the classes: a surface that\n * takes selection away for the whole session is a surface whose text nobody\n * can copy.\n */\nexport function holdSelection(element: HTMLElement) {\n  element.style.setProperty(\"user-select\", \"none\");\n  element.style.setProperty(\"-webkit-user-select\", \"none\");\n  return () => {\n    element.style.removeProperty(\"user-select\");\n    element.style.removeProperty(\"-webkit-user-select\");\n  };\n}\n\n/**\n * Pointer capture, best effort. WebKit throws `NotFoundError` when the pointer\n * is already gone by the time the handler runs — routine on iOS, where the\n * system can claim the touch first — and an uncaught throw takes the rest of\n * the handler, the gesture included, down with it. Touch pointers carry\n * implicit capture anyway, so losing it is never fatal.\n */\nexport function capturePointer(element: Element, pointerId: number) {\n  try {\n    element.setPointerCapture(pointerId);\n  } catch {\n    // Pointer is no longer active — implicit capture still applies on touch.\n  }\n}\n\n/** Release a capture taken with `capturePointer`, ignoring a stale pointer. */\nexport function releasePointer(element: Element, pointerId: number) {\n  try {\n    if (element.hasPointerCapture(pointerId)) {\n      element.releasePointerCapture(pointerId);\n    }\n  } catch {\n    // Capture was already dropped by the browser.\n  }\n}\n\n/**\n * Whether this event came from a pointer that is *hovering*: not a touch, and\n * not currently pressed. Which input the user is holding right now is not\n * something a device capability can answer — a touchscreen laptop hovers and\n * taps, and iPadOS reports a fine hovering pointer for a finger — so both\n * paths stay live and each handler branches on the event it was given.\n *\n * A pen resting on the glass is making contact, not hovering: `buttons` is the\n * tell, and it sends a pen tap down the same route a finger takes.\n *\n * This answers what an *enter* asks. A leave is the other half of a pair and\n * has to be read against the enter that started it — `useHoverGesture` in\n * `lib/hooks/use-hover-gesture` does that, and hover surfaces should use it\n * rather than asking this question twice.\n */\nexport const isHoveringPointer = (event: {\n  pointerType: string;\n  buttons: number;\n}) => event.pointerType !== \"touch\" && event.buttons === 0;\n"}]}