{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tooltip",
  "type": "registry:component",
  "title": "Tooltip",
  "description": "Explains a control on hover or keyboard focus. A real element describes its trigger.",
  "registryDependencies": [
    "https://vlak.dev/r/vlak-base.json",
    "https://vlak.dev/r/inter.json",
    "https://vlak.dev/r/button.json",
    "https://vlak.dev/r/vlak-lib.json"
  ],
  "dependencies": [
    "@stylexjs/stylex"
  ],
  "devDependencies": [
    "@stylexjs/babel-plugin"
  ],
  "docs": "Vlak leaves are StyleX. Compile them with @stylexjs/babel-plugin (Vite: @stylexjs/unplugin, Next: @stylexjs/nextjs-plugin). If you would rather not run a compiler, import @noorddev/vlak-react instead: it ships precompiled with one stylesheet.",
  "files": [
    {
      "path": "vlak/tooltip.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as stylex from \"@stylexjs/stylex\";\nimport { vlak, mq } from \"./tokens.stylex\";\nimport { rs } from \"./rs\";\nimport { cx } from \"./cx\";\n\nexport interface TooltipProps extends React.HTMLAttributes<HTMLSpanElement> {\n  /** Tip text. Shown on hover and on keyboard focus; Escape hides it. */\n  tip: string;\n}\n\nconst styles = stylex.create({\n  tip: {\n    position: \"relative\",\n    display: \"inline-flex\",\n  },\n  bubble: {\n    position: \"absolute\",\n    bottom: \"calc(100% + 8px)\",\n    insetInlineStart: \"50%\",\n    transform: \"translateX(-50%)\",\n    boxSizing: \"border-box\",\n    width: \"max-content\",\n    maxWidth: \"min(280px, calc(100vw - 16px))\",\n    backgroundColor: vlak.ink,\n    color: vlak.paper,\n    fontSize: {\n      default: \"0.71875rem\",\n      [mq.phone]: \"0.8125rem\",\n    },\n    fontWeight: 500,\n    letterSpacing: \"-0.01em\",\n    lineHeight: 1.3,\n    paddingBlock: {\n      default: \"0.3125rem\",\n      [mq.phone]: \"0.5rem\",\n    },\n    paddingInline: {\n      default: \"0.5625rem\",\n      [mq.phone]: \"0.75rem\",\n    },\n    borderRadius: {\n      default: vlak.radiusSm,\n      [mq.phone]: vlak.radiusSm,\n    },\n    zIndex: vlak.zFloat,\n    opacity: {\n      default: 0,\n      [stylex.when.ancestor(\":hover\")]: {\n        default: null,\n        [mq.hover]: 1,\n      },\n      [stylex.when.ancestor(\":focus-within\")]: 1,\n      [stylex.when.ancestor(\":active\")]: {\n        default: null,\n        [mq.touch]: 1,\n      },\n    },\n    visibility: {\n      default: \"hidden\",\n      [stylex.when.ancestor(\":hover\")]: {\n        default: null,\n        [mq.hover]: \"visible\",\n      },\n      [stylex.when.ancestor(\":focus-within\")]: \"visible\",\n      [stylex.when.ancestor(\":active\")]: {\n        default: null,\n        [mq.touch]: \"visible\",\n      },\n    },\n    // The tip itself can be hovered (WCAG 1.4.13); ::before bridges the 8px gap.\n    pointerEvents: {\n      default: \"none\",\n      [stylex.when.ancestor(\":hover\")]: { default: null, [mq.hover]: \"auto\" },\n      [stylex.when.ancestor(\":focus-within\")]: \"auto\",\n    },\n    // The delay is the leave grace: the pointer can cross onto the tip.\n    transition: {\n      default: vlak.transition,\n      [mq.reduce]: \"none\",\n    },\n    \"::before\": {\n      content: '\"\"',\n      position: \"absolute\",\n      top: \"100%\",\n      insetInlineStart: 0,\n      insetInlineEnd: 0,\n      height: \"0.5rem\",\n    },\n  },\n});\n\n/**\n * Whether a React element reaches the tab order on its own. `null`\n * when it cannot be told (a component), so callers leave it alone.\n */\nexport function isFocusableElement(el: React.ReactElement): boolean | null {\n  const props = el.props as Record<string, unknown>;\n  if (props.tabIndex != null) return true;\n  if (typeof el.type !== \"string\") return null;\n  switch (el.type) {\n    case \"a\":\n    case \"area\":\n      return props.href != null;\n    case \"button\":\n    case \"input\":\n    case \"select\":\n    case \"textarea\":\n    case \"summary\":\n      return !props.disabled;\n    case \"iframe\":\n      return true;\n    default:\n      return props.contentEditable === true || props.contentEditable === \"true\";\n  }\n}\n\n/**\n * Clones a trigger element with the description reference; a plain\n * element that is not focusable also gets tabIndex so keyboards\n * reach it. Components are trusted to forward the attribute.\n */\nexport function describeTrigger(el: React.ReactElement, id: string): React.ReactElement {\n  const props = el.props as Record<string, unknown>;\n  const extra: Record<string, unknown> = {\n    \"aria-describedby\": cx(props[\"aria-describedby\"] as string | undefined, id),\n  };\n  if (isFocusableElement(el) === false) extra.tabIndex = 0;\n  return React.cloneElement(el, extra);\n}\n\nexport const Tooltip = React.forwardRef<HTMLSpanElement, TooltipProps>(function Tooltip(\n  { tip, className, style, children, onKeyDown, onPointerLeave, onBlur, ...props },\n  ref,\n) {\n  const id = React.useId();\n  const [dismissed, setDismissed] = React.useState(false);\n  const sx = rs([\"rs-tip\", className], styles.tip, stylex.defaultMarker());\n  const bubble = rs([\"rs-tip-bubble\"], styles.bubble);\n  // A text child makes the wrapper the trigger; an element child is described directly.\n  const triggerIsRoot = !React.isValidElement(children);\n  return (\n    <span\n      ref={ref}\n      {...props}\n      className={sx.className}\n      style={{ ...sx.style, ...style }}\n      data-tip={tip}\n      tabIndex={triggerIsRoot ? 0 : undefined}\n      aria-describedby={triggerIsRoot ? id : undefined}\n      onKeyDown={(e) => {\n        onKeyDown?.(e);\n        if (e.key === \"Escape\") setDismissed(true);\n      }}\n      onPointerLeave={(e) => {\n        onPointerLeave?.(e);\n        setDismissed(false);\n      }}\n      onBlur={(e) => {\n        onBlur?.(e);\n        if (!e.currentTarget.contains(e.relatedTarget as Node | null)) setDismissed(false);\n      }}\n    >\n      {triggerIsRoot ? children : describeTrigger(children as React.ReactElement, id)}\n      <span role=\"tooltip\" id={id} hidden={dismissed} className={bubble.className} style={bubble.style}>\n        {tip}\n      </span>\n    </span>\n  );\n});\n",
      "type": "registry:component",
      "target": "components/vlak/tooltip.tsx"
    },
    {
      "path": "vlak/styles/tooltip.css",
      "content": "/* ── tooltip: generated from packages/react/src/components/tooltip.tsx ── */\n.rs-tip{position:relative;display:inline-flex}\n.rs-tip-bubble{position:absolute;bottom:calc(100% + 8px);inset-inline-start:50%;transform:translateX(-50%);box-sizing:border-box;width:max-content;max-width:min(280px, calc(100vw - 16px));background-color:var(--text);color:var(--bg);font-size:0.71875rem;font-weight:500;letter-spacing:-0.01em;line-height:1.3;padding-block:0.3125rem;padding-inline:0.5625rem;border-radius:var(--radius-sm);z-index:var(--z-float);opacity:0;visibility:hidden;pointer-events:none;transition:var(--transition)}\n.rs-tip:focus-within .rs-tip-bubble{opacity:1;visibility:visible;pointer-events:auto}\n.rs-tip-bubble::before{content:\"\";position:absolute;top:100%;inset-inline-start:0;inset-inline-end:0;height:0.5rem}\n@media (max-width: 640px){.rs-tip-bubble{font-size:0.8125rem;padding-block:0.5rem;padding-inline:0.75rem;border-radius:var(--radius-sm)}}\n@media (hover: hover) and (pointer: fine){.rs-tip:hover .rs-tip-bubble{opacity:1;visibility:visible;pointer-events:auto}}\n@media (hover: none){.rs-tip:active .rs-tip-bubble{opacity:1;visibility:visible}}\n@media (prefers-reduced-motion: reduce){.rs-tip-bubble{transition:none}}\n",
      "type": "registry:file",
      "target": "styles/vlak/tooltip.css"
    }
  ],
  "meta": {
    "vlak": {
      "category": "feedback",
      "classes": [
        "rs-tip",
        "rs-tip-bubble"
      ],
      "snippet": "<span class=\"rs-tip\"><button class=\"rs-btn-ghost rs-btn-sm\" aria-describedby=\"copy-tip\">Copy</button><span class=\"rs-tip-bubble\" role=\"tooltip\" id=\"copy-tip\">Copy to clipboard</span></span>",
      "cssOnly": false,
      "registryDependencies": [
        "button",
        "vlak-lib"
      ],
      "aliases": [
        "Tooltip",
        "Tip",
        "Hint"
      ],
      "example": "import { Button, Tooltip } from \"@noorddev/vlak-react\";\n\n<Tooltip tip=\"Copy to clipboard\">\n  <Button variant=\"ghost\" size=\"sm\" aria-label=\"Copy\">Copy</Button>\n</Tooltip>",
      "usage": {
        "use": [
          "A short label for an icon-only or terse control.",
          "Text that adds to the name, never text that replaces it."
        ],
        "avoid": [
          "Content that must be read to operate the control; put it in view.",
          "Rich content; use HoverCard."
        ]
      },
      "keyboard": [
        {
          "keys": "Tab",
          "does": "Focuses the trigger and shows the tip"
        },
        {
          "keys": "Escape",
          "does": "Hides the tip until the pointer leaves"
        }
      ],
      "a11y": [
        "The tip is a real element with role=\"tooltip\", linked to the trigger through aria-describedby.",
        "An element child is described directly (and given tabIndex when it cannot take focus); a text child makes the wrapper the trigger.",
        "Shown on hover and focus; the bubble stays hoverable."
      ]
    }
  }
}
