{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "hover-card",
  "title": "Hover card",
  "description": "Point at a name and wait a moment: a card opens with the name set large and cropped by its edge.",
  "dependencies": [
    "@radix-ui/react-hover-card"
  ],
  "registryDependencies": [
    "https://thedirectors.agency/ui/r/0nlytype.json"
  ],
  "files": [
    {
      "path": "registry/0nlytype/ui/hover-card.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as HoverCardPrimitive from \"@radix-ui/react-hover-card\"\n\nimport { cn } from \"@/lib/utils\"\n\n/** Waits 450ms for a still pointer; the keyboard gets it at once. */\nfunction HoverCard({ openDelay = 450, ...props }: React.ComponentProps<typeof HoverCardPrimitive.Root>) {\n  return <HoverCardPrimitive.Root data-slot=\"hover-card\" openDelay={openDelay} {...props} />\n}\n\nfunction HoverCardTrigger(props: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) {\n  return <HoverCardPrimitive.Trigger data-slot=\"hover-card-trigger\" {...props} />\n}\n\n/**\n * A small card. name (the default): put a HoverCardName first and the name is set large and cut by\n * the card's top edge. entry: a dictionary entry for the word you point at, its headword broken at\n * its syllables. quote: what the person said, the opening mark hung in the margin.\n */\nfunction HoverCardContent({\n  className,\n  variant = \"name\",\n  align = \"start\",\n  sideOffset = 12,\n  collisionPadding = 20,\n  ...props\n}: React.ComponentProps<typeof HoverCardPrimitive.Content> & { variant?: \"name\" | \"entry\" | \"quote\" }) {\n  return (\n    <HoverCardPrimitive.Portal>\n      <HoverCardPrimitive.Content\n        data-slot=\"hover-card-content\"\n        data-variant={variant === \"name\" ? undefined : variant}\n        align={align}\n        sideOffset={sideOffset}\n        collisionPadding={collisionPadding}\n        className={cn(\"ot-peek\", className)}\n        {...props}\n      />\n    </HoverCardPrimitive.Portal>\n  )\n}\n\n/**\n * The name, set large; the card's edge crops it. Decorative, so screen readers skip it. Given a\n * string with syllable points (\"gro·ˈtesque\"), it breaks there, as a dictionary's headword does, and\n * the syllable after the stress mark (ˈ) is reversed out of the ink.\n */\nfunction HoverCardName({ className, children, ...props }: React.ComponentProps<\"span\">) {\n  const syllables = typeof children === \"string\" && /[·ˈ]/.test(children) ? children.split(\"·\") : null\n  return (\n    <span data-slot=\"hover-card-name\" aria-hidden=\"true\" className={cn(\"ot-peek-name\", className)} {...props}>\n      {syllables\n        ? syllables.map((s, i) => (\n            <React.Fragment key={i}>\n              {i > 0 && <span className=\"ot-peek-point\" style={{ \"--i\": i } as React.CSSProperties} />}\n              {s.startsWith(\"ˈ\") ? <span className=\"ot-peek-stress\">{s.slice(1)}</span> : s}\n            </React.Fragment>\n          ))\n        : children}\n    </span>\n  )\n}\n\nfunction HoverCardMeta({ className, ...props }: React.ComponentProps<\"span\">) {\n  return <span data-slot=\"hover-card-meta\" className={cn(\"ot-peek-meta\", className)} {...props} />\n}\n\nexport { HoverCard, HoverCardTrigger, HoverCardContent, HoverCardName, HoverCardMeta }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/0nlytype/styles/hover-card.css",
      "content": "@layer components {\n  /* ── ot-peek ── hover card: the name, set large and cut by the card's top edge and its end,\n     the way POINT and SPECTRA crop their titles. Radix places it and waits for the pointer;\n     focus opens it at once. */\n  .ot-peek {\n    z-index: 50; display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--ot-space-2); width: min(21rem, 80vw); padding: var(--ot-space-5) var(--ot-space-5) var(--ot-space-4); overflow: clip;\n    background: var(--ot-paper); box-shadow: inset 0 0 0 var(--ot-hairline) var(--ot-rule-strong);\n    font-size: var(--ot-pp); line-height: var(--ot-pp-lh); letter-spacing: var(--ot-pp-tr); color: var(--ot-graphite);\n    animation: ot-peek-in var(--ot-moderato) var(--ot-exhale);\n  }\n  .ot-peek[data-state=\"closed\"] { animation: ot-peek-out var(--ot-allegro) var(--ot-breath) forwards; }\n  @keyframes ot-peek-in { from { opacity: 0; } }\n  @keyframes ot-peek-out { to { opacity: 0; } }\n  /* The top edge falls through the ascenders, so the word reads from its x-height down; a long name\n     also runs off the end. The card's paper covers what the crop takes. */\n  .ot-peek-name {\n    margin-block-start: calc(-1 * var(--ot-space-5) - 0.3em); margin-inline-end: -30%; white-space: nowrap; user-select: none; /* decorative and cropped: a selection would only paint a slab of yellow */\n    font-size: calc(var(--ot-ff) * 1.25); line-height: 0.85; letter-spacing: var(--ot-ff-tr); font-weight: 200; color: var(--ot-ink);\n  }\n  .ot-peek[data-state=\"open\"] .ot-peek-name { animation: ot-peek-slide var(--ot-andante) var(--ot-exhale); }\n  @keyframes ot-peek-slide { from { translate: 0.5em 0; } }\n  .ot-peek[data-state=\"open\"]:is([dir=\"rtl\"], [dir=\"rtl\"] *) .ot-peek-name { animation-name: ot-peek-slide-rtl; }\n  @keyframes ot-peek-slide-rtl { from { translate: -0.5em 0; } }\n  .ot-peek-meta { color: var(--ot-pencil); }\n\n  /* ── entry ── a dictionary entry for the word you point at, after \"the uncreative\": the headword\n     heavy and whole, broken at its syllables by points; the stressed syllable reversed out of the\n     ink; the part of speech in the italic, as a dictionary sets it; the senses numbered in bold\n     figures hung in the margin; the word's history last, in pencil. */\n  .ot-peek[data-variant=\"entry\"] { gap: var(--ot-space-3); padding-block-start: var(--ot-space-4); font-size: var(--ot-p); line-height: var(--ot-p-lh); letter-spacing: 0; }\n  .ot-peek[data-variant=\"entry\"] .ot-peek-name {\n    margin: 0; white-space: normal; user-select: none;\n    font-size: var(--ot-mf); line-height: var(--ot-mf-lh); letter-spacing: var(--ot-mf-tr); font-weight: 800;\n  }\n  .ot-peek-point { display: inline-block; width: 0.42em; height: 0.42em; vertical-align: 0.04em; }\n  .ot-peek-point::before { content: \"\"; display: block; width: 0.14em; height: 0.14em; margin: 0.14em auto; border-radius: 50%; background: currentColor; }\n  .ot-peek-stress { padding-inline: 0.06em; background: var(--ot-ink); color: var(--ot-paper); }\n  .ot-peek[data-variant=\"entry\"] :is(ol, .ot-term) { margin: 0; }\n  .ot-peek[data-variant=\"entry\"] .ot-term { color: var(--ot-ink); }\n  .ot-peek[data-variant=\"entry\"] ol { display: grid; gap: var(--ot-space-2); padding: 0; list-style: none; counter-reset: ot-sense; }\n  .ot-peek[data-variant=\"entry\"] li { counter-increment: ot-sense; position: relative; }\n  .ot-peek[data-variant=\"entry\"] li::before { content: counter(ot-sense) / \"\"; position: absolute; inset-inline-end: calc(100% + var(--ot-space-2)); font-weight: 800; color: var(--ot-ink); font-variant-numeric: lining-nums; }\n  .ot-peek[data-variant=\"entry\"] .ot-peek-meta { font-size: var(--ot-pp); line-height: var(--ot-pp-lh); }\n  /* Spelling out: the headword arrives closed up and opens at its breaks; each point lands in turn. */\n  .ot-peek[data-variant=\"entry\"][data-state=\"open\"] .ot-peek-name { animation: none; }\n  .ot-peek[data-variant=\"entry\"][data-state=\"open\"] .ot-peek-point { animation: ot-peek-open var(--ot-moderato) var(--ot-exhale) calc(var(--ot-arpeggio) * var(--i, 1)) backwards; }\n  .ot-peek[data-variant=\"entry\"][data-state=\"open\"] .ot-peek-point::before { animation: ot-peek-land var(--ot-moderato) var(--ot-spiccato) calc(var(--ot-allegro) + var(--ot-arpeggio) * var(--i, 1)) backwards; }\n  @keyframes ot-peek-open { from { width: 0; } }\n  @keyframes ot-peek-land { from { scale: 0; } }\n\n  /* ── quote ── what the person said, in the italic (their words, not ours), with the opening mark\n     hung in the margin at the size of a title and cut by the card's top edge. The attribution is ours. */\n  .ot-peek[data-variant=\"quote\"] { gap: var(--ot-space-3); padding-inline-start: var(--ot-space-7); }\n  .ot-peek[data-variant=\"quote\"] blockquote {\n    position: relative; margin: 0;\n    font-family: var(--ot-expression); font-style: italic; font-size: calc(var(--ot-expression-scale) * var(--ot-mp)); line-height: 1.25; letter-spacing: 0; color: var(--ot-ink);\n  }\n  .ot-peek[data-variant=\"quote\"] blockquote::before {\n    content: \"\\201C\" / \"\"; position: absolute; inset-inline-end: calc(100% + 0.04em); top: -0.3em;\n    font-size: calc(var(--ot-ff) * 1.6); line-height: 1; color: var(--ot-ink); user-select: none;\n  }\n  .ot-peek[data-variant=\"quote\"][data-state=\"open\"] blockquote::before { animation: ot-peek-mark var(--ot-andante) var(--ot-spiccato) backwards; }\n  @keyframes ot-peek-mark { from { opacity: 0; translate: 0 -0.2em; } }\n\n  @media (forced-colors: active) {\n    .ot-peek { outline: var(--ot-hairline) solid CanvasText; outline-offset: calc(-1 * var(--ot-hairline)); } /* the hairline is a box-shadow, which forced colours drop */\n    .ot-peek-stress, .ot-peek-point::before { forced-color-adjust: none; background: CanvasText; color: Canvas; }\n  }\n}\n",
      "type": "registry:file",
      "target": "styles/0nlytype/hover-card.css"
    }
  ],
  "css": {
    "@import \"../styles/0nlytype/hover-card.css\"": {}
  },
  "meta": {
    "movement": "IX",
    "underneath": "radix",
    "contract": "ot-peek"
  },
  "type": "registry:ui"
}
