{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "field",
  "title": "Field",
  "description": "No box, only a baseline. Your words arrive in italic, focus draws the accent from where you touched, and an error hangs from the line as a callout. Or your words are overprinted on a heavy label, or signed on a form's line.",
  "registryDependencies": [
    "https://thedirectors.agency/ui/r/0nlytype.json"
  ],
  "files": [
    {
      "path": "registry/0nlytype/ui/field.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { useComposedRefs } from \"@/lib/0nlytype/refs\"\nimport { useFormPresentation } from \"@/lib/0nlytype/use-form-presentation\"\nimport { cn } from \"@/lib/utils\"\n\n/** Errors a Form found, keyed by the id of the control they belong to. */\nconst FieldErrors = React.createContext<Record<string, string>>({})\nconst FieldErrorsProvider = FieldErrors.Provider\n\ntype FieldContext = { id: string; hintId?: string; errorId?: string; invalid: boolean; maxLength?: number; sync: () => void }\nconst FieldCtx = React.createContext<FieldContext | null>(null)\n\n/** What a control inside a Field needs: its id, what describes it, whether it's wrong. Empty outside a Field. */\nfunction useFieldControl() {\n  const ctx = React.useContext(FieldCtx)\n  React.useLayoutEffect(() => { ctx?.sync() })\n  return {\n    id: ctx?.id,\n    maxLength: ctx?.maxLength,\n    \"aria-invalid\": ctx?.invalid ? (true as const) : undefined,\n    \"aria-describedby\": [ctx?.hintId, ctx?.errorId].filter(Boolean).join(\" \") || undefined,\n  }\n}\n\ntype FieldProps = Omit<React.ComponentProps<\"div\">, \"id\"> & {\n  label?: React.ReactNode\n  hint?: React.ReactNode\n  /** What to fix, in plain words. Turns the baseline crimson and hangs the message from it. */\n  error?: React.ReactNode\n  /** Show how much is typed, \"12 / 40\", while the field has focus. */\n  count?: boolean\n  maxLength?: number\n  /** The control's id. Made for you when omitted. */\n  id?: string\n  /**\n   * How the label and your words share the line. line: the label above a baseline. overprint: the label is a heavy\n   * condensed word and your italic is printed over it, knocked out in paper. signature: a cross marks where to write\n   * and the label is a caption under the line, as on a printed form.\n   */\n  variant?: \"line\" | \"overprint\" | \"signature\"\n}\n\n/** One control on a baseline. Wraps an Input, Textarea, InputGroup or InputOTP and wires label, hint and error to it. */\nfunction Field({ label, hint, error, count, maxLength, id, variant = \"line\", className, children, onInput, ref: forwardedRef, ...props }: FieldProps) {\n  const made = React.useId()\n  const controlId = id ?? made\n  const formErrors = React.useContext(FieldErrors)\n  const message = error ?? formErrors[controlId]\n  const root = React.useRef<HTMLDivElement>(null)\n  const composedRef = useComposedRefs(root, forwardedRef)\n  const [typed, setTyped] = React.useState(0)\n  const sync = React.useCallback(() => {\n    const el = root.current?.querySelector<HTMLInputElement | HTMLTextAreaElement>('input:not([type=\"hidden\"]), textarea')\n    setTyped(el?.value.length ?? 0)\n  }, [])\n  useFormPresentation(root, sync)\n\n  const ctx: FieldContext = {\n    id: controlId,\n    hintId: hint ? `${controlId}-hint` : undefined,\n    errorId: message ? `${controlId}-error` : undefined,\n    invalid: Boolean(message),\n    maxLength,\n    sync,\n  }\n\n  return (\n    <FieldCtx.Provider value={ctx}>\n      <div\n        ref={composedRef}\n        data-slot=\"field\"\n        data-variant={variant}\n        data-filled={typed > 0 ? \"\" : undefined}\n        className={cn(\"ot-field\", className)}\n        onInput={(e) => {\n          onInput?.(e)\n          if ((e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement)) setTyped(e.target.value.length)\n        }}\n        {...props}\n      >\n        {label ? (\n          <label data-slot=\"field-label\" className=\"ot-label\" htmlFor={controlId}>\n            {label}\n          </label>\n        ) : null}\n        {count ? (\n          <span data-slot=\"field-count\" className=\"ot-field-count\" aria-hidden=\"true\">\n            {typed}\n            {maxLength ? ` / ${maxLength}` : null}\n          </span>\n        ) : null}\n        {variant === \"signature\" ? <span className=\"ot-field-mark\" aria-hidden=\"true\" /> : null}\n        {children}\n        {hint ? (\n          <p data-slot=\"field-hint\" id={ctx.hintId} className=\"ot-field-hint\">\n            {hint}\n          </p>\n        ) : null}\n        {message ? <FieldError id={ctx.errorId}>{message}</FieldError> : null}\n      </div>\n    </FieldCtx.Provider>\n  )\n}\n\n/** The callout: a pill on a leader line, hung from the baseline it's about. */\nfunction FieldError({ className, ...props }: React.ComponentProps<\"p\">) {\n  return <p data-slot=\"field-error\" className={cn(\"ot-field-error\", className)} {...props} />\n}\n\ntype Control<T extends HTMLElement> = React.ComponentProps<T extends HTMLTextAreaElement ? \"textarea\" : \"input\">\n\n/**\n * The accent line grows out from where you touched the line; for the keyboard,\n * from where the line starts (the right in a right-to-left page). Sets --o on `line`.\n */\nfunction useLineOrigin<T extends HTMLInputElement | HTMLTextAreaElement>(line: (el: T) => HTMLElement, { onPointerDown, onFocus }: Control<T>) {\n  const touched = React.useRef(false)\n  return {\n    onPointerDown(e: React.PointerEvent<T>) {\n      onPointerDown?.(e as never)\n      const r = e.currentTarget.getBoundingClientRect()\n      line(e.currentTarget).style.setProperty(\"--o\", `${Math.round(((e.clientX - r.left) / r.width) * 100)}%`)\n      touched.current = true\n    },\n    onFocus(e: React.FocusEvent<T>) {\n      onFocus?.(e as never)\n      if (!touched.current) line(e.currentTarget).style.setProperty(\"--o\", getComputedStyle(e.currentTarget).direction === \"rtl\" ? \"100%\" : \"0%\")\n      touched.current = false\n    },\n  }\n}\n\n/** Your words arrive in italic; the placeholder stays in our voice, in pencil. Works alone or inside a Field. */\nfunction Input({ className, ...props }: React.ComponentProps<\"input\">) {\n  const field = useFieldControl()\n  const origin = useLineOrigin<HTMLInputElement>((el) => el, props)\n  return (\n    <input\n      data-slot=\"input\"\n      className={cn(\"ot-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/** Ruled like paper: the lines sit on the text's baselines and scroll with it. */\nfunction Textarea({ className, ...props }: React.ComponentProps<\"textarea\">) {\n  const field = useFieldControl()\n  const origin = useLineOrigin<HTMLTextAreaElement>((el) => el, props)\n  return (\n    <textarea\n      data-slot=\"textarea\"\n      className={cn(\"ot-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\nexport { Field, FieldError, FieldErrorsProvider, Input, Textarea, useFieldControl, useLineOrigin, type FieldProps }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/0nlytype/styles/field.css",
      "content": "@layer components {\n  /* ── ot-field ── no box, only a baseline; your words arrive in italic */\n  .ot-field { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: baseline; row-gap: var(--ot-space-1); }\n  .ot-field > .ot-label { grid-column: 1; }\n  .ot-field-count { grid-column: 2; direction: ltr; unicode-bidi: isolate; font-size: var(--ot-pp); color: var(--ot-pencil); font-variant-numeric: tabular-nums; opacity: 0; transition: opacity var(--ot-allegro); }\n  .ot-field:focus-within .ot-field-count, .ot-field[data-force~=\"focus\"] .ot-field-count { opacity: 1; }\n  .ot-field > .ot-input { grid-column: 1 / -1; }\n\n  /* The control itself. Works alone, or inside a Field. */\n  .ot-input {\n    appearance: none; width: 100%; margin: 0; border: 0; border-radius: 0; outline: 0;\n    padding: 0.3em 0 0.42em;\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  /* A search field keeps no native clear mark, a blue × in no scheme's palette; Escape still clears it. */\n  .ot-input::-webkit-search-cancel-button, .ot-input::-webkit-search-decoration { appearance: none; }\n  .ot-input::placeholder { font-family: var(--ot-voice); font-style: normal; font-size: var(--ot-mp); color: var(--ot-pencil); opacity: 1; transition: color var(--ot-moderato); }\n  .ot-input:focus::placeholder { color: color-mix(in srgb, var(--ot-pencil) 55%, transparent); } /* it steps back when you arrive */\n  .ot-input:focus,\n  .ot-field[data-force~=\"focus\"] > .ot-input { background-size: 100% var(--ot-stroke), 100% var(--ot-hairline); }\n  .ot-field[data-force~=\"focus\"] { outline: 0; }\n  .ot-input[aria-invalid=\"true\"] { background-image: linear-gradient(var(--ot-signal), var(--ot-signal)), linear-gradient(var(--ot-signal), var(--ot-signal)); }\n  .ot-input: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  textarea.ot-input {\n    --lh: 2.25rem;\n    min-height: calc(var(--lh) * 4); padding: 0; resize: vertical;\n    field-sizing: content; scrollbar-width: none; /* it grows with what you write, so it has nothing to scroll (the browser bar stays away) */\n    line-height: var(--lh);\n    background:\n      linear-gradient(var(--ot-accent), var(--ot-accent)) no-repeat var(--o, 0%) 100% / 0% var(--ot-stroke),\n      repeating-linear-gradient(transparent 0 calc(var(--lh) - 1px), var(--ot-rule-strong) calc(var(--lh) - 1px) var(--lh)) 0 var(--ot-rule-shift, -0.3rem) / 100% var(--lh);\n    background-attachment: local;\n  }\n  textarea.ot-input:focus,\n  .ot-field[data-force~=\"focus\"] > textarea.ot-input { background-size: 100% var(--ot-stroke), 100% var(--lh); }\n  textarea.ot-input[aria-invalid=\"true\"] {\n    background-image:\n      linear-gradient(var(--ot-signal), var(--ot-signal)),\n      repeating-linear-gradient(transparent 0 calc(var(--lh) - 1px), var(--ot-signal) calc(var(--lh) - 1px) var(--lh));\n  }\n\n  .ot-field-hint, .ot-field-error { grid-column: 1 / -1; margin-top: var(--ot-space-1); font-size: var(--ot-pp); line-height: var(--ot-pp-lh); }\n  .ot-field-hint { color: var(--ot-pencil); }\n  /* An error is a callout, as Weingart set them: a pill on a leader line,\n     hung from the baseline it's about. */\n  .ot-field-error {\n    position: relative; justify-self: start; margin-top: var(--ot-space-5);\n    padding: 0.3em 0.9em 0.36em; border-radius: 1em;\n    box-shadow: inset 0 0 0 var(--ot-hairline) var(--ot-signal); color: var(--ot-signal);\n    animation: ot-callout var(--ot-moderato) var(--ot-exhale) 220ms backwards;\n  }\n  .ot-field-error::before { content: \"\"; position: absolute; inset-inline-start: 1.1em; bottom: 100%; width: var(--ot-hairline); height: var(--ot-space-5); background: var(--ot-signal); transform-origin: top; animation: ot-draw-y var(--ot-moderato) var(--ot-breath) 60ms backwards; }\n  .ot-field-error::after { content: \"\"; position: absolute; inset-inline-start: calc(1.1em - 2px); bottom: calc(100% + var(--ot-space-5) - 2.5px); width: 5px; height: 5px; border-radius: 50%; background: var(--ot-signal); animation: ot-land var(--ot-moderato) var(--ot-spiccato) backwards; }\n  @keyframes ot-callout { from { color: transparent; box-shadow: inset 0 0 0 var(--ot-hairline) transparent; } }\n  /* ── overprint ── \"It has to be design.\": the label is a heavy condensed word, and your italic is printed over it,\n     knocked out of it by a paper outline. Focus takes a breath in (the word condenses) and your words take the accent. */\n  .ot-field[data-variant=\"overprint\"] > .ot-label {\n    grid-row: 1; grid-column: 1 / -1; align-self: start; margin-bottom: 0.45em;\n    font-size: var(--ot-f); line-height: 0.9; letter-spacing: var(--ot-f-tr); font-weight: 800; font-stretch: 78%; color: var(--ot-ink);\n    transition: font-stretch var(--ot-moderato) var(--ot-exhale), color var(--ot-moderato);\n  }\n  .ot-field[data-variant=\"overprint\"] > .ot-field-count { grid-row: 1; align-self: start; position: relative; }\n  .ot-field[data-variant=\"overprint\"] > .ot-input {\n    grid-row: 1; align-self: end; position: relative; padding-bottom: 0.22em; line-height: 1.2;\n    font-size: calc(var(--ot-mf) * var(--ot-expression-scale)); caret-color: var(--ot-accent);\n    -webkit-text-stroke: 0.11em var(--ot-paper); paint-order: stroke fill;\n    background-size: 0% var(--ot-stroke), 100% var(--ot-hairline); /* the accent is in your words, so the line stays a hairline */\n    transition: color var(--ot-moderato);\n  }\n  .ot-field[data-variant=\"overprint\"] > .ot-input::placeholder { color: transparent; } /* the label is the prompt */\n  .ot-field[data-variant=\"overprint\"]:is(:focus-within, [data-force~=\"focus\"]) > .ot-label { font-stretch: 62%; }\n  .ot-field[data-variant=\"overprint\"]:is(:focus-within, [data-force~=\"focus\"]) > .ot-input { color: var(--ot-accent); background-size: 0% var(--ot-stroke), 100% var(--ot-hairline); }\n  .ot-field[data-variant=\"overprint\"]:has(> .ot-input:disabled) > .ot-label { color: var(--ot-pencil); }\n\n  /* ── signature ── the printed form: a cross marks where to write, the label is a caption under the line.\n     Arriving, the + turns to the × of \"sign here\" and inks; once you've written, it steps back. */\n  .ot-field[data-variant=\"signature\"] > .ot-input { grid-row: 1; padding-inline-start: 0.7em; padding-bottom: 0.22em; line-height: 1.2; font-size: calc(var(--ot-mf) * var(--ot-expression-scale)); }\n  .ot-field[data-variant=\"signature\"] > .ot-field-mark {\n    grid-row: 1; grid-column: 1; align-self: end; justify-self: start; position: relative;\n    width: 0.4em; height: 0.4em; margin-bottom: 0.46em; margin-inline-start: 0.04em; font-size: var(--ot-mf); color: var(--ot-pencil); pointer-events: none;\n    background:\n      linear-gradient(currentColor, currentColor) no-repeat center / 100% var(--ot-stroke),\n      linear-gradient(currentColor, currentColor) no-repeat center / var(--ot-stroke) 100%;\n    transition: rotate var(--ot-moderato) var(--ot-spiccato), color var(--ot-moderato);\n  }\n  .ot-field[data-variant=\"signature\"]:is(:focus-within, [data-force~=\"focus\"]) > .ot-field-mark { rotate: 45deg; color: var(--ot-ink); }\n  .ot-field[data-variant=\"signature\"][data-filled]:not(:focus-within, [data-force~=\"focus\"]) > .ot-field-mark { rotate: 45deg; }\n  .ot-field[data-variant=\"signature\"] > .ot-label { grid-row: 2; align-self: start; }\n  .ot-field[data-variant=\"signature\"] > .ot-field-count { grid-row: 2; align-self: start; }\n  /* The caption holds the start under the line, so the callout hangs from the end. */\n  .ot-field[data-variant=\"signature\"] > .ot-field-error { grid-row: 2; justify-self: end; }\n  .ot-field[data-variant=\"signature\"] > .ot-field-error::before { inset-inline-start: auto; inset-inline-end: 1.1em; }\n  .ot-field[data-variant=\"signature\"] > .ot-field-error::after { inset-inline-start: auto; inset-inline-end: calc(1.1em - 2px); }\n  /* Forced colours: the lines are drawn, so the field restates its tokens in system colours and keeps them. */\n  @media (forced-colors: active) {\n    .ot-field, .ot-input { forced-color-adjust: none; color: CanvasText; --ot-paper: Canvas; --ot-ink: CanvasText; --ot-graphite: CanvasText; --ot-pencil: GrayText; --ot-rule-strong: GrayText; --ot-accent: Highlight; --ot-signal: CanvasText; }\n  }\n}\n",
      "type": "registry:file",
      "target": "styles/0nlytype/field.css"
    }
  ],
  "css": {
    "@import \"../styles/0nlytype/field.css\"": {}
  },
  "meta": {
    "movement": "VI",
    "underneath": "native",
    "contract": "ot-field"
  },
  "type": "registry:ui"
}
