{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"toggle-group","type":"registry:component","title":"Toggle Group","description":"Composable toolbar toggles: a standalone aria-pressed Toggle plus a shadcn-shaped ToggleGroup of ToggleGroupItem children in single or multiple mode, with default/outline variants, sm/md/lg sizes, one roving tab stop with arrow/Home/End keys, a pressed surface that glides between items in single mode and pops per item in multiple mode — the composable, icon-first counterpart to SegmentedControl's radiogroup.","author":"kara <ztzyrant@gmail.com>","dependencies":["clsx","motion","tailwind-merge"],"registryDependencies":[],"files":[{"path":"components/motion/toggle-group.tsx","type":"registry:component","target":"@components/motion/toggle-group.tsx","content":"\"use client\";\n// karaui.kamisama-v0.my.id/components/motion/toggle-group\n\nimport {\n  AnimatePresence,\n  type HTMLMotionProps,\n  motion,\n  useReducedMotion,\n} from \"motion/react\";\nimport {\n  createContext,\n  forwardRef,\n  type KeyboardEvent,\n  type ReactNode,\n  useCallback,\n  useContext,\n  useEffect,\n  useId,\n  useRef,\n  useState,\n} from \"react\";\nimport { SPRING_LAYOUT, SPRING_PRESS, SPRING_SWAP } from \"@/lib/ease\";\nimport { TOUCH_TARGET_CLASS } from \"@/lib/touch\";\nimport { cn } from \"@/lib/utils\";\n\n// Toggle / ToggleGroup vs SegmentedControl: `SegmentedControl`\n// (segmented-control.tsx) is a radiogroup form control that always holds one\n// value and ships a data-driven `ToggleGroup` for quick `items` arrays. This\n// file is the composable, shadcn-shaped toolbar primitive: a standalone\n// `Toggle`, and a `ToggleGroup` you fill with `ToggleGroupItem` children\n// (icons, labels, tooltips, anything), with `default`/`outline` variants. In\n// single mode one pressed surface glides between items; in multiple mode each\n// item pops its own surface in and out.\n\nexport type ToggleVariant = \"default\" | \"outline\";\nexport type ToggleSize = \"sm\" | \"md\" | \"lg\";\n\nconst sizeClass: Record<ToggleSize, string> = {\n  sm: \"h-7 min-w-7 gap-1.5 px-2 text-xs [&_svg]:size-3.5\",\n  md: \"h-9 min-w-9 gap-2 px-2.5 text-sm [&_svg]:size-4\",\n  lg: \"h-11 min-w-11 gap-2 px-3.5 text-sm [&_svg]:size-[18px]\",\n};\n\nconst surfaceClass: Record<ToggleVariant, string> = {\n  default: \"bg-muted ring-1 ring-border-strong\",\n  outline: \"bg-muted\",\n};\n\nconst itemBase =\n  \"relative isolate inline-flex shrink-0 select-none items-center justify-center whitespace-nowrap font-medium outline-none transition-colors duration-200 focus-visible:z-10 focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50\";\n\nfunction useControllable<T>(\n  value: T | undefined,\n  defaultValue: T,\n  onChange: ((next: T) => void) | undefined,\n) {\n  const [internal, setInternal] = useState<T>(defaultValue);\n  const controlled = value !== undefined;\n  const current = controlled ? value : internal;\n  const set = useCallback(\n    (next: T) => {\n      if (!controlled) setInternal(next);\n      onChange?.(next);\n    },\n    [controlled, onChange],\n  );\n  return [current, set] as const;\n}\n\ntype ButtonBaseProps = Omit<HTMLMotionProps<\"button\">, \"children\" | \"value\" | \"type\">;\n\n/** The pressed surface that pops in and out behind a standalone toggle or a multiple-mode item. */\nfunction PopSurface({ show, variant }: { show: boolean; variant: ToggleVariant }) {\n  const reduce = useReducedMotion();\n  return (\n    <AnimatePresence initial={false}>\n      {show ? (\n        <motion.span\n          key=\"on\"\n          aria-hidden\n          initial={reduce ? { opacity: 0 } : { opacity: 0, scale: 0.6 }}\n          animate={{ opacity: 1, scale: 1 }}\n          exit={\n            reduce\n              ? { opacity: 0 }\n              : { opacity: 0, scale: 0.75, transition: { duration: 0.12 } }\n          }\n          transition={SPRING_SWAP}\n          className={cn(\"absolute inset-0 -z-10 rounded-[inherit]\", surfaceClass[variant])}\n        />\n      ) : null}\n    </AnimatePresence>\n  );\n}\n\n/* ------------------------------------------------------------------------ */\n/* Toggle                                                                    */\n/* ------------------------------------------------------------------------ */\n\nexport interface ToggleProps extends ButtonBaseProps {\n  pressed?: boolean;\n  defaultPressed?: boolean;\n  onPressedChange?: (pressed: boolean) => void;\n  variant?: ToggleVariant;\n  size?: ToggleSize;\n  children?: ReactNode;\n}\n\n/** A two-state button (`aria-pressed`) whose pressed surface springs in and out. */\nexport const Toggle = forwardRef<HTMLButtonElement, ToggleProps>(function Toggle(\n  {\n    pressed,\n    defaultPressed = false,\n    onPressedChange,\n    variant = \"default\",\n    size = \"md\",\n    disabled,\n    className,\n    children,\n    onClick,\n    ...rest\n  },\n  ref,\n) {\n  const reduce = useReducedMotion();\n  const [on, setOn] = useControllable(pressed, defaultPressed, onPressedChange);\n  return (\n    <motion.button\n      ref={ref}\n      type=\"button\"\n      aria-pressed={on}\n      data-state={on ? \"on\" : \"off\"}\n      disabled={disabled}\n      whileTap={reduce || disabled ? undefined : { scale: 0.9 }}\n      transition={SPRING_PRESS}\n      onClick={(event) => {\n        onClick?.(event);\n        if (!event.defaultPrevented) setOn(!on);\n      }}\n      className={cn(\n        itemBase,\n        TOUCH_TARGET_CLASS,\n        \"rounded-lg focus-visible:ring-offset-1 focus-visible:ring-offset-background\",\n        sizeClass[size],\n        variant === \"outline\" && \"border border-border\",\n        on\n          ? \"text-foreground\"\n          : \"text-muted-foreground enabled:hover:bg-muted/60 enabled:hover:text-foreground\",\n        className,\n      )}\n      {...rest}\n    >\n      <PopSurface show={on} variant={variant} />\n      {children}\n    </motion.button>\n  );\n});\n\n/* ------------------------------------------------------------------------ */\n/* ToggleGroup                                                               */\n/* ------------------------------------------------------------------------ */\n\ninterface ToggleGroupContextValue {\n  type: \"single\" | \"multiple\";\n  pressed: string[];\n  toggle: (value: string) => void;\n  variant: ToggleVariant;\n  size: ToggleSize;\n  disabled: boolean;\n  layoutId: string;\n  tabStop: string | null;\n  setTabStop: (value: string) => void;\n}\n\nconst ToggleGroupContext = createContext<ToggleGroupContextValue | null>(null);\n\ntype ToggleGroupBase = Omit<\n  HTMLMotionProps<\"div\">,\n  \"children\" | \"defaultValue\" | \"onChange\" | \"dir\"\n> & {\n  variant?: ToggleVariant;\n  size?: ToggleSize;\n  disabled?: boolean;\n  /** Arrow-key axis for roving focus. Default \"horizontal\" (both axes still work). */\n  orientation?: \"horizontal\" | \"vertical\";\n  children?: ReactNode;\n};\n\nexport type ToggleGroupSingleProps = ToggleGroupBase & {\n  type: \"single\";\n  /** The pressed item, or `\"\"` when none is pressed. */\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (value: string) => void;\n  /** Disallow un-pressing the active item, so one is always pressed. */\n  required?: boolean;\n};\n\nexport type ToggleGroupMultipleProps = ToggleGroupBase & {\n  type: \"multiple\";\n  value?: string[];\n  defaultValue?: string[];\n  onValueChange?: (value: string[]) => void;\n};\n\nexport type ToggleGroupProps = ToggleGroupSingleProps | ToggleGroupMultipleProps;\n\nconst ITEM_SELECTOR = \"[data-toggle-group-item]\";\n\nfunction enabledItems(root: HTMLElement | null) {\n  if (!root) return [];\n  return Array.from(root.querySelectorAll<HTMLButtonElement>(ITEM_SELECTOR)).filter(\n    (el) => !el.disabled && el.closest(\"[data-toggle-group]\") === root,\n  );\n}\n\n/**\n * Toolbar-style group of `ToggleGroupItem`s. `type=\"single\"` behaves like a\n * set of mutually exclusive toggles (deselectable unless `required`) whose\n * pressed surface glides between items; `type=\"multiple\"` presses any subset.\n * One tab stop, arrow keys / Home / End rove between items, Space/Enter press.\n */\nexport function ToggleGroup(props: ToggleGroupProps) {\n  const {\n    type,\n    value: _value,\n    defaultValue: _defaultValue,\n    onValueChange: _onValueChange,\n    variant = \"default\",\n    size = \"md\",\n    disabled = false,\n    orientation = \"horizontal\",\n    className,\n    children,\n    onKeyDown,\n    ...rest\n  } = props;\n  // `required` only exists on the single variant; keep it off the DOM.\n  const { required: _required, ...domProps } = rest as typeof rest & { required?: boolean };\n  const single = props.type === \"single\";\n  const required = props.type === \"single\" && Boolean(props.required);\n\n  const [pressed, setPressed] = useControllable<string[]>(\n    props.value === undefined\n      ? undefined\n      : props.type === \"single\"\n        ? props.value\n          ? [props.value]\n          : []\n        : props.value,\n    props.type === \"single\"\n      ? props.defaultValue\n        ? [props.defaultValue]\n        : []\n      : (props.defaultValue ?? []),\n    (next) => {\n      if (props.type === \"single\") props.onValueChange?.(next[0] ?? \"\");\n      else props.onValueChange?.(next);\n    },\n  );\n\n  const layoutId = useId();\n  const rootRef = useRef<HTMLDivElement>(null);\n  const [tabStop, setTabStop] = useState<string | null>(pressed[0] ?? null);\n\n  // Keep exactly one reachable tab stop: if the remembered item vanished or\n  // got disabled, fall back to the first pressed item, then the first enabled.\n  useEffect(() => {\n    const items = enabledItems(rootRef.current);\n    if (items.some((el) => el.dataset.value === tabStop)) return;\n    const fallback =\n      items.find((el) => pressed.includes(el.dataset.value ?? \"\")) ?? items[0];\n    const next = fallback?.dataset.value ?? null;\n    if (next !== tabStop) setTabStop(next);\n  });\n\n  const toggle = (itemValue: string) => {\n    const isOn = pressed.includes(itemValue);\n    if (single) {\n      if (isOn && required) return;\n      setPressed(isOn ? [] : [itemValue]);\n    } else {\n      setPressed(isOn ? pressed.filter((v) => v !== itemValue) : [...pressed, itemValue]);\n    }\n  };\n\n  const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {\n    onKeyDown?.(event);\n    if (event.defaultPrevented) return;\n    const items = enabledItems(rootRef.current);\n    const index = items.indexOf(document.activeElement as HTMLButtonElement);\n    if (index < 0) return;\n    const forward = orientation === \"vertical\" ? \"ArrowDown\" : \"ArrowRight\";\n    const backward = orientation === \"vertical\" ? \"ArrowUp\" : \"ArrowLeft\";\n    let next: number;\n    if (event.key === forward || event.key === \"ArrowDown\" || event.key === \"ArrowRight\")\n      next = (index + 1) % items.length;\n    else if (event.key === backward || event.key === \"ArrowUp\" || event.key === \"ArrowLeft\")\n      next = (index - 1 + items.length) % items.length;\n    else if (event.key === \"Home\") next = 0;\n    else if (event.key === \"End\") next = items.length - 1;\n    else return;\n    event.preventDefault();\n    // Focus moves; pressing stays on Space/Enter — these are toggles, not radios.\n    items[next].focus();\n  };\n\n  return (\n    <ToggleGroupContext.Provider\n      value={{\n        type,\n        pressed,\n        toggle,\n        variant,\n        size,\n        disabled,\n        layoutId,\n        tabStop,\n        setTabStop,\n      }}\n    >\n      {/* layoutRoot keeps the gliding surface's projection local to the group,\n          so scrolled or transformed ancestors never skew the glide. */}\n      <motion.div\n        ref={rootRef}\n        layoutRoot\n        role=\"group\"\n        data-toggle-group=\"\"\n        data-orientation={orientation}\n        aria-disabled={disabled || undefined}\n        onKeyDown={handleKeyDown}\n        className={cn(\n          \"inline-flex w-fit items-center\",\n          orientation === \"vertical\" && \"flex-col items-stretch\",\n          variant === \"default\"\n            ? \"gap-0.5 rounded-xl border border-border bg-card p-1\"\n            : \"rounded-lg\",\n          disabled && \"opacity-60\",\n          className,\n        )}\n        {...domProps}\n      >\n        {children}\n      </motion.div>\n    </ToggleGroupContext.Provider>\n  );\n}\n\nexport interface ToggleGroupItemProps extends ButtonBaseProps {\n  value: string;\n  children?: ReactNode;\n}\n\nexport const ToggleGroupItem = forwardRef<HTMLButtonElement, ToggleGroupItemProps>(\n  function ToggleGroupItem(\n    { value, disabled: itemDisabled, className, children, onClick, onFocus, ...rest },\n    ref,\n  ) {\n    const ctx = useContext(ToggleGroupContext);\n    if (!ctx) throw new Error(\"ToggleGroupItem must be used inside <ToggleGroup>.\");\n    const reduce = useReducedMotion();\n    const disabled = ctx.disabled || Boolean(itemDisabled);\n    const on = ctx.pressed.includes(value);\n    const outline = ctx.variant === \"outline\";\n\n    return (\n      <motion.button\n        ref={ref}\n        type=\"button\"\n        data-toggle-group-item=\"\"\n        data-value={value}\n        data-state={on ? \"on\" : \"off\"}\n        aria-pressed={on}\n        disabled={disabled}\n        tabIndex={ctx.tabStop === value ? 0 : -1}\n        whileTap={reduce || disabled ? undefined : { scale: 0.88 }}\n        transition={SPRING_PRESS}\n        onFocus={(event) => {\n          ctx.setTabStop(value);\n          onFocus?.(event);\n        }}\n        onClick={(event) => {\n          onClick?.(event);\n          if (event.defaultPrevented) return;\n          ctx.setTabStop(value);\n          ctx.toggle(value);\n        }}\n        className={cn(\n          itemBase,\n          TOUCH_TARGET_CLASS,\n          sizeClass[ctx.size],\n          outline\n            ? // Joined segments: neighbours overlap their shared border.\n              \"rounded-none border border-border bg-card -ml-px first:ml-0 first:rounded-l-lg last:rounded-r-lg in-data-[orientation=vertical]:-mt-px in-data-[orientation=vertical]:ml-0 in-data-[orientation=vertical]:first:mt-0 in-data-[orientation=vertical]:first:rounded-l-none in-data-[orientation=vertical]:first:rounded-t-lg in-data-[orientation=vertical]:last:rounded-r-none in-data-[orientation=vertical]:last:rounded-b-lg\"\n            : \"rounded-lg focus-visible:ring-offset-1 focus-visible:ring-offset-card\",\n          on\n            ? \"text-foreground\"\n            : \"text-muted-foreground enabled:hover:bg-muted/60 enabled:hover:text-foreground\",\n          className,\n        )}\n        {...rest}\n      >\n        {ctx.type === \"single\" ? (\n          on ? (\n            <motion.span\n              layoutId={ctx.layoutId}\n              aria-hidden\n              transition={reduce ? { duration: 0 } : SPRING_LAYOUT}\n              className={cn(\n                \"absolute inset-0 -z-10 rounded-[inherit]\",\n                surfaceClass[ctx.variant],\n              )}\n            />\n          ) : null\n        ) : (\n          <PopSurface show={on} variant={ctx.variant} />\n        )}\n        {children}\n      </motion.button>\n    );\n  },\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/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"},{"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"}]}