{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "resizable",
  "title": "Resizable",
  "description": "Panes and the rule between them. Take the rule and it inks; while you hold it, each pane's share is drawn as a dimension.",
  "dependencies": [
    "@chenglou/pretext"
  ],
  "registryDependencies": [
    "https://thedirectors.agency/ui/r/0nlytype.json"
  ],
  "files": [
    {
      "path": "registry/0nlytype/ui/resizable.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { useComposedRefs } from \"@/lib/0nlytype/refs\"\nimport { cn } from \"@/lib/utils\"\n\ntype GroupContext = {\n  id: string\n  direction: \"horizontal\" | \"vertical\"\n  sizes: number[]\n  mins: number[]\n  /** Moves the rule after panel i so panel i is `size` percent, the next panel taking the difference. */\n  resize: (i: number, size: number) => void\n  variant: Variant\n  /** In flow, the lines each panel holds, once laid out. */\n  flow: string[][] | null\n}\n\ntype Variant = \"rule\" | \"fit\" | \"flow\"\n\nconst Group = React.createContext<GroupContext | null>(null)\n/** Which panel a child is (a handle takes the panel before it). */\nconst Index = React.createContext(0)\n\nfunction useGroup() {\n  const ctx = React.useContext(Group)\n  if (!ctx) throw new Error(\"Resizable parts must sit inside <ResizablePanelGroup>.\")\n  return ctx\n}\n\ntype PanelProps = React.ComponentProps<\"div\"> & {\n  /** Its share of the group, in percent. Panels without one share what's left. */\n  defaultSize?: number\n  /** The least it can be, in percent. */\n  minSize?: number\n}\n\ntype GroupProps = React.ComponentProps<\"div\"> & {\n  direction: \"horizontal\" | \"vertical\"\n  /**\n   * rule: panes and the rules between them. fit: each pane's ResizableTitle is set to its measure, as a\n   * compositor sets wood type: it condenses first, then changes size. flow: `text` runs through the panes\n   * as continued columns: the first holds as many lines as its height allows, and the rest carry on in the next.\n   */\n  variant?: Variant\n  /** In flow, the one paragraph that runs through the panes. Plain text: pretext measures it. */\n  text?: string\n}\n\n/**\n * Panels and the rules between them: two or more ResizablePanels with a ResizableHandle between\n * each, as direct children, in that order (panel, handle, panel, ...). Children are told apart by their place, not their type,\n * so this works when they arrive from a Server Component. Moved, each panel's share is drawn as a dimension.\n */\nfunction ResizablePanelGroup({ direction, variant = \"rule\", text = \"\", className, children, ref: forwardedRef, ...props }: GroupProps) {\n  const id = React.useId()\n  const ref = React.useRef<HTMLDivElement>(null)\n  const composedRef = useComposedRefs(ref, forwardedRef)\n  const flow = useFlow(ref, variant === \"flow\" ? text : \"\")\n  const nodes = React.Children.toArray(children)\n  const panels = nodes.filter((_, at) => at % 2 === 0) as React.ReactElement<PanelProps>[]\n  const mins = panels.map((p) => p.props.minSize ?? 10)\n\n  const [sizes, setSizes] = React.useState(() => {\n    // Panels without a defaultSize share what the others leave.\n    const left = 100 - panels.reduce((sum, p) => sum + (p.props.defaultSize ?? 0), 0)\n    const open = panels.filter((p) => p.props.defaultSize === undefined).length\n    return panels.map((p) => p.props.defaultSize ?? left / open)\n  })\n\n  const resize = (i: number, size: number) =>\n    setSizes((now) => {\n      const total = now[i] + now[i + 1]\n      const next = Math.round(Math.min(total - mins[i + 1], Math.max(mins[i], size)))\n      return next === now[i] ? now : now.map((s, at) => (at === i ? next : at === i + 1 ? total - next : s))\n    })\n\n  return (\n    <Group.Provider value={{ id, direction, sizes, mins, resize, variant, flow }}>\n      <FlowText.Provider value={text}>\n      <div ref={composedRef} data-slot=\"resizable\" data-direction={direction} data-variant={variant === \"rule\" ? undefined : variant} className={cn(\"ot-resize\", className)} {...props}>\n        {nodes.map((node, at) => (\n          <Index.Provider key={at} value={Math.floor(at / 2)}>\n            {node}\n          </Index.Provider>\n        ))}\n      </div>\n      </FlowText.Provider>\n    </Group.Provider>\n  )\n}\n\n/**\n * Continued columns: lays `text` out at each pane's measure, the first pane taking as many lines as its\n * height holds, the next the rest, and so on; the last takes what is left. Re-laid when a pane changes\n * size (a drag, the window) and when <html> changes its face.\n */\nfunction useFlow(group: React.RefObject<HTMLDivElement | null>, text: string) {\n  const [flow, setFlow] = React.useState<string[][] | null>(null)\n  React.useEffect(() => {\n    const el = group.current\n    if (!el || !text) return\n    let cancelled = false\n    let frame = 0\n    const off: (() => void)[] = []\n    ;(async () => {\n      let lib: typeof import(\"@chenglou/pretext\")\n      try {\n        lib = await import(\"@chenglou/pretext\")\n      } catch {\n        return // the plain paragraph stays in the first pane\n      }\n      let font = \"\"\n      let prepared: ReturnType<typeof lib.prepareWithSegments> | undefined\n      const lay = async () => {\n        const columns = [...el.querySelectorAll<HTMLElement>(\":scope > [data-slot=resizable-panel] > .ot-resize-flow\")]\n        if (!columns.length) return\n        const style = getComputedStyle(columns[0])\n        const next = `${style.fontStyle} ${style.fontWeight} ${style.fontSize} ${style.fontFamily}`\n        if (next !== font || !prepared) {\n          font = next\n          await document.fonts.load(font, text)\n          prepared = lib.prepareWithSegments(text, font)\n        }\n        const lh = parseFloat(style.lineHeight) || parseFloat(style.fontSize) * 1.5\n        let cursor = { segmentIndex: 0, graphemeIndex: 0 }\n        const out = columns.map((c, at) => {\n          const pane = c.parentElement!\n          const room = pane.clientHeight - parseFloat(getComputedStyle(pane).paddingBottom) - c.offsetTop\n          const count = at === columns.length - 1 ? Infinity : Math.max(0, Math.floor(room / lh))\n          const lines: string[] = []\n          for (let guard = 0; lines.length < count && guard < 500; guard++) {\n            const line = lib.layoutNextLine(prepared!, cursor, Math.max(c.clientWidth, 24))\n            if (!line) break\n            lines.push(line.text.trimEnd())\n            cursor = line.end\n          }\n          return lines\n        })\n        if (!cancelled) setFlow(out)\n      }\n      const soon = () => {\n        cancelAnimationFrame(frame)\n        frame = requestAnimationFrame(() => void lay())\n      }\n      await lay()\n      if (cancelled) return\n      const resized = new ResizeObserver(soon)\n      el.querySelectorAll(\":scope > [data-slot=resizable-panel]\").forEach((pane) => resized.observe(pane))\n      const restyled = new MutationObserver(soon)\n      restyled.observe(document.documentElement, { attributes: true })\n      off.push(() => resized.disconnect(), () => restyled.disconnect(), () => cancelAnimationFrame(frame))\n    })()\n    return () => {\n      cancelled = true\n      off.forEach((f) => f())\n    }\n  }, [group, text])\n  return flow\n}\n\n/** A pane's share of the flowing text. The reader of the page gets the whole paragraph once, in the first. */\nfunction Flow({ lines, first }: { lines?: string[]; first: boolean }) {\n  const { flow } = useGroup()\n  const text = React.useContext(FlowText)\n  if (!flow) return first ? <p className=\"ot-resize-flow\">{text}</p> : <p className=\"ot-resize-flow\" aria-hidden=\"true\" />\n  return (\n    <p className=\"ot-resize-flow\">\n      {first && <span className=\"ot-sr\">{text}</span>}\n      <span aria-hidden=\"true\">\n        {lines?.map((l, at) => (\n          <span key={at} className=\"ot-resize-line\">\n            {l}\n          </span>\n        ))}\n      </span>\n    </p>\n  )\n}\n\nconst FlowText = React.createContext(\"\")\n\nfunction ResizablePanel({ defaultSize, minSize, className, children, style, ...props }: PanelProps) {\n  const { id, sizes, variant, flow } = useGroup()\n  const i = React.useContext(Index)\n  void defaultSize\n  void minSize\n  return (\n    <div\n      id={`${id}-panel-${i}`}\n      data-slot=\"resizable-panel\"\n      style={{ flex: `0 1 ${sizes[i]}%`, ...style }}\n      className={cn(\"ot-resize-pane\", className)}\n      {...props}\n    >\n      {children}\n      {variant === \"flow\" && <Flow lines={flow?.[i]} first={i === 0} />}\n      <div className=\"ot-resize-dim\" aria-hidden=\"true\">\n        <span>{Math.round(sizes[i])}%</span>\n      </div>\n    </div>\n  )\n}\n\n/** The rule between two panels. Drag it, or focus it and use the arrow keys (Home and End go to the limits). */\nfunction ResizableHandle({ className, onKeyDown, ...props }: React.ComponentProps<\"div\">) {\n  const { id, direction, sizes, mins, resize } = useGroup()\n  const i = React.useContext(Index)\n  const [held, setHeld] = React.useState(false)\n  const horizontal = direction === \"horizontal\"\n  const max = Math.round(sizes[i] + sizes[i + 1] - mins[i + 1])\n\n  const group = (el: HTMLElement) => el.closest<HTMLElement>(\"[data-slot=resizable]\")!\n  const rtl = (el: HTMLElement) => horizontal && getComputedStyle(group(el)).direction === \"rtl\"\n\n  return (\n    <div\n      role=\"separator\"\n      tabIndex={0}\n      data-slot=\"resizable-handle\"\n      data-dragging={held ? \"\" : undefined}\n      aria-orientation={horizontal ? \"vertical\" : \"horizontal\"}\n      aria-valuenow={Math.round(sizes[i])}\n      aria-valuemin={mins[i]}\n      aria-valuemax={max}\n      aria-controls={`${id}-panel-${i}`}\n      aria-label=\"Resize\"\n      className={cn(\"ot-resize-handle\", className)}\n      onPointerDown={(e) => {\n        e.currentTarget.setPointerCapture(e.pointerId)\n        setHeld(true)\n      }}\n      onPointerMove={(e) => {\n        if (!e.currentTarget.hasPointerCapture(e.pointerId)) return\n        const r = group(e.currentTarget).getBoundingClientRect()\n        const along = horizontal ? (rtl(e.currentTarget) ? r.right - e.clientX : e.clientX - r.left) / r.width : (e.clientY - r.top) / r.height\n        // The pointer's place, less the panels before this one, is this panel's size.\n        resize(i, along * 100 - sizes.slice(0, i).reduce((sum, s) => sum + s, 0))\n      }}\n      onPointerUp={() => setHeld(false)}\n      onPointerCancel={() => setHeld(false)}\n      onKeyDown={(e) => {\n        onKeyDown?.(e)\n        const flip = rtl(e.currentTarget) ? -1 : 1\n        const step = e.shiftKey ? 10 : 5\n        const to = {\n          [horizontal ? \"ArrowLeft\" : \"ArrowUp\"]: sizes[i] - step * flip,\n          [horizontal ? \"ArrowRight\" : \"ArrowDown\"]: sizes[i] + step * flip,\n          Home: mins[i],\n          End: max,\n        }[e.key]\n        if (to === undefined) return\n        e.preventDefault()\n        resize(i, to)\n      }}\n      {...props}\n    />\n  )\n}\n\n/** Archivo's width axis: how far a title may condense, and how wide it may set. */\nconst NARROW = 62\nconst WIDE = 125\n\n/**\n * A pane's title. In a fit group it is set to the pane's measure, heavy, as wood type is: the width\n * axis goes first (condensed as the pane narrows, extended as it widens), and only past its ends does\n * the size change. Elsewhere it is a plain heading.\n */\nfunction ResizableTitle({ className, children, ref: forwardedRef, ...props }: React.ComponentProps<\"h3\">) {\n  const { variant } = useGroup()\n  const ref = React.useRef<HTMLHeadingElement>(null)\n  const composedRef = useComposedRefs(ref, forwardedRef)\n  React.useEffect(() => {\n    const el = ref.current\n    const word = el?.firstElementChild as HTMLElement | null\n    if (variant !== \"fit\" || !el || !word) return\n    let frame = 0\n    const set = () => {\n      const measure = el.clientWidth\n      const at = (stretch: number) => {\n        word.style.fontStretch = `${stretch}%`\n        return word.getBoundingClientRect().width\n      }\n      el.style.fontSize = \"\"\n      const size = parseFloat(getComputedStyle(el).fontSize) // the size it would be set at, from the stylesheet\n      el.style.fontSize = \"100px\"\n      const narrow = at(NARROW)\n      const wide = at(WIDE)\n      // At that size, the stretch that fills the measure; then the size that makes it exact.\n      const want = (measure / size) * 100\n      const stretch = Math.min(WIDE, Math.max(NARROW, NARROW + ((want - narrow) / (wide - narrow || 1)) * (WIDE - NARROW)))\n      const width = at(stretch)\n      el.style.fontSize = `${(100 * measure) / width}px`\n      el.setAttribute(\"data-set\", \"\")\n    }\n    const soon = () => {\n      cancelAnimationFrame(frame)\n      frame = requestAnimationFrame(set)\n    }\n    document.fonts.ready.then(soon)\n    const resized = new ResizeObserver(soon)\n    resized.observe(el)\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  }, [variant])\n  return (\n    <h3 ref={composedRef} data-slot=\"resizable-title\" className={cn(\"ot-resize-title\", className)} {...props}>\n      <span>{children}</span>\n    </h3>\n  )\n}\n\nexport { ResizablePanelGroup, ResizablePanel, ResizableHandle, ResizableTitle, type GroupProps as ResizablePanelGroupProps }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/0nlytype/styles/resizable.css",
      "content": "@layer components {\n  /* ── ot-resize ── panes and the rules between them; moved, they're measured.\n     Panel sizes are flex-basis percentages set by the component. */\n  /* The frame is Rand's construction line, dotted: it marks where the panes may go, it doesn't box them. */\n  .ot-resize { display: flex; min-width: 0; min-height: 17rem; border: var(--ot-hairline) dotted var(--ot-rule-strong); }\n  .ot-resize[data-direction=\"vertical\"] { flex-direction: column; }\n  .ot-resize-pane { position: relative; min-width: 0; min-height: 0; overflow: clip; padding: var(--ot-space-5) var(--ot-space-5) var(--ot-space-7); }\n  .ot-resize[data-direction=\"vertical\"] > .ot-resize-pane { padding: var(--ot-space-5) var(--ot-space-7) var(--ot-space-5) var(--ot-space-5); }\n  .ot-resize-handle {\n    position: relative; z-index: 1; flex: none; width: var(--ot-space-4); margin-inline: calc(var(--ot-space-4) / -2); cursor: col-resize; touch-action: none; outline: 0;\n    background: linear-gradient(var(--ot-rule-strong), var(--ot-rule-strong)) no-repeat 50% 0 / var(--ot-hairline) 100%;\n  }\n  .ot-resize[data-direction=\"vertical\"] > .ot-resize-handle {\n    width: auto; height: var(--ot-space-4); margin-inline: 0; margin-block: calc(var(--ot-space-4) / -2); cursor: row-resize;\n    background-position: 0 50%; background-size: 100% var(--ot-hairline);\n  }\n  .ot-resize-handle:is(:hover, :focus-visible, [data-dragging], [data-force~=\"hover\"]) { background-image: linear-gradient(var(--ot-ink), var(--ot-ink)); background-size: var(--ot-stroke) 100%; }\n  .ot-resize[data-direction=\"vertical\"] > .ot-resize-handle:is(:hover, :focus-visible, [data-dragging], [data-force~=\"hover\"]) { background-size: 100% var(--ot-stroke); }\n  .ot-resize-handle:focus-visible { background-image: linear-gradient(var(--ot-accent), var(--ot-accent)); }\n  .ot-resize:has(> [data-dragging]) { user-select: none; }\n  .ot-resize[data-direction=\"horizontal\"]:has(> [data-dragging]) { cursor: col-resize; }\n  .ot-resize[data-direction=\"vertical\"]:has(> [data-dragging]) { cursor: row-resize; }\n  /* The share of each pane, drawn as a dimension from edge to edge. */\n  .ot-resize-dim {\n    position: absolute; inset-inline: var(--ot-space-5); bottom: var(--ot-space-4); display: flex; align-items: center; gap: 0.4em;\n    font-size: var(--ot-pp); color: var(--ot-ink); font-variant-numeric: tabular-nums;\n    background: linear-gradient(var(--ot-ink), var(--ot-ink)) 0 50% / var(--ot-hairline) 9px no-repeat, linear-gradient(var(--ot-ink), var(--ot-ink)) 100% 50% / var(--ot-hairline) 9px no-repeat;\n    opacity: 0; transition: opacity var(--ot-moderato);\n  }\n  .ot-resize-dim::before, .ot-resize-dim::after { content: \"\"; flex: 1; height: var(--ot-hairline); background: var(--ot-ink); }\n  .ot-resize[data-direction=\"vertical\"] > .ot-resize-pane > .ot-resize-dim {\n    inset-block: var(--ot-space-5) var(--ot-space-4); inset-inline: auto var(--ot-space-4); flex-direction: column;\n    background: linear-gradient(var(--ot-ink), var(--ot-ink)) 50% 0 / 9px var(--ot-hairline) no-repeat, linear-gradient(var(--ot-ink), var(--ot-ink)) 50% 100% / 9px var(--ot-hairline) no-repeat;\n  }\n  .ot-resize[data-direction=\"vertical\"] > .ot-resize-pane > .ot-resize-dim::before,\n  .ot-resize[data-direction=\"vertical\"] > .ot-resize-pane > .ot-resize-dim::after { width: var(--ot-hairline); height: auto; }\n  .ot-resize:has(> .ot-resize-handle:is(:hover, :focus-visible, [data-dragging], [data-force~=\"hover\"])) > .ot-resize-pane > .ot-resize-dim { opacity: 1; }\n\n  /* Fit (\"It has to be design.\" set heavy and condensed; wood type locked up to the measure): each pane's\n     title fills its pane edge to edge. The script moves the width axis first, then the size, so a\n     narrow pane gets a condensed word and a wide one an extended one, both the full measure. */\n  .ot-resize-title { font-size: var(--ot-f); line-height: var(--ot-f-lh); letter-spacing: var(--ot-f-tr); font-weight: 300; color: var(--ot-ink); }\n  .ot-resize[data-variant=\"fit\"] .ot-resize-title { font-weight: 800; line-height: 0.86; letter-spacing: -0.01em; white-space: nowrap; margin-inline-start: -0.04em; }\n  .ot-resize[data-variant=\"fit\"] .ot-resize-title:not([data-set]) { overflow: clip; text-overflow: clip; }\n  .ot-resize[data-variant=\"fit\"] .ot-resize-title + * { margin-block-start: var(--ot-space-4); }\n\n  /* Flow (the newspaper's continued column): one paragraph runs through the panes. The first pane holds as\n     many lines as its height does; the rest carry on at the top of the next, at its measure. Move the rule\n     and the break moves with it. The group has a fixed height here, or the text would push it taller. */\n  .ot-resize[data-variant=\"flow\"] { height: 17rem; }\n  .ot-resize-flow { margin: 0; }\n  .ot-resize-line { display: block; white-space: pre; }\n  /* Forced colours drop background images, and the rules are drawn with them. */\n  @media (forced-colors: active) {\n    .ot-resize-handle, .ot-resize-dim { forced-color-adjust: none; }\n    .ot-resize-handle { background-image: linear-gradient(CanvasText, CanvasText); }\n    .ot-resize-handle:is(:hover, :focus-visible, [data-dragging], [data-force~=\"hover\"]) { background-image: linear-gradient(Highlight, Highlight); }\n    .ot-resize-dim { color: CanvasText; background-image: linear-gradient(CanvasText, CanvasText), linear-gradient(CanvasText, CanvasText); }\n    .ot-resize-dim::before, .ot-resize-dim::after { background: CanvasText; }\n  }\n  .ot-resize[data-variant=\"flow\"] > .ot-resize-pane:not(:first-child) .ot-resize-flow > [aria-hidden] > :first-child { color: var(--ot-ink); } /* where it carries on */\n}\n",
      "type": "registry:file",
      "target": "styles/0nlytype/resizable.css"
    }
  ],
  "css": {
    "@import \"../styles/0nlytype/resizable.css\"": {}
  },
  "meta": {
    "movement": "IX",
    "underneath": "hook",
    "contract": "ot-resize"
  },
  "type": "registry:ui"
}
