{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "combobox",
  "title": "Combobox",
  "description": "A field that suggests. Type and the letters you typed are marked in each match, the suggestions arrive in turn, and what you chose sits on the line in italic; with multiple, your choices are written there as one sentence.",
  "registryDependencies": [
    "https://thedirectors.agency/ui/r/0nlytype.json",
    "https://thedirectors.agency/ui/r/badge.json",
    "https://thedirectors.agency/ui/r/command.json",
    "https://thedirectors.agency/ui/r/field.json",
    "https://thedirectors.agency/ui/r/popover.json"
  ],
  "files": [
    {
      "path": "registry/0nlytype/ui/combobox.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { useComposedRefs } from \"@/lib/0nlytype/refs\"\nimport { cn } from \"@/lib/utils\"\nimport { Badge } from \"@/components/ui/badge\"\nimport { Command, CommandEmpty, CommandHint, CommandInput, CommandItem, CommandList } from \"@/components/ui/command\"\nimport { Field, useFieldControl, useLineOrigin } from \"@/components/ui/field\"\nimport { Popover, PopoverAnchor, PopoverContent, PopoverTrigger } from \"@/components/ui/popover\"\n\ntype ComboboxOption = { value: string; label: string; hint?: string }\n\ntype ComboboxOptions = {\n  /** Labels should be unique: they are what the list is matched and marked on. */\n  options: ComboboxOption[]\n  /** Set above the line. Left out, label it with a Field, or with aria-label. */\n  label?: string\n  /** What the closed line says before anything is chosen. */\n  placeholder?: string\n  /** Shown when nothing matches. Name something to try. */\n  empty?: string\n  /** Pins a state for documentation (\"hover\", \"focus\"); set on the trigger. */\n  \"data-force\"?: string\n}\n\ntype ComboboxBase = Omit<React.ComponentProps<\"button\">, \"value\" | \"defaultValue\" | \"onChange\" | \"type\"> & ComboboxOptions\n\ntype ComboboxOne = ComboboxBase & {\n  multiple?: false\n  value?: string\n  defaultValue?: string\n  onValueChange?: (value: string) => void\n  /**\n   * list (default): a line that opens a list; what you type is marked in each match.\n   * concordance: the matches hang from what you typed, lined up on it, as a concordance sets a word.\n   * pencil: you type on the line itself, and the rest of the best match is pencilled in after the caret.\n   */\n  variant?: \"list\" | \"concordance\"\n}\n\ntype ComboboxPencilProps = Omit<React.ComponentProps<\"input\">, \"value\" | \"defaultValue\" | \"type\"> & ComboboxOptions & {\n  multiple?: false\n  value?: string\n  defaultValue?: string\n  onValueChange?: (value: string) => void\n  variant: \"pencil\"\n}\n\ntype ComboboxMany = ComboboxBase & {\n  /**\n   * Any number of choices. They are written on the line as one sentence in your italic (\"Didot, Futura and Univers\"),\n   * each struck and taken out by pressing it; the list stays open while you choose, and a chosen row turns italic.\n   */\n  multiple: true\n  value?: string[]\n  defaultValue?: string[]\n  onValueChange?: (value: string[]) => void\n  /** With `name`, each chosen value is sent with the form as its own entry. */\n  name?: string\n  variant?: \"list\" | \"concordance\"\n}\n\ntype ComboboxProps = ComboboxOne | ComboboxMany | ComboboxPencilProps\n\n/** A tempo token in milliseconds: the tokens are written in seconds or milliseconds. */\nconst ms = (v: string, fallback: number) => (v.trim().endsWith(\"ms\") ? parseFloat(v) : parseFloat(v) * 1000) || fallback\nconst still = () => typeof matchMedia === \"undefined\" || matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n\n/**\n * A field that suggests. Open it, type, and the letters you typed are marked in each match.\n * The chosen label is yours, so it sits on the line in italic. Enter, Space or the down arrow\n * opens the list; Enter picks; Escape closes it and puts focus back on the line.\n */\nfunction Combobox(props: ComboboxProps) {\n  const inField = Boolean(useFieldControl().id)\n  const { label, id } = props\n  // On its own, a label brings its own Field so the line, the label and the hint wire up the same way.\n  const own = Boolean(label && !inField)\n  const control = { ...props, id: own ? undefined : id }\n  const node = control.variant === \"pencil\" ? <ComboboxPencil {...control} /> : <ComboboxControl {...control} />\n  return own ? (\n    <Field label={label} id={id} className=\"ot-combo-field\">\n      {node}\n    </Field>\n  ) : (\n    node\n  )\n}\n\n/** Where the query first sits in a label: the words before it, the match, and the words after. */\nfunction split(label: string, query: string) {\n  const q = query.trim()\n  const i = q ? label.toLowerCase().indexOf(q.toLowerCase()) : -1\n  return i < 0 ? [\"\", \"\", label] : [label.slice(0, i), label.slice(i, i + q.length), label.slice(i + q.length)]\n}\n\nfunction ComboboxControl({\n  options,\n  value: valueProp,\n  defaultValue,\n  onValueChange,\n  multiple,\n  variant,\n  label,\n  placeholder = multiple ? \"Choose any\" : \"Choose one\",\n  empty = \"Nothing matches. Try part of a name.\",\n  className,\n  id,\n  onKeyDown,\n  name,\n  ...props\n}: ComboboxOne | ComboboxMany) {\n  const field = useFieldControl()\n  const [open, setOpen] = React.useState(false)\n  const listId = React.useId()\n  const [own, setOwn] = React.useState<string | string[] | undefined>(defaultValue)\n  const value = valueProp ?? own\n  // Single or many, the choices are one list here: what's chosen, in the order it was chosen.\n  const picked = multiple ? ((value as string[] | undefined) ?? []) : value ? [value as string] : []\n  const chosen = multiple ? undefined : options.find((o) => o.value === value)\n  const concordance = variant === \"concordance\"\n  const [query, setQuery] = React.useState(\"\")\n  const list = React.useRef<HTMLDivElement>(null)\n  const was = React.useRef<Map<string, number> | null>(null)\n\n  const set = (next: string | string[]) => {\n    if (valueProp === undefined) setOwn(next)\n    ;(onValueChange as ((v: string | string[]) => void) | undefined)?.(next)\n  }\n  const pick = (next: string) => {\n    if (!multiple) {\n      set(next)\n      return setOpen(false)\n    }\n    // Many: the row toggles and the list stays open for the next.\n    set(picked.includes(next) ? picked.filter((v) => v !== next) : [...picked, next])\n  }\n\n  // The concordance's glide: each row's words were somewhere before the query changed; they slide to the new axis.\n  const starts = () => new Map(Array.from(list.current?.querySelectorAll<HTMLElement>(\"[cmdk-item]\") ?? [], (row) => [row.dataset.value ?? \"\", row.querySelector(\".ot-combo-pre > span\")?.getBoundingClientRect().left ?? 0]))\n  React.useLayoutEffect(() => {\n    const before = was.current\n    was.current = null\n    if (!before || !list.current || still()) return\n    const css = getComputedStyle(list.current)\n    const now = starts()\n    list.current.querySelectorAll<HTMLElement>(\"[cmdk-item]\").forEach((row) => {\n      const from = before.get(row.dataset.value ?? \"\")\n      const to = now.get(row.dataset.value ?? \"\")\n      if (from === undefined || to === undefined || Math.abs(from - to) < 0.5) return\n      row.animate([{ translate: `${from - to}px 0` }, { translate: \"0 0\" }], { duration: ms(css.getPropertyValue(\"--ot-moderato\"), 320), easing: css.getPropertyValue(\"--ot-breath\").trim() || \"ease\" })\n    })\n  }, [query])\n\n  const button = (\n    <PopoverTrigger asChild>\n      <button\n        type=\"button\"\n        role=\"combobox\"\n        aria-haspopup=\"listbox\"\n        aria-expanded={open}\n        aria-controls={listId}\n        data-slot=\"combobox\"\n        data-placeholder={picked.length ? undefined : \"\"}\n        className={cn(\"ot-combo\", !multiple && className)}\n        name={multiple ? undefined : name}\n        {...props}\n        id={id ?? field.id}\n        aria-label={props[\"aria-label\"]}\n        aria-invalid={props[\"aria-invalid\"] ?? field[\"aria-invalid\"]}\n        aria-describedby={props[\"aria-describedby\"] ?? field[\"aria-describedby\"]}\n        onKeyDown={(e) => {\n          onKeyDown?.(e)\n          if (e.key === \"ArrowDown\" && !e.defaultPrevented) {\n            e.preventDefault()\n            setOpen(true)\n          }\n        }}\n      >\n        <span data-slot=\"combobox-value\" className=\"ot-combo-value\">\n          {multiple ? picked.length ? <span className=\"ot-sr\">{picked.length} chosen</span> : placeholder : (chosen?.label ?? placeholder)}\n        </span>\n      </button>\n    </PopoverTrigger>\n  )\n\n  return (\n    <Popover\n      open={open}\n      onOpenChange={(next) => {\n        setOpen(next)\n        if (!next) setQuery(\"\")\n      }}\n    >\n      {multiple ? (\n        // The line holds the sentence of what you chose, then the rest of the line opens the list. The words are\n        // series tags, siblings of the trigger, so taking out the last one hands focus on to it.\n        <PopoverAnchor asChild>\n          <div data-slot=\"combobox-line\" className={cn(\"ot-combo-many\", className)} data-disabled={props.disabled || undefined}>\n            {picked.map((v) => (\n              <Badge\n                key={v}\n                variant=\"series\"\n                onRemove={props.disabled ? undefined : () => set(picked.filter((x) => x !== v))}\n              >\n                {options.find((o) => o.value === v)?.label ?? v}\n              </Badge>\n            ))}\n            {button}\n            {name ? picked.map((v) => <input key={v} type=\"hidden\" name={name} value={v} disabled={props.disabled} />) : null}\n          </div>\n        </PopoverAnchor>\n      ) : (\n        button\n      )}\n      <PopoverContent\n        data-slot=\"combobox-content\"\n        data-variant={concordance ? \"concordance\" : undefined}\n        data-multiple={multiple ? \"\" : undefined}\n        className=\"ot-combo-pop\"\n        aria-label={label ?? placeholder}\n      >\n        <Command id={listId} loop label={label ?? placeholder} defaultValue={chosen?.label}>\n          <CommandInput\n            placeholder=\"Type to narrow the list\"\n            onValueChange={(next) => {\n              if (concordance) was.current = starts()\n              setQuery(next)\n            }}\n          />\n          <CommandList ref={list} data-slot=\"combobox-list\" aria-multiselectable={multiple || undefined}>\n            <CommandEmpty>{empty}</CommandEmpty>\n            {options.map((o) => {\n              const [pre, hit, post] = split(o.label, query)\n              const on = picked.includes(o.value)\n              return (\n                <CommandItem key={o.value} value={o.label} keywords={o.hint ? [o.hint] : undefined} data-chosen={on ? \"\" : undefined} aria-checked={multiple ? on : undefined} onSelect={() => pick(o.value)}>\n                  {concordance ? (\n                    <>\n                      <span className=\"ot-combo-pre\">\n                        <span>{pre}</span>\n                      </span>\n                      <span className=\"ot-combo-hit\">{hit ? <mark>{hit}</mark> : null}</span>\n                      <span className=\"ot-combo-post\">\n                        {post}\n                        {o.hint ? <CommandHint>{o.hint}</CommandHint> : null}\n                      </span>\n                    </>\n                  ) : (\n                    o.label\n                  )}\n                  {!concordance && o.hint ? <CommandHint>{o.hint}</CommandHint> : null}\n                </CommandItem>\n              )\n            })}\n          </CommandList>\n        </Command>\n      </PopoverContent>\n    </Popover>\n  )\n}\n\n/** The highlighter over what you typed, as the list's rows mark it. */\nfunction Marked({ text, query }: { text: string; query: string }) {\n  const [pre, hit, post] = split(text, query)\n  return (\n    <span className=\"ot-combo-name\">\n      {pre}\n      {hit ? <mark>{hit}</mark> : null}\n      {post}\n    </span>\n  )\n}\n\nconst SHOWN = 5 // ponytail: the pencil lists the first five matches, so it never scrolls; typing narrows the rest.\n\n/**\n * The pencil: an input on the line itself. The best match's remaining letters are pencilled in after the caret,\n * in our roman; Tab or the right arrow (at the end) takes them and they ink into your italic. Up and Down move\n * through the few matches below the line, Enter picks one, Escape puts the line back.\n */\nfunction ComboboxPencil({ options, value: valueProp, defaultValue, onValueChange, label, placeholder = \"Start typing\", empty = \"Nothing matches. Try part of a name.\", className, id, disabled, readOnly, dir = \"auto\", \"aria-label\": named, \"data-force\": force, ref, onFocus, onPointerDown, onChange, onClick, onKeyDown, onBlur, variant, multiple, ...props }: ComboboxPencilProps) {\n  void variant\n  void multiple\n  const field = useFieldControl()\n  const listId = React.useId()\n  const input = React.useRef<HTMLInputElement>(null)\n  const composedRef = useComposedRefs(input, ref)\n  const [own, setOwn] = React.useState(defaultValue)\n  const value = valueProp ?? own\n  const chosen = options.find((o) => o.value === value)\n  const [text, setText] = React.useState(chosen?.label ?? \"\")\n  const [open, setOpen] = React.useState(false)\n  const [at, setAt] = React.useState(0)\n  // A controlled value that changes from outside rewrites the line (unless you're typing on it).\n  const [seen, setSeen] = React.useState(chosen?.label)\n  if (seen !== chosen?.label) {\n    setSeen(chosen?.label)\n    if (!open) setText(chosen?.label ?? \"\")\n  }\n\n  const q = text.trim().toLowerCase()\n  const found = q ? options.filter((o) => o.label.toLowerCase().includes(q) || o.hint?.toLowerCase().includes(q)) : options\n  // Rows that begin with what you typed come first: they are the ones the pencil can finish.\n  const shown = [...found.filter((o) => o.label.toLowerCase().startsWith(q)), ...found.filter((o) => !o.label.toLowerCase().startsWith(q))].slice(0, SHOWN)\n  const active = open ? shown[Math.min(at, shown.length - 1)] : undefined\n  const rest = active && text && active.label.toLowerCase().startsWith(text.toLowerCase()) ? active.label.slice(text.length) : \"\"\n  const origin = useLineOrigin<HTMLInputElement>((el) => el, { onFocus, onPointerDown })\n\n  const choose = (o: ComboboxOption) => {\n    if (disabled || readOnly) return\n    if (valueProp === undefined) setOwn(o.value)\n    onValueChange?.(o.value)\n    setText(o.label)\n    setOpen(false)\n    // What was pencil is now yours: the word inks in.\n    const el = input.current\n    if (el && !still()) {\n      const css = getComputedStyle(el)\n      el.animate([{ color: css.getPropertyValue(\"--ot-pencil\").trim() }, { color: css.color }], { duration: ms(css.getPropertyValue(\"--ot-moderato\"), 320), easing: css.getPropertyValue(\"--ot-exhale\").trim() || \"ease-out\" })\n    }\n  }\n\n  const key = (e: React.KeyboardEvent<HTMLInputElement>) => {\n    onKeyDown?.(e)\n    if (e.defaultPrevented || readOnly) return\n    const atEnd = e.currentTarget.selectionStart === text.length\n    if (e.key === \"ArrowDown\" || e.key === \"ArrowUp\") {\n      e.preventDefault()\n      if (!open) return setOpen(true)\n      const n = shown.length || 1\n      setAt((i) => (Math.min(i, n - 1) + (e.key === \"ArrowDown\" ? 1 : n - 1)) % n)\n    } else if (e.key === \"Enter\" && active) {\n      e.preventDefault()\n      choose(active)\n    } else if ((e.key === \"Tab\" && !e.shiftKey) || (e.key === (getComputedStyle(e.currentTarget).direction === \"rtl\" ? \"ArrowLeft\" : \"ArrowRight\") && atEnd)) {\n      if (!rest || !active) return\n      e.preventDefault()\n      choose(active)\n    } else if (e.key === \"Escape\" && open) {\n      e.preventDefault()\n      setOpen(false)\n      setText(chosen?.label ?? \"\")\n    }\n  }\n\n  return (\n    <div data-slot=\"combobox\" data-variant=\"pencil\" data-force={force} className={cn(\"ot-combo-pencil\", className)}>\n      <span className=\"ot-combo-line\">\n        <input\n          {...props}\n          ref={composedRef}\n          type=\"text\"\n          dir={dir} // the line and its pencil copy both take the direction of what's typed, so they stay in register\n          role=\"combobox\"\n          autoComplete={props.autoComplete ?? \"off\"}\n          spellCheck={props.spellCheck ?? false}\n          aria-autocomplete=\"list\"\n          aria-expanded={open}\n          aria-controls={listId}\n          aria-activedescendant={active ? `${listId}-${shown.indexOf(active)}` : undefined}\n          aria-label={named ?? (label && !field.id ? label : undefined)}\n          aria-invalid={props[\"aria-invalid\"] ?? field[\"aria-invalid\"]}\n          aria-describedby={props[\"aria-describedby\"] ?? field[\"aria-describedby\"]}\n          id={id ?? field.id}\n          disabled={disabled}\n          readOnly={readOnly}\n          placeholder={placeholder}\n          value={text}\n          className=\"ot-input\"\n          onChange={(e) => {\n            onChange?.(e)\n            if (e.defaultPrevented) return\n            setText(e.target.value)\n            setOpen(true)\n            setAt(0)\n          }}\n          onClick={(e) => {\n            onClick?.(e)\n            if (!e.defaultPrevented && !readOnly) setOpen(true)\n          }}\n          onKeyDown={key}\n          onBlur={(e) => {\n            onBlur?.(e)\n            if (e.defaultPrevented) return\n            setOpen(false)\n            const exact = options.find((o) => o.label.toLowerCase() === q)\n            if (exact && exact.value !== value) choose(exact)\n            else setText(chosen?.label ?? \"\")\n          }}\n          {...origin}\n        />\n        <span className=\"ot-combo-ghost\" dir={dir} aria-hidden=\"true\">\n          {text}\n          <span>{rest}</span>\n        </span>\n      </span>\n      <ul id={listId} role=\"listbox\" aria-label={label ?? placeholder} hidden={!open || !shown.length} className=\"ot-combo-list\">\n        {shown.map((o, i) => (\n          <li\n            key={o.value}\n            id={`${listId}-${i}`}\n            role=\"option\"\n            aria-selected={o === active}\n            data-chosen={o.value === value ? \"\" : undefined}\n            onMouseDown={(e) => e.preventDefault()} // keep focus on the line, so the pick lands before the blur\n            onPointerMove={() => setAt(i)}\n            onClick={() => choose(o)}\n          >\n            <Marked text={o.label} query={q} />\n            {o.hint ? <span className=\"ot-command-hint\">{o.hint}</span> : null}\n          </li>\n        ))}\n      </ul>\n      {open && !shown.length ? (\n        <p className=\"ot-combo-empty\" role=\"status\">\n          {empty}\n        </p>\n      ) : null}\n    </div>\n  )\n}\n\nexport { Combobox, type ComboboxOption, type ComboboxProps }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/0nlytype/styles/combobox.css",
      "content": "@layer components {\n  /* ── ot-combo ── a field that suggests. The closed line is a baseline like the input's;\n     what you chose is yours, in italic. The open list is a ot-pop hung from it, holding\n     a cmdk Command, so the letters you typed are marked in each match and the rows arrive in turn. */\n  .ot-field > .ot-combo { grid-column: 1 / -1; }\n  .ot-combo {\n    appearance: none; position: relative; display: block; width: 100%; margin: 0; border: 0; border-radius: 0; outline: 0;\n    padding: 0.3em 1.4em 0.42em 0; text-align: start; cursor: pointer;\n    background:\n      linear-gradient(var(--ot-accent), var(--ot-accent)) no-repeat var(--o, 0%) 100% / 0% var(--ot-stroke),\n      linear-gradient(var(--ot-rule-strong), var(--ot-rule-strong)) no-repeat 0 100% / 100% var(--ot-hairline);\n    color: var(--ot-ink);\n    font-family: var(--ot-expression); font-style: italic; font-weight: 400;\n    font-size: calc(var(--ot-mp) * var(--ot-expression-scale)); line-height: 1.3;\n    transition: background-size var(--ot-moderato) var(--ot-exhale);\n  }\n  .ot-combo[data-placeholder] { font-family: var(--ot-voice); font-style: normal; font-size: var(--ot-mp); color: var(--ot-pencil); }\n  .ot-combo-value { display: block; min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }\n  .ot-combo::after {\n    content: \"↓\" / \"\"; position: absolute; inset-inline-end: 0; bottom: 0.5em;\n    font-family: var(--ot-voice); font-style: normal; font-size: var(--ot-pp); line-height: 1; color: var(--ot-pencil);\n    transition: translate var(--ot-moderato) var(--ot-exhale), color var(--ot-allegro);\n  }\n  .ot-combo:is(:hover, [data-force~=\"hover\"])::after { translate: 0 2px; color: var(--ot-ink); }\n  .ot-combo[data-state=\"open\"]::after { color: var(--ot-ink); }\n  .ot-combo:is(:focus-visible, [data-state=\"open\"], [data-force~=\"focus\"]) { background-size: 100% var(--ot-stroke), 100% var(--ot-hairline); }\n  .ot-combo[aria-invalid=\"true\"] { background-image: linear-gradient(var(--ot-signal), var(--ot-signal)), linear-gradient(var(--ot-signal), var(--ot-signal)); }\n  .ot-combo:disabled {\n    color: var(--ot-pencil); cursor: not-allowed;\n    background-image: none, repeating-linear-gradient(90deg, var(--ot-rule-strong) 0 2px, transparent 2px 6px);\n  }\n  .ot-combo:disabled::after { content: none; }\n\n  /* ── multiple ── what you chose is written on the line as one sentence in your italic: the badge's series tags,\n     \"Didot, Futura and Univers\", ours the commas and the \"and\". The baseline runs under the whole line; the rest of it,\n     after the last word, is the trigger that opens the list again. Pressing a word strikes it and the sentence closes up\n     round the gap. It wraps like prose and the line stays under its last row. */\n  .ot-field > .ot-combo-many { grid-column: 1 / -1; }\n  .ot-combo-many {\n    display: flex; flex-wrap: wrap; align-items: baseline; row-gap: 0.12em; padding: 0.3em 0 0.42em;\n    font-size: var(--ot-mp); line-height: 1.3;\n    background:\n      linear-gradient(var(--ot-accent), var(--ot-accent)) no-repeat 0 100% / 0% var(--ot-stroke),\n      linear-gradient(var(--ot-rule-strong), var(--ot-rule-strong)) no-repeat 0 100% / 100% var(--ot-hairline);\n    transition: background-size var(--ot-moderato) var(--ot-exhale);\n  }\n  .ot-combo-many > .ot-tag { line-height: 1.3; }\n  .ot-combo-many > .ot-combo { flex: 1 0 1.8em; align-self: stretch; width: auto; padding-block: 0; background: none; }\n  .ot-combo-many > .ot-combo:not([data-placeholder]) { padding-inline-start: 0.4em; }\n  .ot-combo-many > .ot-combo::after { bottom: 0.2em; }\n  .ot-combo-many:has(> .ot-combo:is(:focus-visible, [data-state=\"open\"], [data-force~=\"focus\"])) { background-size: 100% var(--ot-stroke), 100% var(--ot-hairline); }\n  .ot-combo-many:has(> .ot-combo[aria-invalid=\"true\"]) { background-image: linear-gradient(var(--ot-signal), var(--ot-signal)), linear-gradient(var(--ot-signal), var(--ot-signal)); }\n  .ot-combo-many[data-disabled] { background-image: none, repeating-linear-gradient(90deg, var(--ot-rule-strong) 0 2px, transparent 2px 6px); }\n  .ot-combo-many[data-disabled] > .ot-tag { color: var(--ot-pencil); }\n  :is([dir=\"rtl\"], [dir=\"rtl\"] *).ot-combo-many { background-position: 100% 100%, 0 100%; }\n\n  /* The panel: as wide as the line, the search inside it set a size down from the palette's. */\n  .ot-pop.ot-combo-pop { width: max(var(--radix-popover-trigger-width), 16rem); max-width: calc(100vw - 2 * var(--ot-margin)); padding: var(--ot-space-4) 0 var(--ot-space-2); } /* as wide as its line, past the popover's own cap */\n  .ot-combo-pop .ot-command { gap: var(--ot-space-3); }\n  .ot-combo-pop .ot-command-input { width: calc(100% - 2 * var(--ot-space-5)); margin-inline: var(--ot-space-5); font-size: calc(var(--ot-mp) * var(--ot-expression-scale)); }\n  .ot-combo-pop .ot-command-input::placeholder { font-size: var(--ot-mp); }\n  .ot-combo-pop .ot-command-list { max-height: 17rem; overscroll-behavior: contain; }\n\n  /* A row: a dot marks the one you point at (ink) and the one you chose (accent); the arrow the palette\n     draws is put away. */\n  .ot-pop.ot-combo-pop .ot-command-option {\n    position: relative; padding: 0.32em var(--ot-space-5) 0.32em calc(var(--ot-space-5) + 0.7em); border-bottom: 0;\n  }\n  .ot-pop.ot-combo-pop .ot-command-option::after { content: none; }\n  .ot-pop.ot-combo-pop .ot-command-option::before {\n    content: \"\"; position: absolute; inset-inline-start: var(--ot-space-5); top: calc(50% - 3px); width: 6px; height: 6px; border-radius: 50%;\n    background: var(--ot-ink); scale: 0; transition: scale var(--ot-moderato) var(--ot-spiccato);\n  }\n  .ot-pop.ot-combo-pop .ot-command-option[aria-selected=\"true\"] .ot-command-name { translate: 0 0; }\n  .ot-pop.ot-combo-pop .ot-command-option[aria-selected=\"true\"]::before { scale: 1; }\n  .ot-pop.ot-combo-pop .ot-command-option[data-chosen] { color: var(--ot-ink); }\n  .ot-pop.ot-combo-pop .ot-command-option[data-chosen]::before { background: var(--ot-accent); scale: 1; }\n  .ot-combo-pop .ot-command-empty { padding-inline: var(--ot-space-5); color: var(--ot-pencil); }\n\n  /* multiple: no accent in the list, since there are many choices and one accent. A chosen row is written in your italic,\n     as it is on the line; the ink dot is only the one you point at. The leading is held so a row doesn't grow as it turns. */\n  .ot-pop.ot-combo-pop[data-multiple] .ot-command-option[data-chosen]::before { background: var(--ot-ink); scale: 0; }\n  .ot-pop.ot-combo-pop[data-multiple] .ot-command-option[data-chosen][aria-selected=\"true\"]::before { scale: 1; }\n  .ot-combo-pop[data-multiple] .ot-command-option[data-chosen] :is(.ot-command-name, .ot-combo-pre, .ot-combo-hit, .ot-combo-post) {\n    font-family: var(--ot-expression); font-style: italic; font-size: calc(var(--ot-mp) * var(--ot-expression-scale)); line-height: calc(var(--ot-mp) * var(--ot-mp-lh));\n  }\n  .ot-combo-pop[data-multiple] .ot-command-option[data-chosen] .ot-command-hint { font-family: var(--ot-voice); font-style: normal; }\n\n  /* ── concordance ── as a concordance sets a word in the middle of every line that holds it:\n     each match is split round what you typed, and the typed letters stand in one column. The\n     axis sits just after the longest start, so no row travels further than it must; typing\n     moves it, and the rows glide to it (script, on the row). */\n  .ot-combo-pop[data-variant=\"concordance\"] [cmdk-list-sizer] { display: grid; grid-template-columns: auto auto minmax(0, 1fr); }\n  .ot-combo-pop[data-variant=\"concordance\"] [cmdk-list-sizer] > * { grid-column: 1 / -1; }\n  .ot-pop.ot-combo-pop[data-variant=\"concordance\"] .ot-command-option { grid-template-columns: subgrid; grid-auto-flow: row; column-gap: 0; font-size: var(--ot-mp); line-height: var(--ot-mp-lh); }\n  .ot-combo-pre { text-align: end; white-space: pre; color: var(--ot-pencil); transition: color var(--ot-allegro); }\n  .ot-combo-hit { white-space: pre; color: var(--ot-ink); }\n  .ot-combo-hit > mark { padding: 0; margin: 0; }\n  .ot-combo-post { min-width: 0; overflow: hidden; white-space: pre; text-overflow: ellipsis; }\n  .ot-combo-post > .ot-command-hint { margin-inline-start: var(--ot-space-3); }\n  .ot-command-option:is(:hover, [aria-selected=\"true\"]) .ot-combo-pre { color: inherit; }\n\n  /* ── pencil ── you type on the line itself. The best match's remaining letters are pencilled in\n     after the caret, in our roman, over a copy of what you typed that can't be seen; taking them\n     inks the word into your italic. The few matches sit under the line, in the page, not over it. */\n  .ot-field > .ot-combo-pencil { grid-column: 1 / -1; }\n  .ot-combo-pencil { display: grid; row-gap: var(--ot-space-2); }\n  .ot-combo-line { position: relative; display: block; }\n  .ot-combo-line > .ot-input { position: relative; background-color: transparent; }\n  .ot-combo-ghost {\n    position: absolute; inset: 0; overflow: hidden; padding: 0.3em 0 0.42em; white-space: pre; pointer-events: none; color: transparent;\n    font-family: var(--ot-expression); font-style: italic; font-weight: 400; font-size: calc(var(--ot-mp) * var(--ot-expression-scale)); line-height: 1.3;\n  }\n  .ot-combo-ghost > span { font-family: var(--ot-voice); font-style: normal; font-size: var(--ot-mp); color: var(--ot-pencil); }\n  .ot-combo-pencil[data-force~=\"focus\"] { outline: 0; }\n  .ot-combo-pencil[data-force~=\"focus\"] .ot-input { background-size: 100% var(--ot-stroke), 100% var(--ot-hairline); }\n  .ot-combo-pencil .ot-combo-list { display: grid; margin: 0; padding: 0; list-style: none; }\n  .ot-combo-pencil .ot-combo-list[hidden] { display: none; }\n  .ot-combo-pencil [role=\"option\"] {\n    position: relative; display: flex; align-items: baseline; gap: var(--ot-space-3); padding-block: 0.15em; padding-inline-start: 0.9em; cursor: pointer;\n    font-size: var(--ot-p); line-height: var(--ot-p-lh); color: var(--ot-pencil); transition: color var(--ot-allegro);\n    animation: ot-arrive var(--ot-moderato) var(--ot-exhale) backwards; animation-delay: calc(min(sibling-index(), 6) * var(--ot-arpeggio));\n  }\n  .ot-combo-pencil [role=\"option\"]::before {\n    content: \"\"; position: absolute; inset-inline-start: 0; top: calc(50% - 2.5px); width: 5px; height: 5px; border-radius: 50%;\n    background: var(--ot-ink); scale: 0; transition: scale var(--ot-moderato) var(--ot-spiccato);\n  }\n  .ot-combo-pencil [role=\"option\"][aria-selected=\"true\"] { color: var(--ot-ink); }\n  .ot-combo-pencil [role=\"option\"][aria-selected=\"true\"]::before { scale: 1; }\n  .ot-combo-pencil [role=\"option\"][data-chosen]::before { background: var(--ot-accent); scale: 1; }\n  .ot-combo-pencil [role=\"option\"] mark { padding: 0; margin: 0; }\n  .ot-combo-pencil .ot-combo-empty { margin: 0; font-size: var(--ot-p); color: var(--ot-pencil); }\n\n  @media (forced-colors: active) {\n    .ot-combo-ghost { forced-color-adjust: none; }\n    .ot-combo-ghost > span { color: GrayText; }\n    .ot-combo-pencil [role=\"option\"][aria-selected=\"true\"] { color: Highlight; }\n    .ot-combo-pencil [role=\"option\"]::before { forced-color-adjust: none; background: CanvasText; }\n  }\n}\n",
      "type": "registry:file",
      "target": "styles/0nlytype/combobox.css"
    }
  ],
  "css": {
    "@import \"../styles/0nlytype/combobox.css\"": {}
  },
  "meta": {
    "movement": "VI",
    "underneath": "cmdk",
    "contract": "ot-combo"
  },
  "type": "registry:ui"
}
