{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "sheet",
  "title": "Sheet",
  "description": "A page slid in from the edge, its title running up the spine like a book's; or a row that unfolds into its page.",
  "dependencies": [
    "@radix-ui/react-slot"
  ],
  "registryDependencies": [
    "https://thedirectors.agency/ui/r/0nlytype.json",
    "https://thedirectors.agency/ui/r/dialog.json"
  ],
  "files": [
    {
      "path": "registry/0nlytype/ui/sheet.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { Slot } from \"@radix-ui/react-slot\"\n\nimport { useComposedRefs } from \"@/lib/0nlytype/refs\"\nimport { cn } from \"@/lib/utils\"\nimport { Dialog, DialogActions, DialogClose, DialogSurface, DialogTrigger, useDialog } from \"@/components/ui/dialog\"\n\n/** What opened the sheet, so a fold can grow out of it. */\nconst Opener = React.createContext<React.RefObject<HTMLElement | null> | null>(null)\n\n/** A page slid in from the edge. Same props and behaviour as Dialog: the native <dialog> keeps focus, Escape and the top layer. */\nfunction Sheet(props: React.ComponentProps<typeof Dialog>) {\n  const openerRef = React.useRef<HTMLElement | null>(null)\n  return (\n    <Opener.Provider value={openerRef}>\n      <Dialog {...props} />\n    </Opener.Provider>\n  )\n}\n\n/** Opens the sheet. It remembers itself, so a fold sheet grows from it (a Row, through `<Row asChild><SheetTrigger>`). */\nfunction SheetTrigger({ onClick, ...props }: React.ComponentProps<typeof DialogTrigger>) {\n  const openerRef = React.useContext(Opener)\n  return (\n    <DialogTrigger\n      {...props}\n      onClick={(e) => {\n        if (openerRef) openerRef.current = e.currentTarget\n        onClick?.(e)\n      }}\n    />\n  )\n}\n\nconst SheetClose = DialogClose\n\ntype SheetContentProps = React.ComponentProps<\"dialog\"> & {\n  /** Which edge it slides from. Logical, so start is the right edge in right-to-left. */\n  side?: \"start\" | \"end\" | \"top\" | \"bottom\"\n  /**\n   * spine: the title runs up the spine, in its own column. shelf: a sheet opened from inside it stands in\n   * front, one spine narrower, so this one's spine stays in view like a book on a shelf, and is the way back.\n   * rag: the text is set flush to the window's edge, and the paper is cut to the rag of its lines.\n   * fold: the row that opened it unfolds into the page: its two hairlines part to the window's edges and its\n   * name grows into the page's title. Put it away and the page folds back into the row.\n   */\n  variant?: \"spine\" | \"shelf\" | \"rag\" | \"fold\"\n}\n\ntype Rag = { w: number; h: number; fill: string; line: string }\n\n/**\n * Cuts the paper to the words: every line box in the sheet, merged into rows, stepped out by the padding on\n * the side that faces the page. Measured from the live layout (the fonts, the pair and the width decide the\n * rag), so it re-cuts on resize and when <html> changes its face.\n */\nfunction useRag(probe: React.RefObject<HTMLElement | null>, on: boolean) {\n  const { open } = useDialog()\n  const [rag, setRag] = React.useState<Rag | null>(null)\n\n  React.useEffect(() => {\n    const d = probe.current?.closest(\"dialog\")\n    if (!on || !open || !d) return\n    let frame = 0\n    const cut = () => {\n      const box = d.getBoundingClientRect()\n      const style = getComputedStyle(d)\n      const pad = parseFloat(style.paddingInlineEnd) || 40\n      // Which physical edge it stands on: end is the right in left-to-right, start is the right in right-to-left.\n      const right = (d.dataset.side !== \"start\") !== (style.direction === \"rtl\")\n      const rows: { top: number; bottom: number; x: number }[] = []\n      const walk = document.createTreeWalker(d, NodeFilter.SHOW_TEXT, {\n        acceptNode: (n) => (n.parentElement?.closest(\"svg, .ot-scrollbar, .ot-sr\") || !n.textContent?.trim() ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT),\n      })\n      const range = document.createRange()\n      for (let n = walk.nextNode(); n; n = walk.nextNode()) {\n        range.selectNodeContents(n)\n        for (const r of range.getClientRects()) {\n          if (r.width < 1) continue\n          rows.push({ top: r.top - box.top + d.scrollTop, bottom: r.bottom - box.top + d.scrollTop, x: (right ? r.left : r.right) - box.left })\n        }\n      }\n      if (!rows.length) return\n      rows.sort((a, b) => a.top - b.top)\n      const merged = [rows[0]]\n      for (const r of rows.slice(1)) {\n        const last = merged[merged.length - 1]\n        // Same line when its middle falls inside the last: tight leading makes the next line's box overlap this one.\n        if ((r.top + r.bottom) / 2 < last.bottom) Object.assign(last, { bottom: Math.max(last.bottom, r.bottom), x: right ? Math.min(last.x, r.x) : Math.max(last.x, r.x) })\n        else merged.push({ ...r })\n      }\n      const w = d.clientWidth\n      const h = d.scrollHeight\n      const at = (x: number) => Math.round(Math.min(w, Math.max(0, right ? x - pad : x + pad)))\n      const edge = right ? w : 0\n      let line = `M${at(merged[0].x)} 0`\n      merged.forEach((r, i) => {\n        const next = merged[i + 1]\n        const y = next ? Math.round((r.bottom + next.top) / 2) : h - 1\n        line += ` V${y}`\n        if (next) line += ` H${at(next.x)}`\n      })\n      line += ` H${edge}`\n      setRag({ w, h, line, fill: `M${edge} 0 H${at(merged[0].x)}${line.slice(line.indexOf(\" \"))} Z` })\n      d.setAttribute(\"data-laid\", \"\")\n    }\n    const soon = () => {\n      cancelAnimationFrame(frame)\n      frame = requestAnimationFrame(cut)\n    }\n    document.fonts.ready.then(soon)\n    const resized = new ResizeObserver(soon)\n    resized.observe(d)\n    const restyled = new MutationObserver(soon)\n    restyled.observe(document.documentElement, { attributes: true })\n    return () => {\n      cancelAnimationFrame(frame)\n      resized.disconnect()\n      restyled.disconnect()\n    }\n  }, [probe, on, open])\n\n  return rag\n}\n\nconst ms = (v: string) => (v.trim().endsWith(\"ms\") ? parseFloat(v) : parseFloat(v) * 1000) || 0\n\n/**\n * Fold: the page grows out of what opened it. Before the dialog opens (and again before it closes) the opener's\n * box is written to --fold-t/r/b/l, so the clip and the two hairlines open out from the row and fold back to it\n * in CSS. The title flies between the row's name and its own place by a transform, measured both ways.\n */\nfunction useFold(probe: React.RefObject<HTMLElement | null>, on: boolean) {\n  const { open } = useDialog()\n  const openerRef = React.useContext(Opener)\n  const flip = React.useCallback((d: HTMLElement, from: HTMLElement | null, title: HTMLElement | null) => {\n    if (!from || !title) return null\n    const a = from.getBoundingClientRect()\n    const b = title.getBoundingClientRect()\n    const fa = getComputedStyle(from)\n    const fb = getComputedStyle(title)\n    const s = parseFloat(fa.fontSize) / parseFloat(fb.fontSize)\n    const lh = (st: CSSStyleDeclaration) => parseFloat(st.lineHeight) || parseFloat(st.fontSize) * 1.2\n    const rtl = fb.direction === \"rtl\"\n    title.style.transformOrigin = rtl ? \"100% 0\" : \"0 0\"\n    // The first lines' middles meet, whatever each one's leading.\n    const dx = rtl ? a.right - b.right : a.left - b.left\n    const dy = a.top + lh(fa) / 2 - (b.top + (lh(fb) * s) / 2)\n    const style = getComputedStyle(d)\n    return {\n      transform: `translate(${dx}px, ${dy}px) scale(${s})`,\n      duration: ms(style.getPropertyValue(\"--ot-andante\")),\n      easing: style.getPropertyValue(\"--ot-exhale\").trim() || \"ease-out\",\n    }\n  }, [])\n\n  // Before showModal and before close: where the row is now, and whether it stands reversed out of ink.\n  React.useLayoutEffect(() => {\n    const d = probe.current?.closest(\"dialog\")\n    if (!on || !d) return\n    const from = openerRef?.current ?? null\n    const r = from?.isConnected ? from.getBoundingClientRect() : null\n    const px = (v: number) => `${Math.round(Math.max(0, v))}px`\n    for (const [k, v] of Object.entries(r ? { t: r.top, r: innerWidth - r.right, b: innerHeight - r.bottom, l: r.left } : {})) d.style.setProperty(`--fold-${k}`, px(v))\n    d.toggleAttribute(\"data-from-ink\", !!from?.matches('.ot-rows:not([data-variant]) .ot-rows-link:is(:hover, :focus-visible)'))\n    // Closing (the page still laid out, before close() or while Escape's close plays): the title flies back.\n    const title = d.querySelector<HTMLElement>(\".ot-sheet-title\")\n    if (open || !title?.getClientRects().length) return\n    const f = flip(d, from?.querySelector<HTMLElement>(\"[data-slot=rows-title]\") ?? from, title)\n    if (f && title && !matchMedia(\"(prefers-reduced-motion: reduce)\").matches) title.animate([{ transform: \"none\" }, { transform: f.transform }], { duration: f.duration, easing: f.easing, fill: \"forwards\" })\n  }, [probe, on, open, openerRef, flip])\n\n  // Opened: the row's name grows into the title.\n  React.useEffect(() => {\n    const d = probe.current?.closest(\"dialog\")\n    const title = d?.querySelector<HTMLElement>(\".ot-sheet-title\") ?? null\n    if (!on || !open || !d || !title) return\n    title.getAnimations().forEach((a) => a.cancel())\n    if (matchMedia(\"(prefers-reduced-motion: reduce)\").matches) return\n    const from = openerRef?.current ?? null\n    const f = flip(d, from?.isConnected ? (from.querySelector<HTMLElement>(\"[data-slot=rows-title]\") ?? from) : null, title)\n    if (f) title.animate([{ transform: f.transform }, { transform: \"none\" }], { duration: f.duration, easing: f.easing })\n  }, [probe, on, open, openerRef, flip])\n}\n\nfunction SheetContent({ className, side = \"end\", variant = \"spine\", children, onClick, ...props }: SheetContentProps) {\n  const { setOpen } = useDialog()\n  const probe = React.useRef<HTMLSpanElement>(null) // DialogSurface keeps the dialog's own ref; the rag and the fold find it from here\n  const ragged = variant === \"rag\" && (side === \"start\" || side === \"end\")\n  const rag = useRag(probe, ragged)\n  useFold(probe, variant === \"fold\")\n  return (\n    <DialogSurface\n      data-slot=\"sheet-content\"\n      data-side={side}\n      data-variant={variant === \"spine\" ? undefined : variant}\n      className={cn(\"ot-sheet\", className)}\n      onClick={(e) => {\n        onClick?.(e)\n        // A rag sheet's box is wider than its paper: a click on the bare part is a click outside.\n        if (ragged && e.target === e.currentTarget) setOpen(false)\n      }}\n      {...props}\n    >\n      {children}\n      {ragged && rag && (\n        <svg className=\"ot-sheet-rag\" aria-hidden=\"true\" width={rag.w} height={rag.h} viewBox={`0 0 ${rag.w} ${rag.h}`}>\n          <path className=\"ot-sheet-rag-paper\" d={rag.fill} />\n          <path className=\"ot-sheet-rag-edge\" d={rag.line} pathLength={1} />\n        </svg>\n      )}\n      {(ragged || variant === \"fold\") && <span ref={probe} hidden />}\n    </DialogSurface>\n  )\n}\n\ntype PanelsContextValue = { path: string[]; root: string; go: (to: string, from: HTMLElement | null) => void }\nconst PanelsContext = React.createContext<PanelsContextValue | null>(null)\n\nfunction usePanels() {\n  const ctx = React.useContext(PanelsContext)\n  if (!ctx) throw new Error(\"SheetPanel and SheetPanelLink must sit inside <SheetPanels>.\")\n  return ctx\n}\n\ntype SheetPanelsProps = Omit<React.ComponentProps<\"div\">, \"defaultValue\"> & {\n  /** The panel it opens on, the root of the trail. Each opening of the sheet starts here again. */\n  defaultValue: string\n  /** Called with the panel you arrive at. */\n  onValueChange?: (value: string) => void\n}\n\n/**\n * Pages inside one sheet, the chapters of the book on its spine. Stepping into a panel writes its name up the\n * spine, large, and the name you left shrinks into the pencil below it; the names below are the way back. The\n * panels must be direct children: their titles are read from them to set the trail.\n */\nfunction SheetPanels({ defaultValue, onValueChange, className, children, ref: forwardedRef, ...props }: SheetPanelsProps) {\n  const { open } = useDialog()\n  const [path, setPath] = React.useState([defaultValue])\n  const [step, setStep] = React.useState<\"in\" | \"out\">(\"in\")\n  const openers = React.useRef<(HTMLElement | null)[]>([])\n  const host = React.useRef<HTMLDivElement>(null)\n  const composedRef = useComposedRefs(host, forwardedRef)\n  const titles = new Map<string, React.ReactNode>()\n  React.Children.forEach(children, (c) => {\n    if (React.isValidElement<SheetPanelProps>(c) && c.props.value) titles.set(c.props.value, c.props.title)\n  })\n\n  // Put away, it opens on the root next time.\n  if (!open && path.length > 1) setPath([defaultValue])\n\n  const arrive = (next: string[], dir: \"in\" | \"out\", focus: () => HTMLElement | null | undefined) => {\n    setStep(dir)\n    setPath(next)\n    onValueChange?.(next[next.length - 1])\n    requestAnimationFrame(() => {\n      host.current?.closest(\"dialog\")?.scrollTo({ top: 0 })\n      focus()?.focus({ preventScroll: true })\n    })\n  }\n  const heading = () => host.current?.querySelector<HTMLElement>(\".ot-sheet-panel:not([hidden]) > .ot-sheet-panel-title\")\n  const go = (to: string, from: HTMLElement | null) => {\n    if (to === path[path.length - 1] || !titles.has(to)) return\n    openers.current[path.length - 1] = from\n    arrive([...path, to], \"in\", heading)\n  }\n  const back = (depth: number) => arrive(path.slice(0, depth + 1), \"out\", () => openers.current[depth] ?? heading())\n\n  return (\n    <PanelsContext.Provider value={{ path, root: defaultValue, go }}>\n      <div ref={composedRef} data-slot=\"sheet-panels\" data-step={step} className={cn(\"ot-sheet-panels\", className)} {...props}>\n        {/* The spine is the trail: the root at the foot, the panel you are in written large at the head. */}\n        <nav className=\"ot-sheet-spine ot-sheet-trail\" aria-label=\"Path\">\n          {path.map((v, i) => {\n            const here = i === path.length - 1\n            return (\n              <button\n                key={v}\n                type=\"button\"\n                className=\"ot-sheet-trail-step\"\n                aria-current={here ? \"page\" : undefined}\n                aria-disabled={here || undefined}\n                onClick={() => !here && back(i)}\n              >\n                {titles.get(v)}\n              </button>\n            )\n          })}\n        </nav>\n        {children}\n      </div>\n    </PanelsContext.Provider>\n  )\n}\n\ntype SheetPanelProps = Omit<React.ComponentProps<\"section\">, \"title\"> & {\n  value: string\n  /** Its name: the panel's heading, and its step on the spine. */\n  title: React.ReactNode\n}\n\n/** One page of the sheet. Only the one you are in is shown; the others wait, hidden and inert. */\nfunction SheetPanel({ value, title, className, children, ...props }: SheetPanelProps) {\n  const { path, root } = usePanels()\n  const { titleId } = useDialog()\n  const here = path[path.length - 1] === value\n  return (\n    <section data-slot=\"sheet-panel\" hidden={!here} inert={!here} aria-label={typeof title === \"string\" ? title : undefined} className={cn(\"ot-sheet-panel\", className)} {...props}>\n      <h2 id={value === root ? titleId : undefined} tabIndex={-1} className=\"ot-sheet-title ot-sheet-panel-title\">\n        {title}\n      </h2>\n      {children}\n    </section>\n  )\n}\n\ntype SheetPanelLinkProps = React.ComponentProps<\"button\"> & { to: string; asChild?: boolean }\n\n/** Steps into another panel. Bare, it is a line of type with the arrow that says it opens; asChild lends the move to your own control. */\nfunction SheetPanelLink({ to, asChild = false, className, children, onClick, ...props }: SheetPanelLinkProps) {\n  const { go } = usePanels()\n  const Comp = asChild ? Slot : \"button\"\n  return (\n    <Comp\n      data-slot=\"sheet-panel-link\"\n      className={asChild ? className : cn(\"ot-sheet-panel-link\", className)}\n      onClick={(e: React.MouseEvent<HTMLButtonElement>) => {\n        onClick?.(e)\n        if (!e.defaultPrevented) go(to, e.currentTarget)\n      }}\n      {...(asChild ? {} : { type: \"button\" as const })}\n      {...props}\n    >\n      {asChild ? children : (\n        <>\n          <span>{children}</span>\n          <span aria-hidden=\"true\" className=\"ot-sheet-panel-arrow\">→</span>\n        </>\n      )}\n    </Comp>\n  )\n}\n\n/** The sheet's name, running up its spine like a book's. Decorative: the visible title is SheetTitle. */\nfunction SheetSpine({ className, ...props }: React.ComponentProps<\"p\">) {\n  return <p data-slot=\"sheet-spine\" aria-hidden=\"true\" className={cn(\"ot-sheet-spine\", className)} {...props} />\n}\n\nfunction SheetTitle({ className, ...props }: React.ComponentProps<\"h2\">) {\n  const { titleId } = useDialog()\n  return <h2 data-slot=\"sheet-title\" id={titleId} className={cn(\"ot-sheet-title\", className)} {...props} />\n}\n\nfunction SheetDescription({ className, ...props }: React.ComponentProps<\"p\">) {\n  const { descriptionId } = useDialog()\n  return <p data-slot=\"sheet-description\" id={descriptionId} className={cn(\"ot-sheet-body\", className)} {...props} />\n}\n\nfunction SheetActions(props: React.ComponentProps<\"div\">) {\n  return <DialogActions data-slot=\"sheet-actions\" {...props} />\n}\n\nexport {\n  Sheet, SheetTrigger, SheetContent, SheetSpine, SheetTitle, SheetDescription, SheetActions, SheetActions as SheetFooter, SheetClose,\n  SheetPanels, SheetPanel, SheetPanelLink,\n  type SheetContentProps, type SheetPanelsProps, type SheetPanelProps, type SheetPanelLinkProps,\n}\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/0nlytype/styles/sheet.css",
      "content": "@layer components {\n  /* ── ot-sheet ── a page slid in from the edge; its title runs up the spine.\n     The backdrop lives in dialog.css. Sides are logical: start and end follow\n     the reading direction, so the sheet meets the right edge in right-to-left. */\n  .ot-sheet {\n    --from: 100% 0; /* where it waits, off the edge it comes from */\n    position: fixed; inset-block: 0; inset-inline: auto 0; margin: 0; width: min(32rem, calc(100vw - var(--ot-space-6))); height: 100dvh; max-width: none; max-height: none;\n    padding-block: var(--ot-space-7) var(--ot-space-6); padding-inline: var(--ot-space-8) var(--ot-space-6); overflow: auto;\n    border: 0; border-inline-start: var(--ot-hairline) solid var(--ot-ink); background: var(--ot-paper); color: var(--ot-graphite);\n    translate: var(--from);\n    transition: translate var(--ot-andante) var(--ot-exhale), overlay var(--ot-andante) allow-discrete, display var(--ot-andante) allow-discrete;\n  }\n  .ot-sheet[open] { translate: 0 0; }\n  @starting-style { .ot-sheet[open] { translate: var(--from); } }\n  .ot-sheet:is([dir=\"rtl\"], [dir=\"rtl\"] *) { --from: -100% 0; }\n  .ot-sheet[data-side=\"start\"] { --from: -100% 0; inset-inline: 0 auto; border-inline-start: 0; border-inline-end: var(--ot-hairline) solid var(--ot-ink); }\n  .ot-sheet[data-side=\"start\"]:is([dir=\"rtl\"], [dir=\"rtl\"] *) { --from: 100% 0; }\n  /* A start sheet's edge is on the same side as its scrollbar: the edge is the rail, and the thumb is that edge thickening where you are, not a second line beside it. */\n  .ot-sheet[data-side=\"start\"] > .ot-scrollbar { --line: 0px; }\n  .ot-sheet[data-side=\"start\"] > .ot-scrollbar::before { content: none; }\n  .ot-sheet:is([data-side=\"top\"], [data-side=\"bottom\"]) { inset-inline: 0; width: 100%; height: auto; max-height: 85dvh; border-inline-start: 0; }\n  .ot-sheet[data-side=\"top\"] { --from: 0 -100%; inset-block: 0 auto; border-block-end: var(--ot-hairline) solid var(--ot-ink); }\n  .ot-sheet[data-side=\"bottom\"] { --from: 0 100%; inset-block: auto 0; border-block-start: var(--ot-hairline) solid var(--ot-ink); }\n  /* left and right, not inset-inline-start: in the spine's own vertical writing mode that means top,\n     and the spine fell into the text. The sheet's space-8 inline-start padding is its column. */\n  .ot-sheet-spine {\n    position: absolute; left: var(--ot-space-4); bottom: var(--ot-space-6);\n    writing-mode: vertical-rl; rotate: 180deg; white-space: nowrap;\n    font-size: var(--ot-f); line-height: 1; letter-spacing: var(--ot-f-tr); font-weight: 200; color: var(--ot-ink);\n  }\n  .ot-sheet-spine:is([dir=\"rtl\"], [dir=\"rtl\"] *) { left: auto; right: var(--ot-space-4); }\n  .ot-sheet[open] .ot-sheet-spine { animation: ot-spine var(--ot-andante) var(--ot-exhale) 240ms backwards; }\n  @keyframes ot-spine { from { clip-path: inset(0 0 100% 0); } to { clip-path: inset(0); } }\n  .ot-sheet > * + *:not(.ot-scrollbar) { margin-block-start: var(--ot-space-6); }\n  .ot-sheet-title { font-size: var(--ot-mf); line-height: var(--ot-mf-lh); letter-spacing: var(--ot-mf-tr); font-weight: 300; color: var(--ot-ink); }\n  .ot-sheet-body { max-width: 40ch; }\n\n  /* Shelf (books on a shelf): a sheet opened from inside a shelf sheet stands in front of it, one spine column\n     narrower, so the spine below stays in view, set back to pencil, and pointing there puts the front one away.\n     Only the front sheet blurs the page; the ones behind it are the shelf. */\n  .ot-sheet[data-variant=\"shelf\"] { --col: var(--ot-space-8); --wide: min(32rem, calc(100vw - var(--ot-space-6))); }\n  .ot-sheet[data-variant=\"shelf\"] .ot-sheet { width: calc(var(--wide) - var(--col)); }\n  .ot-sheet[data-variant=\"shelf\"] .ot-sheet .ot-sheet { width: calc(var(--wide) - 2 * var(--col)); }\n  .ot-sheet[data-variant=\"shelf\"] .ot-sheet::backdrop { background: none; -webkit-backdrop-filter: none; backdrop-filter: none; }\n  .ot-sheet[data-variant=\"shelf\"] .ot-sheet-spine { transition: color var(--ot-moderato) var(--ot-breath); }\n  .ot-sheet[data-variant=\"shelf\"]:has(.ot-sheet[open]) > .ot-sheet-spine,\n  .ot-sheet[data-variant=\"shelf\"] .ot-sheet:has(.ot-sheet[open]) > .ot-sheet-spine { color: var(--ot-pencil); }\n  @media (max-width: 40rem) {\n    .ot-sheet[data-variant=\"shelf\"] { --col: var(--ot-space-7); }\n    .ot-sheet[data-variant=\"shelf\"], .ot-sheet[data-variant=\"shelf\"] .ot-sheet { padding-inline-start: var(--col); }\n    .ot-sheet[data-variant=\"shelf\"] .ot-sheet-spine { left: var(--ot-space-3); font-size: var(--ot-mf); }\n    .ot-sheet[data-variant=\"shelf\"] .ot-sheet-spine:is([dir=\"rtl\"], [dir=\"rtl\"] *) { left: auto; right: var(--ot-space-3); }\n  }\n\n  /* Rag (\"Healthy habits\" cut from its paper; \"Less is more.\" set flush right): the words stand flush to the\n     window's edge and rag toward the page, and the paper is cut to that rag, a step for every line, with a\n     hairline along the cut that draws down as it arrives. The script measures the line boxes and draws the\n     paper (.ot-sheet-rag); until it has, the sheet is plain paper. Its box hangs from the top, only as tall\n     as its words, and a click on the bare part of the box is a click outside. */\n  .ot-sheet[data-variant=\"rag\"] { height: auto; max-height: 100dvh; inset-block: 0 auto; isolation: isolate; padding-inline: var(--ot-space-6); text-align: end; }\n  .ot-sheet[data-variant=\"rag\"][data-laid] { background: none; border: 0; }\n  .ot-sheet[data-variant=\"rag\"] > :not(.ot-scrollbar, .ot-sheet-rag) { margin-inline-start: auto; }\n  .ot-sheet[data-variant=\"rag\"] .ot-sheet-title { font-size: var(--ot-f); line-height: var(--ot-f-lh); letter-spacing: var(--ot-f-tr); text-wrap: balance; }\n  .ot-sheet[data-variant=\"rag\"] .ot-sheet-body { max-width: 30ch; }\n  .ot-sheet[data-variant=\"rag\"][data-laid] > .ot-scrollbar { --line: 0px; }\n  .ot-sheet[data-variant=\"rag\"][data-laid] > .ot-scrollbar::before { content: none; }\n  .ot-sheet > .ot-sheet-rag { position: absolute; z-index: -1; top: 0; margin: 0; left: 0; overflow: visible; pointer-events: none; }\n  .ot-sheet-rag-paper { fill: var(--ot-paper); pointer-events: visiblePainted; }\n  .ot-sheet-rag-edge { fill: none; stroke: var(--ot-ink); stroke-width: var(--ot-hairline); stroke-dasharray: 1; shape-rendering: crispEdges; }\n  .ot-sheet[open] .ot-sheet-rag-edge { animation: ot-rag var(--ot-andante) var(--ot-exhale) 200ms backwards; }\n  @keyframes ot-rag { from { stroke-dashoffset: 1; } }\n  /* Panels: pages inside one sheet, the chapters of the book on its spine (\"It has to be design.\": the scale is the\n     drama). The spine is the trail, read up from the foot: the names you came through small in the pencil, the one\n     you are in large in ink. Stepping in, the name you left shrinks into the trail and the new one writes up the\n     spine; the page slides in from the edge the sheet came from while the old one goes the other way. The small\n     names are the way back. Panels share one cell, so the two pages cross in place. */\n  .ot-sheet-panels { --lr: 1; display: grid; }\n  .ot-sheet-panels:is([dir=\"rtl\"], [dir=\"rtl\"] *) { --lr: -1; }\n  .ot-sheet-panels[data-step=\"out\"] { --lr-step: -1; }\n  .ot-sheet-panel {\n    grid-area: 1 / 1; min-inline-size: 0;\n    transition: opacity var(--ot-andante) var(--ot-exhale), translate var(--ot-andante) var(--ot-exhale), display var(--ot-andante) allow-discrete;\n  }\n  .ot-sheet-panel[hidden] { opacity: 0; translate: calc(-1.5em * var(--lr) * var(--lr-step, 1)) 0; pointer-events: none; }\n  @starting-style { .ot-sheet-panel:not([hidden]) { opacity: 0; translate: calc(1.5em * var(--lr) * var(--lr-step, 1)) 0; } }\n  .ot-sheet-panel > * + * { margin-block-start: var(--ot-space-6); }\n  .ot-sheet-panel-title { inline-size: fit-content; }\n  .ot-sheet-panel-title:focus { outline: none; }\n  .ot-sheet-panel-title:focus-visible { outline: var(--ot-stroke) solid var(--ot-accent); outline-offset: 6px; }\n\n  .ot-sheet-trail { display: flex; align-items: baseline; gap: var(--ot-space-4); }\n  .ot-sheet-trail:is([dir=\"rtl\"], [dir=\"rtl\"] *) { flex-direction: row-reverse; } /* the root stays at the foot */\n  .ot-sheet-trail-step {\n    appearance: none; border: 0; padding: 0; background: none; font: inherit; letter-spacing: 0; cursor: pointer;\n    font-size: var(--ot-mp); font-weight: 400; color: var(--ot-pencil);\n    transition: font-size var(--ot-andante) var(--ot-exhale), font-weight var(--ot-andante) var(--ot-exhale), color var(--ot-allegro);\n  }\n  .ot-sheet-trail-step:hover { color: var(--ot-ink); }\n  .ot-sheet-trail-step[aria-current] { font-size: var(--ot-f); font-weight: 200; letter-spacing: var(--ot-f-tr); color: var(--ot-ink); cursor: default; }\n  .ot-sheet-trail-step[aria-current]:last-child:not(:first-child) { animation: ot-spine var(--ot-andante) var(--ot-exhale) backwards; }\n  .ot-sheet-trail-step:focus-visible { outline: var(--ot-stroke) solid var(--ot-accent); outline-offset: 4px; }\n\n  .ot-sheet-panel-link {\n    appearance: none; display: flex; justify-content: space-between; align-items: baseline; gap: var(--ot-space-4); inline-size: 100%;\n    border: 0; border-block-end: var(--ot-hairline) solid var(--ot-rule); background: none; padding: var(--ot-space-3) 0; text-align: start; cursor: pointer;\n    font: inherit; font-size: var(--ot-mp); line-height: var(--ot-mp-lh); letter-spacing: var(--ot-mp-tr); color: var(--ot-ink);\n  }\n  .ot-sheet-panel-link + .ot-sheet-panel-link { margin-block-start: 0; }\n  .ot-sheet-panel-arrow { color: var(--ot-pencil); transition: translate var(--ot-allegro) var(--ot-exhale), color var(--ot-allegro); }\n  .ot-sheet-panel-arrow:is([dir=\"rtl\"], [dir=\"rtl\"] *) { scale: -1 1; }\n  .ot-sheet-panel-link:is(:hover, :focus-visible, [data-force~=\"hover\"]) .ot-sheet-panel-arrow { color: var(--ot-ink); translate: calc(0.25em * var(--lr)) 0; }\n  @media (max-width: 40rem) {\n    .ot-sheet-trail-step[aria-current] { font-size: var(--ot-mf); letter-spacing: var(--ot-mf-tr); }\n    .ot-sheet-trail-step { font-size: var(--ot-p); }\n  }\n  @media (forced-colors: active) {\n    .ot-sheet-rag-paper { fill: Canvas; }\n    .ot-sheet-rag-edge { stroke: CanvasText; }\n  }\n\n  /* Fold (the index entry that opens to its page; \"Less is more.\" set between hairlines): the row that opened it\n     unfolds into the page. The script writes the row's box to --fold-t/r/b/l before it opens and before it closes;\n     the clip opens from that band to the window, the row's two hairlines part with its edges to the top and the\n     foot, and the row's name grows into the title (a transform, in the script). The rest arrives once there is\n     room for it. Put away, it all folds back into the row. With no opener, a line at the middle parts. */\n  .ot-sheet[data-variant=\"fold\"] {\n    --fold: inset(var(--fold-t, 50%) var(--fold-r, 0px) var(--fold-b, 50%) var(--fold-l, 0px));\n    inset: 0; width: 100%; max-width: none; height: 100dvh; border: 0; translate: none; overscroll-behavior: contain;\n    padding-block: var(--ot-space-8) var(--ot-space-7); padding-inline: var(--ot-margin);\n    clip-path: var(--fold);\n    transition: clip-path var(--ot-andante) var(--ot-exhale), background-color var(--ot-allegro) var(--ot-breath), overlay var(--ot-andante) allow-discrete, display var(--ot-andante) allow-discrete;\n  }\n  .ot-sheet[data-variant=\"fold\"][open] { translate: none; clip-path: inset(0); }\n  @starting-style {\n    .ot-sheet[data-variant=\"fold\"][open] { translate: none; clip-path: var(--fold); }\n    .ot-sheet[data-variant=\"fold\"][data-from-ink][open] { background-color: var(--ot-ink); }\n  }\n  /* The row's hairlines, riding the clip's edges. Fixed, so they hold to the window while the page scrolls. */\n  .ot-sheet[data-variant=\"fold\"]::before, .ot-sheet[data-variant=\"fold\"]::after {\n    content: \"\"; position: fixed; z-index: 4; left: var(--fold-l, 0px); right: var(--fold-r, 0px); height: var(--ot-hairline); background: var(--ot-ink); pointer-events: none;\n    transition: top var(--ot-andante) var(--ot-exhale), bottom var(--ot-andante) var(--ot-exhale), left var(--ot-andante) var(--ot-exhale), right var(--ot-andante) var(--ot-exhale);\n  }\n  .ot-sheet[data-variant=\"fold\"]::before { top: var(--fold-t, 50%); }\n  .ot-sheet[data-variant=\"fold\"]::after { bottom: var(--fold-b, 50%); }\n  .ot-sheet[data-variant=\"fold\"][open]::before { top: 0; left: 0; right: 0; }\n  .ot-sheet[data-variant=\"fold\"][open]::after { bottom: 0; left: 0; right: 0; }\n  @starting-style {\n    .ot-sheet[data-variant=\"fold\"][open]::before { top: var(--fold-t, 50%); left: var(--fold-l, 0px); right: var(--fold-r, 0px); }\n    .ot-sheet[data-variant=\"fold\"][open]::after { bottom: var(--fold-b, 50%); left: var(--fold-l, 0px); right: var(--fold-r, 0px); }\n  }\n  .ot-sheet[data-variant=\"fold\"] .ot-sheet-spine { display: none; }\n  .ot-sheet[data-variant=\"fold\"] .ot-sheet-title { max-inline-size: 14ch; font-size: var(--ot-ff); line-height: var(--ot-ff-lh); letter-spacing: var(--ot-ff-tr); font-weight: 300; text-wrap: balance; transition: color var(--ot-allegro) var(--ot-breath); }\n  @starting-style { .ot-sheet[data-variant=\"fold\"][data-from-ink][open] .ot-sheet-title { color: var(--ot-paper); } }\n  .ot-sheet[data-variant=\"fold\"] > :not(.ot-sheet-title, .ot-scrollbar) { opacity: 0; transition: opacity var(--ot-allegro) var(--ot-breath); }\n  .ot-sheet[data-variant=\"fold\"][open] > :not(.ot-sheet-title, .ot-scrollbar) { opacity: 1; transition: opacity var(--ot-moderato) var(--ot-exhale) calc(var(--ot-andante) / 2); }\n  @starting-style { .ot-sheet[data-variant=\"fold\"][open] > :not(.ot-sheet-title, .ot-scrollbar) { opacity: 0; } }\n  .ot-sheet[data-variant=\"fold\"] .ot-sheet-body { max-inline-size: var(--ot-measure); font-size: var(--ot-mp); line-height: var(--ot-mp-lh); letter-spacing: var(--ot-mp-tr); }\n  .ot-sheet[data-variant=\"fold\"] .ot-dialog-actions { justify-content: flex-start; }\n  @media (max-width: 40rem) {\n    .ot-sheet[data-variant=\"fold\"] { padding-block: var(--ot-space-7) var(--ot-space-6); }\n    .ot-sheet[data-variant=\"fold\"] .ot-sheet-title { font-size: var(--ot-f); line-height: var(--ot-f-lh); letter-spacing: var(--ot-f-tr); }\n    .ot-sheet[data-variant=\"fold\"] .ot-sheet-body { font-size: var(--ot-p); line-height: var(--ot-p-lh); }\n  }\n  @media (forced-colors: active) {\n    .ot-sheet[data-variant=\"fold\"]::before, .ot-sheet[data-variant=\"fold\"]::after { forced-color-adjust: none; background: CanvasText; }\n  }\n}\n",
      "type": "registry:file",
      "target": "styles/0nlytype/sheet.css"
    }
  ],
  "css": {
    "@import \"../styles/0nlytype/sheet.css\"": {}
  },
  "meta": {
    "movement": "IX",
    "underneath": "native",
    "contract": "ot-sheet"
  },
  "type": "registry:ui"
}
