{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tooltip",
  "title": "Ninaero tooltip",
  "dependencies": [
    "@base-ui/react@^1.9.0",
    "cn@^0.4.0"
  ],
  "registryDependencies": [
    "https://ninaero.pages.dev/r/theme.json",
    "https://ninaero.pages.dev/r/sound-provider.json"
  ],
  "files": [
    {
      "path": "registry/ninaero/ui/tooltip.tsx",
      "content": "\"use client\";\n\nimport { Tooltip as TooltipPrimitive } from \"@base-ui/react/tooltip\";\nimport { useRender } from \"@base-ui/react/use-render\";\nimport { cn } from \"cn\";\nimport * as React from \"react\";\n\nimport { HoverSound } from \"./sound-feedback\";\n\n// Base UI 1.9 omits tooltip descriptions. Share the content ID, not interaction state.\nfunction createTooltipDescription() {\n  let id: string | undefined;\n  const listeners = new Set<() => void>();\n  return {\n    getSnapshot: () => id,\n    subscribe(listener: () => void) {\n      listeners.add(listener);\n      return () => {\n        listeners.delete(listener);\n      };\n    },\n    setId(next: string | undefined) {\n      if (id === next) return;\n      id = next;\n      listeners.forEach((listener) => {\n        listener();\n      });\n    },\n  };\n}\n\ntype TooltipDescription = ReturnType<typeof createTooltipDescription>;\nconst TooltipDescriptionContext = React.createContext<TooltipDescription | null>(null);\ntype TooltipHandle = Pick<\n  NonNullable<TooltipPrimitive.Root.Props[\"handle\"]>,\n  \"open\" | \"close\" | \"isOpen\"\n>;\nconst handleDescriptions = new WeakMap<TooltipHandle, TooltipDescription>();\nconst noDescription = () => undefined;\nconst noSubscription = () => () => {};\n\nfunction getHandleDescription(handle: TooltipHandle) {\n  let description = handleDescriptions.get(handle);\n  if (!description) {\n    description = createTooltipDescription();\n    handleDescriptions.set(handle, description);\n  }\n  return description;\n}\n\nfunction TooltipProvider({ delay = 0, ...props }: TooltipPrimitive.Provider.Props) {\n  return <TooltipPrimitive.Provider data-slot=\"tooltip-provider\" delay={delay} {...props} />;\n}\n\nfunction Tooltip<Payload>({ handle, ...props }: TooltipPrimitive.Root.Props<Payload>) {\n  const [localDescription] = React.useState(createTooltipDescription);\n  const description = handle ? getHandleDescription(handle) : localDescription;\n  return (\n    <TooltipDescriptionContext.Provider value={description}>\n      <TooltipPrimitive.Root data-slot=\"tooltip\" handle={handle} {...props} />\n    </TooltipDescriptionContext.Provider>\n  );\n}\n\nfunction TooltipTrigger<Payload>({\n  render,\n  handle,\n  sound = true,\n  ...props\n}: TooltipPrimitive.Trigger.Props<Payload> & { sound?: boolean }) {\n  const context = React.useContext(TooltipDescriptionContext);\n  const description = handle ? getHandleDescription(handle) : context;\n  const descriptionId = React.useSyncExternalStore(\n    description?.subscribe ?? noSubscription,\n    description?.getSnapshot ?? noDescription,\n    noDescription,\n  );\n  return (\n    <TooltipPrimitive.Trigger\n      data-slot=\"tooltip-trigger\"\n      handle={handle}\n      {...props}\n      render={(elementProps, state) => (\n        <TooltipTriggerElement\n          elementProps={elementProps}\n          state={state}\n          render={render}\n          descriptionId={descriptionId}\n          sound={sound}\n        />\n      )}\n    />\n  );\n}\n\ntype TriggerRender = Exclude<\n  TooltipPrimitive.Trigger.Props[\"render\"],\n  React.ReactElement | undefined\n>;\n\nfunction TooltipTriggerElement({\n  elementProps,\n  state,\n  render,\n  descriptionId,\n  sound,\n}: {\n  elementProps: Parameters<TriggerRender>[0];\n  state: TooltipPrimitive.Trigger.State;\n  render: TooltipPrimitive.Trigger.Props[\"render\"];\n  descriptionId: string | undefined;\n  sound: boolean;\n}) {\n  const element = useRender({\n    defaultTagName: \"button\",\n    props: { ...elementProps },\n    render,\n    state: { open: state.open },\n    stateAttributesMapping: { open: () => null },\n  });\n  if (!React.isValidElement<React.AriaAttributes>(element)) return element;\n  const ids = new Set(\n    [elementProps[\"aria-describedby\"], element.props[\"aria-describedby\"]].flatMap(\n      (value) => value?.split(/\\s+/).filter(Boolean) ?? [],\n    ),\n  );\n  if (state.open && descriptionId) ids.add(descriptionId);\n  return (\n    <HoverSound sound={sound}>\n      {React.cloneElement(element, { \"aria-describedby\": [...ids].join(\" \") || undefined })}\n    </HoverSound>\n  );\n}\n\nfunction TooltipContent({\n  className,\n  id,\n  side = \"top\",\n  sideOffset = 4,\n  align = \"center\",\n  alignOffset = 0,\n  children,\n  ...props\n}: TooltipPrimitive.Popup.Props &\n  Pick<TooltipPrimitive.Positioner.Props, \"align\" | \"alignOffset\" | \"side\" | \"sideOffset\">) {\n  const description = React.useContext(TooltipDescriptionContext);\n  const generatedId = React.useId();\n  const popupId = id ?? generatedId;\n  React.useLayoutEffect(() => {\n    description?.setId(popupId);\n    return () => description?.setId(undefined);\n  }, [description, popupId]);\n\n  return (\n    <TooltipPrimitive.Portal>\n      <TooltipPrimitive.Positioner\n        align={align}\n        alignOffset={alignOffset}\n        side={side}\n        sideOffset={sideOffset}\n        className=\"isolate z-50\"\n      >\n        <TooltipPrimitive.Popup\n          data-slot=\"tooltip-content\"\n          id={popupId}\n          role=\"tooltip\"\n          className={cn(\n            \"ninaero-tooltip before:ninaero-paper-ruled-4 z-50 inline-flex w-fit items-center gap-1.5 px-4 py-2 text-xs type-caption **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-sm\",\n            className,\n          )}\n          {...props}\n        >\n          {children}\n        </TooltipPrimitive.Popup>\n      </TooltipPrimitive.Positioner>\n    </TooltipPrimitive.Portal>\n  );\n}\n\nexport { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}