{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "drawer",
  "title": "Drawer",
  "description": "A sheet from below, lifted by the fermata's arc. Drag the arc down to put it away.",
  "registryDependencies": [
    "https://thedirectors.agency/ui/r/0nlytype.json",
    "https://thedirectors.agency/ui/r/dialog.json",
    "https://thedirectors.agency/ui/r/fraction.json"
  ],
  "files": [
    {
      "path": "registry/0nlytype/ui/drawer.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Dialog, DialogActions, DialogClose, DialogSurface, DialogTrigger, useDialog } from \"@/components/ui/dialog\"\nimport { Fraction } from \"@/components/ui/fraction\"\n\n/** A sheet from below. Same props and behaviour as Dialog. */\nconst Drawer = Dialog\nconst DrawerTrigger = DialogTrigger\nconst DrawerClose = DialogClose\n\n/** How far the arc is pulled before a let-go puts the drawer away. Solid's is its own silence, measured. */\nconst AWAY = 90\nconst KNOBS = [\"--pull\", \"--solid\", \"--sink\"]\n\ntype Thirds = { third: number; setThird: (n: number) => void }\n\n/** The fermata's arc. Drag it down to put the drawer away; a tap or Enter does the same. */\nfunction DrawerHandle({ label, thirds }: { label: string; thirds?: Thirds }) {\n  const { open, setOpen } = useDialog()\n  const button = React.useRef<HTMLButtonElement>(null)\n  const from = React.useRef<number | null>(null)\n  const pulled = React.useRef(0)\n  const give = React.useRef(AWAY)\n  // A drawer put away by a drag keeps its pull until it's opened again.\n  React.useEffect(() => {\n    const d = button.current?.closest(\"dialog\")\n    if (open) KNOBS.forEach((p) => d?.style.removeProperty(p))\n  }, [open])\n  const surface = (e: React.SyntheticEvent) => e.currentTarget.closest(\"dialog\") as HTMLDialogElement\n  const step = (n: number) => thirds?.setThird(Math.min(3, Math.max(1, n)))\n\n  const letGo = (e: React.PointerEvent<HTMLButtonElement>) => {\n    if (from.current === null) return\n    from.current = null\n    const d = surface(e)\n    const moved = Math.abs(pulled.current) >= 4\n    if (thirds && e.type === \"pointerup\") {\n      // Land on the nearest third; lower than half a third, it goes away. A tap raises it a third, round to one.\n      const third = parseFloat(getComputedStyle(d).maxHeight) / 3\n      const at = d.getBoundingClientRect().height / third\n      d.removeAttribute(\"data-pulling\")\n      d.style.removeProperty(\"--pull\")\n      if (!moved) return step((thirds.third % 3) + 1)\n      if (at < 0.5) return setOpen(false)\n      return step(Math.round(at))\n    }\n    d.removeAttribute(\"data-pulling\")\n    if (e.type === \"pointerup\" && (pulled.current >= give.current || !moved)) setOpen(false)\n    else KNOBS.forEach((p) => d.style.setProperty(p, p === \"--solid\" ? \"0\" : \"0px\"))\n  }\n\n  return (\n    <button\n      ref={button}\n      type=\"button\"\n      data-slot=\"drawer-handle\"\n      aria-label={label}\n      className=\"ot-drawer-handle\"\n      {...(thirds && {\n        role: \"slider\",\n        \"aria-orientation\": \"vertical\" as const,\n        \"aria-valuemin\": 1,\n        \"aria-valuemax\": 3,\n        \"aria-valuenow\": thirds.third,\n        \"aria-valuetext\": `${thirds.third} of 3`,\n      })}\n      onPointerDown={(e) => {\n        from.current = e.clientY\n        pulled.current = 0\n        e.currentTarget.setPointerCapture(e.pointerId)\n        const d = surface(e)\n        d.setAttribute(\"data-pulling\", \"\")\n        give.current = AWAY\n        if (d.dataset.variant === \"solid\") {\n          // Its give is the silence it holds: the height it loses once set solid. Measured unseen, in one frame.\n          const at = d.getBoundingClientRect().height\n          d.style.setProperty(\"--solid\", \"1\")\n          give.current = Math.max(24, at - d.getBoundingClientRect().height)\n          d.style.setProperty(\"--solid\", \"0\")\n        }\n      }}\n      onPointerMove={(e) => {\n        if (from.current === null) return\n        const d = surface(e)\n        // Thirds follows the hand both ways; the others only down.\n        pulled.current = thirds ? e.clientY - from.current : Math.max(0, e.clientY - from.current)\n        d.style.setProperty(\"--pull\", `${pulled.current}px`)\n        d.style.setProperty(\"--solid\", String(Math.min(1, pulled.current / give.current)))\n        d.style.setProperty(\"--sink\", `${Math.max(0, pulled.current - give.current)}px`)\n      }}\n      onPointerUp={letGo}\n      onPointerCancel={letGo}\n      onKeyDown={(e) => {\n        if (!thirds) return\n        const to = { ArrowUp: thirds.third + 1, ArrowRight: thirds.third + 1, ArrowDown: thirds.third - 1, ArrowLeft: thirds.third - 1, Home: 1, End: 3 }[e.key]\n        if (to === undefined) return\n        e.preventDefault()\n        step(to)\n      }}\n      onClick={(e) => {\n        if (e.detail !== 0) return\n        // The keyboard: Enter puts it away; in thirds it raises it a third, round to one.\n        if (thirds) step((thirds.third % 3) + 1)\n        else setOpen(false)\n      }}\n    >\n      <svg viewBox=\"0 0 40 22\" aria-hidden=\"true\">\n        <path d=\"M3 20 A17 17 0 0 1 37 20\" />\n        <circle cx=\"20\" cy=\"17\" r=\"2.6\" />\n      </svg>\n    </button>\n  )\n}\n\ntype DrawerContentProps = React.ComponentProps<\"dialog\"> & {\n  /** The accessible name of the arc that puts the drawer away (in thirds, that sets its height). */\n  handleLabel?: string\n  /**\n   * arc: it rises and you drag the arc down to put it away. thirds: it stops at a third, two thirds or the\n   * whole height, and the share stands in the corner as a fraction. solid: pulling the arc first takes out\n   * the silence between its parts, as a compositor sets type solid; only then does it go.\n   */\n  variant?: \"arc\" | \"thirds\" | \"solid\"\n}\n\nfunction DrawerContent({ className, children, handleLabel, variant = \"arc\", style, ...props }: DrawerContentProps) {\n  const { open } = useDialog()\n  const [third, setThird] = React.useState(1)\n  // Every opening starts at a third: reset while rendering the change, not in an effect after it.\n  const [was, setWas] = React.useState(open)\n  if (open !== was) {\n    setWas(open)\n    if (open) setThird(1)\n  }\n  const thirds = variant === \"thirds\" ? { third, setThird } : undefined\n  return (\n    <DialogSurface\n      data-slot=\"drawer-content\"\n      data-variant={variant === \"arc\" ? undefined : variant}\n      className={cn(\"ot-drawer\", className)}\n      style={thirds ? ({ \"--third\": third, ...style } as React.CSSProperties) : style}\n      {...props}\n    >\n      <DrawerHandle label={handleLabel ?? (thirds ? \"Height of the drawer\" : \"Put this away\")} thirds={thirds} />\n      {thirds && (\n        <span className=\"ot-drawer-share\" aria-hidden=\"true\">\n          <Fraction count={third} total={3} />\n        </span>\n      )}\n      <div data-slot=\"drawer-body\" className=\"ot-drawer-body\">\n        {children}\n      </div>\n    </DialogSurface>\n  )\n}\n\nfunction DrawerTitle({ className, ...props }: React.ComponentProps<\"h2\">) {\n  const { titleId } = useDialog()\n  return <h2 data-slot=\"drawer-title\" id={titleId} className={cn(\"ot-drawer-title\", className)} {...props} />\n}\n\nfunction DrawerDescription({ className, ...props }: React.ComponentProps<\"p\">) {\n  const { descriptionId } = useDialog()\n  return <p data-slot=\"drawer-description\" id={descriptionId} className={className} {...props} />\n}\n\nfunction DrawerActions(props: React.ComponentProps<\"div\">) {\n  return <DialogActions data-slot=\"drawer-actions\" {...props} />\n}\n\nexport { Drawer, DrawerTrigger, DrawerContent, DrawerTitle, DrawerDescription, DrawerActions, DrawerActions as DrawerFooter, DrawerClose, type DrawerContentProps }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/0nlytype/styles/drawer.css",
      "content": "@layer components {\n  /* ── ot-drawer ── a sheet from below, lifted by the fermata's arc.\n     The backdrop lives in dialog.css. --pull is set while the arc is dragged. */\n  .ot-drawer {\n    position: fixed; inset: auto 0 0; margin: 0; width: 100%; max-width: none; max-height: 85dvh;\n    padding: 0 var(--ot-margin) var(--ot-space-7); overflow: auto;\n    border: 0; border-top: var(--ot-hairline) solid var(--ot-ink); background: var(--ot-paper); color: var(--ot-graphite);\n    box-shadow: 0 12rem 0 var(--ot-paper); /* paper under the rebound, not a shadow */\n    translate: 0 100%;\n    transition: translate var(--ot-andante) var(--ot-spiccato), overlay var(--ot-andante) allow-discrete, display var(--ot-andante) allow-discrete;\n  }\n  .ot-drawer[open] { translate: 0 var(--pull, 0px); }\n  @starting-style { .ot-drawer[open] { translate: 0 100%; } }\n  .ot-drawer[data-pulling] { transition: none; }\n  .ot-drawer-handle { display: block; margin: 0 auto var(--ot-space-4); padding: var(--ot-space-4) var(--ot-space-6) var(--ot-space-2); border: 0; background: none; color: var(--ot-ink); cursor: grab; touch-action: none; }\n  .ot-drawer-handle:active { cursor: grabbing; }\n  .ot-drawer-handle svg { width: 40px; height: 22px; overflow: visible; }\n  .ot-drawer-handle path { fill: none; stroke: currentColor; stroke-width: 1.5; stroke-linecap: round; }\n  .ot-drawer-handle circle { fill: var(--ot-accent); }\n  .ot-drawer-body { display: grid; justify-items: start; gap: var(--ot-space-6); max-width: 40rem; margin-inline: auto; }\n  .ot-drawer-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\n\n  /* Thirds (SPECTRA's 1/3): it stops at a third, two thirds or the whole height. The arc follows the hand either\n     way and lets go on the nearest third, or away below half of one; a tap raises it a third, round to one. The share\n     stands in the far corner as a Fraction, yours in italic, turning on its wheel. --third is set by the component. */\n  .ot-drawer[data-variant=\"thirds\"] {\n    --full: calc(100dvh - var(--ot-space-7));\n    max-height: var(--full); height: calc(var(--third, 1) * var(--full) / 3 - var(--pull, 0px)); min-height: var(--ot-space-8);\n    transition: height var(--ot-andante) var(--ot-spiccato), translate var(--ot-andante) var(--ot-spiccato), overlay var(--ot-andante) allow-discrete, display var(--ot-andante) allow-discrete;\n  }\n  .ot-drawer[data-variant=\"thirds\"][open] { translate: 0 0; }\n  .ot-drawer-share { position: absolute; inset-block-start: var(--ot-space-4); inset-inline-end: var(--ot-margin); pointer-events: none; }\n  .ot-drawer-share .ot-fraction { font-size: var(--ot-f); font-weight: 200; }\n  @media (max-width: 40rem) { .ot-drawer-share { inset-block-start: var(--ot-space-3); } .ot-drawer-share .ot-fraction { font-size: var(--ot-mf); } }\n  .ot-drawer:has(> .ot-drawer-handle:is(:hover, :focus-visible, :active)) .ot-drawer-share .ot-fraction > :is(i, :last-child) { color: var(--ot-ink); }\n\n  /* Solid (the compositor's leads): the silence between the drawer's parts is its give. Pulling the arc takes it\n     out first, so the drawer closes up and sinks as it does, set solid; only past that does it slide. Let go set\n     solid and it goes; short of that, the silence springs back. Opening, it is let in. While the arc is pulled the\n     script sets --solid (0 to 1: the pull over the silence it measured) and --sink (the pull beyond it). */\n  .ot-drawer[data-variant=\"solid\"] {\n    padding-block-end: calc(var(--ot-space-7) * (1 - var(--solid, 0)));\n    transition: translate var(--ot-andante) var(--ot-spiccato), padding-block-end var(--ot-andante) var(--ot-spiccato), overlay var(--ot-andante) allow-discrete, display var(--ot-andante) allow-discrete;\n  }\n  .ot-drawer[data-variant=\"solid\"][open] { translate: 0 var(--sink, 0px); }\n  .ot-drawer[data-variant=\"solid\"] .ot-drawer-body { gap: calc(var(--ot-space-6) * (1 - var(--solid, 0))); transition: gap var(--ot-andante) var(--ot-spiccato); }\n  .ot-drawer[data-variant=\"solid\"] .ot-drawer-handle { margin-block-end: calc(var(--ot-space-4) * (1 - var(--solid, 0))); transition: margin var(--ot-andante) var(--ot-spiccato); }\n  @starting-style { .ot-drawer[data-variant=\"solid\"][open] :is(.ot-drawer-body, .ot-drawer-handle) { gap: 0; margin-block-end: 0; } }\n  /* While the arc is held, everything follows the hand at once. */\n  .ot-drawer[data-pulling], .ot-drawer[data-pulling] :is(.ot-drawer-body, .ot-drawer-handle) { transition: none; }\n}\n",
      "type": "registry:file",
      "target": "styles/0nlytype/drawer.css"
    }
  ],
  "css": {
    "@import \"../styles/0nlytype/drawer.css\"": {}
  },
  "meta": {
    "movement": "IX",
    "underneath": "native",
    "contract": "ot-drawer"
  },
  "type": "registry:ui"
}
