{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "scrollbar",
  "title": "Scrollbar",
  "description": "A scrollbar as a ruler: a hairline, a mark where each section begins, and an ink thumb as long as the view.",
  "registryDependencies": [
    "https://thedirectors.agency/ui/r/0nlytype.json"
  ],
  "files": [
    {
      "path": "registry/0nlytype/ui/scrollbar.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { useComposedRefs } from \"@/lib/0nlytype/refs\"\nimport { cn } from \"@/lib/utils\"\n\ndeclare global {\n  interface Window {\n    /** Optional page-scroll adapter: use immediate moves while the rail is dragged. Inner rails stay native. */\n    __otScrollTo?: (target: number, immediate: boolean) => void\n  }\n}\n\ntype ScrollbarSection = {\n  id: string\n  /** The numeral on the mark: \"II\". */\n  num: string\n  name: string\n  /** Pins the mark's place on the rail (0 to 1) instead of measuring it. For documentation. */\n  at?: number\n}\n\ntype ScrollbarOptions = {\n  /** inner: the ruler inside a scroller; page: fixed to the window; numeral: the thumb is how far you are, as a figure; leaves: a disc for every screenful. */\n  variant?: \"inner\" | \"page\" | \"numeral\" | \"leaves\"\n  /** Which way the host scrolls. Inner only: the page always scrolls down. */\n  axis?: \"y\" | \"x\"\n  /** The thumb is never shorter than this many px, so it can be held. */\n  min?: number\n  sections?: ScrollbarSection[]\n  /** Leave the rail as drawn (documentation pins its look). */\n  still?: boolean\n}\n\n/** One leaf's step down the rail, in px: .ot-scrollbar-leaf is 1.125rem tall. */\nconst LEAF = 18 // ponytail: assumes a 16px root; read it from the first leaf if a page ever sets its own\n\nconst reduced = () => matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n\n// A page can contain several rails. Read every rail before any of them changes styles,\n// including the section marks and leaves within each rail.\nconst measurements = new Set<() => () => void>()\nlet measurementFrame = 0\nfunction queueMeasurement(read: () => () => void) {\n  measurements.add(read)\n  if (measurementFrame) return\n  measurementFrame = requestAnimationFrame(() => {\n    measurementFrame = 0\n    const writes = [...measurements].map((measure) => measure())\n    measurements.clear()\n    writes.forEach((write) => write())\n  })\n}\n\n/** The nearest box around the rail that scrolls its way, or the rail's parent when none does (a dialog scrolls only once it is open). */\nfunction scroller(rail: HTMLElement, across: boolean) {\n  for (let p = rail.parentElement; p && p !== document.body && p !== document.documentElement; p = p.parentElement) {\n    if (/(auto|scroll)/.test(getComputedStyle(p)[across ? \"overflowX\" : \"overflowY\"])) return p\n  }\n  return rail.parentElement\n}\n\n/**\n * Returns the ref for the rail. Measures the rail's host (the nearest scroller around it, or the document for the page\n * variant) and drives the rail: --view (thumb length), --max (how far the rail travels), data-idle (nothing to\n * scroll) and data-ready. The thumb's travel itself is CSS, on the scroller's timeline.\n * Once ready the host says so (data-scrollbar-host), which hides the browser's own bar there and keeps smooth-scroll\n * libraries out of it (data-lenis-prevent, while there is something to scroll).\n */\nfunction useScrollbar({ variant = \"inner\", axis = \"y\", min = 24, sections = [], still = false }: ScrollbarOptions) {\n  const key = JSON.stringify(sections)\n  const [el, setEl] = React.useState<HTMLDivElement | null>(null)\n\n  React.useEffect(() => {\n    // The rail needs scroll-driven animation and a pointer; elsewhere the native bar stays.\n    if (!el || still || !matchMedia(\"(pointer: fine)\").matches || !CSS.supports(\"animation-timeline: scroll()\")) return\n    const page = variant === \"page\"\n    const across = axis === \"x\" && variant === \"inner\"\n    const host = page ? document.documentElement : scroller(el, across)\n    if (!host) return\n    const thumb = el.querySelector<HTMLElement>(\"[data-slot=scrollbar-thumb]\")!\n    const marks = [...el.querySelectorAll<HTMLElement>(\"[data-slot=scrollbar-mark]\")]\n    const leafRow = variant === \"leaves\" ? el.querySelector<HTMLElement>(\"[data-slot=scrollbar-leaves]\") : null\n    let leafStarts: number[] = []\n    const wanted = JSON.parse(key) as ScrollbarSection[]\n    const target = (i: number) => document.getElementById(wanted[i].id)\n    const rtl = () => getComputedStyle(host).direction === \"rtl\"\n    const seen = () => (page ? innerHeight : across ? host.clientWidth : host.clientHeight)\n    const whole = () => (across ? host.scrollWidth : host.scrollHeight)\n    const top = () => (page ? scrollY : across ? Math.abs(host.scrollLeft) : host.scrollTop)\n    // A section's offset from the top of the scrolled content (sideways, from its inline start: the right, in right-to-left).\n    const offset = (s: HTMLElement) => {\n      if (across) {\n        const r = s.getBoundingClientRect(), h = host.getBoundingClientRect()\n        return (rtl() ? h.right - r.right : r.left - h.left) + top()\n      }\n      return s.getBoundingClientRect().top - (page ? 0 : host.getBoundingClientRect().top) + top()\n    }\n    // A rail is absolute: its host must be a positioned box (unless it already is: a dialog is fixed).\n    const pin = !page && getComputedStyle(host).position === \"static\"\n    const hadPrevent = host.hasAttribute(\"data-lenis-prevent\")\n    // The layout is read when it can change (a resize, new content); a scroll only moves which mark you're in, from the\n    // offset the scroll event read while the page was still laid out. Scrolling reads no layout.\n    let geo: { shown: number; all: number; max: number; offsets: (number | null)[]; fit: number } | null = null\n    let at = 0\n    const place = () => {\n      if (!geo) return\n      const { shown, all, max, offsets, fit } = geo\n      // The mark for the section in the middle of the view is the one you're in.\n      const middle = at + shown / 2\n      let now = -1\n      offsets.forEach((y, i) => { if (y !== null && y <= middle) now = i })\n      marks.forEach((mark, i) => mark.toggleAttribute(\"data-now\", i === now))\n      if (leafRow) leaves(shown, all, max, fit, at)\n    }\n    const step = () => place\n    // The thumb's top travels (1 − view) of the rail, so a mark sits where the thumb's top\n    // will be when its section reaches the top. A section too near the end to reach the top sits as far on,\n    // inside the thumb's last stretch, where it will be in the view: the last few marks never land on one another.\n    const measure = () => {\n      const shown = seen(), all = page ? host.scrollHeight : whole(), max = all - shown\n      // The numeral's thumb is its figure, so it travels the rail less its own height.\n      const view = variant === \"numeral\" ? thumb.offsetHeight / el.clientHeight || 1 : Math.min(1, Math.max(shown / all, min / (page ? el.clientHeight : shown))) || 1 // a closed box measures 0 / 0\n      const direction = rtl() ? \"-1\" : \"1\"\n      const offsets = marks.map((_, i) => {\n        const s = target(i)\n        return s ? offset(s) : null\n      })\n      const fit = leafRow ? Math.max(1, Math.floor(el.clientHeight / LEAF)) : 1\n      at = top()\n      return () => {\n        geo = { shown, all, max, offsets, fit }\n        el.style.setProperty(\"--view\", String(view))\n        el.style.setProperty(\"--max\", `${max}px`)\n        el.style.setProperty(\"--dir\", direction) // a horizontal rail travels the other way in right-to-left\n        el.toggleAttribute(\"data-idle\", max < 1)\n        // Smooth scrolling leaves this box's wheel alone while it has something to scroll (a sideways one never needs it).\n        if (!page && !across && !hadPrevent) host.toggleAttribute(\"data-lenis-prevent\", max >= 1)\n        marks.forEach((mark, i) => {\n          const y = offsets[i]\n          if (y !== null) mark.style.setProperty(\"--at\", String(max > 0 ? (y / max) * (1 - view) : 0))\n        })\n        place()\n        el.setAttribute(\"data-ready\", \"\")\n        host.setAttribute(\"data-scrollbar-host\", pin ? \"pin\" : \"\")\n      }\n    }\n    // A disc for each screenful (a leaf), from the top. When they won't fit down the rail, a disc holds as many leaves as it must.\n    const leaves = (shown: number, all: number, max: number, fit: number, at: number) => {\n      const n = Math.max(1, Math.ceil(all / shown - 0.01))\n      const per = Math.ceil(n / fit), count = Math.ceil(n / per)\n      leafStarts = Array.from({ length: count }, (_, j) => Math.min(j * per * shown, max))\n      if (leafRow!.childElementCount !== count) {\n        leafRow!.replaceChildren(...leafStarts.map((_, j) => {\n          const leaf = document.createElement(\"span\")\n          leaf.setAttribute(\"data-slot\", \"scrollbar-leaf\")\n          leaf.setAttribute(\"data-leaf\", `${j + 1}/${count}`)\n          leaf.className = \"ot-scrollbar-leaf\"\n          return leaf\n        }))\n      }\n      let now = 0\n      leafStarts.forEach((y, j) => { if (y <= at + 1) now = j })\n      ;[...leafRow!.children].forEach((leaf, j) => {\n        leaf.toggleAttribute(\"data-read\", j < now)\n        leaf.toggleAttribute(\"data-now\", j === now)\n      })\n    }\n\n    const soon = () => queueMeasurement(measure)\n    const scrolled: EventTarget = page ? window : host\n    const resize = new ResizeObserver(soon)\n    resize.observe(el)\n    // Content that redraws changes what there is to scroll without resizing the box.\n    const content = new Set<Element>()\n    const watchContent = () => {\n      const children = [...host.children].filter((child) => child.getAttribute(\"data-slot\") !== \"scrollbar\")\n      content.forEach((child) => {\n        if (children.includes(child)) return\n        resize.unobserve(child)\n        content.delete(child)\n      })\n      children.forEach((child) => {\n        if (content.has(child)) return\n        resize.observe(child)\n        content.add(child)\n      })\n    }\n    const mutation = new MutationObserver(() => {\n      watchContent()\n      soon()\n    })\n    if (page) {\n      resize.observe(document.body)\n      addEventListener(\"resize\", soon)\n    } else {\n      resize.observe(host)\n      watchContent()\n      mutation.observe(host, { childList: true, subtree: true, characterData: true, attributes: true, attributeFilter: [\"open\"] })\n    }\n    const onScroll = () => {\n      at = top()\n      queueMeasurement(step)\n    }\n    if (marks.length || leafRow) scrolled.addEventListener(\"scroll\", onScroll, { passive: true })\n    soon()\n\n    const scrollToY = (y: number, instant: boolean) => {\n      const behavior = instant || reduced() ? \"instant\" : \"smooth\"\n      if (across) host.scrollTo({ left: rtl() ? -y : y, behavior })\n      else if (page && window.__otScrollTo) window.__otScrollTo(y, behavior === \"instant\")\n      else (page ? window : host).scrollTo({ top: y, behavior })\n    }\n    const onPointerDown = (e: PointerEvent) => {\n      if (e.button) return\n      e.preventDefault() // keep focus where it is\n      const mark = (e.target as HTMLElement).closest<HTMLElement>(\"[data-slot=scrollbar-mark]\")\n      if (mark) {\n        const s = target(marks.indexOf(mark))\n        if (s) scrollToY(offset(s), false)\n        return\n      }\n      // Leaves have no thumb: pressing a disc turns to its leaf.\n      if (leafRow) {\n        const leaf = (e.target as HTMLElement).closest(\"[data-slot=scrollbar-leaf]\")\n        const j = leaf ? [...leafRow.children].indexOf(leaf) : -1\n        if (j >= 0) scrollToY(leafStarts[j], false)\n        return\n      }\n      // Take the thumb where you grabbed it; press the rail and the thumb centres on the pointer.\n      const r = el.getBoundingClientRect(), t = thumb.getBoundingClientRect()\n      const grab = e.target === thumb ? (across ? e.clientX - t.left : e.clientY - t.top) : (across ? t.width : t.height) / 2\n      const follow = (ev: PointerEvent) => {\n        const max = (page ? host.scrollHeight : whole()) - seen()\n        let share = across ? (ev.clientX - r.left - grab) / (r.width - t.width) : (ev.clientY - r.top - grab) / (r.height - t.height)\n        if (across && rtl()) share = 1 - share\n        scrollToY(Math.max(0, Math.min(max, share * max)), true)\n      }\n      follow(e)\n      el.setPointerCapture(e.pointerId)\n      el.setAttribute(\"data-dragging\", \"\")\n      el.addEventListener(\"pointermove\", follow)\n      el.addEventListener(\"lostpointercapture\", () => {\n        el.removeEventListener(\"pointermove\", follow)\n        el.removeAttribute(\"data-dragging\")\n      }, { once: true })\n    }\n    el.addEventListener(\"pointerdown\", onPointerDown)\n\n    return () => {\n      measurements.delete(measure)\n      measurements.delete(step)\n      if (!measurements.size) { cancelAnimationFrame(measurementFrame); measurementFrame = 0 }\n      resize.disconnect()\n      mutation.disconnect()\n      removeEventListener(\"resize\", soon)\n      scrolled.removeEventListener(\"scroll\", onScroll)\n      el.removeEventListener(\"pointerdown\", onPointerDown)\n      el.removeAttribute(\"data-ready\")\n      host.removeAttribute(\"data-scrollbar-host\")\n      if (!hadPrevent) host.removeAttribute(\"data-lenis-prevent\")\n    }\n  }, [el, variant, axis, min, key, still])\n  return setEl\n}\n\ntype ScrollbarProps = Omit<React.ComponentProps<\"div\">, \"children\"> &\n  Omit<ScrollbarOptions, \"still\"> & {\n    /** Pins a look for documentation (\"hover\", \"active\") and leaves the rail as drawn: set --view and --p yourself. */\n    \"data-force\"?: string\n  }\n\n/**\n * A scrollbar as a ruler: a hairline and a thumb as long as the view. Place it inside any scroll container\n * (as its last child, or deeper: it finds the scroller around it) and it pins to that container's edge;\n * axis=\"x\" is the same rail along the bottom of a box that scrolls sideways; variant=\"page\" fixes it\n * to the window and measures the document. variant=\"numeral\" makes the thumb a figure, how far down you are\n * (00 to 100), riding the hairline on a dot; variant=\"leaves\" draws a disc for every screenful instead of a thumb:\n * read ones ink, the one you're on the accent, those to come rings. Both run down the side only.\n *\n * It is a pointer aid, hidden from assistive technology: keyboard scrolling stays native, and\n * the sections are marks on the rail that scroll to the element with that id. Anyone who can't point still needs\n * those sections elsewhere on the page (a table of contents).\n */\nfunction Scrollbar({ variant = \"inner\", axis = \"y\", min = variant === \"page\" ? 40 : 24, sections, className, \"data-force\": force, ref: forwardedRef, ...props }: ScrollbarProps) {\n  const rail = useScrollbar({ variant, axis, min, sections, still: force !== undefined })\n  const composedRef = useComposedRefs(rail, forwardedRef)\n  return (\n    <div ref={composedRef} data-force={force} data-slot=\"scrollbar\" data-variant={variant} data-axis={variant === \"inner\" && axis === \"x\" ? \"x\" : undefined} aria-hidden=\"true\" className={cn(\"ot-scrollbar\", className)} {...props}>\n      <span data-slot=\"scrollbar-thumb\" className=\"ot-scrollbar-thumb\" />\n      {variant === \"leaves\" && <span data-slot=\"scrollbar-leaves\" className=\"ot-scrollbar-leaves\" />}\n      {sections?.map((s, i) => (\n        <span key={s.id} data-slot=\"scrollbar-mark\" data-num={s.num} data-name={s.name} style={{ \"--i\": i, \"--at\": s.at } as React.CSSProperties} className=\"ot-scrollbar-mark\" />\n      ))}\n    </div>\n  )\n}\n\nexport { Scrollbar, useScrollbar, type ScrollbarProps, type ScrollbarSection }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/0nlytype/styles/scrollbar.css",
      "content": "@layer components {\n  /* ── ot-scrollbar ── a scrollbar as a ruler: a hairline and a thumb as long as the\n     view. Inside a scroller it is pinned to the edge by the scroller's own timeline;\n     data-axis=\"x\" runs it along the bottom of a box that scrolls sideways;\n     data-variant=\"page\" fixes it to the window and adds a mark at each section.\n     Pointing sketches; holding inks it in the accent. Script sets --view, --max,\n     data-idle (nothing to scroll) and data-ready. data-force pins a look for the docs:\n     the rail is drawn where --view and --p (0 to 1) put the thumb. */\n  .ot-scrollbar {\n    --view: 0.2; --max: 0px; --w: var(--ot-stroke); --line: 0.375rem; /* the hairline sits this far in from the edge */\n    position: absolute; z-index: 3; inset-block: 0; inset-inline-end: 0; width: 0.875rem;\n    display: none; touch-action: none; user-select: none;\n    /* The rail lives inside what scrolls, so it travels down as far as the content does, and stays put. */\n    animation: ot-scrollbar-pin linear both; animation-timeline: scroll(nearest);\n  }\n  /* The rail's host (the scroller around it) needs a position to pin to; the script says so when it has none (a class of yours can override this). */\n  [data-scrollbar-host=\"pin\"] { position: relative; }\n  @keyframes ot-scrollbar-pin { to { translate: 0 var(--max); } }\n  @keyframes ot-scrollbar-pin-x { to { translate: calc(var(--max) * var(--dir, 1)) 0; } }\n  .ot-scrollbar[data-variant=\"page\"] { --line: 0.5rem; position: fixed; z-index: 40; width: 1.5rem; animation: none; }\n  @supports (animation-timeline: scroll()) {\n    @media (pointer: fine) {\n      .ot-scrollbar[data-ready]:not([data-idle]) { display: block; }\n      [data-scrollbar-host] { scrollbar-width: none; }\n      [data-scrollbar-host]::-webkit-scrollbar { display: none; }\n    }\n  }\n  .ot-scrollbar::before {\n    content: \"\"; position: absolute; inset-block: 0; inset-inline-end: var(--line); width: var(--ot-hairline);\n    background: var(--ot-rule); transition: background-color var(--ot-allegro) var(--ot-exhale);\n  }\n  /* Sideways: the same rail along the bottom edge, travelling the other way in right-to-left. */\n  .ot-scrollbar[data-axis=\"x\"] { inset-block: auto 0; inset-inline: 0; width: auto; height: 0.875rem; animation-name: ot-scrollbar-pin-x; animation-timeline: scroll(nearest inline); }\n  .ot-scrollbar[data-axis=\"x\"]::before { inset-block: auto var(--line); inset-inline: 0; width: auto; height: var(--ot-hairline); }\n  .ot-scrollbar-thumb {\n    position: absolute; top: 0; inset-inline-end: var(--line); height: calc(var(--view) * 100%);\n    width: var(--w); margin-inline-end: calc((var(--ot-hairline) - var(--w)) / 2); /* centred on the hairline */\n    background: var(--ot-ink);\n    /* translate is a share of its own height: the rest of the rail is (1 / view − 1) thumbs long */\n    animation: ot-scrollbar-go linear both; animation-timeline: scroll(nearest);\n    transition: width var(--ot-allegro) var(--ot-spiccato), height var(--ot-allegro) var(--ot-spiccato), margin var(--ot-allegro) var(--ot-spiccato), background-color var(--ot-allegro) var(--ot-exhale);\n  }\n  .ot-scrollbar[data-axis=\"x\"] .ot-scrollbar-thumb {\n    top: auto; bottom: var(--line); inset-inline: 0 auto; width: calc(var(--view) * 100%); height: var(--w);\n    margin: 0; margin-block-end: calc((var(--ot-hairline) - var(--w)) / 2);\n    animation-name: ot-scrollbar-go-x; animation-timeline: scroll(nearest inline);\n  }\n  .ot-scrollbar[data-variant=\"page\"] .ot-scrollbar-thumb { animation-timeline: scroll(root); }\n  @keyframes ot-scrollbar-go { to { translate: 0 calc((1 / var(--view) - 1) * 100%); } }\n  @keyframes ot-scrollbar-go-x { to { translate: calc((1 / var(--view) - 1) * 100% * var(--dir, 1)) 0; } }\n  .ot-scrollbar[data-force] { display: block; animation: none; }\n  .ot-scrollbar[data-force] .ot-scrollbar-thumb { animation: none; translate: 0 calc(var(--p, 0) * (1 / var(--view) - 1) * 100%); }\n  .ot-scrollbar-mark {\n    position: absolute; top: calc(var(--at) * 100%); inset-inline: 0; height: 1rem; margin-top: -0.5rem; cursor: pointer;\n  }\n  .ot-scrollbar-mark::before {\n    content: \"\"; position: absolute; top: 50%; inset-inline-end: var(--line); width: 0.5rem; height: var(--ot-hairline);\n    background: var(--ot-rule-strong); transition: background-color var(--ot-allegro) var(--ot-exhale);\n  }\n  .ot-scrollbar-mark[data-now]::before { background: var(--ot-ink); }\n  .ot-scrollbar-mark::after {\n    content: attr(data-num); position: absolute; top: 50%; inset-inline-end: calc(100% + var(--ot-space-1));\n    translate: 0 -50%; padding-inline: 0.4em; white-space: nowrap; background: var(--ot-paper);\n    font-size: var(--ot-pp); line-height: 1; font-variant-numeric: tabular-nums; color: var(--ot-pencil);\n    opacity: 0; visibility: hidden;\n    transition: opacity var(--ot-allegro) var(--ot-exhale), visibility 0s var(--ot-allegro);\n  }\n  /* Sideways, a mark is a tick rising from the rail, its number above it. The number leans into the box as its mark\n     nears either end (by its share of the rail), so it is never cut off by the box's edge. */\n  .ot-scrollbar[data-axis=\"x\"] .ot-scrollbar-mark { inset-block: 0; inset-inline: calc(var(--at) * 100%) auto; width: 1rem; height: auto; margin: 0; margin-inline-start: -0.5rem; }\n  .ot-scrollbar[data-axis=\"x\"] .ot-scrollbar-mark::before { top: auto; bottom: var(--line); inset-inline: 50% auto; width: var(--ot-hairline); height: 0.5rem; }\n  .ot-scrollbar[data-axis=\"x\"] .ot-scrollbar-mark::after { top: auto; bottom: 100%; inset-inline: 50% auto; translate: calc(var(--at) * -100% * var(--dir, 1)) 0; }\n  /* A box is short, so down its side a number leans into it as its mark nears the top or bottom (by its share of the\n     rail), the way the sideways ones do: the first is never cut off by the box's top edge. The page's rail is tall enough. */\n  .ot-scrollbar:not([data-variant=\"page\"], [data-axis]) .ot-scrollbar-mark::after { translate: 0 calc(var(--at) * -100%); }\n  /* Pointing sketches: pointing at what scrolls darkens the rail; pointing at the rail thickens the thumb\n     and brings the page's numbers down the rail in turn. */\n  :hover > .ot-scrollbar::before, .ot-scrollbar:is(:hover, [data-dragging], [data-force~=\"hover\"], [data-force~=\"active\"])::before { background: var(--ot-rule-strong); }\n  .ot-scrollbar:is(:hover, [data-dragging], [data-force~=\"hover\"], [data-force~=\"active\"]) { --w: 4px; }\n  .ot-scrollbar:is(:hover, [data-force~=\"hover\"], [data-force~=\"active\"]) .ot-scrollbar-mark::after {\n    opacity: 1; visibility: visible; transition-delay: calc(var(--i, 0) * var(--ot-arpeggio)), 0s;\n  }\n  .ot-scrollbar-mark:hover::after { content: attr(data-num) \"\\2002\" attr(data-name); color: var(--ot-ink); }\n  .ot-scrollbar-mark[data-num=\"\"]:hover::after { content: attr(data-name); } /* a section without a numeral goes by its name */\n  .ot-scrollbar-mark:hover::before { background: var(--ot-ink); }\n  /* Holding inks it: the thumb is where you are, so it takes the one accent. */\n  .ot-scrollbar:is([data-dragging], [data-force~=\"active\"]) .ot-scrollbar-thumb { background: var(--ot-accent); }\n  .ot-scrollbar[data-dragging] { cursor: grabbing; } /* pointer capture keeps it while the hand is down */\n\n  /* Numeral, after \"the silence that heals\" (14 / 08), where numbers are the data: the thumb is a figure, how far down\n     you are from 00 to 100, riding the hairline on a dot. It counts through every number as the box scrolls: a counter on\n     a registered number that the scroller's own timeline drives, so it keeps time with any smooth scrolling. */\n  @property --ot-scrollbar-read { syntax: \"<number>\"; inherits: true; initial-value: 0; }\n  @property --ot-scrollbar-n { syntax: \"<integer>\"; inherits: false; initial-value: 0; }\n  .ot-scrollbar[data-variant=\"numeral\"] { width: 3.25rem; }\n  .ot-scrollbar[data-variant=\"numeral\"] .ot-scrollbar-thumb { inset-inline-start: 0; width: auto; height: 1.5rem; margin: 0; background: none; animation-name: ot-scrollbar-read; }\n  @keyframes ot-scrollbar-read { to { translate: 0 calc((1 / var(--view) - 1) * 100%); --ot-scrollbar-read: 1; } }\n  .ot-scrollbar[data-variant=\"numeral\"][data-force] .ot-scrollbar-thumb { --ot-scrollbar-read: var(--p, 0); }\n  .ot-scrollbar[data-variant=\"numeral\"] .ot-scrollbar-thumb::before {\n    content: \"\"; position: absolute; top: 50%; inset-inline-end: calc(var(--ot-hairline) / 2 - 0.1875rem); width: 0.375rem; height: 0.375rem;\n    translate: 0 -50%; border-radius: 50%; background: var(--ot-ink); transition: background-color var(--ot-allegro) var(--ot-exhale), scale var(--ot-allegro) var(--ot-spiccato);\n  }\n  .ot-scrollbar[data-variant=\"numeral\"] .ot-scrollbar-thumb::after {\n    --ot-scrollbar-n: round(var(--ot-scrollbar-read) * 100, 1); counter-reset: ot-scrollbar-read var(--ot-scrollbar-n); content: counter(ot-scrollbar-read, decimal-leading-zero);\n    position: absolute; inset-block: 0; inset-inline-end: calc(0.1875rem + var(--ot-space-2)); line-height: 1.5rem; font-size: var(--ot-p); letter-spacing: var(--ot-p-tr);\n    font-variant-numeric: tabular-nums; color: var(--ot-pencil); transition: color var(--ot-allegro) var(--ot-exhale);\n  }\n  .ot-scrollbar[data-variant=\"numeral\"]:is(:hover, [data-dragging], [data-force~=\"hover\"], [data-force~=\"active\"]) .ot-scrollbar-thumb::after { color: var(--ot-ink); }\n  .ot-scrollbar[data-variant=\"numeral\"]:is(:hover, [data-dragging], [data-force~=\"hover\"], [data-force~=\"active\"]) .ot-scrollbar-thumb::before { scale: 1.34; }\n  .ot-scrollbar[data-variant=\"numeral\"]:is([data-dragging], [data-force~=\"active\"]) .ot-scrollbar-thumb { background: none; }\n  .ot-scrollbar[data-variant=\"numeral\"]:is([data-dragging], [data-force~=\"active\"]) .ot-scrollbar-thumb::before { background: var(--ot-accent); }\n  .ot-scrollbar[data-variant=\"numeral\"]:is([data-dragging], [data-force~=\"active\"]) .ot-scrollbar-thumb::after { color: var(--ot-accent); }\n  /* Numbers of the section marks stand clear of the figure. */\n  .ot-scrollbar[data-variant=\"numeral\"] .ot-scrollbar-mark::after { inset-inline-end: calc(100% + var(--ot-space-1)); }\n\n  /* Leaves, after the \"Less but better\" calendar: a disc for every screenful the box holds, down from the top of its rail.\n     Those you've read are ink, the one you're on is the one accent, those to come are hairline rings. There is no thumb:\n     press a disc and the box turns to that leaf. The script lays the discs and marks data-read and data-now as you scroll. */\n  .ot-scrollbar[data-variant=\"leaves\"] { width: 1.5rem; --line: 0.5rem; }\n  .ot-scrollbar[data-variant=\"leaves\"]::before, .ot-scrollbar[data-variant=\"leaves\"] .ot-scrollbar-thumb { display: none; }\n  .ot-scrollbar-leaves { position: absolute; inset: 0; display: grid; align-content: start; }\n  .ot-scrollbar-leaf { position: relative; height: 1.125rem; cursor: pointer; }\n  .ot-scrollbar-leaf::before {\n    content: \"\"; position: absolute; top: 50%; inset-inline-end: calc(var(--line) + var(--ot-hairline) / 2 - 0.3125rem); width: 0.625rem; height: 0.625rem;\n    translate: 0 -50%; box-sizing: border-box; border-radius: 50%; border: var(--ot-hairline) solid var(--ot-rule-strong);\n    transition: background-color var(--ot-allegro) var(--ot-exhale), border-color var(--ot-allegro) var(--ot-exhale), scale var(--ot-allegro) var(--ot-spiccato);\n  }\n  .ot-scrollbar-leaf[data-read]::before { background: var(--ot-ink); border-color: var(--ot-ink); }\n  .ot-scrollbar-leaf[data-now]::before { background: var(--ot-accent); border-color: var(--ot-accent); }\n  .ot-scrollbar-leaf:hover::before { scale: 1.2; }\n  .ot-scrollbar-leaf::after {\n    content: attr(data-leaf); position: absolute; top: 50%; inset-inline-end: calc(100% + var(--ot-space-1)); translate: 0 -50%;\n    padding-inline: 0.4em; white-space: nowrap; background: var(--ot-paper); font-size: var(--ot-pp); line-height: 1; font-variant-numeric: tabular-nums; color: var(--ot-ink);\n    opacity: 0; visibility: hidden; transition: opacity var(--ot-allegro) var(--ot-exhale), visibility 0s var(--ot-allegro);\n  }\n  .ot-scrollbar-leaf:hover::after { opacity: 1; visibility: visible; transition-delay: 0s; }\n\n  /* Forced colours paint over backgrounds, which is how the thumb, the dot and the discs are drawn: keep them, in system colours. */\n  @media (forced-colors: active) {\n    .ot-scrollbar { forced-color-adjust: none; --ot-paper: Canvas; --ot-ink: CanvasText; --ot-pencil: GrayText; --ot-rule: GrayText; --ot-rule-strong: GrayText; --ot-accent: Highlight; }\n  }\n}\n",
      "type": "registry:file",
      "target": "styles/0nlytype/scrollbar.css"
    }
  ],
  "css": {
    "@import \"../styles/0nlytype/scrollbar.css\"": {}
  },
  "meta": {
    "movement": "IX",
    "underneath": "hook",
    "contract": "ot-scrollbar"
  },
  "type": "registry:ui"
}
