{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "input-group",
  "title": "Input group",
  "description": "Ours in roman, yours in italic, on one line. The accent draws only under the part you type. Or your figure stands large with our words stacked beside it, or an arrow leads from your words to the action.",
  "registryDependencies": [
    "https://thedirectors.agency/ui/r/0nlytype.json",
    "https://thedirectors.agency/ui/r/button.json",
    "https://thedirectors.agency/ui/r/field.json"
  ],
  "files": [
    {
      "path": "registry/0nlytype/ui/input-group.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { useComposedRefs } from \"@/lib/0nlytype/refs\"\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/components/ui/button\"\nimport { useFieldControl, useLineOrigin } from \"@/components/ui/field\"\n\n/** What is typed in the group, so our words can agree with it. */\nconst GroupValue = React.createContext(\"\")\n\ntype InputGroupProps = React.ComponentProps<\"div\"> & {\n  /**\n   * How ours and yours share the line. line: prefix, your words, suffix, in reading order. legend: your figure large,\n   * with our words stacked beside it in two small lines. arrow: a hairline arrow runs from your words to the action,\n   * and gives way as you write.\n   */\n  variant?: \"line\" | \"legend\" | \"arrow\"\n  /** Pins a state for documentation (\"focus\"); set on the root. */\n  \"data-force\"?: string\n}\n\n/** Ours and yours on one line. Put text, the input and an action inside; the accent draws only under your part. */\nfunction InputGroup({ className, variant = \"line\", onInput, onPointerDown, children, ref: forwardedRef, ...props }: InputGroupProps) {\n  const root = React.useRef<HTMLDivElement>(null)\n  const composedRef = useComposedRefs(root, forwardedRef)\n  const [value, setValue] = React.useState(\"\")\n  React.useLayoutEffect(() => setValue(root.current?.querySelector(\"input\")?.value ?? \"\"), [])\n  return (\n    <div\n      ref={composedRef}\n      data-slot=\"input-group\"\n      data-variant={variant}\n      data-filled={value ? \"\" : undefined}\n      className={cn(\"ot-input-group\", className)}\n      onInput={(e) => {\n        onInput?.(e)\n        if (e.target instanceof HTMLInputElement) setValue(e.target.value)\n      }}\n      // The whole line is the field: a touch on its empty part writes in the input.\n      onPointerDown={(e) => {\n        onPointerDown?.(e)\n        const input = e.currentTarget.querySelector(\"input\")\n        if (e.target === e.currentTarget && input && !input.disabled) {\n          e.preventDefault()\n          input.focus()\n        }\n      }}\n      {...props}\n    >\n      <GroupValue.Provider value={value}>{children}</GroupValue.Provider>\n    </div>\n  )\n}\n\ntype InputGroupTextProps = React.ComponentProps<\"span\"> & {\n  /** Forms of a unit that agree with the number typed, by plural category: { one: \"night\", other: \"nights\" }. */\n  agree?: Partial<Record<Intl.LDMLPluralRule, string>> & { other: string }\n}\n\n/** What we supply: a prefix or suffix, upright in pencil. With `agree`, it agrees with your number. */\nfunction InputGroupText({ className, agree, children, ...props }: InputGroupTextProps) {\n  const value = React.useContext(GroupValue)\n  // Nothing typed (and the server's render) takes \"other\", so the page's locale can't change the first paint.\n  const form = value ? new Intl.PluralRules().select(Number(value.replace(/[^\\d.-]/g, \"\")) || 0) : \"other\"\n  const unit = agree ? (agree[form] ?? agree.other) : null\n  return (\n    <span data-slot=\"input-group-text\" className={cn(\"ot-input-group-text\", className)} {...props}>\n      {unit}\n      {unit && children ? \" \" : null}\n      {children}\n    </span>\n  )\n}\n\n/** What you type, in italic. Takes the Field's id, hint and error when there is one. */\nfunction InputGroupInput({ className, ...props }: React.ComponentProps<\"input\">) {\n  const field = useFieldControl()\n  const origin = useLineOrigin<HTMLInputElement>((el) => el.closest<HTMLElement>(\".ot-input-group\") ?? el, props)\n  return (\n    <input\n      data-slot=\"input-group-input\"\n      className={cn(\"ot-input-group-input\", className)}\n      {...props}\n      id={props.id ?? field.id}\n      maxLength={props.maxLength ?? field.maxLength}\n      aria-invalid={props[\"aria-invalid\"] ?? field[\"aria-invalid\"]}\n      aria-describedby={props[\"aria-describedby\"] ?? field[\"aria-describedby\"]}\n      {...origin}\n    />\n  )\n}\n\n/** An action at the end of the line, set as a quiet Button. */\nfunction InputGroupButton({ className, variant = \"quiet\", ...props }: React.ComponentProps<typeof Button>) {\n  return <Button data-slot=\"input-group-button\" variant={variant} className={cn(\"ot-input-group-button\", className)} {...props} />\n}\n\nexport { InputGroup, InputGroupText, InputGroupInput, InputGroupButton, type InputGroupProps, type InputGroupTextProps }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/0nlytype/styles/input-group.css",
      "content": "@layer components {\n  /* ── ot-input-group ── ours in roman, yours in italic, on one line; the accent is only under yours */\n  .ot-input-group {\n    grid-column: 1 / -1; position: relative; anchor-scope: --ot-yours;\n    display: flex; align-items: baseline; column-gap: 0.12em;\n    font-size: var(--ot-mp); line-height: 1.3;\n    background: linear-gradient(var(--ot-rule-strong), var(--ot-rule-strong)) no-repeat 0 100% / 100% var(--ot-hairline);\n  }\n  .ot-input-group-text { flex: none; padding-block: 0.3em 0.42em; color: var(--ot-pencil); white-space: nowrap; }\n  .ot-input-group-input {\n    anchor-name: --ot-yours;\n    flex: 1; min-width: 0; appearance: none; margin: 0; border: 0; border-radius: 0; outline: 0; padding: 0.3em 0 0.42em; background: none;\n    color: var(--ot-ink); font-family: var(--ot-expression); font-style: italic; font-weight: 400; font-size: calc(var(--ot-expression-scale) * 1em); line-height: 1.3;\n  }\n  .ot-input-group-input::-webkit-search-cancel-button, .ot-input-group-input::-webkit-search-decoration { appearance: none; } /* no native clear mark; Escape clears */\n  .ot-input-group-input:disabled { color: var(--ot-pencil); cursor: not-allowed; }\n  .ot-input-group-input::placeholder { font-family: var(--ot-voice); font-style: normal; font-size: calc(1em / var(--ot-expression-scale)); color: var(--ot-pencil); opacity: 1; }\n  .ot-input-group::after {\n    content: \"\"; position: absolute; bottom: 0; inset-inline-start: anchor(--ot-yours start, 0px); inset-inline-end: anchor(--ot-yours end, 0px); height: var(--ot-stroke);\n    background: var(--ot-accent); scale: 0 1; transform-origin: var(--o, 0%) 50%; pointer-events: none;\n    transition: scale var(--ot-moderato) var(--ot-exhale);\n  }\n  .ot-input-group[data-force~=\"focus\"] { outline: 0; }\n  .ot-input-group:focus-within::after, .ot-input-group[data-force~=\"focus\"]::after { scale: 1 1; }\n  .ot-input-group:has(> .ot-input-group-input[aria-invalid=\"true\"])::after { scale: 1 1; background: var(--ot-signal); }\n  .ot-input-group > .ot-input-group-button { margin-inline-start: var(--ot-space-4); font-size: var(--ot-p); }\n  /* A suffix follows your words, so the line reads as one phrase: halden.studio/identity/brief. */\n  @supports (field-sizing: content) {\n    .ot-input-group-input:has(+ .ot-input-group-text) { flex: 0 1 auto; field-sizing: content; min-width: 2ch; }\n  }\n\n  /* ── legend ── \"28 / December 2025\": your figure large, our words stacked beside it in two small lines,\n     the first in ink (it can agree with your number), the second in pencil. */\n  .ot-input-group[data-variant=\"legend\"] { display: grid; grid-template-columns: auto minmax(0, 1fr); grid-template-rows: auto 1fr; column-gap: var(--ot-space-3); align-items: start; }\n  .ot-input-group[data-variant=\"legend\"] > .ot-input-group-input {\n    grid-row: 1 / span 2; grid-column: 1; width: 2ch; height: 1.08em; padding-block: 0;\n    font-size: calc(var(--ot-ff) * var(--ot-expression-scale)); line-height: 1; letter-spacing: var(--ot-ff-tr); font-variant-numeric: lining-nums tabular-nums;\n  }\n  @supports (field-sizing: content) {\n    .ot-input-group[data-variant=\"legend\"] > .ot-input-group-input { width: auto; field-sizing: content; min-width: 1ch; }\n  }\n  .ot-input-group[data-variant=\"legend\"] > .ot-input-group-text { grid-column: 2; padding: 0; font-size: var(--ot-p); line-height: var(--ot-p-lh); white-space: normal; }\n  .ot-input-group[data-variant=\"legend\"] > .ot-input-group-text:nth-of-type(1) { margin-top: 0.9em; color: var(--ot-ink); }\n\n  /* ── arrow ── \"———→ Watch this space.\": a hairline arrow runs from your words to the action. The shaft gives way\n     as you write; it inks when what you wrote is ready; pointing at the action steps it forward. */\n  .ot-input-group[data-variant=\"arrow\"] > .ot-input-group-input { flex: 0 1 auto; min-width: 4ch; }\n  @supports (field-sizing: content) {\n    .ot-input-group[data-variant=\"arrow\"] > .ot-input-group-input { field-sizing: content; }\n  }\n  .ot-input-group[data-variant=\"arrow\"]::before {\n    content: \"\"; order: 1; flex: 1 1 var(--ot-space-7); min-width: var(--ot-space-6); height: 0.62em; margin-inline-start: var(--ot-space-3);\n    background: var(--ot-rule-strong); /* drawn by the mask: a hairline shaft and an open head, whatever the face */\n    mask:\n      url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 9'%3E%3Cpath d='M3.5 1l4 3.5-4 3.5M0 4.5h7.5' fill='none' stroke='%23000'/%3E%3C/svg%3E\") no-repeat 100% 50% / var(--ot-space-2) 9px,\n      linear-gradient(#000, #000) no-repeat 0 50% / calc(100% - 2px) var(--ot-hairline);\n    transition: background-color var(--ot-moderato), translate var(--ot-allegro) var(--ot-spiccato);\n  }\n  .ot-input-group[data-variant=\"arrow\"]:is([dir=\"rtl\"], [dir=\"rtl\"] *)::before { scale: -1 1; }\n  .ot-input-group[data-variant=\"arrow\"] > .ot-input-group-button { order: 2; margin-inline-start: var(--ot-space-2); }\n  .ot-input-group[data-variant=\"arrow\"][data-filled]:has(> .ot-input-group-input:valid)::before { background-color: var(--ot-ink); }\n  .ot-input-group[data-variant=\"arrow\"]:has(> .ot-input-group-button:is(:hover, :focus-visible, [data-force~=\"hover\"]))::before { translate: 0.25em 0; }\n  .ot-input-group[data-variant=\"arrow\"]:is([dir=\"rtl\"], [dir=\"rtl\"] *):has(> .ot-input-group-button:is(:hover, :focus-visible, [data-force~=\"hover\"]))::before { translate: -0.25em 0; }\n  @media (forced-colors: active) {\n    .ot-input-group { forced-color-adjust: none; color: CanvasText; --ot-ink: CanvasText; --ot-pencil: GrayText; --ot-rule-strong: GrayText; --ot-accent: Highlight; --ot-signal: CanvasText; }\n  }\n}\n",
      "type": "registry:file",
      "target": "styles/0nlytype/input-group.css"
    }
  ],
  "css": {
    "@import \"../styles/0nlytype/input-group.css\"": {}
  },
  "meta": {
    "movement": "VI",
    "underneath": "native",
    "contract": "ot-input-group"
  },
  "type": "registry:ui"
}
