{
  "name": "KisGo UI kit",
  "version": "1.1.0-draft",
  "date": "2026-10-04",
  "install": {
    "html": [
      "tokens.css",
      "components.css",
      "components.js",
      "kg-elements.js"
    ],
    "rn": [
      "ui-kit/tokens.json",
      "ui-kit/components.spec.json"
    ]
  },
  "conventions": [
    "Пропсы — атрибуты в kebab-case; в RN — camelCase (photo-width → photoWidth).",
    "Булевы атрибуты: наличие = true.",
    "События — CustomEvent kg-*, данные в detail; в RN — колбэки on* (kg-report → onReport).",
    "Тексты по умолчанию берутся из словаря KG.strings по текущему языку; явный атрибут всегда важнее.",
    "Никаких значений вне токенов. Новый компонент сначала добавляется в кит.",
    "Имена котов и людей — всегда --kg-name, никогда не красным."
  ],
  "icons": [
    "paw",
    "camera",
    "grid",
    "map",
    "pin",
    "pin-approx",
    "repeat",
    "report",
    "share",
    "calendar",
    "sparkle",
    "temple",
    "palm",
    "sound",
    "check",
    "alert",
    "info",
    "close",
    "back",
    "chevron",
    "more",
    "user",
    "trash",
    "clock",
    "plus",
    "search",
    "cats",
    "bolt",
    "flip",
    "wifi-off",
    "camera-off",
    "flag-lang",
    "home",
    "settings",
    "pencil",
    "download",
    "lock",
    "eye-off",
    "globe",
    "copy",
    "minus"
  ],
  "components": [
    {
      "id": "button",
      "name": "Button",
      "group": "Базовые",
      "tag": "kg-button",
      "summary": "Кнопка действия. На экране одна primary. Высота задаётся паддингами: минимум 48, у primary 56.",
      "props": [
        {
          "name": "variant",
          "type": "enum",
          "default": "secondary",
          "description": "Вид кнопки.",
          "options": [
            "primary",
            "secondary",
            "ghost",
            "report",
            "danger"
          ]
        },
        {
          "name": "label",
          "type": "string",
          "default": "",
          "description": "Текст кнопки. Не обрезается."
        },
        {
          "name": "icon",
          "type": "icon",
          "default": "",
          "description": "Имя иконки слева (см. Icon)."
        },
        {
          "name": "block",
          "type": "boolean",
          "default": "false",
          "description": "Растянуть на всю ширину."
        },
        {
          "name": "size",
          "type": "enum",
          "default": "md",
          "description": "Размер. xl — главная кнопка экрана (64, иконка 28), только для primary.",
          "options": [
            "md",
            "xl"
          ]
        },
        {
          "name": "disabled",
          "type": "boolean",
          "default": "false",
          "description": "Недоступна."
        },
        {
          "name": "loading",
          "type": "boolean",
          "default": "false",
          "description": "Идёт действие: спиннер, aria-busy, клики игнорируются."
        },
        {
          "name": "loading-label",
          "type": "string",
          "default": "label",
          "description": "Текст на время загрузки."
        },
        {
          "name": "href",
          "type": "string",
          "default": "",
          "description": "Если задан — рендерится ссылкой <a>."
        },
        {
          "name": "type",
          "type": "enum",
          "default": "button",
          "description": "Тип внутри формы.",
          "options": [
            "button",
            "submit"
          ]
        },
        {
          "name": "opens",
          "type": "string (id)",
          "default": "",
          "description": "id шторки или диалога, который откроется по нажатию."
        }
      ],
      "events": [
        {
          "name": "click",
          "detail": "—",
          "description": "Нативный клик. Не срабатывает в disabled и loading."
        }
      ],
      "variants": {
        "prop": "variant",
        "values": [
          "primary",
          "secondary",
          "ghost",
          "report",
          "danger"
        ]
      },
      "states": [
        {
          "label": "По умолчанию",
          "attrs": {}
        },
        {
          "label": "Disabled",
          "attrs": {
            "disabled": ""
          }
        },
        {
          "label": "Loading",
          "attrs": {
            "loading": "",
            "loading-label": {
              "ru": "Сохраняем…",
              "en": "Saving…",
              "th": "กำลังบันทึก…"
            }
          }
        },
        {
          "label": "Block",
          "attrs": {
            "block": ""
          }
        }
      ],
      "tokens": [
        "--kg-brand",
        "--kg-brand-hover",
        "--kg-brand-ink",
        "--kg-outline",
        "--kg-surface",
        "--kg-surface-muted",
        "--kg-error",
        "--kg-radius-pill",
        "--kg-touch-min",
        "--kg-cta-height-min",
        "--kg-stroke",
        "--kg-dur-fast",
        "--kg-ease-out"
      ],
      "a11y": [
        "Нативный <button>; фокус — кольцо 3 px --kg-focus.",
        "loading → aria-busy=\"true\" и disabled.",
        "Только иконка — используйте IconButton с label."
      ],
      "rn": {
        "component": "KgButton",
        "notes": [
          "Pressable + Text; minHeight 48 (primary 56), paddingHorizontal 20/28.",
          "Нажатие: translateY 2 + тень 1 (Reanimated, 150 ms).",
          "haptic.select при onPress не нужен; primary CTA камеры — haptic.shutter."
        ],
        "usage": "<KgButton variant=\"primary\" icon=\"paw\" label={t('cta.meet')} onPress={openCamera} loading={saving} />"
      },
      "htmlUsage": "<kg-button variant=\"primary\" label=\"Встретить кота\" icon=\"paw\"></kg-button>"
    },
    {
      "id": "icon-button",
      "name": "IconButton",
      "group": "Базовые",
      "tag": "kg-icon-button",
      "summary": "Кнопка только с иконкой. Зона нажатия 48×48, название для скринридера обязательно.",
      "props": [
        {
          "name": "icon",
          "type": "icon",
          "default": "more",
          "description": "Иконка."
        },
        {
          "name": "label",
          "type": "string",
          "default": "",
          "description": "Обязательное доступное имя (aria-label)."
        },
        {
          "name": "outline",
          "type": "boolean",
          "default": "false",
          "description": "С обводкой."
        },
        {
          "name": "disabled",
          "type": "boolean",
          "default": "false",
          "description": "Недоступна."
        },
        {
          "name": "opens",
          "type": "string (id)",
          "default": "",
          "description": "id шторки/диалога."
        }
      ],
      "events": [
        {
          "name": "click",
          "detail": "—",
          "description": "Нативный клик."
        }
      ],
      "variants": {
        "prop": "icon",
        "values": [
          "share",
          "report",
          "trash",
          "more",
          "back",
          "close"
        ]
      },
      "states": [
        {
          "label": "Без обводки",
          "attrs": {}
        },
        {
          "label": "С обводкой",
          "attrs": {
            "outline": ""
          }
        },
        {
          "label": "Disabled",
          "attrs": {
            "disabled": ""
          }
        }
      ],
      "tokens": [
        "--kg-touch-min",
        "--kg-line-strong",
        "--kg-surface-muted",
        "--kg-radius-pill"
      ],
      "a11y": [
        "label обязателен — без него кнопка «немая»."
      ],
      "rn": {
        "component": "KgIconButton",
        "notes": [
          "Pressable 48×48, hitSlop не нужен.",
          "accessibilityLabel = label."
        ],
        "usage": "<KgIconButton icon=\"share\" label={t('act.share')} onPress={share} />"
      },
      "htmlUsage": "<kg-icon-button icon=\"share\" label=\"Поделиться\" outline></kg-icon-button>"
    },
    {
      "id": "chip",
      "name": "Chip",
      "group": "Базовые",
      "tag": "kg-chip",
      "summary": "Фильтр, который можно нажать. Видимая высота 40, зона нажатия 48. Для выбора одного варианта оберните чипы в <kg-chip-group single>.",
      "props": [
        {
          "name": "label",
          "type": "string",
          "default": "",
          "description": "Подпись. До ~12 символов, длиннее — используйте список."
        },
        {
          "name": "value",
          "type": "string",
          "default": "label",
          "description": "Значение для событий."
        },
        {
          "name": "pressed",
          "type": "boolean",
          "default": "false",
          "description": "Выбран."
        },
        {
          "name": "icon",
          "type": "icon",
          "default": "",
          "description": "Иконка вместо галочки."
        },
        {
          "name": "disabled",
          "type": "boolean",
          "default": "false",
          "description": "Недоступен."
        }
      ],
      "events": [
        {
          "name": "kg-change",
          "detail": "{ value, pressed }",
          "description": "Переключение (вне single-группы)."
        },
        {
          "name": "kg-change (группа)",
          "detail": "{ value }",
          "description": "На <kg-chip-group single>."
        }
      ],
      "states": [
        {
          "label": "Не выбран",
          "attrs": {}
        },
        {
          "label": "Выбран",
          "attrs": {
            "pressed": ""
          }
        },
        {
          "label": "Disabled",
          "attrs": {
            "disabled": ""
          }
        }
      ],
      "tokens": [
        "--kg-ink",
        "--kg-ink-inverse",
        "--kg-line-strong",
        "--kg-outline",
        "--kg-surface",
        "--kg-radius-pill",
        "--kg-dur-base"
      ],
      "a11y": [
        "aria-pressed отражает выбор.",
        "Выбор виден не только цветом: галочка."
      ],
      "rn": {
        "component": "KgChip / KgChipGroup",
        "notes": [
          "Pressable minHeight 40 + hitSlop 4 сверху/снизу.",
          "Группа: horizontal ScrollView, первый отступ 16.",
          "haptic.select при переключении."
        ],
        "usage": "<KgChipGroup single value={filter} onChange={setFilter}>\n  <KgChip value=\"all\" label={t('chip.all')} />\n  <KgChip value=\"week\" label={t('chip.week')} />\n</KgChipGroup>"
      },
      "htmlUsage": "<kg-chip label=\"За неделю\" pressed></kg-chip>"
    },
    {
      "id": "status-pill",
      "name": "StatusPill",
      "group": "Базовые",
      "tag": "kg-pill",
      "summary": "Статус, не нажимается. Иконка + текст, высота — паддинги. «Возможный повтор» нейтральный, без красного.",
      "props": [
        {
          "name": "status",
          "type": "enum",
          "default": "exact",
          "description": "Тип статуса. Текст по умолчанию берётся из словаря.",
          "options": [
            "exact",
            "approx",
            "dup",
            "new",
            "warn",
            "day",
            "done",
            "stale",
            "nogeo",
            "lastday",
            "public",
            "private",
            "hidden",
            "counted",
            "notcounted",
            "outside"
          ]
        },
        {
          "name": "label",
          "type": "string",
          "default": "",
          "description": "Переопределить текст."
        }
      ],
      "events": [],
      "variants": {
        "prop": "status",
        "values": [
          "exact",
          "approx",
          "dup",
          "new",
          "warn",
          "day",
          "done",
          "stale",
          "nogeo",
          "lastday",
          "public",
          "private",
          "hidden",
          "counted",
          "notcounted",
          "outside"
        ]
      },
      "states": [],
      "tokens": [
        "--kg-info",
        "--kg-info-soft",
        "--kg-neutral-note",
        "--kg-neutral-note-soft",
        "--kg-success",
        "--kg-success-soft",
        "--kg-warning",
        "--kg-warning-soft",
        "--kg-day-*",
        "--kg-radius-pill"
      ],
      "a11y": [
        "Цвет всегда дублируется иконкой и текстом.",
        "status=\"day\" берёт цвет текущего дня недели; в режиме уважения он приглушён."
      ],
      "rn": {
        "component": "KgStatusPill",
        "notes": [
          "View + Icon 16 + Text, paddingVertical 5, paddingHorizontal 10, без height."
        ],
        "usage": "<KgStatusPill status=\"approx\" />"
      },
      "htmlUsage": "<kg-pill status=\"approx\"></kg-pill>"
    },
    {
      "id": "segmented",
      "name": "Segmented",
      "group": "Базовые",
      "tag": "kg-segmented",
      "summary": "Переключатель вида из 2–4 вариантов. Варианты задаются дочерними <option>.",
      "props": [
        {
          "name": "value",
          "type": "string",
          "default": "первый",
          "description": "Выбранное значение."
        },
        {
          "name": "label",
          "type": "string",
          "default": "",
          "description": "Доступное имя группы."
        },
        {
          "name": "<option value>",
          "type": "children",
          "default": "",
          "description": "Варианты."
        }
      ],
      "events": [
        {
          "name": "kg-change",
          "detail": "{ value }",
          "description": "Выбор варианта."
        }
      ],
      "states": [],
      "tokens": [
        "--kg-surface-muted",
        "--kg-surface",
        "--kg-ink",
        "--kg-ink-2",
        "--kg-shadow-1",
        "--kg-radius-pill"
      ],
      "a11y": [
        "Кнопки с aria-pressed внутри role=\"group\"."
      ],
      "rn": {
        "component": "KgSegmented",
        "notes": [
          "Не путать с Tabs: Segmented меняет вид одного списка."
        ],
        "usage": "<KgSegmented value={view} onChange={setView} options={[{value:'cats',label:t('seg.cats')},{value:'badges',label:t('seg.badges')}]} />"
      },
      "htmlUsage": "<kg-segmented value=\"cats\" label=\"Вид\"><option value=\"cats\">Коты</option><option value=\"districts\">Районы</option><option value=\"badges\">Значки</option></kg-segmented>"
    },
    {
      "id": "tabs",
      "name": "Tabs",
      "group": "Базовые",
      "tag": "kg-tabs",
      "summary": "Вкладки с панелями. Каждая панель — дочерний элемент с value и label. Клавиатура: ← → Home End.",
      "props": [
        {
          "name": "selected",
          "type": "string",
          "default": "первая",
          "description": "Открытая вкладка."
        },
        {
          "name": "label",
          "type": "string",
          "default": "",
          "description": "Доступное имя списка вкладок."
        },
        {
          "name": "<section value label>",
          "type": "children",
          "default": "",
          "description": "Панели."
        }
      ],
      "events": [
        {
          "name": "kg-change",
          "detail": "{ value }",
          "description": "Смена вкладки."
        }
      ],
      "states": [],
      "tokens": [
        "--kg-brand",
        "--kg-ink",
        "--kg-ink-2",
        "--kg-line",
        "--kg-touch-min"
      ],
      "a11y": [
        "role=tablist/tab/tabpanel, aria-selected, roving tabindex."
      ],
      "rn": {
        "component": "KgTabs",
        "notes": [
          "Подчёркивание 3 px --kg-brand, анимация 200 ms."
        ],
        "usage": "<KgTabs selected=\"mine\" onChange={setTab} tabs={[{value:'mine',label:t('tab.mine')},{value:'near',label:t('tab.near')}]}>{panel}</KgTabs>"
      },
      "htmlUsage": "<kg-tabs selected=\"mine\" label=\"Коллекция\"><section value=\"mine\" label=\"Мои коты\"><p>Коты, которых вы встретили сами.</p></section><section value=\"near\" label=\"Рядом со мной\"><p>Коты, которых встретили другие поблизости.</p></section><section value=\"badges\" label=\"Значки\"><p>Значки за окрасы и районы, без привязки к породам.</p></section></kg-tabs>"
    },
    {
      "id": "rarity-badge",
      "name": "RarityBadge",
      "group": "Базовые",
      "tag": "kg-rarity-badge",
      "summary": "Уровень редкости: цвет + силуэт + символ + материал. Без номеров и без короны. S — сетка, M — карточки, L — reveal и детальная.",
      "props": [
        {
          "name": "tier",
          "type": "enum",
          "default": "pebble",
          "description": "Уровень.",
          "options": [
            "pebble",
            "jade",
            "sapphire",
            "secret"
          ]
        },
        {
          "name": "size",
          "type": "enum",
          "default": "s",
          "description": "Размер: s 20, m 24 + подпись, l 64 + подпись.",
          "options": [
            "s",
            "m",
            "l"
          ]
        },
        {
          "name": "live",
          "type": "boolean",
          "default": "false",
          "description": "Живой holo у secret. Только на одной открытой карточке."
        }
      ],
      "events": [],
      "variants": {
        "prop": "tier",
        "values": [
          "pebble",
          "jade",
          "sapphire",
          "secret"
        ]
      },
      "states": [
        {
          "label": "S",
          "attrs": {
            "size": "s"
          }
        },
        {
          "label": "M",
          "attrs": {
            "size": "m"
          }
        },
        {
          "label": "L",
          "attrs": {
            "size": "l"
          }
        },
        {
          "label": "L · live holo",
          "attrs": {
            "size": "l",
            "tier": "secret",
            "live": ""
          }
        }
      ],
      "tokens": [
        "--kg-r1-fill",
        "--kg-r1-ink",
        "--kg-r1-edge",
        "--kg-r2-fill",
        "--kg-r2-edge",
        "--kg-r3-fill",
        "--kg-r3-edge",
        "--kg-r4-fill",
        "--kg-r4-fill-2",
        "--kg-r4-edge",
        "--kg-holo"
      ],
      "a11y": [
        "role=\"img\" + aria-label «Редкость: Сапфир».",
        "Граница бейджа ≥ 3:1 к фону, текст ≥ 4,5:1."
      ],
      "rn": {
        "component": "KgRarityBadge",
        "notes": [
          "Силуэты — SVG (react-native-svg), symbols rb-1..4.",
          "live holo: Skia RuntimeEffect + гироскоп, только один экземпляр на экране.",
          "Без live — статичный градиент."
        ],
        "usage": "<KgRarityBadge tier=\"sapphire\" size=\"m\" />"
      },
      "htmlUsage": "<kg-rarity-badge tier=\"sapphire\" size=\"m\"></kg-rarity-badge>"
    },
    {
      "id": "text-field",
      "name": "TextField",
      "group": "Базовые",
      "tag": "kg-text-field",
      "summary": "Поле с постоянной подписью, подсказкой, счётчиком и ошибкой под полем. variant=\"name\" — поле имени кота: шрифт как у имени на карточке, цвет имени не краснеет.",
      "props": [
        {
          "name": "label",
          "type": "string",
          "default": "",
          "description": "Подпись (видна всегда)."
        },
        {
          "name": "value",
          "type": "string",
          "default": "",
          "description": "Начальное значение."
        },
        {
          "name": "helper",
          "type": "string",
          "default": "",
          "description": "Подсказка под полем."
        },
        {
          "name": "error",
          "type": "string",
          "default": "словарь",
          "description": "Текст ошибки: причина + что сделать."
        },
        {
          "name": "required",
          "type": "boolean",
          "default": "false",
          "description": "Обязательно; проверка при уходе с поля."
        },
        {
          "name": "maxlength",
          "type": "number",
          "default": "",
          "description": "Лимит + счётчик. Имя кота — 30."
        },
        {
          "name": "multiline",
          "type": "boolean",
          "default": "false",
          "description": "Многострочное."
        },
        {
          "name": "variant",
          "type": "enum",
          "default": "default",
          "description": "Вид.",
          "options": [
            "default",
            "name"
          ]
        },
        {
          "name": "invalid",
          "type": "boolean",
          "default": "false",
          "description": "Принудительно показать ошибку."
        },
        {
          "name": "disabled",
          "type": "boolean",
          "default": "false",
          "description": "Недоступно."
        },
        {
          "name": "name",
          "type": "string",
          "default": "",
          "description": "Имя поля в форме."
        }
      ],
      "events": [
        {
          "name": "kg-input",
          "detail": "{ value }",
          "description": "Каждый ввод."
        },
        {
          "name": "kg-change",
          "detail": "{ value, valid }",
          "description": "Уход с поля (blur) + результат проверки."
        }
      ],
      "states": [
        {
          "label": "По умолчанию",
          "attrs": {}
        },
        {
          "label": "Ошибка",
          "attrs": {
            "invalid": "",
            "value": ""
          }
        },
        {
          "label": "Многострочное",
          "attrs": {
            "multiline": "",
            "variant": "default",
            "label": {
              "ru": "Комментарий",
              "en": "Comment",
              "th": "ความคิดเห็น"
            },
            "maxlength": "300",
            "value": "",
            "required": null
          }
        },
        {
          "label": "Disabled",
          "attrs": {
            "disabled": ""
          }
        }
      ],
      "tokens": [
        "--kg-line-strong",
        "--kg-focus",
        "--kg-error",
        "--kg-name",
        "--kg-font-display",
        "--kg-radius-md",
        "--kg-touch-min",
        "--kg-lh-body"
      ],
      "a11y": [
        "label for → input; aria-describedby → подсказка и ошибка.",
        "Ошибка: aria-invalid, текст под полем, не только цвет.",
        "Проверка на blur, не на каждый ввод."
      ],
      "rn": {
        "component": "KgTextField",
        "notes": [
          "TextInput без фиксированной height: paddingVertical 10/12, minHeight 48.",
          "Тайский: lineHeight = fontSize × 1.6; на Android includeFontPadding:false + paddingTop компенсация (RN #28331).",
          "Счётчик по графемам ([...value].length)."
        ],
        "usage": "<KgTextField variant=\"name\" label={t('field.name')} maxLength={30} required value={name} onChangeText={setName} helper={t('field.name.help')} />"
      },
      "htmlUsage": "<kg-text-field variant=\"name\" label=\"Как зовут кота?\" value=\"Баронесса Мурка фон Колбаскина\" helper=\"Имя увидят те, кто встретит этого кота после вас.\" maxlength=\"30\" required></kg-text-field>"
    },
    {
      "id": "switch",
      "name": "Switch",
      "group": "Базовые",
      "tag": "kg-switch",
      "summary": "Переключатель настройки: подпись + подсказка слева, переключатель справа. Строка целиком — зона нажатия.",
      "props": [
        {
          "name": "label",
          "type": "string",
          "default": "",
          "description": "Подпись."
        },
        {
          "name": "helper",
          "type": "string",
          "default": "",
          "description": "Подсказка до 2 строк."
        },
        {
          "name": "checked",
          "type": "boolean",
          "default": "false",
          "description": "Включено."
        },
        {
          "name": "disabled",
          "type": "boolean",
          "default": "false",
          "description": "Недоступно."
        }
      ],
      "events": [
        {
          "name": "kg-change",
          "detail": "{ checked }",
          "description": "Переключение."
        }
      ],
      "states": [
        {
          "label": "Выкл",
          "attrs": {}
        },
        {
          "label": "Вкл",
          "attrs": {
            "checked": ""
          }
        },
        {
          "label": "Disabled",
          "attrs": {
            "disabled": ""
          }
        }
      ],
      "tokens": [
        "--kg-brand",
        "--kg-brand-ink",
        "--kg-surface-muted",
        "--kg-line-strong",
        "--kg-dur-base"
      ],
      "a11y": [
        "input role=\"switch\" внутри label."
      ],
      "rn": {
        "component": "KgSwitch",
        "notes": [
          "Своя отрисовка 48×28, не системный Switch — единый вид на Android и iOS."
        ],
        "usage": "<KgSwitch label={t('switch.haptic')} helper={t('switch.haptic.help')} value={haptics} onValueChange={setHaptics} />"
      },
      "htmlUsage": "<kg-switch label=\"Вибрация\" helper=\"Отклик при съёмке и встрече\" checked></kg-switch>"
    },
    {
      "id": "choice-group",
      "name": "ChoiceGroup",
      "group": "Базовые",
      "tag": "kg-choice-group",
      "summary": "Выбор одного варианта карточками. По умолчанию ничего не выбрано (требование PRD для точности геолокации).",
      "props": [
        {
          "name": "legend",
          "type": "string",
          "default": "",
          "description": "Вопрос."
        },
        {
          "name": "value",
          "type": "string",
          "default": "",
          "description": "Выбранное значение. Пусто = без предвыбора."
        },
        {
          "name": "name",
          "type": "string",
          "default": "авто",
          "description": "Имя группы radio."
        },
        {
          "name": "<option value description>",
          "type": "children",
          "default": "",
          "description": "Варианты."
        }
      ],
      "events": [
        {
          "name": "kg-change",
          "detail": "{ value }",
          "description": "Выбор."
        }
      ],
      "states": [
        {
          "label": "Без выбора",
          "attrs": {}
        },
        {
          "label": "Выбрано",
          "attrs": {
            "value": "approx"
          }
        }
      ],
      "tokens": [
        "--kg-line-strong",
        "--kg-outline",
        "--kg-surface",
        "--kg-brand",
        "--kg-radius-md"
      ],
      "a11y": [
        "fieldset + legend, нативные radio, стрелки внутри группы."
      ],
      "rn": {
        "component": "KgChoiceGroup",
        "notes": [
          "accessibilityRole=\"radiogroup\"; value=null по умолчанию.",
          "Кнопка «Продолжить» неактивна, пока value=null."
        ],
        "usage": "<KgChoiceGroup legend={t('geo.legend')} value={precision} onChange={setPrecision} options={[{value:'exact',label:t('geo.exact'),description:t('geo.exact.d')},{value:'approx',label:t('geo.approx'),description:t('geo.approx.d')}]} />"
      },
      "htmlUsage": "<kg-choice-group legend=\"Как показывать место встречи?\"><option value=\"exact\" description=\"Метка на месте встречи. Подходит для дворов и сои.\">Точно</option><option value=\"approx\" description=\"Только район, радиус около 500 м. Ваш дом и маршруты не видны.\">Примерно</option></kg-choice-group>"
    },
    {
      "id": "progress",
      "name": "Progress",
      "group": "Базовые",
      "tag": "kg-progress",
      "summary": "Линейный прогресс с подписью и числом. Число не переносится.",
      "props": [
        {
          "name": "value",
          "type": "number",
          "default": "0",
          "description": "Текущее."
        },
        {
          "name": "max",
          "type": "number",
          "default": "100",
          "description": "Максимум."
        },
        {
          "name": "label",
          "type": "string",
          "default": "",
          "description": "Подпись слева."
        },
        {
          "name": "tone",
          "type": "enum",
          "default": "brand",
          "description": "Цвет полосы.",
          "options": [
            "brand",
            "day"
          ]
        }
      ],
      "events": [],
      "variants": {
        "prop": "tone",
        "values": [
          "brand",
          "day"
        ]
      },
      "states": [
        {
          "label": "0",
          "attrs": {
            "value": "0"
          }
        },
        {
          "label": "Готово",
          "attrs": {
            "value": "12"
          }
        }
      ],
      "tokens": [
        "--kg-brand",
        "--kg-day-*",
        "--kg-surface-muted",
        "--kg-dur-slow"
      ],
      "a11y": [
        "role=\"progressbar\" + aria-valuenow/max."
      ],
      "rn": {
        "component": "KgProgress",
        "notes": [
          "Анимировать transform scaleX, не width."
        ],
        "usage": "<KgProgress label={t('progress.district')} value={7} max={12} />"
      },
      "htmlUsage": "<kg-progress value=\"7\" max=\"12\" label=\"Коты района Khlong Toei\"></kg-progress>"
    },
    {
      "id": "counter",
      "name": "Counter",
      "group": "Базовые",
      "tag": "kg-counter",
      "summary": "Счётчик коллекции: число и подпись — два отдельных блока.",
      "props": [
        {
          "name": "value",
          "type": "number",
          "default": "0",
          "description": "Число."
        },
        {
          "name": "total",
          "type": "number",
          "default": "",
          "description": "Из скольких (необязательно)."
        },
        {
          "name": "caption",
          "type": "string",
          "default": "",
          "description": "Подпись под числом."
        }
      ],
      "events": [],
      "states": [
        {
          "label": "Без total",
          "attrs": {
            "total": null
          }
        }
      ],
      "tokens": [
        "--kg-font-display",
        "--kg-ink-2"
      ],
      "a11y": [
        "Числа tabular-nums, не переносятся."
      ],
      "rn": {
        "component": "KgCounter",
        "notes": [
          "fontSize clamp → используйте 32 на 360 dp, 40 на ≥ 393."
        ],
        "usage": "<KgCounter value={24} total={50} caption={t('counter.caption')} />"
      },
      "htmlUsage": "<kg-counter value=\"24\" total=\"50\" caption=\"котов в коллекции\"></kg-counter>"
    },
    {
      "id": "skeleton",
      "name": "Skeleton",
      "group": "Базовые",
      "tag": "kg-skeleton",
      "summary": "Заглушка загрузки той же формы, что контент. Показывать, если ожидание > 300 мс.",
      "props": [
        {
          "name": "shape",
          "type": "enum",
          "default": "row",
          "description": "Форма.",
          "options": [
            "row",
            "text",
            "thumb"
          ]
        },
        {
          "name": "lines",
          "type": "number",
          "default": "2",
          "description": "Строк текста."
        }
      ],
      "events": [],
      "variants": {
        "prop": "shape",
        "values": [
          "row",
          "text",
          "thumb"
        ]
      },
      "states": [],
      "tokens": [
        "--kg-surface-muted",
        "--kg-surface-sunk",
        "--kg-radius-sm",
        "--kg-radius-thumb"
      ],
      "a11y": [
        "aria-busy + aria-label «Загрузка»; мерцание отключается при Reduce Motion."
      ],
      "rn": {
        "component": "KgSkeleton",
        "notes": [
          "Reanimated shimmer; при reduceMotion — статичный цвет."
        ],
        "usage": "<KgSkeleton shape=\"row\" lines={2} />"
      },
      "htmlUsage": "<kg-skeleton shape=\"row\"></kg-skeleton>"
    },
    {
      "id": "notice",
      "name": "Notice",
      "group": "Базовые",
      "tag": "kg-notice",
      "summary": "Встроенное сообщение в потоке экрана. Текст — дочерний контент. В тексте ошибок нет имён.",
      "props": [
        {
          "name": "tone",
          "type": "enum",
          "default": "info",
          "description": "Тон.",
          "options": [
            "info",
            "warn",
            "error",
            "success"
          ]
        },
        {
          "name": "(content)",
          "type": "children",
          "default": "",
          "description": "Текст сообщения."
        }
      ],
      "events": [],
      "variants": {
        "prop": "tone",
        "values": [
          "info",
          "warn",
          "error",
          "success"
        ]
      },
      "states": [],
      "tokens": [
        "--kg-info",
        "--kg-info-soft",
        "--kg-warning",
        "--kg-warning-soft",
        "--kg-error",
        "--kg-error-soft",
        "--kg-success",
        "--kg-success-soft"
      ],
      "a11y": [
        "error → role=\"alert\", остальные → role=\"status\"."
      ],
      "rn": {
        "component": "KgNotice",
        "notes": [
          "accessibilityLiveRegion=\"polite\" (Android)."
        ],
        "usage": "<KgNotice tone=\"warn\">{t('notice.heat')}</KgNotice>"
      },
      "htmlUsage": "<kg-notice tone=\"warn\">Телефон нагрелся. Камера отключится через минуту, чтобы остыть.</kg-notice>"
    },
    {
      "id": "sticker",
      "name": "Sticker",
      "group": "Базовые",
      "tag": "kg-sticker",
      "summary": "Кот без фона с белым «вырубным» обводом на бумажной фактуре. Пропорции фото сохраняются, обрезки нет.",
      "props": [
        {
          "name": "src",
          "type": "string (url)",
          "default": "img/cat-eur.png",
          "description": "PNG с прозрачностью."
        },
        {
          "name": "w",
          "type": "number",
          "default": "557",
          "description": "Исходная ширина."
        },
        {
          "name": "h",
          "type": "number",
          "default": "824",
          "description": "Исходная высота."
        },
        {
          "name": "alt",
          "type": "string",
          "default": "",
          "description": "Описание кота."
        },
        {
          "name": "width",
          "type": "number",
          "default": "160",
          "description": "Ширина на экране, px."
        },
        {
          "name": "outline",
          "type": "enum",
          "default": "md",
          "description": "Толщина обвода: sm 6, md 8, lg 10.",
          "options": [
            "sm",
            "md",
            "lg"
          ]
        },
        {
          "name": "muted",
          "type": "boolean",
          "default": "false",
          "description": "Приглушённый (пустые состояния)."
        }
      ],
      "events": [],
      "variants": {
        "prop": "outline",
        "values": [
          "sm",
          "md",
          "lg"
        ]
      },
      "states": [
        {
          "label": "Muted",
          "attrs": {
            "muted": ""
          }
        }
      ],
      "tokens": [
        "--kg-diecut-sm",
        "--kg-diecut-md",
        "--kg-diecut-lg",
        "--kg-shadow-sticker",
        "--kg-grain",
        "--kg-surface-sunk"
      ],
      "a11y": [
        "alt описывает кота, если рядом нет имени."
      ],
      "rn": {
        "component": "KgSticker",
        "notes": [
          "Обвод готовится на сервере/в пайплайне удаления фона (PNG с белым контуром), а не фильтром на клиенте.",
          "Image resizeMode=\"contain\", aspectRatio из исходника."
        ],
        "usage": "<KgSticker source={{uri: cat.cutoutUrl}} aspectRatio={cat.w / cat.h} width={160} outline=\"md\" />"
      },
      "htmlUsage": "<kg-sticker src=\"img/cat-eur.png\" w=\"557\" h=\"824\" alt=\"Мурка\" width=\"160\"></kg-sticker>"
    },
    {
      "id": "steps",
      "name": "Steps",
      "group": "Базовые",
      "tag": "kg-steps",
      "summary": "Индикатор этапов долгой операции (обработка захвата). У каждого этапа состояние иконкой и словом: ждёт, идёт, готово, не удалось, пропущен. Подпись «дольше обычного» — отдельной строкой.",
      "props": [
        {
          "name": "steps",
          "type": "string",
          "default": "5 этапов обработки",
          "description": "Подписи этапов через «|»."
        },
        {
          "name": "current",
          "type": "number",
          "default": "0",
          "description": "Индекс текущего этапа; все до него — готово."
        },
        {
          "name": "failed",
          "type": "number",
          "default": "",
          "description": "Индекс этапа со сбоем."
        },
        {
          "name": "skipped",
          "type": "string",
          "default": "",
          "description": "Индексы пропущенных этапов через запятую (например, нет места → район)."
        },
        {
          "name": "done",
          "type": "boolean",
          "default": "false",
          "description": "Все этапы готовы."
        },
        {
          "name": "slow",
          "type": "boolean",
          "default": "false",
          "description": "Показать «Занимает чуть дольше обычного»."
        },
        {
          "name": "label",
          "type": "string",
          "default": "Загрузка",
          "description": "Подпись списка для скринридера."
        }
      ],
      "events": [],
      "states": [
        {
          "label": "Дольше обычного",
          "attrs": {
            "current": "2",
            "slow": ""
          }
        },
        {
          "label": "Сбой",
          "attrs": {
            "current": "2",
            "failed": "2"
          }
        },
        {
          "label": "Без места",
          "attrs": {
            "done": "",
            "skipped": "4"
          }
        }
      ],
      "tokens": [
        "--kg-success",
        "--kg-success-soft",
        "--kg-warning",
        "--kg-warning-soft",
        "--kg-brand-text",
        "--kg-line-strong"
      ],
      "a11y": [
        "Список ol, текущий этап — aria-current=\"step\".",
        "Состояние продублировано словом, не только цветом и иконкой.",
        "Подпись о задержке — role=\"status\"."
      ],
      "rn": {
        "component": "KgSteps",
        "notes": [
          "Состояния этапов приходят от сервера обработки; клиент не угадывает прогресс."
        ],
        "usage": "<KgSteps current={job.step} failed={job.failedStep} skipped={noGeo ? [4] : []} slow={elapsed > 6000} />"
      },
      "htmlUsage": "<kg-steps current=\"2\"></kg-steps>"
    },
    {
      "id": "loader",
      "name": "Loader",
      "group": "Базовые",
      "tag": "kg-loader",
      "summary": "Индикатор ожидания: три «подушечки лапы» и подпись, что именно происходит. Для ожиданий без известной длительности: запуск, создание профиля, отправка.",
      "props": [
        {
          "name": "label",
          "type": "string",
          "default": "Загрузка",
          "description": "Что происходит — глагол в процессе: «Готовим ваш профиль…»."
        },
        {
          "name": "size",
          "type": "enum",
          "default": "md",
          "description": "Размер точек.",
          "options": [
            "md",
            "lg"
          ]
        }
      ],
      "events": [],
      "variants": {
        "prop": "size",
        "values": [
          "md",
          "lg"
        ]
      },
      "states": [],
      "tokens": [
        "--kg-brand",
        "--kg-ink-2",
        "--kg-ease-inout"
      ],
      "a11y": [
        "role=\"status\" + aria-live: подпись объявляется скринридером.",
        "Reduce Motion — точки статичные, подпись остаётся."
      ],
      "rn": {
        "component": "KgLoader",
        "notes": [
          "Reanimated withRepeat; при reduceMotion — без анимации.",
          "Если ожидание известно — используйте Progress."
        ],
        "usage": "<KgLoader label={t('splash.loading')} size=\"lg\" />"
      },
      "htmlUsage": "<kg-loader label=\"Готовим ваш профиль…\" size=\"lg\"></kg-loader>"
    },
    {
      "id": "logo",
      "name": "Logo",
      "group": "Базовые",
      "tag": "kg-logo",
      "summary": "Знак KisGo с необязательным слоганом. Используется на сплэше, в шапках и на share-картинках.",
      "props": [
        {
          "name": "size",
          "type": "enum",
          "default": "md",
          "description": "Размер: sm 20, md 28, lg 48.",
          "options": [
            "sm",
            "md",
            "lg"
          ]
        },
        {
          "name": "tagline",
          "type": "string",
          "default": "",
          "description": "Слоган под знаком."
        }
      ],
      "events": [],
      "variants": {
        "prop": "size",
        "values": [
          "sm",
          "md",
          "lg"
        ]
      },
      "states": [],
      "tokens": [
        "--kg-ink",
        "--kg-brand-text",
        "--kg-ink-2"
      ],
      "a11y": [
        "Знак — role=\"img\" с именем «KisGo»."
      ],
      "rn": {
        "component": "KgLogo",
        "notes": [
          "Векторный логотип (react-native-svg) появится после брендинга; сейчас — шрифт Nunito 800."
        ],
        "usage": "<KgLogo size=\"lg\" tagline={t('splash.tagline')} />"
      },
      "htmlUsage": "<kg-logo size=\"lg\" tagline=\"Встречай котов своего района\"></kg-logo>"
    },
    {
      "id": "checkbox",
      "name": "Checkbox",
      "group": "Базовые",
      "tag": "kg-checkbox",
      "summary": "Отметка согласия или подтверждения. По умолчанию не отмечена. Строка целиком — зона нажатия 48. Для обязательных согласий: кнопка «Продолжить» неактивна, пока отметки нет.",
      "props": [
        {
          "name": "label",
          "type": "string",
          "default": "",
          "description": "Текст отметки."
        },
        {
          "name": "helper",
          "type": "string",
          "default": "",
          "description": "Пояснение под текстом."
        },
        {
          "name": "checked",
          "type": "boolean",
          "default": "false",
          "description": "Отмечено."
        },
        {
          "name": "required",
          "type": "boolean",
          "default": "false",
          "description": "Обязательно (подпись «обязательно»)."
        },
        {
          "name": "disabled",
          "type": "boolean",
          "default": "false",
          "description": "Недоступно (например, документы не загрузились)."
        },
        {
          "name": "invalid",
          "type": "boolean",
          "default": "false",
          "description": "Показать ошибку под отметкой."
        },
        {
          "name": "error",
          "type": "string",
          "default": "словарь",
          "description": "Текст ошибки."
        },
        {
          "name": "name",
          "type": "string",
          "default": "",
          "description": "Имя для форм и событий."
        }
      ],
      "events": [
        {
          "name": "kg-change",
          "detail": "{ checked, name }",
          "description": "Переключение."
        }
      ],
      "states": [
        {
          "label": "Не отмечено",
          "attrs": {}
        },
        {
          "label": "Отмечено",
          "attrs": {
            "checked": ""
          }
        },
        {
          "label": "Недоступно",
          "attrs": {
            "disabled": ""
          }
        },
        {
          "label": "Ошибка",
          "attrs": {
            "invalid": ""
          }
        }
      ],
      "tokens": [
        "--kg-brand",
        "--kg-brand-ink",
        "--kg-line-strong",
        "--kg-focus",
        "--kg-error",
        "--kg-radius-sm"
      ],
      "a11y": [
        "Нативный input type=\"checkbox\" внутри label; aria-required, aria-invalid.",
        "Отметка видна не только цветом: галочка."
      ],
      "rn": {
        "component": "KgCheckbox",
        "notes": [
          "Pressable на всю строку, accessibilityRole=\"checkbox\", accessibilityState={{checked, disabled}}.",
          "haptic.select при переключении."
        ],
        "usage": "<KgCheckbox label={t('a1.age')} required checked={age} onChange={setAge} />"
      },
      "htmlUsage": "<kg-checkbox label=\"Мне 13 лет или больше\" helper=\"KisGo доступен с 13 лет\" required></kg-checkbox>"
    },
    {
      "id": "rule",
      "name": "Rule",
      "group": "Базовые",
      "tag": "kg-rule",
      "summary": "Строка правила или подсказки: иконка в круге, короткий заголовок, пояснение. Для правил безопасной съёмки, этикета, онбординга.",
      "props": [
        {
          "name": "icon",
          "type": "icon",
          "default": "info",
          "description": "Иконка."
        },
        {
          "name": "heading",
          "type": "string",
          "default": "",
          "description": "Короткое правило — повелительное, без запретительного тона."
        },
        {
          "name": "text",
          "type": "string",
          "default": "",
          "description": "Пояснение, 1–2 строки."
        }
      ],
      "events": [],
      "states": [],
      "tokens": [
        "--kg-surface-muted",
        "--kg-ink",
        "--kg-ink-2"
      ],
      "a11y": [
        "Иконка декоративная, смысл передаёт текст.",
        "В указующих иллюстрациях — открытая ладонь, не палец."
      ],
      "rn": {
        "component": "KgRule",
        "notes": [
          "View в строку; список правил — обычный map без FlatList."
        ],
        "usage": "<KgRule icon=\"paw\" heading={t('rule.distance')} text={t('rule.distance.d')} />"
      },
      "htmlUsage": "<kg-rule icon=\"paw\" heading=\"Не мешай коту\" text=\"Снимай с расстояния и оставь коту путь, чтобы уйти.\"></kg-rule>"
    },
    {
      "id": "app-bar",
      "name": "AppBar",
      "group": "Каркас",
      "tag": "kg-app-bar",
      "summary": "Верхняя панель. large — заголовок раздела; nested — вложенный экран с «Назад».",
      "props": [
        {
          "name": "heading",
          "type": "string",
          "default": "",
          "description": "Заголовок. В nested обрезается многоточием."
        },
        {
          "name": "variant",
          "type": "enum",
          "default": "nested",
          "description": "Вид.",
          "options": [
            "nested",
            "large"
          ]
        },
        {
          "name": "back",
          "type": "boolean",
          "default": "false",
          "description": "Кнопка «Назад» (nested)."
        },
        {
          "name": "action-icon",
          "type": "icon",
          "default": "",
          "description": "Иконка действия справа."
        },
        {
          "name": "action-label",
          "type": "string",
          "default": "",
          "description": "Имя действия."
        }
      ],
      "events": [
        {
          "name": "kg-back",
          "detail": "—",
          "description": "«Назад»."
        },
        {
          "name": "kg-action",
          "detail": "—",
          "description": "Действие справа."
        }
      ],
      "variants": {
        "prop": "variant",
        "values": [
          "nested",
          "large"
        ]
      },
      "states": [],
      "tokens": [
        "--kg-bg",
        "--kg-line",
        "--kg-touch-min",
        "--kg-font-display"
      ],
      "a11y": [
        "Заголовок — h1 экрана.",
        "Кнопки с доступными именами."
      ],
      "rn": {
        "component": "KgAppBar",
        "notes": [
          "Учитывает safe-area top (react-native-safe-area-context).",
          "«Назад» восстанавливает скролл предыдущего экрана."
        ],
        "usage": "<KgAppBar variant=\"nested\" back heading={t('title.cat')} actionIcon=\"share\" actionLabel={t('act.share')} onBack={goBack} onAction={share} />"
      },
      "htmlUsage": "<kg-app-bar heading=\"Карточка кота\" variant=\"nested\" back action-icon=\"share\" action-label=\"Поделиться\"></kg-app-bar>"
    },
    {
      "id": "tab-bar",
      "name": "TabBar",
      "group": "Каркас",
      "tag": "kg-tab-bar",
      "summary": "Нижняя навигация, максимум 5 пунктов, иконка + подпись. Пункты — дочерние <option value icon>.",
      "props": [
        {
          "name": "value",
          "type": "string",
          "default": "первый",
          "description": "Текущий раздел."
        },
        {
          "name": "label",
          "type": "string",
          "default": "",
          "description": "Имя навигации."
        },
        {
          "name": "<option value icon href>",
          "type": "children",
          "default": "",
          "description": "Пункты (≤ 5)."
        }
      ],
      "events": [
        {
          "name": "kg-change",
          "detail": "{ value }",
          "description": "Переход в раздел."
        }
      ],
      "states": [],
      "tokens": [
        "--kg-surface",
        "--kg-line",
        "--kg-brand-soft",
        "--kg-brand-text",
        "--kg-ink-2"
      ],
      "a11y": [
        "aria-current=\"page\" у текущего; выбор виден подложкой и цветом иконки."
      ],
      "rn": {
        "component": "KgTabBar",
        "notes": [
          "Кастомный tabBar для @react-navigation/bottom-tabs.",
          "Учитывает safe-area bottom."
        ],
        "usage": "<Tab.Navigator tabBar={props => <KgTabBar {...props} />}>…</Tab.Navigator>"
      },
      "htmlUsage": "<kg-tab-bar value=\"collection\" label=\"Разделы\"><option value=\"camera\" icon=\"camera\">Камера</option><option value=\"collection\" icon=\"grid\">Коллекция</option><option value=\"map\" icon=\"map\">Карта</option><option value=\"quest\" icon=\"calendar\">Задание</option></kg-tab-bar>"
    },
    {
      "id": "sheet",
      "name": "Sheet",
      "group": "Каркас",
      "tag": "kg-sheet",
      "summary": "Нижняя шторка на <dialog>. Закрывается кнопкой, Esc и нажатием на подложку; фокус возвращается.",
      "props": [
        {
          "name": "heading",
          "type": "string",
          "default": "",
          "description": "Заголовок."
        },
        {
          "name": "contained",
          "type": "boolean",
          "default": "false",
          "description": "Открывать внутри ближайшего контейнера с position (экран телефона), а не поверх всей страницы. Esc и подложка закрывают."
        },
        {
          "name": "persistent",
          "type": "boolean",
          "default": "false",
          "description": "Без крестика; Esc и подложка не закрывают — выход только через кнопки внутри (явное решение, например запрос разрешения)."
        },
        {
          "name": "(content)",
          "type": "children",
          "default": "",
          "description": "Содержимое."
        }
      ],
      "methods": [
        "show()",
        "close(returnValue?)"
      ],
      "events": [
        {
          "name": "kg-open",
          "detail": "—",
          "description": "Открыта."
        },
        {
          "name": "kg-close",
          "detail": "{ returnValue }",
          "description": "Закрыта."
        }
      ],
      "states": [],
      "tokens": [
        "--kg-surface",
        "--kg-scrim",
        "--kg-radius-lg",
        "--kg-shadow-3",
        "--kg-dur-move"
      ],
      "a11y": [
        "Модальный <dialog>: фокус-ловушка, Esc.",
        "Не используйте шторку для основной навигации."
      ],
      "rn": {
        "component": "KgSheet",
        "notes": [
          "@gorhom/bottom-sheet с backdrop; snap по контенту."
        ],
        "usage": "<KgSheet ref={sheet} heading={t('sheet.title')}>{children}</KgSheet>\nsheet.current.present()"
      },
      "htmlUsage": "<kg-sheet heading=\"Фильтры\"><p class=\"kg-t-body\">Содержимое шторки: любые компоненты кита.</p><kg-button variant=\"primary\" label=\"Применить\" block></kg-button></kg-sheet>"
    },
    {
      "id": "dialog",
      "name": "Dialog",
      "group": "Каркас",
      "tag": "kg-dialog",
      "summary": "Подтверждение действия. Опасное действие (удаление) — всегда через диалог, кнопка danger. Имена в тексте — не красным.",
      "props": [
        {
          "name": "heading",
          "type": "string",
          "default": "",
          "description": "Вопрос."
        },
        {
          "name": "text",
          "type": "string",
          "default": "",
          "description": "Последствия."
        },
        {
          "name": "confirm-label",
          "type": "string",
          "default": "Удалить",
          "description": "Кнопка подтверждения."
        },
        {
          "name": "cancel-label",
          "type": "string",
          "default": "Отмена",
          "description": "Кнопка отмены (в фокусе по умолчанию)."
        },
        {
          "name": "destructive",
          "type": "boolean",
          "default": "false",
          "description": "Подтверждение — danger."
        },
        {
          "name": "confirm-icon",
          "type": "icon",
          "default": "",
          "description": "Иконка подтверждения."
        },
        {
          "name": "contained",
          "type": "boolean",
          "default": "false",
          "description": "Открывать внутри ближайшего контейнера с position (экран телефона), со своей подложкой; Esc = отмена."
        }
      ],
      "methods": [
        "show()",
        "close(returnValue?)"
      ],
      "events": [
        {
          "name": "kg-confirm",
          "detail": "—",
          "description": "Подтверждено."
        },
        {
          "name": "kg-cancel",
          "detail": "—",
          "description": "Отмена / Esc / подложка."
        }
      ],
      "states": [],
      "tokens": [
        "--kg-surface",
        "--kg-scrim",
        "--kg-error",
        "--kg-radius-lg"
      ],
      "a11y": [
        "Фокус на «Отмена» — безопасный выбор."
      ],
      "rn": {
        "component": "KgDialog",
        "notes": [
          "Modal transparent + свой layout; не Alert.alert (нет брендинга и тайских интервалов)."
        ],
        "usage": "<KgDialog visible={confirm} destructive heading={t('del.title')} text={t('del.text')} onConfirm={remove} onCancel={close} />"
      },
      "htmlUsage": "<kg-dialog heading=\"Удалить кота из коллекции?\" text=\"Карточка исчезнет из коллекции и с карты. Это действие нельзя отменить.\" destructive confirm-icon=\"trash\" confirm-label=\"Удалить\" cancel-label=\"Отмена\"></kg-dialog>"
    },
    {
      "id": "status-row",
      "name": "StatusRow",
      "group": "Каркас",
      "tag": "kg-status-row",
      "summary": "Строка статуса: иконка в круге + заголовок + пояснение. Для места на A7, статуса публикации на A9/A10, «засчитано / не засчитано». С href — вся строка ссылка; с action-label — кнопка справа.",
      "props": [
        {
          "name": "tone",
          "type": "enum",
          "default": "info",
          "description": "Тон.",
          "options": [
            "ok",
            "info",
            "warn",
            "muted"
          ]
        },
        {
          "name": "icon",
          "type": "icon",
          "default": "по tone",
          "description": "Иконка."
        },
        {
          "name": "heading",
          "type": "string",
          "default": "",
          "description": "Главная строка."
        },
        {
          "name": "text",
          "type": "string",
          "default": "",
          "description": "Пояснение."
        },
        {
          "name": "href",
          "type": "string",
          "default": "",
          "description": "Ссылка: вся строка нажимается, справа шеврон."
        },
        {
          "name": "action-label",
          "type": "string",
          "default": "",
          "description": "Кнопка справа (kg-action)."
        },
        {
          "name": "action-icon",
          "type": "icon",
          "default": "",
          "description": "Иконка кнопки."
        },
        {
          "name": "busy",
          "type": "boolean",
          "default": "false",
          "description": "Спиннер вместо иконки (идёт загрузка статуса)."
        }
      ],
      "events": [
        {
          "name": "kg-action",
          "detail": "—",
          "description": "Нажатие строки-ссылки или кнопки."
        }
      ],
      "variants": {
        "prop": "tone",
        "values": [
          "ok",
          "info",
          "warn",
          "muted"
        ]
      },
      "states": [
        {
          "label": "Ссылка",
          "attrs": {
            "href": "#",
            "tone": "info",
            "icon": "globe"
          }
        },
        {
          "label": "С кнопкой",
          "attrs": {
            "tone": "muted",
            "action-label": {
              "ru": "Открыть настройки",
              "en": "Open settings",
              "th": "เปิดการตั้งค่า"
            }
          }
        },
        {
          "label": "Загрузка",
          "attrs": {
            "busy": ""
          }
        }
      ],
      "tokens": [
        "--kg-success-soft",
        "--kg-info-soft",
        "--kg-warning-soft",
        "--kg-surface-muted",
        "--kg-radius-md",
        "--kg-touch-min"
      ],
      "a11y": [
        "Без href — role=\"status\".",
        "Цвет дублируется иконкой и текстом."
      ],
      "rn": {
        "component": "KgStatusRow",
        "notes": [
          "Pressable, если есть onPress; minHeight 48."
        ],
        "usage": "<KgStatusRow tone=\"ok\" heading={t('place.counts')} text={district} />"
      },
      "htmlUsage": "<kg-status-row tone=\"ok\" icon=\"pin\" heading=\"Засчитается в задание\" text=\"Бангкок · Khlong Toei\"></kg-status-row>"
    },
    {
      "id": "menu-row",
      "name": "MenuRow",
      "group": "Каркас",
      "tag": "kg-menu-row",
      "summary": "Пункт меню профиля: иконка, подпись, значение под ней, шеврон. Группа пунктов — обёртка .kg-menu с разделителями.",
      "props": [
        {
          "name": "icon",
          "type": "icon",
          "default": "chevron",
          "description": "Иконка в круге."
        },
        {
          "name": "label",
          "type": "string",
          "default": "",
          "description": "Подпись."
        },
        {
          "name": "value",
          "type": "string",
          "default": "",
          "description": "Значение или пояснение под подписью."
        },
        {
          "name": "href",
          "type": "string",
          "default": "",
          "description": "Ссылка; без неё — кнопка (kg-action)."
        },
        {
          "name": "disabled",
          "type": "boolean",
          "default": "false",
          "description": "Недоступен: шеврон скрыт."
        },
        {
          "name": "tone",
          "type": "enum",
          "default": "default",
          "description": "muted — приглушённая подпись.",
          "options": [
            "default",
            "muted"
          ]
        }
      ],
      "events": [
        {
          "name": "kg-action",
          "detail": "—",
          "description": "Нажатие пункта."
        }
      ],
      "variants": null,
      "states": [
        {
          "label": "Со значением",
          "attrs": {
            "value": {
              "ru": "Тема, язык, разрешения",
              "en": "Theme, language, permissions",
              "th": "ธีม ภาษา สิทธิ์"
            }
          }
        },
        {
          "label": "Недоступен",
          "attrs": {
            "disabled": ""
          }
        }
      ],
      "tokens": [
        "--kg-surface",
        "--kg-surface-muted",
        "--kg-line",
        "--kg-ink-2",
        "--kg-radius-md"
      ],
      "a11y": [
        "Вся строка — одна ссылка или кнопка, высота ≥ 56.",
        "Значение читается вместе с подписью."
      ],
      "rn": {
        "component": "KgMenuRow",
        "notes": [
          "Pressable, minHeight 56; группа — View с разделителями hairline."
        ],
        "usage": "<KgMenuRow icon=\"settings\" label={t('profile.settings')} onPress={() => nav.navigate('Settings')} />"
      },
      "htmlUsage": "<kg-menu-row icon=\"settings\" label=\"Настройки\"></kg-menu-row>"
    },
    {
      "id": "publish-row",
      "name": "PublishRow",
      "group": "Продуктовые",
      "tag": "kg-publish-row",
      "summary": "Строка карточки на экране публикации: миниатюра, имя, редкость, переключатель «Показать». У выбранной — чип режима места (открывает A15) или «Метки не будет», если места нет. Скрытая модератором видна, но выбрать её нельзя.",
      "props": [
        {
          "name": "name",
          "type": "string",
          "default": "",
          "description": "Имя кота."
        },
        {
          "name": "photo",
          "type": "string (url)",
          "default": "",
          "description": "Путь к PNG кота без фона."
        },
        {
          "name": "photo-width",
          "type": "number",
          "default": "",
          "description": "Исходная ширина файла (px) — для резервирования места."
        },
        {
          "name": "photo-height",
          "type": "number",
          "default": "",
          "description": "Исходная высота файла (px)."
        },
        {
          "name": "focus",
          "type": "string",
          "default": "",
          "description": "Кадр фото, обрезанный до квадрата."
        },
        {
          "name": "rarity",
          "type": "enum",
          "default": "pebble",
          "description": "Уровень редкости. Номеров нет — только названия.",
          "options": [
            "pebble",
            "jade",
            "sapphire",
            "secret"
          ]
        },
        {
          "name": "checked",
          "type": "boolean",
          "default": "false",
          "description": "Карточка выбрана для публикации."
        },
        {
          "name": "mode",
          "type": "enum",
          "default": "",
          "description": "Режим места.",
          "options": [
            "exact",
            "approx",
            "nopin"
          ]
        },
        {
          "name": "geo",
          "type": "enum",
          "default": "exact",
          "description": "Какое место сохранено.",
          "options": [
            "exact",
            "approx",
            "none"
          ]
        },
        {
          "name": "moderated",
          "type": "boolean",
          "default": "false",
          "description": "Скрыта модератором — переключатель недоступен."
        },
        {
          "name": "needs-mode",
          "type": "boolean",
          "default": "false",
          "description": "Выбрана без режима места — подсветка."
        },
        {
          "name": "name-error",
          "type": "boolean",
          "default": "false",
          "description": "Имя не прошло фильтр — ошибка и «Переименовать»."
        },
        {
          "name": "disabled",
          "type": "boolean",
          "default": "false",
          "description": "Всё недоступно (запрет публикации, нет сети, сохранение)."
        },
        {
          "name": "highlight",
          "type": "boolean",
          "default": "false",
          "description": "Фокус: открыли из карточки (A10)."
        }
      ],
      "events": [
        {
          "name": "kg-toggle",
          "detail": "{ checked }",
          "description": "Переключатель «Показать»."
        },
        {
          "name": "kg-mode",
          "detail": "—",
          "description": "Тап по чипу режима места → A15."
        },
        {
          "name": "kg-rename",
          "detail": "—",
          "description": "«Переименовать» у ошибки имени."
        }
      ],
      "variants": null,
      "states": [
        {
          "label": "Выбрана, режима нет",
          "attrs": {
            "checked": "",
            "needs-mode": ""
          }
        },
        {
          "label": "Выбрана, точно",
          "attrs": {
            "checked": "",
            "mode": "exact"
          }
        },
        {
          "label": "Без места",
          "attrs": {
            "checked": "",
            "geo": "none"
          }
        },
        {
          "label": "Скрыта модератором",
          "attrs": {
            "moderated": ""
          }
        },
        {
          "label": "Ошибка имени",
          "attrs": {
            "checked": "",
            "mode": "nopin",
            "name-error": ""
          }
        }
      ],
      "tokens": [
        "--kg-surface",
        "--kg-warning",
        "--kg-warning-soft",
        "--kg-error",
        "--kg-brand",
        "--kg-radius-pill",
        "--kg-touch-min"
      ],
      "a11y": [
        "Переключатель role=\"switch\" с именем «Показать: имя кота»; подпись-имя связана с ним.",
        "Ошибка имени — role=\"alert\", без красного имени: красный только текст ошибки."
      ],
      "rn": {
        "component": "KgPublishRow",
        "notes": [
          "Switch + Pressable-чип; minHeight 48."
        ],
        "usage": "<KgPublishRow cat={cat} checked={sel} mode={mode} onToggle={toggle} onMode={openA15} />"
      },
      "htmlUsage": "<kg-publish-row name=\"Пончик\" photo=\"img/cat-eur.png\" photo-width=\"557\" photo-height=\"824\" rarity=\"sapphire\" checked mode=\"exact\"></kg-publish-row>"
    },
    {
      "id": "precision-option",
      "name": "PrecisionOption",
      "group": "Продуктовые",
      "tag": "kg-precision-option",
      "summary": "Вариант точности места в шторке A15: радио, название, пояснение и мини-превью того, что увидят другие, — точка (точное) или точка с кругом (примерное). По умолчанию ничего не выбрано.",
      "props": [
        {
          "name": "mode",
          "type": "enum",
          "default": "exact",
          "description": "Режим.",
          "options": [
            "exact",
            "approx"
          ]
        },
        {
          "name": "radius",
          "type": "number (м)",
          "default": "300",
          "description": "Радиус круга примерного места; задаёт backend."
        },
        {
          "name": "selected",
          "type": "boolean",
          "default": "false",
          "description": "Выбран. Ставит экран — значения по умолчанию нет."
        },
        {
          "name": "disabled",
          "type": "boolean",
          "default": "false",
          "description": "Недоступен (например, точное при примерном источнике)."
        },
        {
          "name": "reason",
          "type": "string",
          "default": "",
          "description": "Причина недоступности — показывается при disabled."
        },
        {
          "name": "current",
          "type": "boolean",
          "default": "false",
          "description": "Этот режим опубликован сейчас (изменение опубликованного)."
        },
        {
          "name": "name",
          "type": "string",
          "default": "kg-precision",
          "description": "Имя радиогруппы."
        }
      ],
      "events": [
        {
          "name": "kg-select",
          "detail": "{ mode, name }",
          "description": "Выбран вариант; остальные варианты группы снимаются."
        }
      ],
      "variants": {
        "prop": "mode",
        "values": [
          "exact",
          "approx"
        ]
      },
      "states": [
        {
          "label": "Не выбран",
          "attrs": {}
        },
        {
          "label": "Выбран",
          "attrs": {
            "selected": ""
          }
        },
        {
          "label": "Недоступен",
          "attrs": {
            "disabled": "",
            "reason": {
              "ru": "Устройство дало только примерное место",
              "en": "The device gave only an approximate place",
              "th": "อุปกรณ์ให้เพียงตำแหน่งโดยประมาณ"
            }
          }
        },
        {
          "label": "Сейчас опубликовано",
          "attrs": {
            "current": "",
            "selected": ""
          }
        }
      ],
      "tokens": [
        "--kg-surface",
        "--kg-outline",
        "--kg-brand",
        "--kg-success",
        "--kg-radius-md",
        "--kg-touch-min"
      ],
      "a11y": [
        "Нативный input type=\"radio\" внутри label: вся строка — зона нажатия.",
        "Превью — role=\"img\" с подписью «Так увидят другие: …».",
        "Недоступность объясняется текстом, а не только приглушением."
      ],
      "rn": {
        "component": "KgPrecisionOption",
        "notes": [
          "Pressable + accessibilityRole=\"radio\", accessibilityState { checked, disabled }.",
          "Превью — статичная SVG, без карты."
        ],
        "usage": "<KgPrecisionOption mode=\"approx\" radius={300} selected={pick === 'approx'} onSelect={setPick} />"
      },
      "htmlUsage": "<kg-precision-option mode=\"approx\" radius=\"300\"></kg-precision-option>"
    },
    {
      "id": "publish-summary",
      "name": "PublishSummary",
      "group": "Продуктовые",
      "tag": "kg-publish-summary",
      "summary": "Сводка перед публикацией (A16): сколько карточек станет публичными и сколько из них с точным местом, примерным и без места. Числа считает экран.",
      "props": [
        {
          "name": "total",
          "type": "number",
          "default": "exact+approx+none",
          "description": "Всего карточек."
        },
        {
          "name": "exact",
          "type": "number",
          "default": "0",
          "description": "С точным местом."
        },
        {
          "name": "approx",
          "type": "number",
          "default": "0",
          "description": "С примерным местом."
        },
        {
          "name": "none",
          "type": "number",
          "default": "0",
          "description": "Без места — только по ссылке."
        },
        {
          "name": "radius",
          "type": "number (м)",
          "default": "300",
          "description": "Радиус примерного места."
        }
      ],
      "events": [],
      "variants": null,
      "states": [
        {
          "label": "Смешанная",
          "attrs": {
            "exact": "2",
            "approx": "1",
            "none": "1"
          }
        },
        {
          "label": "Без точных",
          "attrs": {
            "exact": "0",
            "approx": "2",
            "none": "1"
          }
        }
      ],
      "tokens": [
        "--kg-surface",
        "--kg-line",
        "--kg-ink-2",
        "--kg-font-display"
      ],
      "a11y": [
        "section с подписью; строки — список, нулевые строки приглушены, но остаются в списке."
      ],
      "rn": {
        "component": "KgPublishSummary",
        "notes": [
          "Числа — tabular-nums."
        ],
        "usage": "<KgPublishSummary exact={2} approx={1} none={1} />"
      },
      "htmlUsage": "<kg-publish-summary exact=\"2\" approx=\"1\" none=\"1\"></kg-publish-summary>"
    },
    {
      "id": "toast",
      "name": "Toast",
      "group": "Каркас",
      "tag": null,
      "api": "KG.toast(text, { tone })",
      "summary": "Короткое уведомление внизу экрана, 3,2 с. Это функция, а не тег: вызывайте KG.toast().",
      "props": [
        {
          "name": "text",
          "type": "string",
          "default": "",
          "description": "Текст (1–2 строки)."
        },
        {
          "name": "tone",
          "type": "enum",
          "default": "info",
          "description": "Тон.",
          "options": [
            "info",
            "success",
            "warn"
          ]
        },
        {
          "name": "duration",
          "type": "number (ms)",
          "default": "3200",
          "description": "Время показа."
        },
        {
          "name": "host",
          "type": "Element",
          "default": "body",
          "description": "Контейнер с position (экран телефона): тост внутри него."
        }
      ],
      "events": [],
      "states": [],
      "tokens": [
        "--kg-cocoa-900",
        "--kg-shadow-3",
        "--kg-dur-slow",
        "--kg-exit-ratio"
      ],
      "a11y": [
        "Регион aria-live=\"polite\".",
        "Не используйте тост для ошибок, требующих действия — нужен Notice."
      ],
      "rn": {
        "component": "KgToast (provider)",
        "notes": [
          "Над tabBar, учитывает safe-area."
        ],
        "usage": "const toast = useKgToast();\ntoast.show(t('toast.saved'), { tone: 'success' });"
      },
      "htmlUsage": "KG.toast(text, { tone })"
    },
    {
      "id": "cat-card",
      "name": "CatCard",
      "group": "Продуктовые",
      "tag": "kg-cat-card",
      "summary": "Карточка кота: стикер, редкость, имя (самый крупный текст, цвет чернил), дата, район th + RTGS, геостатус, отметка повтора. public — чужая карточка: автор и постоянная кнопка «Пожаловаться».",
      "props": [
        {
          "name": "name",
          "type": "string",
          "default": "",
          "description": "Имя кота, до 30 символов. Переносится, не обрезается."
        },
        {
          "name": "photo",
          "type": "string (url)",
          "default": "",
          "description": "Путь к PNG кота без фона."
        },
        {
          "name": "photo-width",
          "type": "number",
          "default": "",
          "description": "Исходная ширина файла (px) — для резервирования места."
        },
        {
          "name": "photo-height",
          "type": "number",
          "default": "",
          "description": "Исходная высота файла (px)."
        },
        {
          "name": "photo-alt",
          "type": "string",
          "default": "name",
          "description": "Описание фото."
        },
        {
          "name": "rarity",
          "type": "enum",
          "default": "pebble",
          "description": "Уровень редкости. Номеров нет — только названия.",
          "options": [
            "pebble",
            "jade",
            "sapphire",
            "secret"
          ]
        },
        {
          "name": "date",
          "type": "string",
          "default": "",
          "description": "Дата и время, уже отформатированные под локаль."
        },
        {
          "name": "district-th",
          "type": "string",
          "default": "",
          "description": "Район по-тайски."
        },
        {
          "name": "district-latn",
          "type": "string",
          "default": "",
          "description": "Район латиницей (RTGS)."
        },
        {
          "name": "geo",
          "type": "enum",
          "default": "approx",
          "description": "Точность геоданных; none — без плашки.",
          "options": [
            "exact",
            "approx",
            "nogeo",
            "none"
          ]
        },
        {
          "name": "duplicate",
          "type": "boolean",
          "default": "false",
          "description": "Отметка «Возможный повтор»."
        },
        {
          "name": "public",
          "type": "boolean",
          "default": "false",
          "description": "Чужая карточка."
        },
        {
          "name": "author",
          "type": "string",
          "default": "",
          "description": "Ник автора (public)."
        },
        {
          "name": "live",
          "type": "boolean",
          "default": "false",
          "description": "Живой holo у secret."
        },
        {
          "name": "editable",
          "type": "boolean",
          "default": "false",
          "description": "Карандаш рядом с именем (kg-edit) — своя карточка."
        },
        {
          "name": "edit-disabled",
          "type": "boolean",
          "default": "false",
          "description": "Карандаш виден, но недоступен (нет сети)."
        },
        {
          "name": "placeholder",
          "type": "string",
          "default": "Без имени",
          "description": "Текст вместо пустого имени (A7, пока имя не введено)."
        },
        {
          "name": "framed",
          "type": "boolean",
          "default": "false",
          "description": "Рамка цвета уровня (A7, после раскрытия)."
        },
        {
          "name": "compact",
          "type": "boolean",
          "default": "false",
          "description": "Компактный вид в строку — когда открыта клавиатура."
        },
        {
          "name": "badges",
          "type": "string (list)",
          "default": "",
          "description": "Доп. плашки через запятую: public, private, hidden, counted, notcounted, outside, nogeo."
        },
        {
          "name": "no-place",
          "type": "string",
          "default": "",
          "description": "Строка вместо района, когда места нет («Место не сохранено»). Используйте с geo=\"none\"."
        },
        {
          "name": "no-actions",
          "type": "boolean",
          "default": "false",
          "description": "Без кнопок внизу (действия — на экране)."
        }
      ],
      "events": [
        {
          "name": "kg-share",
          "detail": "{ name }",
          "description": "Поделиться."
        },
        {
          "name": "kg-report",
          "detail": "{ name }",
          "description": "Пожаловаться (public)."
        },
        {
          "name": "kg-delete",
          "detail": "{ name }",
          "description": "Удалить (своя) → покажите Dialog."
        },
        {
          "name": "kg-edit",
          "detail": "{ name }",
          "description": "Карандаш у имени → шторка переименования."
        }
      ],
      "variants": {
        "prop": "rarity",
        "values": [
          "pebble",
          "jade",
          "sapphire",
          "secret"
        ]
      },
      "states": [
        {
          "label": "Своя",
          "attrs": {}
        },
        {
          "label": "Публичная",
          "attrs": {
            "public": "",
            "author": "@noi.bkk"
          }
        },
        {
          "label": "Повтор + точная",
          "attrs": {
            "duplicate": "",
            "geo": "exact"
          }
        },
        {
          "label": "Новая (A7)",
          "attrs": {
            "framed": "",
            "no-actions": "",
            "editable": null,
            "name": "",
            "geo": "exact"
          }
        },
        {
          "label": "Своя (A10)",
          "attrs": {
            "editable": "",
            "no-actions": "",
            "badges": "public"
          }
        },
        {
          "label": "Компактная",
          "attrs": {
            "compact": "",
            "no-actions": ""
          }
        }
      ],
      "tokens": [
        "--kg-surface",
        "--kg-outline",
        "--kg-name",
        "--kg-radius-card",
        "--kg-shadow-2",
        "--kg-grain",
        "--kg-diecut-md"
      ],
      "a11y": [
        "article; имя — h3.",
        "Кнопка «Пожаловаться» всегда видима на public."
      ],
      "rn": {
        "component": "KgCatCard",
        "notes": [
          "Имя: lineHeight по письменности имени (тайский 1.45 для display).",
          "Дату форматирует Intl c календарём buddhist для th."
        ],
        "usage": "<KgCatCard cat={cat} isPublic={cat.ownerId !== me.id} onShare={share} onReport={openReport} onDelete={confirmDelete} />"
      },
      "htmlUsage": "<kg-cat-card name=\"Мурка\" photo=\"img/cat-eur.png\" photo-width=\"557\" photo-height=\"824\" rarity=\"sapphire\" date=\"3 октября 2026, 17:42\" district-th=\"คลองเตย\" district-latn=\"Khlong Toei\" geo=\"exact\"></kg-cat-card>"
    },
    {
      "id": "cat-thumb",
      "name": "CatThumb",
      "group": "Продуктовые",
      "tag": "kg-cat-thumb",
      "summary": "Миниатюра в сетке коллекции: стикер целиком в квадрате, бейдж S в углу, имя одной строкой (полное — в карточке). empty — ещё не встреченный слот.",
      "props": [
        {
          "name": "name",
          "type": "string",
          "default": "",
          "description": "Имя (обрезается многоточием)."
        },
        {
          "name": "photo",
          "type": "string (url)",
          "default": "",
          "description": "Путь к PNG кота без фона."
        },
        {
          "name": "photo-width",
          "type": "number",
          "default": "",
          "description": "Исходная ширина файла (px) — для резервирования места."
        },
        {
          "name": "photo-height",
          "type": "number",
          "default": "",
          "description": "Исходная высота файла (px)."
        },
        {
          "name": "rarity",
          "type": "enum",
          "default": "pebble",
          "description": "Уровень редкости. Номеров нет — только названия.",
          "options": [
            "pebble",
            "jade",
            "sapphire",
            "secret"
          ]
        },
        {
          "name": "href",
          "type": "string",
          "default": "#",
          "description": "Ссылка на карточку."
        },
        {
          "name": "empty",
          "type": "boolean",
          "default": "false",
          "description": "Пустой слот."
        },
        {
          "name": "badges",
          "type": "string (list)",
          "default": "",
          "description": "Теги под именем через запятую: public, dup, nogeo, hidden. hidden приглушает изображение."
        },
        {
          "name": "focus",
          "type": "string",
          "default": "",
          "description": "Если задан — показывать кадр снимка, обрезанный до квадрата, с этой точкой фокуса («50% 40%»)."
        },
        {
          "name": "rarity-label",
          "type": "boolean",
          "default": "false",
          "description": "Подпись с названием уровня под именем."
        }
      ],
      "events": [
        {
          "name": "click",
          "detail": "—",
          "description": "Переход по href."
        }
      ],
      "variants": {
        "prop": "rarity",
        "values": [
          "pebble",
          "jade",
          "sapphire",
          "secret"
        ]
      },
      "states": [
        {
          "label": "Пустой слот",
          "attrs": {
            "empty": ""
          }
        },
        {
          "label": "С тегами",
          "attrs": {
            "badges": "public,dup",
            "rarity-label": ""
          }
        },
        {
          "label": "Скрыта",
          "attrs": {
            "badges": "hidden",
            "rarity-label": ""
          }
        }
      ],
      "tokens": [
        "--kg-radius-thumb",
        "--kg-diecut-sm",
        "--kg-name",
        "--kg-line"
      ],
      "a11y": [
        "Ссылка; alt = имя кота.",
        "Только статичные изображения — без шейдеров."
      ],
      "rn": {
        "component": "KgCatThumb",
        "notes": [
          "FlatList numColumns=3, gap 12; изображения заранее уменьшены."
        ],
        "usage": "<FlatList numColumns={3} data={cats} renderItem={({item}) => <KgCatThumb cat={item} onPress={open} />} />"
      },
      "htmlUsage": "<kg-cat-thumb name=\"Мурка\" photo=\"img/cat-eur.png\" photo-width=\"557\" photo-height=\"824\" rarity=\"jade\"></kg-cat-thumb>"
    },
    {
      "id": "cat-row",
      "name": "CatRow",
      "group": "Продуктовые",
      "tag": "kg-cat-row",
      "summary": "Строка списка: миниатюра 56, имя (обрезается), район, бейдж S, шеврон.",
      "props": [
        {
          "name": "name",
          "type": "string",
          "default": "",
          "description": "Имя."
        },
        {
          "name": "photo",
          "type": "string (url)",
          "default": "",
          "description": "Путь к PNG кота без фона."
        },
        {
          "name": "photo-width",
          "type": "number",
          "default": "",
          "description": "Исходная ширина файла (px) — для резервирования места."
        },
        {
          "name": "photo-height",
          "type": "number",
          "default": "",
          "description": "Исходная высота файла (px)."
        },
        {
          "name": "rarity",
          "type": "enum",
          "default": "pebble",
          "description": "Уровень редкости. Номеров нет — только названия.",
          "options": [
            "pebble",
            "jade",
            "sapphire",
            "secret"
          ]
        },
        {
          "name": "district-th",
          "type": "string",
          "default": "",
          "description": "Район по-тайски."
        },
        {
          "name": "district-latn",
          "type": "string",
          "default": "",
          "description": "Район латиницей."
        },
        {
          "name": "href",
          "type": "string",
          "default": "#",
          "description": "Ссылка."
        }
      ],
      "events": [
        {
          "name": "click",
          "detail": "—",
          "description": "Переход по href."
        }
      ],
      "variants": {
        "prop": "rarity",
        "values": [
          "pebble",
          "jade",
          "sapphire",
          "secret"
        ]
      },
      "states": [],
      "tokens": [
        "--kg-surface",
        "--kg-surface-sunk",
        "--kg-name",
        "--kg-ink-2",
        "--kg-radius-thumb"
      ],
      "a11y": [
        "Вся строка — одна ссылка ≥ 72 высотой."
      ],
      "rn": {
        "component": "KgCatRow",
        "notes": [
          "Pressable с android_ripple в цвете --kg-surface-sunk."
        ],
        "usage": "<KgCatRow cat={cat} onPress={open} />"
      },
      "htmlUsage": "<kg-cat-row name=\"Баронесса Мурка фон Колбаскина\" photo=\"img/cat-eur.png\" photo-width=\"557\" photo-height=\"824\" rarity=\"sapphire\" district-th=\"คลองเตย\" district-latn=\"Khlong Toei\"></kg-cat-row>"
    },
    {
      "id": "cat-peek",
      "name": "CatPeek",
      "group": "Продуктовые",
      "tag": "kg-cat-peek",
      "summary": "Превью кота поверх карты: миниатюра, имя, район, геостатус, «Пожаловаться», «Открыть карточку». variant=\"sheet\" — шторка метки A18: немодальная, карта и круг примерного места остаются видны; автор, дата, «Коллекция автора», состояния загрузки, «недоступна» и «своя».",
      "props": [
        {
          "name": "name",
          "type": "string",
          "default": "",
          "description": "Имя."
        },
        {
          "name": "photo",
          "type": "string (url)",
          "default": "",
          "description": "Путь к PNG кота без фона."
        },
        {
          "name": "photo-width",
          "type": "number",
          "default": "",
          "description": "Исходная ширина файла (px) — для резервирования места."
        },
        {
          "name": "photo-height",
          "type": "number",
          "default": "",
          "description": "Исходная высота файла (px)."
        },
        {
          "name": "rarity",
          "type": "enum",
          "default": "pebble",
          "description": "Уровень редкости. Номеров нет — только названия.",
          "options": [
            "pebble",
            "jade",
            "sapphire",
            "secret"
          ]
        },
        {
          "name": "district-th",
          "type": "string",
          "default": "",
          "description": "Район по-тайски."
        },
        {
          "name": "district-latn",
          "type": "string",
          "default": "",
          "description": "Район латиницей."
        },
        {
          "name": "geo",
          "type": "enum",
          "default": "approx",
          "description": "Точность.",
          "options": [
            "exact",
            "approx"
          ]
        },
        {
          "name": "href",
          "type": "string",
          "default": "#",
          "description": "Ссылка на карточку."
        },
        {
          "name": "variant",
          "type": "enum",
          "default": "",
          "description": "sheet — шторка метки (A18).",
          "options": [
            "",
            "sheet"
          ]
        },
        {
          "name": "state",
          "type": "enum",
          "default": "",
          "description": "Состояние шторки.",
          "options": [
            "",
            "loading",
            "gone"
          ]
        },
        {
          "name": "author",
          "type": "string",
          "default": "",
          "description": "Никнейм автора (@nick)."
        },
        {
          "name": "date",
          "type": "string",
          "default": "",
          "description": "Дата и время фото."
        },
        {
          "name": "focus",
          "type": "string",
          "default": "",
          "description": "Кадр фото, обрезанный до квадрата."
        },
        {
          "name": "radius",
          "type": "number (м)",
          "default": "300",
          "description": "Радиус примерного места."
        },
        {
          "name": "own",
          "type": "boolean",
          "default": "false",
          "description": "Своя карточка: «Это ваша» и «Открыть карточку» вместо «Пожаловаться»."
        }
      ],
      "events": [
        {
          "name": "kg-report",
          "detail": "{ name }",
          "description": "Пожаловаться → A20."
        },
        {
          "name": "kg-open",
          "detail": "{ name }",
          "description": "Открыть карточку (своя → A10)."
        },
        {
          "name": "kg-author",
          "detail": "{ author }",
          "description": "Коллекция автора → A19."
        },
        {
          "name": "kg-close",
          "detail": "—",
          "description": "Закрыть шторку (✕, Esc)."
        }
      ],
      "variants": {
        "prop": "geo",
        "values": [
          "exact",
          "approx"
        ]
      },
      "states": [
        {
          "label": "Шторка",
          "attrs": {
            "variant": "sheet",
            "author": "@mali.cats",
            "date": {
              "ru": "3 окт, 17:42",
              "en": "Oct 3, 5:42 PM",
              "th": "3 ต.ค. 17:42"
            },
            "geo": "approx"
          }
        },
        {
          "label": "Своя",
          "attrs": {
            "variant": "sheet",
            "own": "",
            "geo": "exact",
            "author": "@noi.bkk"
          }
        },
        {
          "label": "Загрузка",
          "attrs": {
            "variant": "sheet",
            "state": "loading"
          }
        },
        {
          "label": "Недоступна",
          "attrs": {
            "variant": "sheet",
            "state": "gone"
          }
        }
      ],
      "tokens": [
        "--kg-surface",
        "--kg-outline",
        "--kg-radius-card",
        "--kg-shadow-3"
      ],
      "a11y": [
        "Точность — только текстом и иконкой, на самой метке её нет."
      ],
      "rn": {
        "component": "KgCatPeek",
        "notes": [
          "Анимация появления снизу 300 ms; закрытие свайпом вниз."
        ],
        "usage": "<KgCatPeek cat={selected} onReport={openReport} onOpen={openCard} />"
      },
      "htmlUsage": "<kg-cat-peek name=\"Тыква\" photo=\"img/cat-van.png\" photo-width=\"792\" photo-height=\"1375\" rarity=\"secret\" district-th=\"บางรัก\" district-latn=\"Bang Rak\" geo=\"approx\"></kg-cat-peek>"
    },
    {
      "id": "dup-compare",
      "name": "DupCompare",
      "group": "Продуктовые",
      "tag": "kg-dup-compare",
      "summary": "Сравнение нового снимка с карточкой из коллекции (экран повтора). Две плитки рядом: «Новый снимок» (пунктир) и «Уже в коллекции» с именем и редкостью. single — только карточка из коллекции.",
      "props": [
        {
          "name": "single",
          "type": "boolean",
          "default": "false",
          "description": "Показать только карточку из коллекции (явный повтор)."
        },
        {
          "name": "new-photo",
          "type": "string (url)",
          "default": "",
          "description": "Новый снимок."
        },
        {
          "name": "new-photo-width",
          "type": "number",
          "default": "",
          "description": ""
        },
        {
          "name": "new-photo-height",
          "type": "number",
          "default": "",
          "description": ""
        },
        {
          "name": "new-focus",
          "type": "string",
          "default": "50% 50%",
          "description": "Фокус кадрирования плитки."
        },
        {
          "name": "new-date",
          "type": "string",
          "default": "",
          "description": "Когда снято."
        },
        {
          "name": "new-district-th",
          "type": "string",
          "default": "",
          "description": "Район (тайский)."
        },
        {
          "name": "new-district-latn",
          "type": "string",
          "default": "",
          "description": "Район (латиница)."
        },
        {
          "name": "existing-photo",
          "type": "string (url)",
          "default": "",
          "description": "Фото карточки из коллекции."
        },
        {
          "name": "existing-photo-width",
          "type": "number",
          "default": "",
          "description": ""
        },
        {
          "name": "existing-photo-height",
          "type": "number",
          "default": "",
          "description": ""
        },
        {
          "name": "existing-focus",
          "type": "string",
          "default": "50% 50%",
          "description": ""
        },
        {
          "name": "existing-name",
          "type": "string",
          "default": "",
          "description": "Имя кота (никогда не красным)."
        },
        {
          "name": "existing-rarity",
          "type": "enum",
          "default": "pebble",
          "description": "Редкость карточки из коллекции.",
          "options": [
            "pebble",
            "jade",
            "sapphire",
            "secret"
          ]
        },
        {
          "name": "existing-date",
          "type": "string",
          "default": "",
          "description": "Дата встречи."
        },
        {
          "name": "existing-district-th",
          "type": "string",
          "default": "",
          "description": ""
        },
        {
          "name": "existing-district-latn",
          "type": "string",
          "default": "",
          "description": ""
        },
        {
          "name": "more",
          "type": "number",
          "default": "0",
          "description": "Сколько ещё похожих — кнопка «и ещё N похожих» (kg-more)."
        },
        {
          "name": "existing-deleted",
          "type": "boolean",
          "default": "false",
          "description": "Карточку из коллекции удалили — плейсхолдер."
        },
        {
          "name": "compare",
          "type": "boolean",
          "default": "false",
          "description": "Кнопка «Посмотреть» у карточки из коллекции (kg-compare)."
        }
      ],
      "events": [],
      "states": [
        {
          "label": "Явный повтор",
          "attrs": {
            "single": ""
          }
        }
      ],
      "tokens": [
        "--kg-outline",
        "--kg-line-strong",
        "--kg-surface",
        "--kg-radius-card",
        "--kg-name"
      ],
      "a11y": [
        "Подписи колонок — текстом, не только стилем рамки.",
        "Редкость — бейдж с aria-label и видимое название."
      ],
      "rn": {
        "component": "KgDupCompare",
        "notes": [
          "Плитки квадратные, фото кадрируется по точке фокуса из детектора."
        ],
        "usage": "<KgDupCompare newShot={shot} existing={match.card} single={match.kind === 'hard'} />"
      },
      "htmlUsage": "<kg-dup-compare new-photo=\"img/cam-siam-square.jpg\" new-photo-width=\"960\" new-photo-height=\"1280\" new-focus=\"55% 40%\" new-date=\"Сегодня, 9:41\" new-district-th=\"ปทุมวัน\" new-district-latn=\"Pathum Wan\" existing-photo=\"img/cam-siam-square.jpg\" existing-photo-width=\"960\" existing-photo-height=\"1280\" existing-focus=\"62% 22%\" existing-name=\"Шоколадка\" existing-rarity=\"jade\" existing-date=\"2 окт\" existing-district-th=\"ปทุมวัน\" existing-district-latn=\"Pathum Wan\"></kg-dup-compare>"
    },
    {
      "id": "share-card",
      "name": "ShareCard",
      "group": "Продуктовые",
      "tag": "kg-share-card",
      "summary": "Шаблон share-картинки 1080×1920 / 1080×1350 / 1080×1080. Всё важное — в безопасной зоне. Holo запечён. Нет фона, координат и EXIF.",
      "props": [
        {
          "name": "date",
          "type": "string",
          "default": "",
          "description": "Дата встречи вместо района — для картинки без места (A11)."
        },
        {
          "name": "generating",
          "type": "boolean",
          "default": "false",
          "description": "Картинка собирается: мерцание и лоадер той же формы."
        },
        {
          "name": "generating-label",
          "type": "string",
          "default": "Собираем картинку…",
          "description": ""
        },
        {
          "name": "format",
          "type": "enum",
          "default": "story",
          "description": "Формат.",
          "options": [
            "story",
            "feed",
            "square"
          ]
        },
        {
          "name": "name",
          "type": "string",
          "default": "",
          "description": "Имя кота."
        },
        {
          "name": "photo",
          "type": "string (url)",
          "default": "",
          "description": "Путь к PNG кота без фона."
        },
        {
          "name": "photo-width",
          "type": "number",
          "default": "",
          "description": "Исходная ширина файла (px) — для резервирования места."
        },
        {
          "name": "photo-height",
          "type": "number",
          "default": "",
          "description": "Исходная высота файла (px)."
        },
        {
          "name": "rarity",
          "type": "enum",
          "default": "secret",
          "description": "Уровень.",
          "options": [
            "pebble",
            "jade",
            "sapphire",
            "secret"
          ]
        },
        {
          "name": "district-th",
          "type": "string",
          "default": "",
          "description": "Район по-тайски."
        },
        {
          "name": "district-latn",
          "type": "string",
          "default": "",
          "description": "Район латиницей."
        },
        {
          "name": "cta",
          "type": "string",
          "default": "словарь",
          "description": "Призыв внизу."
        },
        {
          "name": "qr",
          "type": "boolean",
          "default": "false",
          "description": "Место под QR."
        },
        {
          "name": "safe-zone",
          "type": "boolean",
          "default": "false",
          "description": "Показать направляющие безопасной зоны (только в дизайне)."
        },
        {
          "name": "width",
          "type": "number",
          "default": "280",
          "description": "Ширина превью, px."
        }
      ],
      "events": [],
      "variants": {
        "prop": "format",
        "values": [
          "story",
          "feed",
          "square"
        ]
      },
      "states": [
        {
          "label": "Безопасная зона",
          "attrs": {
            "safe-zone": ""
          }
        }
      ],
      "tokens": [
        "--kg-paper-50",
        "--kg-cocoa-800",
        "--kg-berry-600",
        "--kg-grain",
        "--kg-diecut-lg"
      ],
      "a11y": [
        "На картинке обязательно есть символ уровня, не только цвет."
      ],
      "rn": {
        "component": "renderShareImage()",
        "notes": [
          "Рендер через Skia в offscreen-surface 1080 px; переносы тайского через ICU (Paragraph API).",
          "Безопасная зона story: top 14 %, bottom 20 %, бока 6 %.",
          "Шеринг PNG через системный share sheet (LINE не принимает картинки по URL)."
        ],
        "usage": "const png = await renderShareImage({ format: 'story', cat, rarity: cat.rarity });\nawait Share.open({ url: png });"
      },
      "htmlUsage": "<kg-share-card format=\"story\" name=\"Тыква\" photo=\"img/cat-van.png\" photo-width=\"792\" photo-height=\"1375\" rarity=\"secret\" district-th=\"บางรัก\" district-latn=\"Bang Rak\" qr safe-zone width=\"240\"></kg-share-card>"
    },
    {
      "id": "quest-card",
      "name": "QuestCard",
      "group": "Продуктовые",
      "tag": "kg-quest-card",
      "summary": "Недельное задание. Две отдельные шкалы (коты и дни), период, строка «Что засчитывается». Числа приходят с backend — компонент не пересчитывает. Состояния: активно, выполнено, скоро, ошибка с «Повторить», загрузка. Цвет дня — в шкалах и точке, не фоном.",
      "props": [
        {
          "name": "status",
          "type": "enum",
          "default": "active",
          "description": "Состояние блока.",
          "options": [
            "active",
            "done",
            "upcoming",
            "ended",
            "error",
            "loading"
          ]
        },
        {
          "name": "last-day",
          "type": "boolean",
          "default": "false",
          "description": "Последний день периода: плашка и акцент на сроке."
        },
        {
          "name": "heading",
          "type": "string",
          "default": "",
          "description": "Название задания (для upcoming — заголовок вместо «Новое задание скоро»)."
        },
        {
          "name": "done",
          "type": "number",
          "default": "0",
          "description": "Котов засчитано (с backend)."
        },
        {
          "name": "total",
          "type": "number",
          "default": "3",
          "description": "Котов нужно (≤ 9)."
        },
        {
          "name": "days-done",
          "type": "number",
          "default": "0",
          "description": "Дней со встречами (с backend)."
        },
        {
          "name": "days-total",
          "type": "number",
          "default": "",
          "description": "Дней нужно. Если задано — вторая шкала вместо слотов."
        },
        {
          "name": "period",
          "type": "string",
          "default": "",
          "description": "Даты задания."
        },
        {
          "name": "due",
          "type": "string",
          "default": "",
          "description": "Сколько осталось."
        },
        {
          "name": "counts",
          "type": "string",
          "default": "словарь",
          "description": "Что засчитывается (показывается при days-total)."
        },
        {
          "name": "starts",
          "type": "string",
          "default": "",
          "description": "Дата начала для upcoming; без неё — только «скоро»."
        },
        {
          "name": "stale",
          "type": "string",
          "default": "",
          "description": "Время последних данных; показывает плашку «Могли устареть»."
        },
        {
          "name": "retrying",
          "type": "boolean",
          "default": "false",
          "description": "error: повтор выполняется."
        },
        {
          "name": "error-text",
          "type": "string",
          "default": "словарь",
          "description": "error: пояснение."
        },
        {
          "name": "hide-cta",
          "type": "boolean",
          "default": "false",
          "description": "Скрыть CTA (когда главная кнопка есть на экране)."
        },
        {
          "name": "cta-label",
          "type": "string",
          "default": "Встретить кота",
          "description": "Кнопка."
        },
        {
          "name": "kicker",
          "type": "string",
          "default": "Задание недели",
          "description": "Надзаголовок."
        },
        {
          "name": "tz-note",
          "type": "boolean",
          "default": "false",
          "description": "Строка «Дни считаются по времени Бангкока (UTC+7)» под периодом (A12)."
        },
        {
          "name": "delta",
          "type": "number",
          "default": "0",
          "description": "Приращение котов от последней карточки — «(+1)» у шкалы (A8)."
        },
        {
          "name": "days-delta",
          "type": "number",
          "default": "0",
          "description": "Приращение дней."
        }
      ],
      "events": [
        {
          "name": "kg-action",
          "detail": "—",
          "description": "Нажатие CTA."
        },
        {
          "name": "kg-retry",
          "detail": "—",
          "description": "error: «Повторить» — перезапросить только задание."
        }
      ],
      "variants": {
        "prop": "status",
        "values": [
          "active",
          "done",
          "upcoming",
          "error",
          "loading"
        ]
      },
      "states": [
        {
          "label": "Новый игрок",
          "attrs": {
            "done": "0",
            "days-done": "0"
          }
        },
        {
          "label": "Выполнено",
          "attrs": {
            "status": "done",
            "done": "3",
            "days-done": "2"
          }
        },
        {
          "label": "Нет сети",
          "attrs": {
            "stale": "9:12"
          }
        },
        {
          "label": "Повтор",
          "attrs": {
            "status": "error",
            "retrying": ""
          }
        },
        {
          "label": "Приращение",
          "attrs": {
            "delta": "1",
            "days-delta": "1"
          }
        }
      ],
      "tokens": [
        "--kg-day-*",
        "--kg-success",
        "--kg-warning",
        "--kg-outline",
        "--kg-surface",
        "--kg-surface-muted",
        "--kg-radius-card",
        "--kg-shadow-2"
      ],
      "a11y": [
        "Каждая шкала — role=\"progressbar\" со своей подписью и числом; не только цвет.",
        "Ошибка — role=\"status\", кнопка «Повторить» с aria-busy во время запроса.",
        "Даты и числа не переносятся (nowrap)."
      ],
      "rn": {
        "component": "KgQuestCard",
        "notes": [
          "Цвет дня — по дню недели устройства, токены dayColors.",
          "Прогресс — только из ответа API, без локального пересчёта.",
          "weekly_quest_completed отправляет экран, а не компонент."
        ],
        "usage": "<KgQuestCard status={q.status} heading={q.title} done={q.cats.done} total={q.cats.total} daysDone={q.days.done} daysTotal={q.days.total} period={periodText} due={leftText} hideCta onRetry={refetchQuest} />"
      },
      "htmlUsage": "<kg-quest-card heading=\"Поймай 3 разных котов за 2 дня в Бангкоке\" done=\"2\" total=\"3\" days-done=\"1\" days-total=\"2\" period=\"29 сен — 5 окт\" due=\"ещё 2 дня\" hide-cta></kg-quest-card>"
    },
    {
      "id": "collection-stats",
      "name": "CollectionStats",
      "group": "Продуктовые",
      "tag": "kg-collection-stats",
      "summary": "Статистика коллекции на главном: всего карточек и разбивка по четырём уровням (бейдж + название + число, без номеров уровней), ссылка «Вся коллекция». Пустая коллекция — «Поймай первого кота» без отдельной кнопки.",
      "props": [
        {
          "name": "total",
          "type": "number",
          "default": "0",
          "description": "Всего карточек. 0 — пустое состояние."
        },
        {
          "name": "pebble",
          "type": "number",
          "default": "0",
          "description": "Галька."
        },
        {
          "name": "jade",
          "type": "number",
          "default": "0",
          "description": "Нефрит."
        },
        {
          "name": "sapphire",
          "type": "number",
          "default": "0",
          "description": "Сапфир."
        },
        {
          "name": "secret",
          "type": "number",
          "default": "0",
          "description": "Секрет."
        },
        {
          "name": "href",
          "type": "string",
          "default": "#",
          "description": "Ссылка «Вся коллекция»."
        },
        {
          "name": "loading",
          "type": "boolean",
          "default": "false",
          "description": "Скелетон той же формы."
        },
        {
          "name": "error",
          "type": "boolean",
          "default": "false",
          "description": "Ошибка загрузки статистики с «Повторить» (kg-retry)."
        },
        {
          "name": "retrying",
          "type": "boolean",
          "default": "false",
          "description": "error: повтор идёт."
        },
        {
          "name": "offline",
          "type": "boolean",
          "default": "false",
          "description": "Нет сети и нет кэша."
        },
        {
          "name": "stale",
          "type": "string",
          "default": "",
          "description": "Время последних данных — плашка «Могли устареть»."
        },
        {
          "name": "heading",
          "type": "string",
          "default": "Моя коллекция",
          "description": "Заголовок."
        },
        {
          "name": "empty-photo",
          "type": "string (url)",
          "default": "img/cat-eur.png",
          "description": "Кот-стикер в пустом состоянии."
        },
        {
          "name": "empty-photo-width",
          "type": "number",
          "default": "557",
          "description": ""
        },
        {
          "name": "empty-photo-height",
          "type": "number",
          "default": "824",
          "description": ""
        }
      ],
      "events": [
        {
          "name": "kg-open",
          "detail": "—",
          "description": "Нажатие «Вся коллекция»."
        }
      ],
      "states": [
        {
          "label": "Загрузка",
          "attrs": {
            "loading": ""
          }
        },
        {
          "label": "Пусто",
          "attrs": {
            "total": "0"
          }
        },
        {
          "label": "Нет сети",
          "attrs": {
            "stale": "9:12"
          }
        }
      ],
      "tokens": [
        "--kg-r1..r4-*",
        "--kg-surface",
        "--kg-surface-muted",
        "--kg-brand-text",
        "--kg-font-display"
      ],
      "a11y": [
        "Уровень — бейдж с aria-label «Редкость: …» плюс видимое название; не только цвет.",
        "Числа tabular-nums, не переносятся."
      ],
      "rn": {
        "component": "KgCollectionStats",
        "notes": [
          "Счётчики — из API коллекции; при офлайне — кэш + stale."
        ],
        "usage": "<KgCollectionStats total={s.total} byTier={s.byTier} onOpen={() => nav.navigate('Collection')} />"
      },
      "htmlUsage": "<kg-collection-stats total=\"14\" pebble=\"8\" jade=\"4\" sapphire=\"1\" secret=\"1\"></kg-collection-stats>"
    },
    {
      "id": "reveal",
      "name": "Reveal",
      "group": "Продуктовые",
      "tag": "kg-reveal",
      "summary": "Момент встречи: сначала свечение цвета уровня, затем кот. Всегда можно пропустить. Reduce Motion → смена прозрачности 200 ms.",
      "props": [
        {
          "name": "name",
          "type": "string",
          "default": "",
          "description": "Имя кота."
        },
        {
          "name": "photo",
          "type": "string (url)",
          "default": "",
          "description": "Путь к PNG кота без фона."
        },
        {
          "name": "photo-width",
          "type": "number",
          "default": "",
          "description": "Исходная ширина файла (px) — для резервирования места."
        },
        {
          "name": "photo-height",
          "type": "number",
          "default": "",
          "description": "Исходная высота файла (px)."
        },
        {
          "name": "tier",
          "type": "enum",
          "default": "pebble",
          "description": "Уровень.",
          "options": [
            "pebble",
            "jade",
            "sapphire",
            "secret"
          ]
        },
        {
          "name": "autoplay",
          "type": "boolean",
          "default": "false",
          "description": "Проиграть при появлении и смене пропсов."
        },
        {
          "name": "revealed",
          "type": "boolean",
          "default": "false",
          "description": "Сразу финальный кадр (после «Пропустить анимацию», при возврате в приложение)."
        },
        {
          "name": "skip-label",
          "type": "string",
          "default": "Пропустить",
          "description": "Текст кнопки пропуска."
        }
      ],
      "methods": [
        "play(tier?)",
        "skip()"
      ],
      "events": [
        {
          "name": "kg-reveal",
          "detail": "{ tier }",
          "description": "Старт."
        },
        {
          "name": "kg-skip",
          "detail": "—",
          "description": "Пропуск."
        }
      ],
      "variants": null,
      "states": [],
      "tokens": [
        "--kg-reveal-1-glow … --kg-reveal-4-total",
        "--kg-reveal-reduced",
        "--kg-ease-sticker",
        "--kg-holo"
      ],
      "a11y": [
        "aria-live: имя и уровень объявляются после показа.",
        "Тактильная отдача: Light → Medium → Heavy → Heavy+Success."
      ],
      "rn": {
        "component": "KgReveal",
        "notes": [
          "Skia + Reanimated; шейдер holo только здесь и в детальной карточке.",
          "expo-haptics в try/catch с фолбэком Vibration.",
          "Звук с учётом беззвучного режима."
        ],
        "usage": "<KgReveal cat={cat} tier={cat.rarity} onDone={goToNaming} />"
      },
      "htmlUsage": "<kg-reveal name=\"Мурка\" photo=\"img/cat-eur.png\" photo-width=\"557\" photo-height=\"824\" tier=\"sapphire\" autoplay></kg-reveal>"
    },
    {
      "id": "camera",
      "name": "CameraOverlay",
      "group": "Продуктовые",
      "tag": "kg-camera",
      "summary": "Камера: полноэкранный предпросмотр, рамки вокруг котов, плашка статуса с конкретной подсказкой, крестик и кнопка съёмки. Кнопка съёмки активна только в «Кот найден» (ровно один кот, качество в порядке). Галереи и импорта нет.",
      "props": [
        {
          "name": "state",
          "type": "enum",
          "default": "none",
          "description": "Состояние.",
          "options": [
            "init",
            "none",
            "found",
            "many",
            "poor",
            "offline",
            "captured",
            "interrupted",
            "capture-failed"
          ]
        },
        {
          "name": "reason",
          "type": "enum",
          "default": "",
          "description": "poor: конкретная причина.",
          "options": [
            "dark",
            "blur",
            "far",
            "cropped"
          ]
        },
        {
          "name": "live",
          "type": "boolean",
          "default": "false",
          "description": "Резкий предпросмотр (кадр масштабируется как cover вокруг фокуса)."
        },
        {
          "name": "boxes",
          "type": "string",
          "default": "",
          "description": "Рамки вокруг котов в % кадра: \"x,y,w,h;x,y,w,h\"."
        },
        {
          "name": "focus-x",
          "type": "number",
          "default": "0.5",
          "description": "Фокус кадра по горизонтали, 0…1."
        },
        {
          "name": "focus-y",
          "type": "number",
          "default": "0.5",
          "description": "Фокус по вертикали, 0…1."
        },
        {
          "name": "zoom",
          "type": "number",
          "default": "1",
          "description": "Увеличение кадра (≥ 1)."
        },
        {
          "name": "safety",
          "type": "boolean",
          "default": "false",
          "description": "Напоминание о безопасности на первых захватах; закрывается крестиком."
        },
        {
          "name": "no-collection",
          "type": "boolean",
          "default": "false",
          "description": "Скрыть кнопку коллекции слева."
        },
        {
          "name": "frozen",
          "type": "boolean",
          "default": "false",
          "description": "captured: замерший кадр без вспышки (фон под шторкой)."
        },
        {
          "name": "no-flip",
          "type": "boolean",
          "default": "false",
          "description": "Без кнопки смены камеры (MVP: только задняя)."
        },
        {
          "name": "torch",
          "type": "enum",
          "default": "",
          "description": "Фонарик вместо вспышки: off | on (опционально для MVP).",
          "options": [
            "off",
            "on"
          ]
        },
        {
          "name": "alert",
          "type": "enum",
          "default": "",
          "description": "Предупреждение над статусом.",
          "options": [
            "heat",
            "battery"
          ]
        },
        {
          "name": "hint",
          "type": "string",
          "default": "этикет",
          "description": "Подсказка сверху (если нет safety); пустая строка — скрыть."
        },
        {
          "name": "feed",
          "type": "string (url)",
          "default": "",
          "description": "Кадр предпросмотра (в продукте — превью камеры)."
        },
        {
          "name": "feed-width",
          "type": "number",
          "default": "",
          "description": "Ширина кадра."
        },
        {
          "name": "feed-height",
          "type": "number",
          "default": "",
          "description": "Высота кадра."
        }
      ],
      "events": [
        {
          "name": "kg-shutter",
          "detail": "—",
          "description": "Снимок (только в found)."
        },
        {
          "name": "kg-close",
          "detail": "—",
          "description": "Закрыть."
        },
        {
          "name": "kg-flash",
          "detail": "—",
          "description": "Вспышка."
        },
        {
          "name": "kg-flip",
          "detail": "—",
          "description": "Сменить камеру."
        },
        {
          "name": "kg-collection",
          "detail": "—",
          "description": "В коллекцию."
        },
        {
          "name": "kg-dismiss",
          "detail": "—",
          "description": "Напоминание о безопасности закрыто."
        }
      ],
      "variants": {
        "prop": "state",
        "values": [
          "init",
          "none",
          "found",
          "many",
          "poor",
          "offline",
          "captured"
        ]
      },
      "states": [
        {
          "label": "Темно",
          "attrs": {
            "state": "poor",
            "reason": "dark"
          }
        },
        {
          "label": "Далеко",
          "attrs": {
            "state": "poor",
            "reason": "far"
          }
        },
        {
          "label": "С напоминанием",
          "attrs": {
            "safety": ""
          }
        }
      ],
      "tokens": [
        "--kg-cam-scrim",
        "--kg-cam-ink",
        "--kg-cam-accent",
        "--kg-cam-warn",
        "--kg-cam-idle",
        "--kg-cam-shutter-ring"
      ],
      "a11y": [
        "Статус — role=\"status\"; кнопка съёмки ссылается на него через aria-describedby, поэтому причина неактивности озвучивается.",
        "Рамки различаются формой: пунктир / толстые углы / зигзаг; не только цветом.",
        "Вспышка снимка отключается при Reduce Motion."
      ],
      "rn": {
        "component": "KgCameraOverlay",
        "notes": [
          "Поверх vision-camera; рамки — из детектора в координатах кадра.",
          "Кнопка съёмки disabled, пока детектор не вернул ровно одного кота с достаточным качеством.",
          "Без сети съёмка недоступна: проверка и сохранение на сервере."
        ],
        "usage": "<KgCameraOverlay state={det.state} reason={det.reason} boxes={det.boxes} safety={captures < 3} onShutter={capture} onClose={goBack} />"
      },
      "htmlUsage": "<kg-camera state=\"found\" live feed=\"img/cam-siam-square.jpg\" feed-width=\"960\" feed-height=\"1280\" boxes=\"25,12,62,78\" focus-x=\"0.55\" no-collection></kg-camera>"
    },
    {
      "id": "map",
      "name": "Map + MapPin",
      "group": "Продуктовые",
      "tag": "kg-map",
      "summary": "Карта с одинаковыми нейтральными метками (форма, цвет и размер одни — точность и владелец не кодируются) и превью выбранного кота в слоте peek. scheme=\"bangkok\" — схема города (Чао Прайя, районы) с видом cx/cy/zoom; круг примерного места; ошибка подложки. Кластер — kg-map-pin count.",
      "props": [
        {
          "name": "height",
          "type": "number",
          "default": "520",
          "description": "Минимальная высота, px."
        },
        {
          "name": "label",
          "type": "string",
          "default": "Карта",
          "description": "Имя региона."
        },
        {
          "name": "scheme",
          "type": "enum",
          "default": "",
          "description": "Схема города.",
          "options": [
            "",
            "bangkok"
          ]
        },
        {
          "name": "cx",
          "type": "number",
          "default": "50",
          "description": "Центр вида по X (мир 0–100)."
        },
        {
          "name": "cy",
          "type": "number",
          "default": "50",
          "description": "Центр вида по Y."
        },
        {
          "name": "zoom",
          "type": "number",
          "default": "1",
          "description": "Масштаб: 1 — весь Бангкок."
        },
        {
          "name": "tiles-error",
          "type": "boolean",
          "default": "false",
          "description": "Подложка не загрузилась: нейтральный фон, метки остаются."
        },
        {
          "name": "area-x",
          "type": "number",
          "default": "",
          "description": "Круг примерного места: X в мире."
        },
        {
          "name": "area-y",
          "type": "number",
          "default": "",
          "description": "Y в мире."
        },
        {
          "name": "area-r",
          "type": "number",
          "default": "1.5",
          "description": "Радиус в единицах мира (100 ≈ 20 км)."
        },
        {
          "name": "radius",
          "type": "number (м)",
          "default": "300",
          "description": "Радиус для подписи круга."
        },
        {
          "name": "<kg-map-pin x y selected label count>",
          "type": "children",
          "default": "",
          "description": "Метки: x, y в % вида; count ≥ 2 — кластер."
        },
        {
          "name": "<… slot=\"peek\">",
          "type": "children",
          "default": "",
          "description": "Превью или шторка метки (kg-cat-peek)."
        }
      ],
      "events": [
        {
          "name": "kg-select",
          "detail": "{ id }",
          "description": "С метки: выбор."
        }
      ],
      "variants": null,
      "states": [
        {
          "label": "Схема Бангкока",
          "attrs": {
            "scheme": "bangkok",
            "zoom": "2",
            "cx": "55",
            "cy": "50"
          }
        },
        {
          "label": "Круг примерного места",
          "attrs": {
            "scheme": "bangkok",
            "zoom": "4",
            "cx": "60",
            "cy": "50",
            "area-x": "60",
            "area-y": "47"
          }
        },
        {
          "label": "Подложка не загрузилась",
          "attrs": {
            "scheme": "bangkok",
            "tiles-error": ""
          }
        }
      ],
      "tokens": [
        "--kg-cocoa-800",
        "--kg-paper-0",
        "--kg-brand"
      ],
      "a11y": [
        "Метки — кнопки с aria-pressed и именем."
      ],
      "rn": {
        "component": "KgMap / KgMapPin",
        "notes": [
          "Google provider или MapLibre + OSM с name:th/en/ru; провайдер не выбран.",
          "Метка — PNG/SVG маркер, одинаковый для всех."
        ],
        "usage": "<KgMap>{cats.map(c => <KgMapPin key={c.id} coordinate={c.coord} selected={c.id === sel} onPress={() => setSel(c.id)} />)}</KgMap>"
      },
      "htmlUsage": "<kg-map height=\"480\"><kg-map-pin x=\"30\" y=\"26\"></kg-map-pin><kg-map-pin x=\"62\" y=\"40\" selected></kg-map-pin><kg-map-pin x=\"44\" y=\"56\"></kg-map-pin><kg-cat-peek slot=\"peek\" name=\"Тыква\" photo=\"img/cat-van.png\" photo-width=\"792\" photo-height=\"1375\" rarity=\"secret\" district-th=\"บางรัก\" district-latn=\"Bang Rak\" geo=\"exact\"></kg-cat-peek></kg-map>"
    },
    {
      "id": "report-sheet",
      "name": "ReportSheet",
      "group": "Продуктовые",
      "tag": "kg-report-sheet",
      "summary": "Жалоба (A20): объект (миниатюра и тип — карточка или коллекция), причина, пояснение со счётчиком (обязательно для части причин), отправка, успех. Срок проверки не обещаем — он не утверждён в PRD. Ошибка и нет сети сохраняют ответы; «объект уже удалён или скрыт» — отдельное состояние.",
      "props": [
        {
          "name": "target-name",
          "type": "string",
          "default": "",
          "description": "Имя карточки или никнейм автора."
        },
        {
          "name": "target-type",
          "type": "enum",
          "default": "card",
          "description": "Объект жалобы.",
          "options": [
            "card",
            "collection"
          ]
        },
        {
          "name": "target-photo",
          "type": "string (url)",
          "default": "",
          "description": "Миниатюра объекта."
        },
        {
          "name": "target-photo-width",
          "type": "number",
          "default": "",
          "description": "Ширина файла."
        },
        {
          "name": "target-photo-height",
          "type": "number",
          "default": "",
          "description": "Высота файла."
        },
        {
          "name": "target-focus",
          "type": "string",
          "default": "",
          "description": "Кадр фото, обрезанный до квадрата."
        },
        {
          "name": "contained",
          "type": "boolean",
          "default": "false",
          "description": "Внутри экрана телефона, как Sheet contained."
        },
        {
          "name": "controlled",
          "type": "boolean",
          "default": "false",
          "description": "Состоянием управляет экран: kg-submit → state."
        },
        {
          "name": "state",
          "type": "enum",
          "default": "",
          "description": "Состояние.",
          "options": [
            "",
            "sending",
            "done",
            "srv",
            "net",
            "gone"
          ]
        },
        {
          "name": "error",
          "type": "enum",
          "default": "",
          "description": "Ошибка проверки (ставится при «Отправить»).",
          "options": [
            "",
            "reason",
            "comment"
          ]
        },
        {
          "name": "reason",
          "type": "enum",
          "default": "",
          "description": "Начальная причина. Список — заглушка, его утверждает PO.",
          "options": [
            "photo",
            "name",
            "privacy",
            "cruelty",
            "spam",
            "other"
          ]
        },
        {
          "name": "comment",
          "type": "string",
          "default": "",
          "description": "Начальное пояснение, до 300 символов. Обязательно для cruelty и other."
        }
      ],
      "methods": [
        "show()",
        "close()",
        "reset()"
      ],
      "events": [
        {
          "name": "kg-submit",
          "detail": "{ reason, comment, target, type }",
          "description": "Отправка после проверки."
        },
        {
          "name": "kg-close",
          "detail": "—",
          "description": "Закрыта (ответы сбрасываются)."
        }
      ],
      "variants": {
        "prop": "target-type",
        "values": [
          "card",
          "collection"
        ]
      },
      "states": [
        {
          "label": "Причина не выбрана",
          "attrs": {
            "error": "reason"
          }
        },
        {
          "label": "Пояснение обязательно",
          "attrs": {
            "reason": "other",
            "error": "comment"
          }
        },
        {
          "label": "Отправка",
          "attrs": {
            "reason": "photo",
            "state": "sending"
          }
        },
        {
          "label": "Нет сети",
          "attrs": {
            "reason": "photo",
            "state": "net"
          }
        },
        {
          "label": "Успех",
          "attrs": {
            "state": "done"
          }
        },
        {
          "label": "Объект недоступен",
          "attrs": {
            "state": "gone"
          }
        }
      ],
      "tokens": [
        "--kg-surface",
        "--kg-scrim",
        "--kg-success",
        "--kg-error",
        "--kg-name"
      ],
      "a11y": [
        "Ошибки показываются при «Отправить», рядом с полем, role=\"alert\"; кнопка не блокируется заранее.",
        "Метка поля говорит, обязательно ли пояснение.",
        "После отправки фокус на заголовке подтверждения."
      ],
      "rn": {
        "component": "KgReportSheet",
        "notes": [
          "Срок модерации в тексте не обещаем, пока он не утверждён (PRD, раздел 8).",
          "Причины и правило «пояснение обязательно» синхронизировать с бэкендом."
        ],
        "usage": "<KgReportSheet ref={report} onSubmit={({reason, comment}) => api.report(card.id, reason, comment)} />"
      },
      "htmlUsage": "<kg-report-sheet contained controlled target-type=\"card\" target-name=\"Тыква\"></kg-report-sheet>"
    },
    {
      "id": "perm-row",
      "name": "PermissionRow",
      "group": "Продуктовые",
      "tag": "kg-perm-row",
      "summary": "Строка разрешения в настройках (A21): камера или геолокация, статус текстом и иконкой, переход в системные настройки. Статус обновляется, когда пользователь возвращается.",
      "props": [
        {
          "name": "kind",
          "type": "enum",
          "default": "camera",
          "description": "Разрешение.",
          "options": [
            "camera",
            "location"
          ]
        },
        {
          "name": "status",
          "type": "enum",
          "default": "never",
          "description": "Статус от ОС.",
          "options": [
            "granted",
            "denied",
            "blocked",
            "never",
            "approx",
            "once"
          ]
        },
        {
          "name": "updated",
          "type": "boolean",
          "default": "false",
          "description": "Подсветка сразу после возврата из настроек."
        },
        {
          "name": "disabled",
          "type": "boolean",
          "default": "false",
          "description": "Недоступна."
        }
      ],
      "events": [
        {
          "name": "kg-action",
          "detail": "{ kind }",
          "description": "Открыть системные настройки приложения."
        }
      ],
      "variants": {
        "prop": "status",
        "values": [
          "granted",
          "denied",
          "blocked",
          "never",
          "approx",
          "once"
        ]
      },
      "states": [
        {
          "label": "Геолокация · примерно",
          "attrs": {
            "kind": "location",
            "status": "approx"
          }
        },
        {
          "label": "Обновлено",
          "attrs": {
            "status": "granted",
            "updated": ""
          }
        }
      ],
      "tokens": [
        "--kg-surface",
        "--kg-success",
        "--kg-warning",
        "--kg-error",
        "--kg-brand-text"
      ],
      "a11y": [
        "Статус — текстом и иконкой, не только цветом.",
        "Вся строка — одна кнопка ≥ 72 px."
      ],
      "rn": {
        "component": "KgPermissionRow",
        "notes": [
          "Статус — из expo-permissions / react-native-permissions при AppState active.",
          "Linking.openSettings()."
        ],
        "usage": "<KgPermissionRow kind=\"location\" status={locStatus} onPress={Linking.openSettings} />"
      },
      "htmlUsage": "<kg-perm-row kind=\"camera\" status=\"granted\"></kg-perm-row>"
    },
    {
      "id": "empty-state",
      "name": "EmptyState",
      "group": "Продуктовые",
      "tag": "kg-empty-state",
      "summary": "Пустое состояние или ошибка: причина + что сделать + действие. Пока нет маскота — кот-стикер с иконкой ситуации.",
      "props": [
        {
          "name": "kind",
          "type": "enum",
          "default": "collection",
          "description": "Сценарий; тексты по умолчанию из словаря.",
          "options": [
            "collection",
            "camera",
            "offline",
            "photo"
          ]
        },
        {
          "name": "heading",
          "type": "string",
          "default": "словарь",
          "description": "Заголовок."
        },
        {
          "name": "text",
          "type": "string",
          "default": "словарь",
          "description": "Пояснение."
        },
        {
          "name": "action-label",
          "type": "string",
          "default": "словарь",
          "description": "Кнопка."
        },
        {
          "name": "icon",
          "type": "icon",
          "default": "по kind",
          "description": "Иконка ситуации на стикере."
        },
        {
          "name": "action-icon",
          "type": "icon",
          "default": "по kind",
          "description": "Иконка кнопки."
        },
        {
          "name": "action-variant",
          "type": "enum",
          "default": "по kind",
          "description": "Вид кнопки.",
          "options": [
            "primary",
            "secondary"
          ]
        },
        {
          "name": "loading",
          "type": "boolean",
          "default": "false",
          "description": "Действие выполняется: спиннер, кнопка недоступна."
        },
        {
          "name": "loading-label",
          "type": "string",
          "default": "Загрузка",
          "description": "Текст кнопки во время загрузки."
        },
        {
          "name": "(content)",
          "type": "children",
          "default": "",
          "description": "Дополнительные действия под кнопкой (например, ghost-ссылка)."
        },
        {
          "name": "photo",
          "type": "string (url)",
          "default": "",
          "description": "Путь к PNG кота без фона."
        },
        {
          "name": "photo-width",
          "type": "number",
          "default": "",
          "description": "Исходная ширина файла (px) — для резервирования места."
        },
        {
          "name": "photo-height",
          "type": "number",
          "default": "",
          "description": "Исходная высота файла (px)."
        }
      ],
      "events": [
        {
          "name": "kg-action",
          "detail": "{ kind }",
          "description": "Нажатие кнопки."
        }
      ],
      "variants": {
        "prop": "kind",
        "values": [
          "collection",
          "camera",
          "offline",
          "photo"
        ]
      },
      "states": [],
      "tokens": [
        "--kg-brand-text",
        "--kg-warning",
        "--kg-outline",
        "--kg-ink-2"
      ],
      "a11y": [
        "Текст ошибок без имён; не только иконка — заголовок и пояснение."
      ],
      "rn": {
        "component": "KgEmptyState",
        "notes": [
          "Заменить кота-стикер на маскота, когда он появится."
        ],
        "usage": "<KgEmptyState kind=\"camera\" onAction={Linking.openSettings} />"
      },
      "htmlUsage": "<kg-empty-state kind=\"collection\"></kg-empty-state>"
    }
  ]
}