{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "radio-group",
  "title": "Radio group",
  "description": "Words in a row. The one you choose turns italic, because it's yours now. One dot glides beneath it along a slur, or the italic itself slides through the words to it and sets a full stop after it. A ballot cross can be written after it, or, in a heavy column, its italic swells.",
  "registryDependencies": [
    "https://thedirectors.agency/ui/r/0nlytype.json"
  ],
  "files": [
    {
      "path": "registry/0nlytype/ui/radio-group.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { useComposedRefs } from \"@/lib/0nlytype/refs\"\nimport { cn } from \"@/lib/utils\"\n\ntype Variant = \"legato\" | \"glissando\" | \"ballot\" | \"sforzando\"\n\ntype RadioGroupContextValue = {\n  name: string\n  value: string | undefined\n  select: (value: string) => void\n  variant: Variant\n}\n\nconst RadioGroupContext = React.createContext<RadioGroupContextValue | null>(null)\n\nfunction useRadioGroup() {\n  const ctx = React.useContext(RadioGroupContext)\n  if (!ctx) throw new Error(\"RadioGroupItem must sit inside <RadioGroup>.\")\n  return ctx\n}\n\ntype Pt = { x: number; y: number }\ntype Flight = { a: Pt; c: Pt; b: Pt; anim: Animation }\n\nconst BREATH = \"cubic-bezier(.65,0,.35,1)\" // --ot-breath\nconst EXHALE = \"cubic-bezier(.16,1,.3,1)\" // --ot-exhale\nconst SPICCATO = \"cubic-bezier(.34,1.5,.5,1)\" // --ot-spiccato\nconst still = () => matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n// Registered lengths come back from getComputedStyle resolved to px.\nconst length = (el: Element, prop: string) => parseFloat(getComputedStyle(el).getPropertyValue(prop)) || 0\n\n// Where the dot sits for a word, in the group's own px: under the word's middle, or hung in the margin before it\n// when the words stand in a column. The CSS ring (label::before) reads the same two lengths.\n// offset* and computed lengths both ignore a zoomed ancestor (the landing page previews pieces at zoom 0.7),\n// so nothing here needs dividing back by the scale.\nfunction seat(l: HTMLElement, column: boolean, rtl: boolean, drop: number, nudge: number): Pt {\n  if (!column) return { x: l.offsetLeft + l.offsetWidth / 2, y: l.offsetTop + l.offsetHeight + drop }\n  return { x: rtl ? l.offsetLeft + l.offsetWidth + drop : l.offsetLeft - drop, y: l.offsetTop + l.offsetHeight / 2 + nudge }\n}\n\n// The slur from one seat to another: a quadratic curve that bows away from the words, the way a slur hangs under\n// a legato phrase (or out into the margin, in a column). c is its control point; the curve bows half as far,\n// and the bow stays shallow however long the slur.\nfunction bow(a: Pt, b: Pt, out: Pt, em: number): Pt {\n  const d = Math.min(Math.hypot(b.x - a.x, b.y - a.y) * 0.22, em * 1.1)\n  return { x: (a.x + b.x) / 2 + out.x * d, y: (a.y + b.y) / 2 + out.y * d }\n}\n// The curve's blossom: f(t, t) is the point at t, and f(t0, t1) the control point of the piece from t0 to t1.\nfunction blossom(a: Pt, c: Pt, b: Pt, u: number, v: number): Pt {\n  const [p, q, r] = [(1 - u) * (1 - v), (1 - u) * v + u * (1 - v), u * v]\n  return { x: p * a.x + q * c.x + r * b.x, y: p * a.y + q * c.y + r * b.y }\n}\nconst xy = (p: Pt) => `${p.x.toFixed(1)} ${p.y.toFixed(1)}`\n\n// The pencil slur, stopped short of the dot and the ring so it reads as a mark between them, not through them.\nfunction slurPath(a: Pt, c: Pt, b: Pt, gap: number) {\n  const t = Math.min(0.3, gap / (Math.hypot(b.x - a.x, b.y - a.y) || 1))\n  return `M${xy(blossom(a, c, b, t, t))}Q${xy(blossom(a, c, b, t, 1 - t))} ${xy(blossom(a, c, b, 1 - t, 1 - t))}`\n}\n\n// Runs a pencil line's dash offset from one value to another (1 is not yet drawn, 0 drawn, -1 passed through).\n// The inline style holds the end, so cancelling leaves the line where it was going.\nfunction pen(path: SVGPathElement, from: number, to: number, duration: number, easing: string) {\n  path.getAnimations().forEach((a) => a.cancel())\n  path.style.strokeDashoffset = `${to}`\n  if (duration > 0 && from !== to && !still()) path.animate({ strokeDashoffset: [`${from}`, `${to}`] }, { duration, easing })\n}\n\n// Glissando's lens: the stretch of the line of words where the italic shows, in px along that line.\nfunction setLens(el: HTMLElement, [a, b]: [number, number]) {\n  el.style.setProperty(\"--ot-choice-a\", `${a}px`)\n  el.style.setProperty(\"--ot-choice-b\", `${b}px`)\n}\n// Sets the lens without its transition (a resize, the first place), then lets it move again.\nfunction quiet(el: HTMLElement, span: [number, number]) {\n  el.setAttribute(\"data-still\", \"\")\n  setLens(el, span)\n  getComputedStyle(el).getPropertyValue(\"--ot-choice-a\")\n  el.removeAttribute(\"data-still\")\n}\nconst NOWHERE: [number, number] = [-1e5, -1e5]\n\n// Ballot: the cross a hand sets after its choice on a ballot paper, two strokes in one path (the dash runs on from\n// the first into the second): a short one down to the right, then a longer one, set off higher, that bows and runs\n// past it, in a 20 × 20 box.\nconst BALLOT = \"M3.4 5.2C7.2 8.1 11.6 12.3 16.4 17.6M18.1 0.9C13.6 6.2 9.4 11.4 4.2 19.3\"\nconst crossOf = (l: Element | null | undefined) => l?.querySelector<SVGPathElement>(\":scope > .ot-choice-ballot > path\") ?? null\n// Runs a pen mark from one dash offset to another (1 not yet drawn, 0 drawn, -1 passed through, which looks the same as 1).\n// It ends on the stylesheet's own value unless held; a held sketch stays drawn until the next trace cancels it.\nfunction trace(path: SVGPathElement | null, from: number, to: number, duration: number, hold = false) {\n  path?.getAnimations().forEach((a) => a.cancel())\n  if (path && !still()) path.animate({ strokeDashoffset: [`${from}`, `${to}`] }, { duration, easing: BREATH, fill: hold ? \"forwards\" : \"none\" })\n}\n\ntype RadioGroupProps = Omit<React.ComponentProps<\"fieldset\">, \"defaultValue\" | \"onChange\"> & {\n  /** Shared by every radio, so a form submits the choice. Defaults to a generated name. */\n  name?: string\n  /** The small label over the words. Without one, give the group an aria-label. */\n  legend?: React.ReactNode\n  value?: string\n  defaultValue?: string\n  onValueChange?: (value: string) => void\n  /** legato: one accent dot glides from word to word along a slur. glissando: the italic itself slides through the words between, and an accent full stop lands after the chosen one. ballot: a pen cross in the accent is written after the chosen word. sforzando: the words stand in a heavy column and the chosen one swells into a large accent italic over its neighbours. */\n  variant?: Variant\n  /** The words run in a line and wrap, or stand in a column. */\n  orientation?: \"horizontal\" | \"vertical\"\n}\n\n/**\n * Words in a row (or a column). The chosen word turns italic, because it's yours now. Legato: one accent dot\n * glides beneath it along a slur, and pointing sketches that slur in pencil. Glissando: the italic slides\n * through the words between, and an accent full stop lands after the word. Ballot: a pen cross is written after\n * it in the accent. Sforzando: the words stand in a heavy column and the chosen one swells into a large accent\n * italic across its neighbours. Native radios underneath, so arrow keys move the choice.\n */\nfunction RadioGroup({\n  name,\n  legend,\n  value: controlled,\n  defaultValue,\n  onValueChange,\n  variant = \"legato\",\n  orientation = \"horizontal\",\n  className,\n  children,\n  onPointerOver,\n  onPointerLeave,\n  ref: forwardedRef, ...props\n}: RadioGroupProps) {\n  const generated = React.useId()\n  const [uncontrolled, setUncontrolled] = React.useState(defaultValue)\n  const value = controlled ?? uncontrolled\n  const box = React.useRef<HTMLFieldSetElement>(null)\n  const composedRef = useComposedRefs(box, forwardedRef)\n  const dot = React.useRef<HTMLSpanElement>(null)\n  const slur = React.useRef<SVGPathElement>(null)\n  const was = React.useRef(value)\n  const seen = React.useRef(false) // painted at least once, so a change is the person's and not the first render's\n  const where = React.useRef<Pt | null>(null) // legato: the chosen word's seat, where the dot rests\n  const flight = React.useRef<Flight | null>(null)\n  const sketched = React.useRef<HTMLLabelElement | null>(null) // legato: the word the pencil slur runs to\n  const lens = React.useRef<[number, number] | null>(null) // glissando: the chosen word's span along the line of words\n\n  const select = React.useCallback(\n    (next: string) => {\n      if (controlled === undefined) setUncontrolled(next)\n      onValueChange?.(next)\n    },\n    [controlled, onValueChange],\n  )\n  const ctx = React.useMemo(() => ({ name: name ?? generated, value, select, variant }), [name, generated, value, select, variant])\n\n  // The geometry both variants need, read fresh each time: the words, the chosen one, which way the slur bows.\n  const read = React.useCallback(() => {\n    const el = box.current!\n    const labels = [...el.querySelectorAll<HTMLLabelElement>(\":scope > label\")]\n    const column = orientation === \"vertical\"\n    const rtl = getComputedStyle(el).direction === \"rtl\"\n    const em = parseFloat(getComputedStyle(el).fontSize) || 16\n    const out = column ? { x: rtl ? 1 : -1, y: 0 } : { x: 0, y: 1 }\n    // A legend moves where the dot and the slur are laid from (they sit below it) but not the words' offsets,\n    // so the seat is taken back by the slur's own corner, which stands where the dot's origin does.\n    const r = el.getBoundingClientRect()\n    const k = r.width / (el.offsetWidth || r.width) || 1\n    const s = el.querySelector(\":scope > .ot-choice-slur\")?.getBoundingClientRect()\n    const o = s ? { x: (s.left - r.left) / k, y: (s.top - r.top) / k } : { x: 0, y: 0 }\n    const at = (l: HTMLElement) => {\n      const p = seat(l, column, rtl, length(el, \"--ot-choice-drop\"), length(el, \"--ot-choice-nudge\"))\n      return { x: p.x - o.x, y: p.y - o.y }\n    }\n    return { el, labels, chosen: labels.find((l) => l.querySelector(\"input:checked\")) ?? null, em, out, at }\n  }, [orientation])\n\n  // Glissando lays the words end to end on one line of reading, so the italic can slide from word to word\n  // whichever row or column they sit in. Each word learns its start on that line (--ot-choice-s) and the width\n  // of its italic and its roman (--ot-choice-w, -r: where the full stop lands, and the room it keeps);\n  // the lens (--ot-choice-a to -b) is where the italic shows.\n  const measure = React.useCallback((g: ReturnType<typeof read>) => {\n    const f = length(g.el, \"--ot-choice-f\")\n    const width = (l: HTMLElement, part: string) => `${l.querySelector<HTMLElement>(part)?.offsetWidth ?? 0}px`\n    g.labels.forEach((l) => {\n      l.style.setProperty(\"--ot-choice-w\", width(l, \".ot-choice-yours\"))\n      l.style.setProperty(\"--ot-choice-r\", width(l, \".ot-choice-word\"))\n    })\n    let s = 0\n    let span: [number, number] | null = null\n    for (const l of g.labels) {\n      l.style.setProperty(\"--ot-choice-s\", `${s}px`)\n      if (l === g.chosen) span = [s, s + l.offsetWidth]\n      s += l.offsetWidth + f * 2 // two feathers apart, so a word's soft edge never reaches its neighbour\n    }\n    return span\n  }, [])\n\n  // Puts every mark in its place without moving it: the dot, the pencil slur the docs pin, the italic's lens.\n  const place = React.useCallback(() => {\n    if (!box.current) return\n    const g = read()\n    if (variant === \"glissando\") {\n      lens.current = measure(g)\n      quiet(g.el, lens.current ?? NOWHERE)\n    } else {\n      where.current = g.chosen ? g.at(g.chosen) : null\n      if (where.current) {\n        g.el.style.setProperty(\"--ot-choice-x\", `${where.current.x}px`)\n        g.el.style.setProperty(\"--ot-choice-y\", `${where.current.y}px`)\n      }\n      // The docs pin a word as pointed at; it shows its pencil slur without a pointer.\n      const pinned = g.labels.find((l) => l !== g.chosen && l.matches('[data-force~=\"hover\"]'))\n      if (slur.current && pinned && where.current) {\n        const [a, b] = [where.current, g.at(pinned)]\n        slur.current.setAttribute(\"d\", slurPath(a, bow(a, b, g.out, g.em), b, dot.current?.offsetWidth || 7))\n        pen(slur.current, 0, 0, 0, \"\")\n      }\n    }\n    g.el.setAttribute(\"data-ready\", \"\")\n  }, [read, measure, variant])\n\n  // Pointing sketches: a pencil slur runs from the dot to a ring at the word you point at.\n  // Leaving, the line passes through, the way it was drawn.\n  const sketch = (l: HTMLLabelElement | null) => {\n    if (variant === \"ballot\") return sketchCross(l)\n    const path = slur.current\n    if (!path || !box.current || l === sketched.current) return\n    sketched.current = l\n    const a = where.current\n    if (l && a && !l.querySelector(\"input:checked, input:disabled\")) {\n      const g = read()\n      const b = g.at(l)\n      path.setAttribute(\"d\", slurPath(a, bow(a, b, g.out, g.em), b, dot.current?.offsetWidth || 7))\n      return pen(path, 1, 0, 320, EXHALE) // --ot-moderato\n    }\n    const now = parseFloat(getComputedStyle(path).strokeDashoffset) || 0\n    pen(path, Math.abs(now) < 1 ? now : -1, -1, 160, EXHALE) // --ot-allegro\n  }\n\n  // Ballot: pointing sketches the cross in pencil after the word; leaving, the pen runs on and the cross passes through.\n  const sketchCross = (l: HTMLLabelElement | null) => {\n    if (l === sketched.current) return\n    const left = sketched.current\n    sketched.current = l\n    if (left && !left.querySelector(\"input:checked\")) trace(crossOf(left), 0, -1, 160) // --ot-allegro\n    if (l && !l.querySelector(\"input:checked, input:disabled\")) trace(crossOf(l), 1, 0, 480, true) // between moderato and andante\n  }\n\n  React.useLayoutEffect(() => {\n    const before = was.current\n    const moved = before !== value && seen.current\n    was.current = value\n    if (!box.current) return\n    if (!moved) return place()\n    const g = read()\n\n    if (variant === \"ballot\") {\n      // The old cross runs on and passes through; the new one is written after its word, or, if the pencil already\n      // sketched it there, only inks (a sketch still being written runs on to the end in the accent).\n      const old = g.labels.find((l) => l.querySelector<HTMLInputElement>(\"input\")?.value === before)\n      if (old) trace(crossOf(old), 0, -1, 320) // --ot-moderato\n      const path = crossOf(g.chosen)\n      if (g.chosen && sketched.current !== g.chosen) trace(path, 1, 0, 480)\n      return\n    }\n\n    if (variant === \"glissando\") {\n      const from = lens.current\n      const to = measure(g)\n      lens.current = to\n      if (!to) return quiet(g.el, NOWHERE)\n      // The italic travels leading edge first, then gathers itself in: a longer slide takes a little longer.\n      g.el.setAttribute(\"data-moved\", \"\") // from now on the full stop lands each time; the first render's never does\n      g.el.setAttribute(\"data-heading\", from && to[0] < from[0] ? \"start\" : \"end\")\n      g.el.style.setProperty(\"--ot-choice-far\", `${from ? Math.min(Math.abs(to[0] - from[0]) / 480, 1) : 0}`)\n      // With nothing chosen before, the italic opens out of the middle of the word.\n      if (!from) quiet(g.el, [(to[0] + to[1]) / 2, (to[0] + to[1]) / 2])\n      return setLens(g.el, to)\n    }\n\n    const f = flight.current\n    const mid = f?.anim.playState === \"running\" ? f.anim.effect?.getComputedTiming().progress : null\n    const from = f && mid != null ? blossom(f.a, f.c, f.b, mid, mid) : where.current\n    place()\n    const to = where.current\n    if (!dot.current) return\n    f?.anim.cancel()\n    flight.current = null\n    const path = slur.current\n    const far = from && to ? Math.hypot(to.x - from.x, to.y - from.y) : 0\n    if (!to || !from || far < 1 || still()) {\n      if (path) pen(path, -1, -1, 0, \"\")\n      sketched.current = null\n      // The first choice has nowhere to glide from: the dot lands where it is.\n      if (to && !from && !still()) dot.current.animate({ scale: [\"0\", \"1\"] }, { duration: 320, easing: SPICCATO }) // --ot-moderato\n      return\n    }\n    // The dot glides along the slur like a drop of ink: drawn out along its path at mid-flight, round again as it lands.\n    const c = bow(from, to, g.out, g.em)\n    const pull = Math.min(far, 240) / 110\n    const duration = 320 + Math.min(far, 320) // --ot-moderato, a little longer the further it goes, never past --ot-andante\n    const frames = Array.from({ length: 17 }, (_, i) => {\n      const t = i / 16\n      const p = blossom(from, c, to, t, t)\n      const [dx, dy] = [(1 - t) * (c.x - from.x) + t * (to.x - c.x), (1 - t) * (c.y - from.y) + t * (to.y - c.y)]\n      const s = 1 + pull * Math.sin(Math.PI * t)\n      return { offset: t, translate: `${p.x}px ${p.y}px`, rotate: `${Math.atan2(dy, dx)}rad`, scale: `${s} ${1 / Math.sqrt(s)}` }\n    })\n    flight.current = { a: from, c, b: to, anim: dot.current.animate(frames, { duration, easing: BREATH }) }\n    // The pencil slur the pointer sketched is used up as the dot runs along it; a choice made by key runs clean.\n    if (path) pen(path, sketched.current === g.chosen ? 0 : -1, -1, duration, BREATH)\n    sketched.current = g.chosen\n  }, [value, place, read, measure, variant])\n\n  // Words re-flow on resize and when the fonts arrive; the marks follow without moving.\n  // ponytail: watches the words present at mount; add a MutationObserver if items come and go.\n  React.useEffect(() => {\n    const el = box.current\n    if (!el) return\n    const ro = new ResizeObserver(() => place())\n    ro.observe(el)\n    el.querySelectorAll(\"label\").forEach((l) => ro.observe(l))\n    document.fonts?.ready.then(() => place())\n    const frame = requestAnimationFrame(() => (seen.current = true))\n    return () => {\n      ro.disconnect()\n      cancelAnimationFrame(frame)\n    }\n  }, [place])\n\n  return (\n    <RadioGroupContext.Provider value={ctx}>\n      <fieldset\n        ref={composedRef}\n        data-slot=\"radio-group\"\n        data-variant={variant}\n        data-orientation={orientation}\n        className={cn(\"ot-choice\", className)}\n        onPointerOver={(e) => {\n          onPointerOver?.(e)\n          if (variant === \"legato\" || variant === \"ballot\") sketch((e.target as Element).closest(\"label\"))\n        }}\n        onPointerLeave={(e) => {\n          onPointerLeave?.(e)\n          if (variant === \"legato\" || variant === \"ballot\") sketch(null)\n        }}\n        {...props}\n      >\n        {legend ? <legend className=\"ot-label\">{legend}</legend> : null}\n        {children}\n        {variant === \"legato\" ? (\n          <>\n            <svg data-slot=\"radio-group-slur\" className=\"ot-choice-slur\" aria-hidden=\"true\">\n              <path ref={slur} pathLength={1} />\n            </svg>\n            <span ref={dot} data-slot=\"radio-group-dot\" className=\"ot-choice-dot\" aria-hidden=\"true\" />\n          </>\n        ) : null}\n      </fieldset>\n    </RadioGroupContext.Provider>\n  )\n}\n\ntype RadioGroupItemProps = Omit<React.ComponentProps<\"input\">, \"type\" | \"name\" | \"checked\" | \"defaultChecked\" | \"value\" | \"children\"> & {\n  value: string\n  /** The word. It is the control, and it turns italic when chosen. */\n  children: string\n  /** Classes for the label, which is the root. className goes to the input. */\n  labelClassName?: string\n  /** Pins a state for documentation (\"hover\", \"focus\"); set on the root. */\n  \"data-force\"?: string\n}\n\nfunction RadioGroupItem({ value, children, className, labelClassName, \"data-force\": force, onChange, ...props }: RadioGroupItemProps) {\n  const group = useRadioGroup()\n  return (\n    <label data-slot=\"radio-group-item\" data-force={force} className={labelClassName}>\n      <input\n        type=\"radio\"\n        name={group.name}\n        value={value}\n        checked={group.value === value}\n        className={className}\n        onChange={(e) => {\n          onChange?.(e)\n          group.select(value)\n        }}\n        {...props}\n      />\n      <span className=\"ot-choice-word\">{children}</span>\n      {/* The same word in the expression italic, laid in the same cell. The radio's name comes from the roman. */}\n      <span data-slot=\"radio-group-yours\" className=\"ot-choice-yours\" aria-hidden=\"true\">\n        {children}\n      </span>\n      {group.variant === \"ballot\" ? (\n        <svg data-slot=\"radio-group-ballot\" className=\"ot-choice-ballot\" viewBox=\"0 0 20 20\" aria-hidden=\"true\">\n          <path d={BALLOT} pathLength={1} />\n        </svg>\n      ) : null}\n    </label>\n  )\n}\n\nexport { RadioGroup, RadioGroupItem, type RadioGroupProps, type RadioGroupItemProps }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/0nlytype/styles/radio-group.css",
      "content": "@layer components {\n  /* ── ot-choice ── radio: the chosen word turns italic, because it's yours now.\n     legato: one accent dot glides beneath it along a slur; pointing sketches the slur in pencil.\n     glissando: the italic itself slides through the words between, and an accent full stop lands after the word. */\n  @property --ot-choice-a { syntax: \"<length>\"; inherits: true; initial-value: -100000px; }\n  @property --ot-choice-b { syntax: \"<length>\"; inherits: true; initial-value: -100000px; }\n  @property --ot-choice-f { syntax: \"<length>\"; inherits: true; initial-value: 0px; }\n  @property --ot-choice-drop { syntax: \"<length>\"; inherits: true; initial-value: 0px; }\n  @property --ot-choice-nudge { syntax: \"<length>\"; inherits: true; initial-value: 0px; }\n  .ot-choice {\n    /* Where the dot and the hover ring sit: this far below the word (or before it, in a column). Script reads these too.\n       In ems, so small words (the command line's runners) keep their dot as close as large ones: 14.5px at the mp step. */\n    --ot-choice-drop: 0.66em;\n    --ot-choice-nudge: 0px;\n    --ot-choice-x: 0px; --ot-choice-y: 0px;\n    position: relative; border: 0; margin: 0; padding: 0 0 calc(var(--ot-choice-drop) + var(--ot-dot) / 2); min-inline-size: 0;\n    display: flex; flex-wrap: wrap; align-items: baseline; column-gap: var(--ot-space-6); row-gap: var(--ot-space-5);\n    font-size: var(--ot-mp); line-height: var(--ot-mp-lh);\n  }\n  .ot-choice > legend { padding: 0; margin-bottom: var(--ot-space-3); }\n  .ot-choice > label { --ot-choice-s: 0px; --ot-choice-w: 0px; --ot-choice-r: 0px; position: relative; display: inline-grid; align-items: baseline; cursor: pointer; color: var(--ot-graphite); -webkit-tap-highlight-color: transparent; }\n  .ot-choice > label > input { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; pointer-events: none; }\n  .ot-choice > label > span { grid-area: 1 / 1; justify-self: start; transition: opacity var(--ot-moderato) var(--ot-breath), translate var(--ot-moderato) var(--ot-exhale), color var(--ot-allegro); }\n  .ot-choice-yours { font-family: var(--ot-expression); font-style: italic; font-size: calc(var(--ot-expression-scale) * 1em); line-height: 1; color: var(--ot-ink); user-select: none; }\n\n  /* The italic cross-fades in and the roman lifts away; once one word is chosen, the others step back to pencil.\n     The face that leaves goes quickly and the one that arrives takes its time, so the two are never printed over each other. */\n  .ot-choice > label > .ot-choice-yours { opacity: 0; translate: 0 0.12em; transition-duration: var(--ot-allegro); }\n  .ot-choice > label:has(input:checked) > .ot-choice-word { opacity: 0; translate: 0 -0.12em; transition-duration: var(--ot-allegro); }\n  .ot-choice > label:has(input:checked) > .ot-choice-yours { opacity: 1; translate: 0 0; transition-duration: var(--ot-moderato); transition-delay: calc(var(--ot-arpeggio) * 2); }\n  .ot-choice:has(input:checked) > label > .ot-choice-word { color: var(--ot-pencil); }\n  .ot-choice > label:is(:hover, [data-force~=\"hover\"]):not(:has(input:disabled)) > .ot-choice-word { color: var(--ot-ink); }\n  .ot-choice > label:has(input:focus-visible),\n  .ot-choice > label[data-force~=\"focus\"] { outline: var(--ot-stroke) solid var(--ot-accent); outline-offset: 6px; }\n  .ot-choice > label:has(input:disabled) { color: var(--ot-pencil); opacity: 0.55; cursor: not-allowed; }\n\n  /* legato: pointing shows a pencil ring where the dot would land, a beat after the pencil slur sets off toward it. */\n  .ot-choice[data-variant=\"legato\"] > label::before {\n    content: \"\"; position: absolute; inset-inline-start: 50%; top: calc(100% + var(--ot-choice-drop));\n    width: var(--ot-dot); height: var(--ot-dot); margin-block-start: calc(var(--ot-dot) / -2); margin-inline-start: calc(var(--ot-dot) / -2); box-sizing: border-box;\n    border: var(--ot-hairline) solid var(--ot-rule-strong); border-radius: 50%; scale: 0; pointer-events: none;\n    transition: scale var(--ot-allegro) var(--ot-exhale);\n  }\n  .ot-choice[data-variant=\"legato\"] > label:is(:hover, [data-force~=\"hover\"]):not(:has(input:checked, input:disabled))::before { scale: 1; transition: scale var(--ot-moderato) var(--ot-spiccato) calc(var(--ot-arpeggio) * 3); }\n  .ot-choice-dot {\n    position: absolute; left: 0; top: 0; width: var(--ot-dot); height: var(--ot-dot); margin: calc(var(--ot-dot) / -2) 0 0 calc(var(--ot-dot) / -2);\n    border-radius: 50%; background: var(--ot-accent); pointer-events: none;\n    translate: var(--ot-choice-x) var(--ot-choice-y);\n    transition: opacity var(--ot-allegro);\n  }\n  .ot-choice:is(:not([data-ready]), :not(:has(input:checked))) > .ot-choice-dot { opacity: 0; }\n  .ot-choice-slur { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; pointer-events: none; }\n  .ot-choice-slur > path { fill: none; stroke: var(--ot-rule-strong); stroke-width: var(--ot-hairline); stroke-dasharray: 1; stroke-dashoffset: 1; }\n\n  /* A column: the dot hangs in the margin before the word, level with its x-height, and the slur bows out into it. */\n  .ot-choice[data-orientation=\"vertical\"] { flex-direction: column; align-items: flex-start; row-gap: var(--ot-space-2); }\n  .ot-choice[data-orientation=\"vertical\"][data-variant=\"legato\"] {\n    --ot-choice-drop: var(--ot-space-4); --ot-choice-nudge: 0.08em;\n    padding-block-end: 0; padding-inline-start: calc(var(--ot-space-4) + var(--ot-dot));\n  }\n  .ot-choice[data-orientation=\"vertical\"][data-variant=\"legato\"] > legend { margin-inline-start: calc(-1 * (var(--ot-space-4) + var(--ot-dot))); }\n  .ot-choice[data-orientation=\"vertical\"][data-variant=\"legato\"] > label::before { inset-inline-start: calc(-1 * var(--ot-choice-drop)); top: calc(50% + var(--ot-choice-nudge)); }\n\n  /* glissando: the italic shows only inside a lens (--ot-choice-a to -b, along the line of words, soft at both ends\n     by --ot-choice-f); the roman shows everywhere else. Choosing moves the lens leading edge first, so the italic\n     runs through every word between like a glissando through the keys, then gathers itself into the chosen word. */\n  .ot-choice[data-variant=\"glissando\"] {\n    --ot-choice-a: -100000px; --ot-choice-b: -100000px; --ot-choice-f: 0.2em; --ot-choice-to: to right;\n    --ot-choice-far: 0; --ot-choice-t: calc(var(--ot-moderato) + var(--ot-choice-far) * (var(--ot-andante) - var(--ot-moderato)));\n    padding-block-end: 0;\n  }\n  .ot-choice[data-variant=\"glissando\"]:is([dir=\"rtl\"], [dir=\"rtl\"] *) { --ot-choice-to: to left; }\n  .ot-choice[data-variant=\"glissando\"][data-ready] { transition: --ot-choice-a var(--ot-choice-t) var(--ot-breath) calc(var(--ot-choice-t) * 0.3), --ot-choice-b var(--ot-choice-t) var(--ot-breath); }\n  .ot-choice[data-variant=\"glissando\"][data-ready][data-heading=\"start\"] { transition-delay: 0s, calc(var(--ot-choice-t) * 0.3); }\n  .ot-choice[data-variant=\"glissando\"][data-ready][data-still] { transition: none; }\n  .ot-choice[data-variant=\"glissando\"][data-ready] > label > span { opacity: 1; translate: none; transition: color var(--ot-allegro); }\n  .ot-choice[data-variant=\"glissando\"][data-ready] > label > .ot-choice-word {\n    mask-image: linear-gradient(var(--ot-choice-to), #000 calc(var(--ot-choice-a) - var(--ot-choice-s) - var(--ot-choice-f)), transparent calc(var(--ot-choice-a) - var(--ot-choice-s)), transparent calc(var(--ot-choice-b) - var(--ot-choice-s)), #000 calc(var(--ot-choice-b) - var(--ot-choice-s) + var(--ot-choice-f)));\n  }\n  .ot-choice[data-variant=\"glissando\"][data-ready] > label > .ot-choice-yours {\n    mask-image: linear-gradient(var(--ot-choice-to), transparent calc(var(--ot-choice-a) - var(--ot-choice-s) - var(--ot-choice-f)), #000 calc(var(--ot-choice-a) - var(--ot-choice-s)), #000 calc(var(--ot-choice-b) - var(--ot-choice-s)), transparent calc(var(--ot-choice-b) - var(--ot-choice-s) + var(--ot-choice-f)));\n  }\n  /* The full stop sits on the baseline after the word. Its room is always kept (after the wider of roman and\n     italic), so nothing moves its neighbours; pointing sketches it as a pencil ring there, and choosing lifts\n     the ring, lets the italic arrive, then lands the accent stop just after the italic's last letter. */\n  .ot-choice[data-variant=\"glissando\"] > label::before {\n    content: \"\"; grid-area: 1 / 1; justify-self: start; align-self: baseline;\n    width: max(0.22em, 5px); height: max(0.22em, 5px); margin-inline-start: calc(max(var(--ot-choice-w), var(--ot-choice-r)) + 0.16em); box-sizing: border-box;\n    border: var(--ot-hairline) solid var(--ot-rule-strong); border-radius: 50%; scale: 0;\n    transition: scale var(--ot-allegro) var(--ot-exhale);\n  }\n  .ot-choice[data-variant=\"glissando\"] > label:is(:hover, [data-force~=\"hover\"]):not(:has(input:checked, input:disabled))::before { scale: 1; transition: scale var(--ot-moderato) var(--ot-spiccato); }\n  .ot-choice[data-variant=\"glissando\"] > label:has(input:checked)::before {\n    --ot-choice-in: calc(var(--ot-choice-w) - max(var(--ot-choice-w), var(--ot-choice-r)));\n    scale: 1; translate: var(--ot-choice-in) 0; background: var(--ot-accent); border-color: var(--ot-accent);\n  }\n  .ot-choice[data-variant=\"glissando\"] > label:has(input:checked):is([dir=\"rtl\"], [dir=\"rtl\"] *)::before { translate: calc(-1 * var(--ot-choice-in)) 0; }\n  .ot-choice[data-variant=\"glissando\"][data-moved] > label:has(input:checked)::before { animation: ot-choice-land var(--ot-moderato) var(--ot-spiccato) calc(var(--ot-choice-t) * 0.8) backwards; }\n  @keyframes ot-choice-land { from { scale: 0; } }\n\n  /* ballot: the cross a hand sets in the box after a name on a ballot paper, in the accent, in a margin every word\n     keeps after it (so nothing moves, and the words still line up under the legend). Two strokes, as large as a\n     capital and never quite square: the pen writes the short one, lifts and writes the longer one past it (one path,\n     one dash). Pointing sketches it in pencil; leaving, the\n     pen runs on and it passes through. Choosing inks the sketch where it is, or writes it; the old cross runs on off\n     the page. Script runs the dash; the stylesheet holds where it rests. */\n  .ot-choice[data-variant=\"ballot\"] { column-gap: var(--ot-space-6); }\n  .ot-choice[data-variant=\"ballot\"] > label { padding-inline-end: 1.2em; }\n  .ot-choice-ballot {\n    position: absolute; inset-inline-end: 0; top: 50%; width: 0.78em; height: 0.78em; translate: 0 -50%;\n    overflow: visible; pointer-events: none;\n  }\n  .ot-choice-ballot:is([dir=\"rtl\"], [dir=\"rtl\"] *) { scale: -1 1; } /* the short stroke still comes first, from the inline start */\n  .ot-choice-ballot > path {\n    fill: none; stroke: var(--ot-rule-strong); stroke-width: calc(var(--ot-stroke) * 1.35); stroke-linecap: round;\n    stroke-dasharray: 1 2; stroke-dashoffset: 1;\n    transition: stroke 0s linear var(--ot-moderato); /* a cross that leaves keeps its ink until it has passed through */\n  }\n  .ot-choice > label:has(input:checked) > .ot-choice-ballot > path { stroke: var(--ot-accent); stroke-dashoffset: 0; transition: stroke var(--ot-allegro); }\n  .ot-choice > label[data-force~=\"hover\"]:not(:has(input:checked)) > .ot-choice-ballot > path { stroke-dashoffset: 0; }\n\n  /* sforzando: the poster's scale contrast. The words stand in a column, set heavy and narrow in the voice, packed\n     close; the chosen one's roman steps out and its italic, in the accent, swells to 1.75 times from its baseline, so it\n     runs up under the word above the way \"has\" crosses \"It\" and \"design.\" crosses \"to be\". Nothing reflows: the swell is\n     a scale. It lands with spiccato, the sudden accent the mark is named for; the one it leaves falls back at once. */\n  .ot-choice[data-variant=\"sforzando\"] {\n    flex-direction: column; align-items: flex-start; row-gap: 0; padding-block: 0.55em 0.1em;\n    font-size: var(--ot-mf); line-height: 1.02;\n  }\n  .ot-choice[data-variant=\"sforzando\"] > label > .ot-choice-word { font-weight: 780; font-stretch: 72%; letter-spacing: -0.015em; }\n  .ot-choice[data-variant=\"sforzando\"]:has(input:checked) > label > .ot-choice-word { color: var(--ot-graphite); }\n  .ot-choice[data-variant=\"sforzando\"] > label:is(:hover, [data-force~=\"hover\"]):not(:has(input:disabled)) > .ot-choice-word { color: var(--ot-ink); }\n  /* The swell runs under the word above, so that word still reads; focus rings the swell, not the box it left. */\n  .ot-choice[data-variant=\"sforzando\"] > label:not(:has(input:checked)) { z-index: 1; }\n  .ot-choice[data-variant=\"sforzando\"] > label:has(input:checked):is(:has(input:focus-visible), [data-force~=\"focus\"]) { outline: 0; }\n  .ot-choice[data-variant=\"sforzando\"] > label:has(input:checked):is(:has(input:focus-visible), [data-force~=\"focus\"]) > .ot-choice-yours {\n    outline: calc(var(--ot-stroke) / 1.75) solid var(--ot-accent); outline-offset: 0.06em;\n  }\n  .ot-choice[data-variant=\"sforzando\"] > label > .ot-choice-yours { color: var(--ot-accent); transform-origin: 0 80%; }\n  .ot-choice[data-variant=\"sforzando\"] > label > .ot-choice-yours:is([dir=\"rtl\"], [dir=\"rtl\"] *) { transform-origin: 100% 80%; }\n  .ot-choice[data-variant=\"sforzando\"] > label:has(input:checked) > .ot-choice-yours {\n    scale: 1.75; translate: calc(-0.08em * var(--ot-choice-dir, 1)) 0;\n    transition: opacity var(--ot-allegro), scale var(--ot-moderato) var(--ot-spiccato), translate var(--ot-moderato) var(--ot-spiccato);\n  }\n  .ot-choice[data-variant=\"sforzando\"]:is([dir=\"rtl\"], [dir=\"rtl\"] *) { --ot-choice-dir: -1; }\n\n  @media (forced-colors: active) {\n    .ot-choice > label:is(:has(input:focus-visible), [data-force~=\"focus\"]) { outline-color: Highlight; }\n    .ot-choice > label:is(:has(input:focus-visible), [data-force~=\"focus\"]) > .ot-choice-yours { outline-color: Highlight; }\n    .ot-choice-ballot > path { stroke: CanvasText; }\n    .ot-choice > label:has(input:checked) > .ot-choice-ballot > path { stroke: Highlight; }\n    .ot-choice-dot, .ot-choice-slur, .ot-choice-ballot, .ot-choice > label::before { forced-color-adjust: none; }\n    .ot-choice-dot { background: Highlight; }\n    .ot-choice-slur > path { stroke: CanvasText; }\n    .ot-choice > label::before { border-color: CanvasText; }\n    .ot-choice[data-variant=\"glissando\"] > label:has(input:checked)::before { background: Highlight; border-color: Highlight; }\n  }\n}\n",
      "type": "registry:file",
      "target": "styles/0nlytype/radio-group.css"
    }
  ],
  "css": {
    "@import \"../styles/0nlytype/radio-group.css\"": {}
  },
  "meta": {
    "movement": "VI",
    "underneath": "native",
    "contract": "ot-choice"
  },
  "type": "registry:ui"
}
