{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "font-editor",
  "title": "Font Editor",
  "description": "Ridiculously typed CSS font-shorthand editor with a compile-time strict-order parse — an order-free style/variant/weight/stretch prefix, a mandatory <font-size>, optional / <line-height>, and a mandatory <font-family> list, or a system-font keyword. Ships the cssFont helper, parseFont/formatFont, and a live text preview rendered with the built font.",
  "dependencies": [],
  "registryDependencies": [
    "ridiculous-type-kit",
    "unit-input",
    "button",
    "popover",
    "input",
    "label",
    "select"
  ],
  "files": [
    {
      "path": "src/components/ui/font-editor/index.ts",
      "content": "export type {\n  FamilyEditorProps,\n  FontEditorPanelProps,\n  FontEditorProps,\n  FontPreviewProps,\n  PropertyFieldProps,\n} from \"./font-editor\"\nexport {\n  FamilyEditor,\n  FontEditor,\n  FontEditorPanel,\n  FontPreview,\n  PropertyField,\n} from \"./font-editor\"\nexport {\n  ABSOLUTE_SIZES,\n  classifyFamilyToken,\n  classifyLineHeight,\n  classifySize,\n  classifyStretch,\n  classifyStyle,\n  classifyVariant,\n  classifyWeight,\n  defaultParts,\n  FONT_STRETCHES,\n  FONT_STYLES,\n  FONT_VARIANTS,\n  FONT_WEIGHT_KEYWORDS,\n  fontFamilies,\n  formatFont,\n  GENERIC_FAMILIES,\n  parseFont,\n  SYSTEM_FONTS,\n  WEB_SAFE_FAMILIES,\n} from \"./font-editor.helpers\"\nexport type {\n  FamiliesOf,\n  FontGenericFamily,\n  FontLiteral,\n  FontParts,\n  FontString,\n  FontStringKey,\n  FontStringMap,\n  IsFamilyToken,\n  IsFontSize,\n  IsFontStretch,\n  IsFontStyle,\n  IsFontVariant,\n  IsFontWeight,\n  IsLineHeight,\n  IsSystemFont,\n  LineHeightOf,\n  SizeOf,\n  SystemFontKeyword,\n} from \"./font-editor.types\"\nexport { cssFont } from \"./font-editor.types\"\n",
      "type": "registry:ui",
      "target": "components/ui/font-editor/index.ts"
    },
    {
      "path": "src/components/ui/font-editor/font-editor.tsx",
      "content": "\"use client\"\n\nimport type { ReactNode } from \"react\"\nimport { useEffect, useId, useRef, useState } from \"react\"\nimport { Button } from \"@/components/ui/button\"\nimport { Input } from \"@/components/ui/input\"\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@/components/ui/popover\"\nimport { cn } from \"@/lib/utils\"\nimport {\n  ABSOLUTE_SIZES,\n  defaultParts,\n  FONT_STRETCHES,\n  FONT_STYLES,\n  FONT_VARIANTS,\n  FONT_WEIGHT_KEYWORDS,\n  formatFont,\n  GENERIC_FAMILIES,\n  parseFont,\n  SYSTEM_FONTS,\n  WEB_SAFE_FAMILIES,\n} from \"./font-editor.helpers\"\nimport type { FontParts, FontString } from \"./font-editor.types\"\n\nconst SIZE_UNITS = [\"px\", \"rem\", \"em\", \"%\", \"vw\", \"vh\", \"pt\"] as const\n\nconst SAMPLE_TEXT_DEFAULT = \"The quick brown fox jumps over the lazy dog\"\n\n// Monotonic source of stable per-row ids for the family list (see FamilyEditor).\nlet nextFamilyId = 0\n\n// ---------------------------------------------------------------------------\n// Shared props\n// ---------------------------------------------------------------------------\n\nexport interface FontEditorPanelProps {\n  value: FontString | (string & {})\n  onChange: (value: FontString) => void\n  className?: string\n  \"aria-label\"?: string\n}\n\nexport interface FontEditorProps extends FontEditorPanelProps {}\n\n// ---------------------------------------------------------------------------\n// shorthand-parts helpers (UI-local)\n// ---------------------------------------------------------------------------\n\ninterface Shorthand {\n  kind: \"shorthand\"\n  style?: string\n  variant?: string\n  weight?: string\n  stretch?: string\n  size: string\n  lineHeight?: string\n  family: string[]\n}\n\nfunction asShorthand(parts: FontParts): Shorthand {\n  if (parts.kind === \"shorthand\") return parts\n  // Converting from a system keyword → seed a shorthand. `defaultParts()` is\n  // typed as the wider `FontParts`, so narrow on the discriminant instead of\n  // casting; the fallback keeps a real shorthand without an unchecked `as`.\n  const seed = defaultParts()\n  if (seed.kind === \"shorthand\") return seed\n  return { kind: \"shorthand\", size: \"16px\", family: [\"sans-serif\"] }\n}\n\n// A number for the dual control: mandatory mantissa (≥1 digit, so a lone `-`\n// or a bare unit never matches) with an optional exponent, then an optional\n// unit. Empty / sign-only / unit-only inputs fall through to the opaque path.\nconst SIZE_NUM_UNIT_RE = /^([+-]?(?:\\d+\\.?\\d*|\\.\\d+)(?:e[+-]?\\d+)?)([a-z%]*)$/i\n\n/** Split a size like \"16px\" into number + unit for the dual control. */\nfunction splitSize(size: string): {\n  num: string\n  unit: string\n  opaque: boolean\n} {\n  if ((ABSOLUTE_SIZES as readonly string[]).includes(size)) {\n    return { num: size, unit: \"\", opaque: true }\n  }\n  const m = size.match(SIZE_NUM_UNIT_RE)\n  if (!m) return { num: size, unit: \"\", opaque: true }\n  return { num: m[1], unit: m[2], opaque: false }\n}\n\n// ---------------------------------------------------------------------------\n// FontEditor — popover-wrapped\n// ---------------------------------------------------------------------------\n\nexport function FontEditor(props: FontEditorProps) {\n  const {\n    value,\n    className,\n    \"aria-label\": ariaLabel = \"Edit a CSS font shorthand\",\n  } = props\n  return (\n    <Popover>\n      <PopoverTrigger asChild>\n        <Button\n          variant=\"outline\"\n          className={cn(\"h-9 gap-2 px-3 font-mono\", className)}\n          aria-label={ariaLabel}\n        >\n          <span aria-hidden=\"true\" className=\"text-foreground/60\">\n            Aa\n          </span>\n          <span className=\"max-w-[220px] truncate text-xs\">{value}</span>\n        </Button>\n      </PopoverTrigger>\n      <PopoverContent className=\"w-auto p-0\" align=\"start\">\n        <FontEditorPanel {...props} />\n      </PopoverContent>\n    </Popover>\n  )\n}\n\n// ---------------------------------------------------------------------------\n// FontEditorPanel — inline\n// ---------------------------------------------------------------------------\n\nexport function FontEditorPanel({\n  value,\n  onChange,\n  className,\n  \"aria-label\": ariaLabel = \"CSS font shorthand editor\",\n}: FontEditorPanelProps) {\n  const [parts, setParts] = useState<FontParts>(\n    () => parseFont(String(value)) ?? defaultParts(),\n  )\n  const lastEmittedRef = useRef<string | null>(null)\n\n  // Resync from external value (skip our own emits).\n  useEffect(() => {\n    if (value === lastEmittedRef.current) return\n    const parsed = parseFont(String(value))\n    if (parsed !== null) setParts(parsed)\n  }, [value])\n\n  const commit = (next: FontParts) => {\n    setParts(next)\n    const str = formatFont(next)\n    lastEmittedRef.current = str\n    onChange(str as FontString)\n  }\n\n  const setShorthand = (patch: Partial<Shorthand>) => {\n    const base = asShorthand(parts)\n    commit({ ...base, ...patch })\n  }\n\n  const isSystem = parts.kind === \"system\"\n\n  return (\n    <fieldset\n      className={cn(\n        \"m-0 w-[420px] space-y-3 border-0 bg-background p-3\",\n        className,\n      )}\n      aria-label={ariaLabel}\n    >\n      <ModeToggle\n        mode={isSystem ? \"system\" : \"shorthand\"}\n        onMode={(mode) => {\n          if (mode === \"system\") {\n            commit({ kind: \"system\", keyword: \"caption\" })\n          } else {\n            commit(defaultParts())\n          }\n        }}\n      />\n\n      {isSystem ? (\n        <SystemKeywordSelect\n          value={parts.kind === \"system\" ? parts.keyword : \"caption\"}\n          onChange={(keyword) => commit({ kind: \"system\", keyword })}\n        />\n      ) : (\n        <ShorthandFields\n          parts={asShorthand(parts)}\n          onChange={(patch) => setShorthand(patch)}\n        />\n      )}\n\n      <LiveString value={formatFont(parts)} />\n      <FontPreview value={formatFont(parts)} />\n    </fieldset>\n  )\n}\n\n// ---------------------------------------------------------------------------\n// ModeToggle (internal)\n// ---------------------------------------------------------------------------\n\nfunction ModeToggle({\n  mode,\n  onMode,\n}: {\n  mode: \"shorthand\" | \"system\"\n  onMode: (mode: \"shorthand\" | \"system\") => void\n}) {\n  return (\n    <div className=\"flex gap-1 rounded-md border p-0.5\">\n      {([\"shorthand\", \"system\"] as const).map((m) => (\n        <button\n          key={m}\n          type=\"button\"\n          aria-pressed={mode === m}\n          onClick={() => onMode(m)}\n          className={cn(\n            \"flex-1 rounded px-2 py-1 font-mono text-xs\",\n            mode === m\n              ? \"bg-primary text-primary-foreground\"\n              : \"text-muted-foreground hover:bg-muted\",\n          )}\n        >\n          {m === \"shorthand\" ? \"shorthand\" : \"system font\"}\n        </button>\n      ))}\n    </div>\n  )\n}\n\nfunction SystemKeywordSelect({\n  value,\n  onChange,\n}: {\n  value: string\n  onChange: (keyword: (typeof SYSTEM_FONTS)[number]) => void\n}) {\n  return (\n    <PropertyField label=\"System font\">\n      <select\n        aria-label=\"System font keyword\"\n        value={value}\n        onChange={(e) =>\n          onChange(e.target.value as (typeof SYSTEM_FONTS)[number])\n        }\n        className=\"h-8 w-full rounded border bg-background px-1.5 font-mono text-xs\"\n      >\n        {SYSTEM_FONTS.map((k) => (\n          <option key={k} value={k}>\n            {k}\n          </option>\n        ))}\n      </select>\n    </PropertyField>\n  )\n}\n\n// ---------------------------------------------------------------------------\n// ShorthandFields (internal) — the property rows\n// ---------------------------------------------------------------------------\n\nfunction ShorthandFields({\n  parts,\n  onChange,\n}: {\n  parts: Shorthand\n  onChange: (patch: Partial<Shorthand>) => void\n}) {\n  return (\n    <div className=\"space-y-2\">\n      <div className=\"grid grid-cols-2 gap-2\">\n        <PropertyField label=\"Style\">\n          <KeywordSelect\n            ariaLabel=\"Font style\"\n            options={FONT_STYLES}\n            value={parts.style ?? \"normal\"}\n            allowEmpty\n            onChange={(v) => onChange({ style: v })}\n          />\n        </PropertyField>\n        <PropertyField label=\"Variant\">\n          <KeywordSelect\n            ariaLabel=\"Font variant\"\n            options={FONT_VARIANTS}\n            value={parts.variant ?? \"normal\"}\n            allowEmpty\n            onChange={(v) => onChange({ variant: v })}\n          />\n        </PropertyField>\n        <PropertyField label=\"Weight\">\n          <WeightControl\n            value={parts.weight ?? \"normal\"}\n            onChange={(v) => onChange({ weight: v })}\n          />\n        </PropertyField>\n        <PropertyField label=\"Stretch\">\n          <KeywordSelect\n            ariaLabel=\"Font stretch\"\n            options={FONT_STRETCHES}\n            value={parts.stretch ?? \"normal\"}\n            allowEmpty\n            onChange={(v) => onChange({ stretch: v })}\n          />\n        </PropertyField>\n      </div>\n\n      <div className=\"grid grid-cols-2 gap-2\">\n        <PropertyField label=\"Size\">\n          <SizeControl\n            value={parts.size}\n            onChange={(v) => onChange({ size: v })}\n          />\n        </PropertyField>\n        <PropertyField label=\"Line height\">\n          <LineHeightControl\n            value={parts.lineHeight ?? \"\"}\n            onChange={(v) => onChange({ lineHeight: v === \"\" ? undefined : v })}\n          />\n        </PropertyField>\n      </div>\n\n      <PropertyField label=\"Font family\">\n        <FamilyEditor\n          value={parts.family}\n          onChange={(family) => onChange({ family })}\n        />\n      </PropertyField>\n    </div>\n  )\n}\n\n// ---------------------------------------------------------------------------\n// PropertyField (public)\n// ---------------------------------------------------------------------------\n\nexport interface PropertyFieldProps {\n  label: string\n  children: ReactNode\n  className?: string\n}\n\nexport function PropertyField({\n  label,\n  children,\n  className,\n}: PropertyFieldProps) {\n  return (\n    <div className={cn(\"flex flex-col gap-1\", className)}>\n      <span className=\"font-mono text-[10px] text-muted-foreground uppercase tracking-wider\">\n        {label}\n      </span>\n      {children}\n    </div>\n  )\n}\n\n// ---------------------------------------------------------------------------\n// Field controls (internal)\n// ---------------------------------------------------------------------------\n\nfunction KeywordSelect({\n  ariaLabel,\n  options,\n  value,\n  allowEmpty,\n  onChange,\n}: {\n  ariaLabel: string\n  options: readonly string[]\n  value: string\n  allowEmpty?: boolean\n  onChange: (value: string) => void\n}) {\n  return (\n    <select\n      aria-label={ariaLabel}\n      value={value}\n      onChange={(e) => onChange(e.target.value)}\n      className=\"h-8 w-full rounded border bg-background px-1.5 font-mono text-xs\"\n    >\n      {allowEmpty ? <option value=\"normal\">normal</option> : null}\n      {options\n        .filter((o) => !(allowEmpty && o === \"normal\"))\n        .map((o) => (\n          <option key={o} value={o}>\n            {o}\n          </option>\n        ))}\n    </select>\n  )\n}\n\nfunction WeightControl({\n  value,\n  onChange,\n}: {\n  value: string\n  onChange: (value: string) => void\n}) {\n  const isNumeric = /^\\d+$/.test(value)\n  return (\n    <span className=\"inline-flex w-full items-center gap-1\">\n      <select\n        aria-label=\"Font weight keyword\"\n        value={isNumeric ? \"__number\" : value}\n        onChange={(e) => {\n          const v = e.target.value\n          onChange(v === \"__number\" ? \"400\" : v)\n        }}\n        className=\"h-8 flex-1 rounded border bg-background px-1.5 font-mono text-xs\"\n      >\n        {FONT_WEIGHT_KEYWORDS.map((k) => (\n          <option key={k} value={k}>\n            {k}\n          </option>\n        ))}\n        <option value=\"__number\">number…</option>\n      </select>\n      {isNumeric ? (\n        <Input\n          aria-label=\"Font weight number\"\n          value={value}\n          inputMode=\"numeric\"\n          spellCheck={false}\n          autoComplete=\"off\"\n          onChange={(e) => onChange(e.target.value)}\n          className=\"h-8 w-16 font-mono text-xs\"\n        />\n      ) : null}\n    </span>\n  )\n}\n\nfunction SizeControl({\n  value,\n  onChange,\n}: {\n  value: string\n  onChange: (value: string) => void\n}) {\n  const { num, unit, opaque } = splitSize(value)\n  const usingKeyword = (ABSOLUTE_SIZES as readonly string[]).includes(value)\n\n  return (\n    <span className=\"inline-flex w-full items-center\">\n      <Input\n        aria-label=\"Font size\"\n        value={value}\n        spellCheck={false}\n        autoComplete=\"off\"\n        onChange={(e) => onChange(e.target.value)}\n        className=\"h-8 flex-1 rounded-r-none border-r-0 font-mono text-xs\"\n      />\n      <select\n        aria-label=\"Font size unit\"\n        value={usingKeyword ? \"keyword\" : opaque ? \"\" : unit || \"px\"}\n        onChange={(e) => {\n          const u = e.target.value\n          if (u === \"keyword\") {\n            onChange(\"medium\")\n          } else if (u !== \"\") {\n            onChange(`${num || \"16\"}${u}`)\n          }\n        }}\n        className=\"h-8 rounded-r-md rounded-l-none border border-input bg-background px-1 font-mono text-xs\"\n      >\n        {SIZE_UNITS.map((u) => (\n          <option key={u} value={u}>\n            {u}\n          </option>\n        ))}\n        <option value=\"keyword\">abs</option>\n        {opaque && !usingKeyword ? <option value=\"\">—</option> : null}\n      </select>\n    </span>\n  )\n}\n\nfunction LineHeightControl({\n  value,\n  onChange,\n}: {\n  value: string\n  onChange: (value: string) => void\n}) {\n  return (\n    <Input\n      aria-label=\"Line height\"\n      value={value}\n      placeholder=\"normal\"\n      spellCheck={false}\n      autoComplete=\"off\"\n      onChange={(e) => onChange(e.target.value)}\n      className=\"h-8 w-full font-mono text-xs\"\n    />\n  )\n}\n\n// ---------------------------------------------------------------------------\n// FamilyEditor (public) — the comma-separated family list\n// ---------------------------------------------------------------------------\n\nexport interface FamilyEditorProps {\n  value: string[]\n  onChange: (value: string[]) => void\n  className?: string\n}\n\nexport function FamilyEditor({\n  value,\n  onChange,\n  className,\n}: FamilyEditorProps) {\n  // One stable id per row, created when the row is created. Keying on this id\n  // (instead of the array index) keeps a surviving row's DOM element identity\n  // across add/remove, so focus/IME state on a row above a removal is not lost.\n  const idsRef = useRef<number[]>([])\n  // Reconcile id count with the current value length: handler-driven mutations\n  // keep them in lockstep below, so this only fires for external replacements\n  // (resync / fallback). Existing ids are preserved by position; new slots get\n  // fresh ids.\n  if (idsRef.current.length !== value.length) {\n    const next = idsRef.current.slice(0, value.length)\n    while (next.length < value.length) next.push(nextFamilyId++)\n    idsRef.current = next\n  }\n  const ids = idsRef.current\n\n  const setAt = (index: number, next: string) => {\n    onChange(value.map((f, i) => (i === index ? next : f)))\n  }\n  const removeAt = (index: number) => {\n    const next = value.filter((_, i) => i !== index)\n    if (next.length === 0) {\n      // fall back to a single default family — one fresh row, one fresh id\n      idsRef.current = [nextFamilyId++]\n      onChange([\"sans-serif\"])\n      return\n    }\n    idsRef.current = ids.filter((_, i) => i !== index)\n    onChange(next)\n  }\n  const add = (family: string) => {\n    idsRef.current = [...ids, nextFamilyId++]\n    onChange([...value, family])\n  }\n\n  return (\n    <div className={cn(\"space-y-1.5\", className)}>\n      {value.map((family, i) => (\n        <div key={ids[i]} className=\"flex items-center gap-1\">\n          <Input\n            aria-label={`Font family ${i + 1}`}\n            value={family}\n            spellCheck={false}\n            autoComplete=\"off\"\n            onChange={(e) => setAt(i, e.target.value)}\n            className=\"h-8 flex-1 font-mono text-xs\"\n          />\n          <button\n            type=\"button\"\n            onClick={() => removeAt(i)}\n            aria-label={`Remove family ${i + 1}`}\n            className=\"rounded p-1 text-muted-foreground hover:bg-destructive/10 hover:text-destructive\"\n          >\n            <span aria-hidden=\"true\">×</span>\n          </button>\n        </div>\n      ))}\n      <select\n        aria-label=\"Add a font family\"\n        value=\"\"\n        onChange={(e) => {\n          const f = e.target.value\n          if (f) add(f)\n          e.target.value = \"\"\n        }}\n        className=\"h-8 w-full rounded-md border border-dashed bg-background px-2 font-mono text-muted-foreground text-xs\"\n      >\n        <option value=\"\">+ add family…</option>\n        <optgroup label=\"generic\">\n          {GENERIC_FAMILIES.map((f) => (\n            <option key={f} value={f}>\n              {f}\n            </option>\n          ))}\n        </optgroup>\n        <optgroup label=\"web-safe\">\n          {WEB_SAFE_FAMILIES.map((f) => (\n            <option key={f} value={f}>\n              {f}\n            </option>\n          ))}\n        </optgroup>\n      </select>\n    </div>\n  )\n}\n\n// ---------------------------------------------------------------------------\n// LiveString (internal)\n// ---------------------------------------------------------------------------\n\nfunction LiveString({ value }: { value: string }) {\n  return (\n    <code className=\"block overflow-x-auto rounded bg-muted/50 px-2 py-1.5 font-mono text-foreground text-xs\">\n      {`font: ${value};`}\n    </code>\n  )\n}\n\n// ---------------------------------------------------------------------------\n// FontPreview (public) — the live text preview\n// ---------------------------------------------------------------------------\n\nexport interface FontPreviewProps {\n  value: string\n  sampleText?: string\n  editable?: boolean\n  className?: string\n}\n\n/**\n * Live text preview: renders sample text with the built `font` shorthand\n * applied via inline style. The sample text is editable when `editable`\n * (default true). This is a React element rendering text — no browser\n * tooling involved.\n */\nexport function FontPreview({\n  value,\n  sampleText,\n  editable = true,\n  className,\n}: FontPreviewProps) {\n  const id = useId()\n  const [text, setText] = useState(sampleText ?? SAMPLE_TEXT_DEFAULT)\n  const shown = sampleText ?? text\n\n  return (\n    <div className={cn(\"space-y-2 rounded-lg border p-3\", className)}>\n      <div className=\"flex items-center justify-between\">\n        <span className=\"text-muted-foreground text-xs\">preview</span>\n        {editable && sampleText === undefined ? (\n          <input\n            id={`${id}-sample`}\n            aria-label=\"Sample text\"\n            value={text}\n            onChange={(e) => setText(e.target.value)}\n            className=\"h-7 w-40 rounded border bg-background px-1.5 text-xs\"\n          />\n        ) : null}\n      </div>\n      <div\n        data-font-preview\n        style={{ font: value === \"\" ? undefined : value }}\n        className=\"min-h-[3rem] break-words rounded-md bg-muted/30 p-3 text-foreground\"\n      >\n        {shown}\n      </div>\n    </div>\n  )\n}\n",
      "type": "registry:ui",
      "target": "components/ui/font-editor/font-editor.tsx"
    },
    {
      "path": "src/components/ui/font-editor/font-editor.types.ts",
      "content": "// =====================================================================\n// font-editor.types.ts\n//\n// The \"ridiculous\" tier: a compile-time STRICT-ORDER PARSE of the CSS\n// `font` shorthand. Unlike the order-free function-list dispatch of\n// transform/filter, the `font` shorthand is the most ORDER-SENSITIVE of\n// the common shorthands:\n//\n//   [ style || variant || weight || stretch ]?  <size>  [ / <line-height> ]?  <family>#\n//\n// …or a single system-font keyword as the WHOLE value.\n//\n//   \"italic bold 16px/1.5 'Times New Roman', serif\"  →  the literal\n//   \"16px serif\"                                      →  the literal\n//   \"16px\"                                            →  never (no family)\n//   \"italic oblique 16px serif\"                       →  never (two styles)\n//   \"caption\"                                         →  \"caption\"\n//\n// The ORDER is the point: prefix tokens are order-free but each KIND\n// appears at most once; <size> is mandatory and precedes the family; the\n// optional `/ <line-height>` attaches to the size; <family> is a\n// mandatory comma-separated list that ends the value.\n//\n// Built entirely on `ridiculous-type-kit`. Structure mirrors\n// transform-builder.types.ts: kit imports → classifiers → ordered-parse\n// state machine → FontLiteral + cssFont → suggestion strings → utility\n// types → internal discriminated-union state.\n// =====================================================================\n\nimport type {\n  Digit,\n  IsLength,\n  IsNumber,\n  IsPercentage,\n  Letter,\n  Or,\n  SplitByComma,\n  SplitBySpace,\n  Trim,\n} from \"@/lib/ridiculous-type-kit\"\n\n// =====================================================================\n// 1. KEYWORD UNIONS\n// =====================================================================\n\n/** A system-font keyword usable as the WHOLE `font` value. */\nexport type SystemFontKeyword =\n  | \"caption\"\n  | \"icon\"\n  | \"menu\"\n  | \"message-box\"\n  | \"small-caption\"\n  | \"status-bar\"\n\n/** The CSS generic font-family keywords. */\nexport type FontGenericFamily =\n  | \"serif\"\n  | \"sans-serif\"\n  | \"monospace\"\n  | \"cursive\"\n  | \"fantasy\"\n  | \"system-ui\"\n  | \"ui-serif\"\n  | \"ui-sans-serif\"\n  | \"ui-monospace\"\n  | \"ui-rounded\"\n\ntype FontStyleKeyword = \"normal\" | \"italic\" | \"oblique\"\ntype FontVariantKeyword = \"normal\" | \"small-caps\"\ntype FontWeightKeyword = \"normal\" | \"bold\" | \"bolder\" | \"lighter\"\ntype FontStretchKeyword =\n  | \"ultra-condensed\"\n  | \"extra-condensed\"\n  | \"condensed\"\n  | \"semi-condensed\"\n  | \"normal\"\n  | \"semi-expanded\"\n  | \"expanded\"\n  | \"extra-expanded\"\n  | \"ultra-expanded\"\ntype AbsoluteSizeKeyword =\n  | \"xx-small\"\n  | \"x-small\"\n  | \"small\"\n  | \"medium\"\n  | \"large\"\n  | \"x-large\"\n  | \"xx-large\"\n  | \"xxx-large\"\n  | \"larger\"\n  | \"smaller\"\n\n// =====================================================================\n// 2. TOKEN CLASSIFIERS — one boolean predicate per grammar slot.\n//    Each operates on an already-trimmed token.\n// =====================================================================\n\n/** Characters allowed anywhere in a bare family ident (incl. inner spaces). */\ntype IdentChar = Letter | Digit | \"-\" | \"_\" | \" \"\n\ntype AllIdentChars<S extends string> = S extends \"\"\n  ? true\n  : S extends `${infer C}${infer R}`\n    ? C extends IdentChar\n      ? AllIdentChars<R>\n      : false\n    : false\n\nexport type IsFontStyle<T extends string> = T extends FontStyleKeyword\n  ? true\n  : false\n\nexport type IsFontVariant<T extends string> = T extends FontVariantKeyword\n  ? true\n  : false\n\nexport type IsFontWeight<T extends string> = T extends FontWeightKeyword\n  ? true\n  : IsNumber<T>\n\nexport type IsFontStretch<T extends string> = T extends FontStretchKeyword\n  ? true\n  : IsPercentage<T>\n\nexport type IsFontSize<T extends string> = T extends AbsoluteSizeKeyword\n  ? true\n  : Or<IsLength<T>, IsPercentage<T>>\n\nexport type IsLineHeight<T extends string> = T extends \"normal\"\n  ? true\n  : Or<IsNumber<T>, Or<IsLength<T>, IsPercentage<T>>>\n\n/**\n * One font-family token: a generic-family keyword, OR a quoted string\n * (any inner content), OR a bare custom-ident (weak-validated as\n * ident-safe — first char a letter/`_`/`-`, body letters/digits/`-`/`_`/\n * spaces). The full CSS custom-ident grammar is deferred (documented).\n */\nexport type IsFamilyToken<T extends string> = T extends FontGenericFamily\n  ? true\n  : T extends `\"${string}\"`\n    ? true\n    : T extends `'${string}'`\n      ? true\n      : T extends `${infer First}${string}`\n        ? First extends Letter | \"_\" | \"-\"\n          ? AllIdentChars<T>\n          : false\n        : false\n\n// =====================================================================\n// 3. ORDERED-PARSE STATE MACHINE — the namesake.\n//\n//  ParsePrefix consumes order-free prefix tokens (<=1 of each kind, with a\n//  4-flag \"Used\" accumulator). The first token that is NOT a still-free\n//  prefix kind starts the mandatory <size>. ParseSizeAndRest validates the\n//  size and the optional `/ <line-height>` (attached + spaced forms), then\n//  ParseFamily rejoins the remaining tokens, splits on commas, and requires\n//  a non-empty list of family tokens.\n// =====================================================================\n\ninterface Used {\n  s: boolean // style consumed\n  v: boolean // variant consumed\n  w: boolean // weight consumed\n  t: boolean // stretch consumed\n}\n\ntype EmptyUsed = { s: false; v: false; w: false; t: false }\n\n// Try to consume H as a still-FREE prefix kind. `normal` takes the first\n// free kind (style → variant → weight → stretch). Returns the next Used on\n// success, or `false` when H is not a free prefix kind.\ntype ConsumePrefix<H extends string, U extends Used> =\n  IsFontStyle<H> extends true\n    ? U[\"s\"] extends false\n      ? { s: true; v: U[\"v\"]; w: U[\"w\"]; t: U[\"t\"] }\n      : TryVariant<H, U>\n    : TryVariant<H, U>\n\ntype TryVariant<H extends string, U extends Used> =\n  IsFontVariant<H> extends true\n    ? U[\"v\"] extends false\n      ? { s: U[\"s\"]; v: true; w: U[\"w\"]; t: U[\"t\"] }\n      : TryWeight<H, U>\n    : TryWeight<H, U>\n\ntype TryWeight<H extends string, U extends Used> =\n  IsFontWeight<H> extends true\n    ? U[\"w\"] extends false\n      ? { s: U[\"s\"]; v: U[\"v\"]; w: true; t: U[\"t\"] }\n      : TryStretch<H, U>\n    : TryStretch<H, U>\n\ntype TryStretch<H extends string, U extends Used> =\n  IsFontStretch<H> extends true\n    ? U[\"t\"] extends false\n      ? { s: U[\"s\"]; v: U[\"v\"]; w: U[\"w\"]; t: true }\n      : false\n    : false\n\ntype ParsePrefix<Tokens extends string[], U extends Used> = Tokens extends [\n  infer H extends string,\n  ...infer T extends string[],\n]\n  ? ConsumePrefix<H, U> extends infer Next\n    ? Next extends Used\n      ? ParsePrefix<T, Next>\n      : ParseSizeAndRest<Tokens> // H was not a free prefix kind → it is the size\n    : false\n  : false // ran out of tokens without ever reaching a size → invalid\n\ntype ParseSizeAndRest<Tokens extends string[]> = Tokens extends [\n  infer H extends string,\n  ...infer T extends string[],\n]\n  ? // attached form: \"16px/1.5\", or trailing-slash \"16px/\" with the\n    // line-height in the next token (\"16px/ 1.5\")\n    H extends `${infer Sz}/${infer Lh}`\n    ? IsFontSize<Trim<Sz>> extends true\n      ? Lh extends \"\" // \"16px/\" — line-height is the next token\n        ? T extends [infer N extends string, ...infer R extends string[]]\n          ? IsLineHeight<Trim<N>> extends true\n            ? ParseFamily<R>\n            : false\n          : false\n        : IsLineHeight<Trim<Lh>> extends true\n          ? ParseFamily<T>\n          : false\n      : false\n    : IsFontSize<H> extends true\n      ? // half-spaced \"16px /1.5\" — next token starts with `/`\n        T extends [infer N extends string, ...infer R extends string[]]\n        ? N extends `/${infer Lh}`\n          ? Lh extends \"\" // fully spaced \"16px / 1.5\" — `/` is its own token\n            ? R extends [infer Lh2 extends string, ...infer R2 extends string[]]\n              ? IsLineHeight<Trim<Lh2>> extends true\n                ? ParseFamily<R2>\n                : false\n              : false\n            : IsLineHeight<Trim<Lh>> extends true\n              ? ParseFamily<R>\n              : false\n          : ParseFamily<T> // no line-height\n        : false // size with nothing after → no family\n      : false // mandatory size missing / invalid\n  : false\n\n// Rejoin the remaining (space-split) tokens, split on commas, require a\n// non-empty list where every comma-segment is a family token.\ntype ParseFamily<Tokens extends string[]> = Tokens extends []\n  ? false // family is mandatory\n  : SplitByComma<Join<Tokens, \" \">> extends infer Segs extends string[]\n    ? Segs extends []\n      ? false\n      : AllFamily<Segs>\n    : false\n\ntype AllFamily<Segs extends string[]> = Segs extends [\n  infer H extends string,\n  ...infer T extends string[],\n]\n  ? IsFamilyToken<Trim<H>> extends true\n    ? AllFamily<T>\n    : false\n  : true\n\ntype Join<T extends string[], Sep extends string> = T extends [\n  infer H extends string,\n  ...infer R extends string[],\n]\n  ? R extends []\n    ? H\n    : `${H}${Sep}${Join<R, Sep>}`\n  : \"\"\n\n// =====================================================================\n// 4. STRICT VALIDATOR + CALL-SITE HELPER\n// =====================================================================\n\n/**\n * Strict literal validator. Resolves to `S` when `S` is a structurally and\n * dimensionally valid CSS `font` shorthand (or a system-font keyword),\n * `never` otherwise. The ordered grammar is enforced: order-free prefix\n * (≤1 each of style/variant/weight/stretch) → mandatory `<size>` →\n * optional `/ <line-height>` → mandatory `<font-family>` list.\n *\n * `var()` / `calc()` resolve to `never` here (undecidable at compile time)\n * — use the casual / IntelliSense tier; the runtime parser accepts them.\n *\n * @example\n * type A = FontLiteral<\"italic bold 16px/1.5 serif\"> // the literal\n * type B = FontLiteral<\"16px\">                        // never (no family)\n * type C = FontLiteral<\"caption\">                     // \"caption\"\n */\nexport type FontLiteral<S extends string> =\n  Trim<S> extends SystemFontKeyword\n    ? S\n    : Trim<S> extends \"\"\n      ? never\n      : SplitBySpace<Trim<S>> extends infer Toks extends string[]\n        ? Toks extends []\n          ? never\n          : ParsePrefix<Toks, EmptyUsed> extends true\n            ? S\n            : never\n        : never\n\n/**\n * Call-site validator helper. Mirrors `cssTransform()` / `color()` /\n * `easing()`. An invalid font shorthand becomes a type error at the\n * argument.\n */\nexport const cssFont = <S extends string>(value: S & FontLiteral<S>): S => value\n\n// =====================================================================\n// 5. SUGGESTION STRINGS — IntelliSense + onChange return types\n// =====================================================================\n\n/**\n * Suggestion union — \"this is a font value\". Deliberately loose: a precise\n * template-literal union for \"optional-prefix size /lh family-list\" would be\n * enormous and slow `tsc`. Strictness lives in `FontLiteral`. A non-system\n * shorthand always contains at least one space (size + family), so the\n * `` `${string} ${string}` `` arm covers it; system keywords surface in\n * autocomplete via the keyword union. Also the `onChange` return type.\n */\nexport type FontString = SystemFontKeyword | `${string} ${string}`\n\n/** Representative output-string shapes. Mirrors `TransformStringMap`. */\nexport interface FontStringMap {\n  system: SystemFontKeyword\n  sizeFamily: `${string} ${string}`\n  full: `${string} ${string}`\n}\n\nexport type FontStringKey = keyof FontStringMap\n\n// =====================================================================\n// 6. UTILITY TYPES — operate on font literals at the type level\n// =====================================================================\n\n/** `true` when `S` is a system-font keyword. */\nexport type IsSystemFont<S extends string> =\n  Trim<S> extends SystemFontKeyword ? true : false\n\n/**\n * The ordered tuple of comma-separated family tokens in a font string.\n * `[]` for a system keyword or an unparseable value.\n *\n * @example\n * type F = FamiliesOf<\"16px Times New Roman, serif\"> // [\"Times New Roman\",\"serif\"]\n */\nexport type FamiliesOf<S extends string> =\n  Trim<S> extends SystemFontKeyword\n    ? []\n    : SplitBySpace<Trim<S>> extends infer Toks extends string[]\n      ? FamiliesFromTokens<Toks, EmptyUsed>\n      : []\n\n// Walk past the prefix + size(+lh), then return the comma-split family list.\ntype FamiliesFromTokens<\n  Tokens extends string[],\n  U extends Used,\n> = Tokens extends [infer H extends string, ...infer T extends string[]]\n  ? ConsumePrefix<H, U> extends infer Next\n    ? Next extends Used\n      ? FamiliesFromTokens<T, Next>\n      : FamiliesAfterSize<Tokens>\n    : []\n  : []\n\ntype FamiliesAfterSize<Tokens extends string[]> = Tokens extends [\n  infer H extends string,\n  ...infer T extends string[],\n]\n  ? H extends `${infer _Sz}/${infer Lh}`\n    ? Lh extends \"\" // \"16px/\" — line-height is the next token; family follows\n      ? T extends [string, ...infer R extends string[]]\n        ? FamilySegs<R>\n        : []\n      : FamilySegs<T>\n    : T extends [infer N extends string, ...infer R extends string[]]\n      ? N extends `/${infer Lh}`\n        ? Lh extends \"\"\n          ? R extends [string, ...infer R2 extends string[]]\n            ? FamilySegs<R2>\n            : []\n          : FamilySegs<R>\n        : FamilySegs<T>\n      : []\n  : []\n\ntype FamilySegs<Tokens extends string[]> = Tokens extends []\n  ? []\n  : SplitByComma<Join<Tokens, \" \">> extends infer Segs extends string[]\n    ? TrimSegs<Segs>\n    : []\n\ntype TrimSegs<Segs extends string[]> = Segs extends [\n  infer H extends string,\n  ...infer T extends string[],\n]\n  ? [Trim<H>, ...TrimSegs<T>]\n  : []\n\n/**\n * The `<size>` token of a font shorthand, or `never`.\n *\n * @example\n * type S = SizeOf<\"italic 16px/1.5 serif\"> // \"16px\"\n */\nexport type SizeOf<S extends string> =\n  Trim<S> extends SystemFontKeyword\n    ? never\n    : SplitBySpace<Trim<S>> extends infer Toks extends string[]\n      ? SizeFromTokens<Toks, EmptyUsed>\n      : never\n\ntype SizeFromTokens<Tokens extends string[], U extends Used> = Tokens extends [\n  infer H extends string,\n  ...infer T extends string[],\n]\n  ? ConsumePrefix<H, U> extends infer Next\n    ? Next extends Used\n      ? SizeFromTokens<T, Next>\n      : H extends `${infer Sz}/${string}`\n        ? Trim<Sz>\n        : H\n    : never\n  : never\n\n/**\n * The `<line-height>` token of a font shorthand, or `never` when absent.\n *\n * @example\n * type L = LineHeightOf<\"16px/1.5 serif\"> // \"1.5\"\n */\nexport type LineHeightOf<S extends string> =\n  Trim<S> extends SystemFontKeyword\n    ? never\n    : SplitBySpace<Trim<S>> extends infer Toks extends string[]\n      ? LineHeightFromTokens<Toks, EmptyUsed>\n      : never\n\ntype LineHeightFromTokens<\n  Tokens extends string[],\n  U extends Used,\n> = Tokens extends [infer H extends string, ...infer T extends string[]]\n  ? ConsumePrefix<H, U> extends infer Next\n    ? Next extends Used\n      ? LineHeightFromTokens<T, Next>\n      : H extends `${string}/${infer Lh}`\n        ? Lh extends \"\" // \"16px/\" — line-height is the bare next token\n          ? T extends [infer N extends string, ...string[]]\n            ? Trim<N>\n            : never\n          : Trim<Lh>\n        : LhFromNext<T>\n    : never\n  : never\n\ntype LhFromNext<Tokens extends string[]> = Tokens extends [\n  infer N extends string,\n  ...infer R extends string[],\n]\n  ? N extends `/${infer Lh}`\n    ? Lh extends \"\"\n      ? R extends [infer Lh2 extends string, ...string[]]\n        ? Trim<Lh2>\n        : never\n      : Trim<Lh>\n    : never\n  : never\n\n// =====================================================================\n// 7. INTERNAL STATE — discriminated union (exported)\n//\n// The editor's state is a single FontParts, discriminated by `kind`.\n// Exported for advanced use (custom serialization, programmatic build).\n// Values are kept as strings (they carry units / quoting), mirroring how\n// the literal preserves the raw text.\n// =====================================================================\n\nexport type FontParts =\n  | { kind: \"system\"; keyword: SystemFontKeyword }\n  | {\n      kind: \"shorthand\"\n      style?: string\n      variant?: string\n      weight?: string\n      stretch?: string\n      size: string\n      lineHeight?: string\n      family: string[]\n    }\n",
      "type": "registry:ui",
      "target": "components/ui/font-editor/font-editor.types.ts"
    },
    {
      "path": "src/components/ui/font-editor/font-editor.helpers.ts",
      "content": "// =====================================================================\n// font-editor.helpers.ts\n//\n// Pure runtime parse / format for the CSS `font` shorthand. This is the\n// SUPERSET of the strict type tier: it tolerates var() inside the family\n// list (kept verbatim, opaque), accepts arbitrary whitespace, and runs the\n// full ordered parse — order-free prefix (≤1 of each kind) → mandatory\n// size → optional `/line-height` → mandatory comma-separated family list.\n//\n// The classifier helpers + option-list constants are the single source of\n// truth for both parsing and the UI (mirroring transform-builder's\n// ARG_SPEC table).\n// =====================================================================\n\nimport type {\n  FontGenericFamily,\n  FontParts,\n  SystemFontKeyword,\n} from \"./font-editor.types\"\n\n// ---------------------------------------------------------------------------\n// Option lists (drive the UI selects + the classifiers)\n// ---------------------------------------------------------------------------\n\nexport const SYSTEM_FONTS: readonly SystemFontKeyword[] = [\n  \"caption\",\n  \"icon\",\n  \"menu\",\n  \"message-box\",\n  \"small-caption\",\n  \"status-bar\",\n]\n\nexport const FONT_STYLES = [\"normal\", \"italic\", \"oblique\"] as const\nexport const FONT_VARIANTS = [\"normal\", \"small-caps\"] as const\nexport const FONT_WEIGHT_KEYWORDS = [\n  \"normal\",\n  \"bold\",\n  \"bolder\",\n  \"lighter\",\n] as const\nexport const FONT_STRETCHES = [\n  \"ultra-condensed\",\n  \"extra-condensed\",\n  \"condensed\",\n  \"semi-condensed\",\n  \"normal\",\n  \"semi-expanded\",\n  \"expanded\",\n  \"extra-expanded\",\n  \"ultra-expanded\",\n] as const\nexport const ABSOLUTE_SIZES = [\n  \"xx-small\",\n  \"x-small\",\n  \"small\",\n  \"medium\",\n  \"large\",\n  \"x-large\",\n  \"xx-large\",\n  \"xxx-large\",\n  \"larger\",\n  \"smaller\",\n] as const\n\nexport const GENERIC_FAMILIES: readonly FontGenericFamily[] = [\n  \"serif\",\n  \"sans-serif\",\n  \"monospace\",\n  \"cursive\",\n  \"fantasy\",\n  \"system-ui\",\n  \"ui-serif\",\n  \"ui-sans-serif\",\n  \"ui-monospace\",\n  \"ui-rounded\",\n]\n\n/** A small web-safe family list for the UI family picker. */\nexport const WEB_SAFE_FAMILIES: readonly string[] = [\n  \"Arial\",\n  \"Helvetica\",\n  \"Times New Roman\",\n  \"Georgia\",\n  \"Courier New\",\n  \"Verdana\",\n  \"Tahoma\",\n  \"Trebuchet MS\",\n  \"Inter\",\n  \"Roboto\",\n  \"JetBrains Mono\",\n]\n\n// ---------------------------------------------------------------------------\n// Numeric / dimension predicates (runtime mirror of the kit primitives)\n// ---------------------------------------------------------------------------\n\nconst NUMBER_RE = /^[+-]?(\\d+\\.?\\d*|\\.\\d+)$/\nconst LENGTH_UNIT_RE =\n  /^[+-]?(\\d+\\.?\\d*|\\.\\d+)(px|rem|em|ex|ch|cap|ic|lh|rlh|vw|vh|vi|vb|vmin|vmax|svw|svh|lvw|lvh|dvw|dvh|cqw|cqh|cqi|cqb|cqmin|cqmax|cm|mm|in|pt|pc|Q)$/i\nconst PERCENT_RE = /^[+-]?(\\d+\\.?\\d*|\\.\\d+)%$/\n\nfunction isNumber(s: string): boolean {\n  return NUMBER_RE.test(s)\n}\nfunction isLength(s: string): boolean {\n  return LENGTH_UNIT_RE.test(s)\n}\nfunction isPercentage(s: string): boolean {\n  return PERCENT_RE.test(s)\n}\n\n// ---------------------------------------------------------------------------\n// Token classifiers — exported, mirror the type predicates\n// ---------------------------------------------------------------------------\n\nexport function classifyStyle(t: string): boolean {\n  return (FONT_STYLES as readonly string[]).includes(t)\n}\nexport function classifyVariant(t: string): boolean {\n  return (FONT_VARIANTS as readonly string[]).includes(t)\n}\nexport function classifyWeight(t: string): boolean {\n  return (FONT_WEIGHT_KEYWORDS as readonly string[]).includes(t) || isNumber(t)\n}\nexport function classifyStretch(t: string): boolean {\n  return (FONT_STRETCHES as readonly string[]).includes(t) || isPercentage(t)\n}\nexport function classifySize(t: string): boolean {\n  return (\n    (ABSOLUTE_SIZES as readonly string[]).includes(t) ||\n    isLength(t) ||\n    isPercentage(t)\n  )\n}\nexport function classifyLineHeight(t: string): boolean {\n  return t === \"normal\" || isNumber(t) || isLength(t) || isPercentage(t)\n}\n\nconst IDENT_RE = /^[A-Za-z_-][A-Za-z0-9_\\- ]*$/\n\n/**\n * One family token: a generic keyword, a quoted string, a bare ident-safe\n * name, OR a `var()` reference (runtime-tolerant; not validated strictly).\n */\nexport function classifyFamilyToken(t: string): boolean {\n  if (t === \"\") return false\n  if ((GENERIC_FAMILIES as readonly string[]).includes(t)) return true\n  if (\n    (t.startsWith('\"') && t.endsWith('\"') && t.length >= 2) ||\n    (t.startsWith(\"'\") && t.endsWith(\"'\") && t.length >= 2)\n  ) {\n    return true\n  }\n  if (t.startsWith(\"var(\") && t.endsWith(\")\")) return true\n  return IDENT_RE.test(t)\n}\n\n// ---------------------------------------------------------------------------\n// Paren-aware top-level splitters (runtime mirror of the kit combinators)\n// ---------------------------------------------------------------------------\n\n/** Split on `sep` only at bracket depth 0, ignoring quoted runs. */\nfunction splitTopLevel(src: string, sep: string): string[] {\n  const out: string[] = []\n  let depth = 0\n  let quote: '\"' | \"'\" | null = null\n  let cur = \"\"\n  for (const ch of src) {\n    if (quote) {\n      cur += ch\n      if (ch === quote) quote = null\n      continue\n    }\n    if (ch === '\"' || ch === \"'\") {\n      quote = ch\n      cur += ch\n      continue\n    }\n    if (ch === \"(\" || ch === \"[\") depth++\n    else if (ch === \")\" || ch === \"]\") depth--\n    if (ch === sep && depth === 0) {\n      out.push(cur)\n      cur = \"\"\n    } else {\n      cur += ch\n    }\n  }\n  out.push(cur)\n  return out\n}\n\n/** Split on top-level whitespace runs, dropping empties. */\nfunction splitSpace(src: string): string[] {\n  // Normalize a standalone or attached `/` so the slash never glues tokens\n  // in a way the walker can't see — but keep `16px/1.5` attached. We split on\n  // spaces only; the parser handles `/` placement.\n  return splitTopLevel(src, \" \")\n    .flatMap((s) => splitTopLevel(s, \"\\t\"))\n    .flatMap((s) => splitTopLevel(s, \"\\n\"))\n    .map((s) => s.trim())\n    .filter((s) => s.length > 0)\n}\n\n/** Split a comma-separated family string at depth 0; trim, drop empties. */\nfunction splitFamily(src: string): string[] {\n  return splitTopLevel(src, \",\")\n    .map((s) => s.trim())\n    .filter((s) => s.length > 0)\n}\n\nfunction isSystemKeyword(s: string): s is SystemFontKeyword {\n  return (SYSTEM_FONTS as readonly string[]).includes(s)\n}\n\n// ---------------------------------------------------------------------------\n// parseFont — string → FontParts | null\n// ---------------------------------------------------------------------------\n\ninterface PrefixAcc {\n  style?: string\n  variant?: string\n  weight?: string\n  stretch?: string\n}\n\n/** Consume `t` as the first still-free prefix kind. Returns true on success. */\nfunction consumePrefix(t: string, acc: PrefixAcc): boolean {\n  if (classifyStyle(t) && acc.style === undefined) {\n    acc.style = t\n    return true\n  }\n  if (classifyVariant(t) && acc.variant === undefined) {\n    acc.variant = t\n    return true\n  }\n  if (classifyWeight(t) && acc.weight === undefined) {\n    acc.weight = t\n    return true\n  }\n  if (classifyStretch(t) && acc.stretch === undefined) {\n    acc.stretch = t\n    return true\n  }\n  return false\n}\n\n/**\n * Parse a CSS `font` shorthand into typed parts, or `null` on any structural\n * error (missing size, missing family, duplicate prefix kind, junk). A\n * system-font keyword → `{ kind: \"system\", … }`. Tolerates `var()` family\n * tokens and arbitrary whitespace.\n */\nexport function parseFont(src: string): FontParts | null {\n  const trimmed = src.trim()\n  if (trimmed === \"\") return null\n  if (isSystemKeyword(trimmed)) return { kind: \"system\", keyword: trimmed }\n\n  const tokens = splitSpace(trimmed)\n  if (tokens.length === 0) return null\n\n  const acc: PrefixAcc = {}\n  let i = 0\n  // 1. Order-free prefix.\n  while (i < tokens.length && consumePrefix(tokens[i], acc)) {\n    i++\n  }\n  if (i >= tokens.length) return null // never reached a size\n\n  // 2. Mandatory size, with optional `/line-height` in attached/spaced forms.\n  let size: string\n  let lineHeight: string | undefined\n  const head = tokens[i]\n\n  const slashIdx = head.indexOf(\"/\")\n  if (slashIdx >= 0) {\n    // attached forms: \"16px/1.5\" or trailing-slash \"16px/\"\n    const sz = head.slice(0, slashIdx)\n    const lh = head.slice(slashIdx + 1)\n    if (!classifySize(sz)) return null\n    size = sz\n    i++\n    if (lh !== \"\") {\n      if (!classifyLineHeight(lh)) return null\n      lineHeight = lh\n    } else {\n      // line-height is the next token\n      if (i >= tokens.length || !classifyLineHeight(tokens[i])) return null\n      lineHeight = tokens[i]\n      i++\n    }\n  } else {\n    if (!classifySize(head)) return null\n    size = head\n    i++\n    // half-spaced \"16px /1.5\" or fully spaced \"16px / 1.5\"\n    if (i < tokens.length && tokens[i].startsWith(\"/\")) {\n      const after = tokens[i].slice(1)\n      i++\n      if (after !== \"\") {\n        if (!classifyLineHeight(after)) return null\n        lineHeight = after\n      } else {\n        if (i >= tokens.length || !classifyLineHeight(tokens[i])) return null\n        lineHeight = tokens[i]\n        i++\n      }\n    }\n  }\n\n  // 3. Mandatory family list (rejoin remaining tokens, split on commas).\n  const familySrc = tokens.slice(i).join(\" \")\n  const family = splitFamily(familySrc)\n  if (family.length === 0) return null\n  for (const f of family) {\n    if (!classifyFamilyToken(f)) return null\n  }\n\n  return {\n    kind: \"shorthand\",\n    ...(acc.style !== undefined ? { style: acc.style } : {}),\n    ...(acc.variant !== undefined ? { variant: acc.variant } : {}),\n    ...(acc.weight !== undefined ? { weight: acc.weight } : {}),\n    ...(acc.stretch !== undefined ? { stretch: acc.stretch } : {}),\n    size,\n    ...(lineHeight !== undefined ? { lineHeight } : {}),\n    family,\n  }\n}\n\n// ---------------------------------------------------------------------------\n// formatFont — canonical serialization\n// ---------------------------------------------------------------------------\n\n/**\n * Canonical re-serialization of font parts. Order:\n * `style variant weight stretch size[/lh] family`. System keyword → itself.\n * Omitted prefix fields are dropped; family joined with `, `.\n */\nexport function formatFont(parts: FontParts): string {\n  if (parts.kind === \"system\") return parts.keyword\n  const prefix = [parts.style, parts.variant, parts.weight, parts.stretch]\n    .filter((p): p is string => p !== undefined)\n    .join(\" \")\n  const sizeLh =\n    parts.lineHeight !== undefined\n      ? `${parts.size}/${parts.lineHeight}`\n      : parts.size\n  const family = parts.family.join(\", \")\n  return [prefix, `${sizeLh} ${family}`].filter((s) => s !== \"\").join(\" \")\n}\n\n// ---------------------------------------------------------------------------\n// fontFamilies / defaultParts — UI conveniences\n// ---------------------------------------------------------------------------\n\n/** Runtime mirror of `FamiliesOf` — the family tokens in order, or `[]`. */\nexport function fontFamilies(src: string): string[] {\n  const parts = parseFont(src)\n  if (parts === null || parts.kind === \"system\") return []\n  return parts.family\n}\n\n/** A sensible default for a freshly-initialized editor. */\nexport function defaultParts(): FontParts {\n  return { kind: \"shorthand\", size: \"16px\", family: [\"sans-serif\"] }\n}\n",
      "type": "registry:ui",
      "target": "components/ui/font-editor/font-editor.helpers.ts"
    }
  ],
  "type": "registry:ui"
}