{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"tipover","title":"Tipover","description":"A touch-friendly tooltip/popover hybrid.","files":[{"path":"ui/tipover.tsx","type":"registry:ui","content":"\"use client\";\n\nimport { Popover as PopoverPrimitive } from \"@base-ui/react/popover\";\nimport { Tooltip as TooltipPrimitive } from \"@base-ui/react/tooltip\";\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nimport { usePointerCapability } from \"@/hooks/use-pointer-capability\";\n\ntype TipoverContextValue = {\n  isTouchDevice: boolean;\n};\n\nconst TipoverContext = React.createContext<TipoverContextValue | null>(null);\n\nfunction Tipover({ ...props }: PopoverPrimitive.Root.Props & TooltipPrimitive.Root.Props) {\n  const { isTouchDevice } = usePointerCapability();\n  const contextValue = React.useMemo(() => ({ isTouchDevice }), [isTouchDevice]);\n\n  return (\n    <TipoverContext.Provider value={contextValue}>\n      {isTouchDevice ? (\n        <PopoverPrimitive.Root data-slot=\"tipover\" {...props} />\n      ) : (\n        <TooltipPrimitive.Provider delay={0}>\n          <TooltipPrimitive.Root data-slot=\"tipover\" {...props} />\n        </TooltipPrimitive.Provider>\n      )}\n    </TipoverContext.Provider>\n  );\n}\n\nfunction TipoverTrigger({\n  nativeButton,\n  closeDelay,\n  ...props\n}: Omit<TooltipPrimitive.Trigger.Props & PopoverPrimitive.Trigger.Props, \"handle\"> &\n  Pick<PopoverPrimitive.Trigger.Props, \"nativeButton\">) {\n  const ctx = useTipoverContext(\"TipoverTrigger\");\n  const { Trigger } = ctx.isTouchDevice ? PopoverPrimitive : TooltipPrimitive;\n  const triggerProps = ctx.isTouchDevice\n    ? ({\n        nativeButton,\n        openOnHover: true,\n      } satisfies PopoverPrimitive.Trigger.Props)\n    : ({\n        closeDelay: closeDelay ?? 150,\n      } satisfies TooltipPrimitive.Trigger.Props);\n\n  return <Trigger data-slot=\"tipover-trigger\" {...props} {...triggerProps} />;\n}\n\nfunction TipoverContent({\n  className,\n  side = \"top\",\n  sideOffset = 4,\n  align = \"center\",\n  alignOffset = 0,\n  children,\n  ...props\n}: TooltipPrimitive.Popup.Props &\n  PopoverPrimitive.Popup.Props &\n  Pick<TooltipPrimitive.Positioner.Props, \"align\" | \"alignOffset\" | \"side\" | \"sideOffset\">) {\n  const ctx = useTipoverContext(\"TipoverContent\");\n  const { Portal, Positioner, Popup, Arrow } = ctx.isTouchDevice\n    ? PopoverPrimitive\n    : TooltipPrimitive;\n\n  return (\n    <Portal>\n      <Positioner\n        align={align}\n        alignOffset={alignOffset}\n        side={side}\n        sideOffset={sideOffset}\n        className=\"pointer-events-auto isolate z-50\"\n      >\n        <Popup\n          data-slot=\"tipover-content\"\n          className={cn(\n            \"relative z-50 inline-flex w-fit max-w-xs origin-(--transform-origin) items-center gap-1.5 overflow-visible rounded-md bg-foreground px-3 py-1.5 text-xs text-background shadow-md selection:bg-background selection:text-foreground data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95\",\n            className,\n          )}\n          {...props}\n        >\n          {children}\n          <Arrow className=\"z-50 size-2.5 translate-y-[calc(-50%-2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground data-[side=bottom]:top-1 data-[side=inline-end]:top-1/2! data-[side=inline-end]:-left-1 data-[side=inline-end]:-translate-y-1/2 data-[side=inline-start]:top-1/2! data-[side=inline-start]:-right-1 data-[side=inline-start]:-translate-y-1/2 data-[side=left]:top-1/2! data-[side=left]:-right-1 data-[side=left]:-translate-y-1/2 data-[side=right]:top-1/2! data-[side=right]:-left-1 data-[side=right]:-translate-y-1/2 data-[side=top]:-bottom-2.5\" />\n        </Popup>\n      </Positioner>\n    </Portal>\n  );\n}\n\nfunction useTipoverContext(componentName: string) {\n  const ctx = React.useContext(TipoverContext);\n\n  if (!ctx) {\n    throw new Error(`${componentName} must be used within <Tipover>.`);\n  }\n\n  return ctx;\n}\n\nexport { Tipover, TipoverTrigger, TipoverContent };","target":"@ui/tipover.tsx"},{"path":"hooks/use-pointer-capability.ts","type":"registry:hook","content":"\"use client\";\n\nimport * as React from \"react\";\n\nconst HOVER_QUERY = \"(hover: hover)\";\nconst COARSE_POINTER_QUERY = \"(pointer: coarse)\";\n\ntype PointerCapability = {\n  supportsHover: boolean;\n  isCoarsePointer: boolean;\n  isTouchDevice: boolean;\n};\n\nexport function usePointerCapability(): PointerCapability {\n  const supportsHover = useMediaQuery(HOVER_QUERY);\n  const isCoarsePointer = useMediaQuery(COARSE_POINTER_QUERY);\n\n  return {\n    supportsHover: supportsHover ?? false,\n    isCoarsePointer: isCoarsePointer ?? false,\n    isTouchDevice: supportsHover === false || isCoarsePointer === true,\n  };\n}\n\nfunction useMediaQuery(query: string) {\n  const [matches, setMatches] = React.useState<boolean>();\n\n  React.useEffect(() => {\n    const mediaQueryList = window.matchMedia(query);\n    const updateMatches = () => setMatches(mediaQueryList.matches);\n\n    updateMatches();\n    mediaQueryList.addEventListener(\"change\", updateMatches);\n\n    return () => mediaQueryList.removeEventListener(\"change\", updateMatches);\n  }, [query]);\n\n  return matches;\n}","target":"@hooks/use-pointer-capability.ts"}],"type":"registry:ui","dependencies":["@base-ui/react"]}