{
  "package": "@9klabs/design",
  "version": "0.1.0",
  "styleImport": "@9klabs/design/style.css",
  "rules": [
    "I9kInput names its visual scale prop `uiSize`, not `size`, so the native HTML `size` attribute stays available on the underlying input.",
    "Every component owns its appearance in `<style scoped>` and must not rely on global classes for its look.",
    "Components declare component-local custom properties on their root class and redefine them per size modifier, rather than consuming raw brand tokens for sizing.",
    "Sizes and tones come from the shared `I9kComponentSize` and `I9kTone` types. Do not redeclare those string unions per component.",
    "I9kButton renders a `<button>`, an `<a>`, or a caller-supplied component: pass `to` or `href` for a link, and `link-component=\"RouterLink\"` in Vue Router apps.",
    "I9kIcon renders from the local `src/icons/paths.json` set. Add new icons to that file rather than inlining SVG in a component.",
    "Components emit legacy classes alongside their `i9k-` ones while the website migration is in progress. Do not remove a legacy selector or prop until its migration ledger row is complete.",
    "Import the stylesheet once, at the application entry: `@9klabs/design/style.css`. It is the only CSS a consumer needs.",
    "Any visual change needs checking in light and dark themes and in both LTR and RTL directions."
  ],
  "components": [
    {
      "name": "I9kGrid",
      "section": "layout",
      "summary": "CSS grid layout wrapper with a fixed 1/2/3-column or auto-filling track and a size-driven gap. Collapses to one column on narrow viewports.",
      "props": [
        {
          "name": "as",
          "type": "string | Component",
          "required": false,
          "default": "'div'"
        },
        {
          "name": "columns",
          "type": "1 | 2 | 3 | 'auto'",
          "required": false,
          "default": "1"
        },
        {
          "name": "size",
          "type": "'sm' | 'md' | 'lg'",
          "required": false,
          "default": "'md'"
        }
      ],
      "emits": [],
      "slots": [
        "default"
      ],
      "referencedTypes": {},
      "gotchas": [
        "`size` sets the gap only — it has no effect on column count or track width, so pick columns via `columns`.",
        "`columns=\"auto\"` fills as many 280px-minimum tracks as fit; it is not the same as a fixed column count.",
        "Every multi-column value (2, 3, auto) collapses to a single column at viewports under 768px."
      ],
      "prompt": "Use I9kGrid from @9klabs/design to lay out a set of cards or panels in a responsive grid.\n\nimport { I9kGrid } from '@9klabs/design';\n\nProps:\n- as?: string | Component (default 'div') — the rendered root tag or component.\n- columns?: 1 | 2 | 3 | 'auto' (default 1)\n- size?: 'sm' | 'md' | 'lg' (default 'md') — sets the gap between tracks only; it does not affect column count or track width.\n\nEmits: none.\n\nSlots: default — the grid items. Each direct child occupies one cell; wrap items yourself (e.g. in I9kPanel) if they need their own padding or surface.\n\nColumn behavior: `columns={1|2|3}` renders that many equal-width tracks (`repeat(n, minmax(0, 1fr))`). `columns=\"auto\"` renders `repeat(auto-fill, minmax(280px, 1fr))` — as many equal tracks as fit at a 280px minimum, wrapping to new rows as the container narrows, with no JS breakpoint logic involved. Below a 768px viewport, every multi-column value (2, 3, and 'auto') collapses to a single column; `columns={1}` is already one column and is unaffected.\n\nIMPORTANT: `size` only changes the gap (sm/md/lg spacing tokens) — pick the column count with `columns`, not `size`.\n\nUsage:\n<I9kGrid :columns=\"3\" size=\"md\"><I9kPanel>One</I9kPanel><I9kPanel>Two</I9kPanel><I9kPanel>Three</I9kPanel></I9kGrid>"
    },
    {
      "name": "I9kPageContainer",
      "section": "layout",
      "summary": "Centered, width-capped page wrapper with a size-driven horizontal gutter. Use it once per page as the outermost content wrapper.",
      "props": [
        {
          "name": "as",
          "type": "string | Component",
          "required": false,
          "default": "'div'"
        },
        {
          "name": "size",
          "type": "'sm' | 'md' | 'lg'",
          "required": false,
          "default": "'md'"
        }
      ],
      "emits": [],
      "slots": [
        "default"
      ],
      "referencedTypes": {},
      "gotchas": [
        "`size` only sets the horizontal gutter (padding-inline) — the 1000px max width is fixed regardless of size.",
        "Below 768px viewports the gutter always drops to the `sm` spacing, overriding whatever `size` was passed.",
        "The container does not space its children vertically; group related content yourself."
      ],
      "prompt": "Use I9kPageContainer from @9klabs/design as the outermost wrapper for a page's content.\n\nimport { I9kPageContainer } from '@9klabs/design';\n\nProps:\n- as?: string | Component (default 'div') — the rendered root tag or component.\n- size?: 'sm' | 'md' | 'lg' (default 'md') — sets the inline gutter (padding-inline) only.\n\nEmits: none.\n\nSlots: default — the page content.\n\nBehavior: renders a flex column, max 1000px wide, centered with `margin-inline: auto`, and a `min-height: calc(100vh - 250px)` so short pages still fill the viewport. Below a 768px viewport the width becomes 100% and the gutter is forced to the 'sm' spacing regardless of the `size` prop.\n\nIMPORTANT: `size` only changes the horizontal gutter — it does not change the 1000px max width or add vertical spacing between children. Wrap groups of children yourself (e.g. in I9kCluster or a styled div) if they need gaps.\n\nUsage:\n<I9kPageContainer size=\"md\"><I9kText variant=\"lede\">Welcome</I9kText><I9kPanel size=\"sm\">Page content</I9kPanel></I9kPageContainer>"
    },
    {
      "name": "I9kCluster",
      "section": "layout",
      "summary": "Flex-wrap row that keeps items center-aligned and evenly gapped, wrapping onto new lines instead of overflowing. Use it for groups of buttons, badges, or other inline controls.",
      "props": [
        {
          "name": "as",
          "type": "string | Component",
          "required": false,
          "default": "'div'"
        },
        {
          "name": "size",
          "type": "'sm' | 'md' | 'lg'",
          "required": false,
          "default": "'md'"
        }
      ],
      "emits": [],
      "slots": [
        "default"
      ],
      "referencedTypes": {},
      "gotchas": [
        "`size` sets the gap only — there is no column or breakpoint prop, wrapping is automatic flex-wrap.",
        "Items are center-aligned on the cross axis, not baseline-aligned."
      ],
      "prompt": "Use I9kCluster from @9klabs/design to lay out a horizontal group of items that should wrap instead of overflow.\n\nimport { I9kCluster } from '@9klabs/design';\n\nProps:\n- as?: string | Component (default 'div') — the rendered root tag or component.\n- size?: 'sm' | 'md' | 'lg' (default 'md') — sets the gap between items only.\n\nEmits: none.\n\nSlots: default — the items to cluster.\n\nBehavior: renders `display: flex; flex-wrap: wrap; align-items: center;` with a size-driven gap. Items wrap onto new rows as the container narrows; there is no column count or breakpoint logic to configure.\n\nIMPORTANT: items are vertically centered (`align-items: center`), not baseline-aligned — mixed-height items (e.g. a button next to a badge) line up on their centers, not their text baselines.\n\nUsage:\n<I9kCluster size=\"md\"><I9kButton>Primary action</I9kButton><I9kButton variant=\"link\">Secondary action</I9kButton><I9kBadge variant=\"outline\">Status</I9kBadge></I9kCluster>"
    },
    {
      "name": "I9kPanel",
      "section": "layout",
      "summary": "Bordered, blurred glass surface for grouping content. Use it as the standard card/surface wrapper wherever content needs visual separation from the page background.",
      "props": [
        {
          "name": "as",
          "type": "string | Component",
          "required": false,
          "default": "'div'"
        },
        {
          "name": "size",
          "type": "'sm' | 'md' | 'lg'",
          "required": false,
          "default": "'md'"
        },
        {
          "name": "variant",
          "type": "'default' | 'feature' | 'flat'",
          "required": false,
          "default": "'default'"
        }
      ],
      "emits": [],
      "slots": [
        "default"
      ],
      "referencedTypes": {},
      "gotchas": [
        "`variant=\"flat\"` removes the border, background, and backdrop-filter, keeping only padding and radius.",
        "`size` controls padding and, on the `default`/`flat` variants, border radius — it does not control width, since the panel is only as wide as its container allows.",
        "`variant=\"feature\"` always renders the large border radius, overriding whatever `size` would otherwise set — pick `size` on a feature panel for padding only, not radius."
      ],
      "prompt": "Use I9kPanel from @9klabs/design to wrap content in a bordered surface.\n\nimport { I9kPanel } from '@9klabs/design';\n\nProps:\n- as?: string | Component (default 'div') — the rendered root tag or component.\n- size?: 'sm' | 'md' | 'lg' (default 'md') — sets padding, and border radius on the 'default' and 'flat' variants.\n- variant?: 'default' | 'feature' | 'flat' (default 'default') — sets the border and background treatment.\n\nEmits: none.\n\nSlots: default — the panel content.\n\nBehavior: 'default' renders a 1px border, glass background, and backdrop blur. 'feature' emphasizes the border and background with an accent-tinted gradient for content that should stand out (e.g. a highlighted pricing tier), and always renders with the large border radius, regardless of `size`. 'flat' removes the border, background, and backdrop-filter entirely, leaving only the size-driven padding — useful when you want the padding/radius rhythm without a visible surface, e.g. nested inside another panel.\n\nIMPORTANT: `variant=\"flat\"` strips the border and background — do not combine it with content that depends on the panel having a visible surface.\n\nIMPORTANT: `variant=\"feature\"` hardcodes the large border radius and ignores `size` for radius — `size` on a feature panel changes padding only.\n\nUsage:\n<I9kPanel variant=\"feature\" size=\"lg\"><I9kText variant=\"lede\">Highlighted content</I9kText></I9kPanel>"
    },
    {
      "name": "I9kText",
      "section": "content",
      "summary": "Text primitive for body copy and intros. Use it for any paragraph-level content that should follow the design system type scale rather than reaching for a bare <p>.",
      "props": [
        {
          "name": "as",
          "type": "string | Component",
          "required": false,
          "default": "'p'"
        },
        {
          "name": "size",
          "type": "'sm' | 'md' | 'lg'",
          "required": false,
          "default": "'md'"
        },
        {
          "name": "variant",
          "type": "'body' | 'lede'",
          "required": false,
          "default": "'body'"
        }
      ],
      "emits": [],
      "slots": [
        "default"
      ],
      "referencedTypes": {},
      "gotchas": [
        "`variant=\"lede\"` caps the measure at 62ch and adds bottom margin — meant for intro paragraphs under a heading, not short or already-constrained copy."
      ],
      "prompt": "Use I9kText from @9klabs/design for paragraph-level copy.\n\nimport { I9kText } from '@9klabs/design';\n\nProps:\n- as?: string | Component (default 'p') — the rendered root tag or component.\n- size?: 'sm' | 'md' | 'lg' (default 'md')\n- variant?: 'body' | 'lede' (default 'body')\n\nEmits: none.\n\nSlots: default — the text content.\n\nBehavior: 'lede' sets a wider line height, a quieter color, and caps the measure at 62ch, for the introductory paragraph under a heading. 'body' is normal flowing copy with no measure cap.\n\nIMPORTANT: 'lede' adds bottom margin and a max-width of 62ch — do not use it for short inline copy or content already inside a constrained container, or the extra spacing will look wrong.\n\nUsage:\n<I9kText variant=\"lede\" size=\"lg\">A practical text primitive for branded content.</I9kText>"
    },
    {
      "name": "I9kSectionHeading",
      "section": "content",
      "summary": "Heading with an optional description for introducing a section of a page. Use it above any grouped block of content — a card grid, a list, a feature set.",
      "props": [
        {
          "name": "title",
          "type": "string",
          "required": true,
          "default": null
        },
        {
          "name": "description",
          "type": "string | null",
          "required": false,
          "default": "null"
        },
        {
          "name": "id",
          "type": "string | null",
          "required": false,
          "default": "null"
        },
        {
          "name": "level",
          "type": "2 | 3 | 4 | 5 | 6",
          "required": false,
          "default": "2"
        }
      ],
      "emits": [],
      "slots": [],
      "referencedTypes": {},
      "gotchas": [
        "title and description are plain string props, not slots — there is no way to pass rich markup into the heading or description.",
        "`level` defaults to 2 and is not inferred from context; set it to match the surrounding document outline."
      ],
      "prompt": "Use I9kSectionHeading from @9klabs/design to introduce a page section.\n\nimport { I9kSectionHeading } from '@9klabs/design';\n\nProps:\n- title: string (required)\n- description?: string | null (default null)\n- id?: string | null (default null) — set on the rendered heading element, e.g. for an in-page anchor link.\n- level?: 2 | 3 | 4 | 5 | 6 (default 2) — the rendered heading level (renders <h{level}>).\n\nEmits: none.\n\nSlots: none — title and description are text-only props, not slots.\n\nIMPORTANT: pick `level` to match the surrounding document outline; the component does not infer nesting from context, and the default is h2.\n\nUsage:\n<I9kSectionHeading title=\"Speaking\" description=\"Practical sessions grounded in building, shipping, and leading with AI.\" />"
    },
    {
      "name": "I9kPageHeader",
      "section": "content",
      "summary": "Large hero-style heading for the top of a page, with optional subtitle, description, actions, and avatar. Use it once per page, at the top.",
      "props": [
        {
          "name": "title",
          "type": "string",
          "required": true,
          "default": null
        },
        {
          "name": "description",
          "type": "string | null",
          "required": false,
          "default": "null"
        },
        {
          "name": "id",
          "type": "string | null",
          "required": false,
          "default": "null"
        },
        {
          "name": "level",
          "type": "1 | 2 | 3 | 4 | 5 | 6",
          "required": false,
          "default": "1"
        }
      ],
      "emits": [],
      "slots": [
        "subtitle",
        "actions",
        "avatar"
      ],
      "referencedTypes": {},
      "gotchas": [
        "Reserve I9kPageHeader for the single main heading at the top of a page — use I9kSectionHeading for headings inside the page body.",
        "The side-by-side avatar layout only activates when the `avatar` slot is filled; otherwise the header always stacks in one column."
      ],
      "prompt": "Use I9kPageHeader from @9klabs/design for the hero heading at the top of a page.\n\nimport { I9kPageHeader } from '@9klabs/design';\n\nProps:\n- title: string (required)\n- description?: string | null (default null)\n- id?: string | null (default null) — set on the rendered heading element.\n- level?: 1 | 2 | 3 | 4 | 5 | 6 (default 1) — the rendered heading level (renders <h{level}>).\n\nEmits: none.\n\nSlots:\n- subtitle — rendered directly under the title, above the description.\n- actions — rendered after the description, e.g. for buttons.\n- avatar — when present, switches the header to a side-by-side layout with the avatar next to the title/description/actions block, stacking to centered-column on narrow viewports.\n\nBehavior: the layout only changes shape when the avatar slot is used — with no avatar slot, everything renders as a single stacked column.\n\nIMPORTANT: use I9kPageHeader once per page, at the top — it renders a display-scale title (clamp up to 4rem) meant for the page's main heading, not for section headings (use I9kSectionHeading for those).\n\nUsage:\n<I9kPageHeader title=\"Practical AI from someone who ships.\" description=\"Practical talks for builders and technology teams, grounded in real product work.\"><template #actions><I9kButton variant=\"primary\">Book a session</I9kButton></template></I9kPageHeader>"
    },
    {
      "name": "I9kArticleHeader",
      "section": "content",
      "summary": "Wide banner image for the top of an article, with a generated watermark fallback when there is no image. Use it once, at the top of an article body.",
      "props": [
        {
          "name": "title",
          "type": "string",
          "required": true,
          "default": null
        },
        {
          "name": "imageSrc",
          "type": "string | null",
          "required": false,
          "default": "null"
        },
        {
          "name": "imageAlt",
          "type": "string",
          "required": false,
          "default": "''"
        },
        {
          "name": "watermark",
          "type": "string",
          "required": false,
          "default": "'9k'"
        },
        {
          "name": "eager",
          "type": "boolean",
          "required": false,
          "default": "false"
        }
      ],
      "emits": [],
      "slots": [],
      "referencedTypes": {},
      "gotchas": [
        "`title` only supplies the fallback alt text for the image — it is never rendered as visible text, so pair this with a real heading elsewhere on the page.",
        "With no `imageSrc`, the component renders a decorative gradient watermark, not a loading or empty state — it never fetches anything.",
        "Set `eager` only for an above-the-fold header; the default is lazy loading with normal fetch priority."
      ],
      "prompt": "Use I9kArticleHeader from @9klabs/design at the top of an article for a banner image or a branded fallback.\n\nimport { I9kArticleHeader } from '@9klabs/design';\n\nProps:\n- title: string (required) — used to build the image's alt text when `imageAlt` is not given; not rendered as visible text.\n- imageSrc?: string | null (default null)\n- imageAlt?: string (default '')\n- watermark?: string (default '9k') — short text shown in the gradient fallback when there is no imageSrc.\n- eager?: boolean (default false) — when true, loads the image eagerly with high fetchpriority instead of lazily; use only for an above-the-fold hero image.\n\nEmits: none.\n\nSlots: none.\n\nBehavior: with `imageSrc` set, renders a 2:1 <figure><img></figure> capped at 420px tall, object-fit cover. With no `imageSrc`, renders an aria-hidden gradient panel showing `#{{ watermark }}` instead — a decorative placeholder, not a loading state.\n\nIMPORTANT: `title` is not rendered as visible text anywhere — it only feeds the image's default alt text, so still write a real, separate heading (e.g. I9kPageHeader) for the article's visible title.\n\nUsage:\n<I9kArticleHeader title=\"Are AI coding tools ready to replace programmers?\" image-src=\"https://i.ytimg.com/vi/NfRC9Lj4-rU/hqdefault.jpg\" eager />"
    },
    {
      "name": "I9kBadge",
      "section": "content",
      "summary": "Small inline label for status, category, or tag content. Use it next to a heading or inside a card to mark a short piece of metadata.",
      "props": [
        {
          "name": "as",
          "type": "string | Component",
          "required": false,
          "default": "'span'"
        },
        {
          "name": "size",
          "type": "'sm' | 'md' | 'lg'",
          "required": false,
          "default": "'md'"
        },
        {
          "name": "variant",
          "type": "'solid' | 'outline' | 'tag'",
          "required": false,
          "default": "'outline'"
        }
      ],
      "emits": [],
      "slots": [
        "default"
      ],
      "referencedTypes": {},
      "gotchas": [
        "Badge text is uppercased by CSS automatically — pass normal-case text, do not pre-uppercase it.",
        "`variant=\"tag\"` adds a decorative leading \"#\" automatically; do not include one in the slot content."
      ],
      "prompt": "Use I9kBadge from @9klabs/design for a short inline label.\n\nimport { I9kBadge } from '@9klabs/design';\n\nProps:\n- as?: string | Component (default 'span') — the rendered root tag or component.\n- size?: 'sm' | 'md' | 'lg' (default 'md')\n- variant?: 'solid' | 'outline' | 'tag' (default 'outline')\n\nEmits: none.\n\nSlots: default — the badge text.\n\nBehavior: 'solid' fills with the primary color for a featured/callout badge. 'outline' is a bordered, transparent badge for general metadata (the default). 'tag' renders a subtler filled chip with a leading \"#\" decoration, for topic/category tags.\n\nIMPORTANT: badge text renders uppercase via CSS (text-transform) regardless of the casing you pass — write it in normal case in the slot content, do not pre-uppercase it yourself.\n\nUsage:\n<I9kBadge variant=\"tag\" size=\"sm\">AI</I9kBadge>"
    },
    {
      "name": "I9kStat",
      "section": "content",
      "summary": "Value/label/source stack for a single statistic. Use it in a grid of a few key numbers, such as metrics on an about page or a pricing comparison.",
      "props": [
        {
          "name": "as",
          "type": "string | Component",
          "required": false,
          "default": "'div'"
        },
        {
          "name": "label",
          "type": "string",
          "required": false,
          "default": "undefined"
        },
        {
          "name": "size",
          "type": "'sm' | 'md' | 'lg'",
          "required": false,
          "default": "'md'"
        },
        {
          "name": "source",
          "type": "string",
          "required": false,
          "default": "undefined"
        },
        {
          "name": "value",
          "type": "string | number",
          "required": false,
          "default": "undefined"
        }
      ],
      "emits": [],
      "slots": [
        "value",
        "label",
        "source"
      ],
      "referencedTypes": {},
      "gotchas": [
        "Each of value/label/source only renders when its prop is set or its same-named slot is filled — an I9kStat with none of the three renders an empty shell.",
        "When both the prop and the matching slot are given, the slot content is what renders."
      ],
      "prompt": "Use I9kStat from @9klabs/design to display one statistic (value, label, and optional source).\n\nimport { I9kStat } from '@9klabs/design';\n\nProps:\n- as?: string | Component (default 'div') — the rendered root tag or component.\n- label?: string (default undefined)\n- size?: 'sm' | 'md' | 'lg' (default 'md')\n- source?: string (default undefined)\n- value?: string | number (default undefined)\n\nEmits: none.\n\nSlots:\n- value — overrides the `value` prop's rendering, e.g. to bold or link the number.\n- label — overrides the `label` prop's rendering.\n- source — overrides the `source` prop's rendering, e.g. to link a citation.\n\nBehavior: each of value/label/source renders only when its prop is set OR its matching slot is filled — pass either the prop or the slot for a given piece, not neither.\n\nIMPORTANT: nothing renders for value, label, or source unless you supply the prop or the matching slot — do not rely on a slot alone without checking the prop is left unset (they are not mutually exclusive, but the slot always takes rendering priority when both are present).\n\nUsage:\n<I9kStat label=\"monthly npm downloads\" value=\"480k+\" source=\"npm snapshot\" size=\"lg\" />"
    },
    {
      "name": "I9kLinkCard",
      "section": "content",
      "summary": "Clickable card linking out to an external resource, with a name, description, and optional image/badge/arrow. Use it for a grid of projects, articles, or external links.",
      "props": [
        {
          "name": "name",
          "type": "string",
          "required": true,
          "default": null
        },
        {
          "name": "url",
          "type": "string",
          "required": true,
          "default": null
        },
        {
          "name": "description",
          "type": "string",
          "required": true,
          "default": null
        },
        {
          "name": "image",
          "type": "string | null",
          "required": false,
          "default": "null"
        },
        {
          "name": "badge",
          "type": "string | null",
          "required": false,
          "default": "null"
        },
        {
          "name": "showImage",
          "type": "boolean",
          "required": false,
          "default": "true"
        },
        {
          "name": "arrow",
          "type": "boolean",
          "required": false,
          "default": "false"
        },
        {
          "name": "arrowLabel",
          "type": "string",
          "required": false,
          "default": "'↗'"
        },
        {
          "name": "size",
          "type": "'sm' | 'md' | 'lg'",
          "required": false,
          "default": "'md'"
        }
      ],
      "emits": [
        {
          "name": "click",
          "payload": "[event: MouseEvent]"
        }
      ],
      "slots": [],
      "referencedTypes": {},
      "gotchas": [
        "The entire card renders as one <a> element opened in a new tab — never nest another link or button inside it.",
        "`click` fires alongside normal navigation, not instead of it; it does not stop the link from opening.",
        "`showImage` only hides the image slot area — it does not remove the badge/arrow header or change card size."
      ],
      "prompt": "Use I9kLinkCard from @9klabs/design for a clickable card that links to an external URL.\n\nimport { I9kLinkCard } from '@9klabs/design';\n\nProps:\n- name: string (required)\n- url: string (required) — the card is an <a href=\"url\"> opened in a new tab (target=\"_blank\", rel=\"noopener\").\n- description: string (required)\n- image?: string | null (default null)\n- badge?: string | null (default null)\n- showImage?: boolean (default true) — set false to hide the image even when `image` is given.\n- arrow?: boolean (default false) — shows a trailing arrow glyph in the top-right corner.\n- arrowLabel?: string (default '↗') — the arrow glyph itself.\n- size?: 'sm' | 'md' | 'lg' (default 'md')\n\nEmits: click with the native MouseEvent — the link still navigates; use this only for side effects like analytics, not to prevent navigation.\n\nSlots: none — name, description, image, and badge are all props.\n\nIMPORTANT: the whole card is a single <a> to `url` opened in a new tab; do not nest another interactive element (button, link) inside it.\n\nUsage:\n<I9kLinkCard name=\"vue3-carousel\" url=\"https://github.com/ismail9k/vue3-carousel\" description=\"A flexible, responsive carousel component for Vue 3.\" badge=\"Open source\" arrow />"
    },
    {
      "name": "I9kTimelineCard",
      "section": "content",
      "summary": "Dated entry in a vertical timeline/rail, with a title, body, and optional thumbnail. Use it for a chronological list of talks, posts, or events.",
      "props": [
        {
          "name": "date",
          "type": "string | Date",
          "required": true,
          "default": null
        },
        {
          "name": "linked",
          "type": "boolean",
          "required": false,
          "default": "false"
        },
        {
          "name": "locale",
          "type": "string",
          "required": false,
          "default": "'en'"
        },
        {
          "name": "size",
          "type": "'sm' | 'md' | 'lg'",
          "required": false,
          "default": "'md'"
        }
      ],
      "emits": [],
      "slots": [
        "title",
        "default",
        "thumbnail"
      ],
      "referencedTypes": {},
      "gotchas": [
        "`linked` only adds hover styling and a full-card click target over an `<a>` inside the `title` slot — you must put that `<a>` there yourself.",
        "Dates are formatted in UTC, so a plain date string like \"2026-01-25\" always shows as that calendar day regardless of the viewer's timezone.",
        "The `thumbnail` slot is hidden entirely below a 600px viewport — do not rely on it for content that has no alternative in `default`."
      ],
      "prompt": "Use I9kTimelineCard from @9klabs/design for one entry in a vertical, dated timeline. Stack multiple instances to build the full timeline — the rail connects visually between adjacent cards via CSS, with no wrapping list component required.\n\nimport { I9kTimelineCard } from '@9klabs/design';\n\nProps:\n- date: string | Date (required) — an ISO date string ('2026-01-25') or a Date object; formatted with Intl.DateTimeFormat as a long date (e.g. \"January 25, 2026\") in UTC, so a date-only string never shifts to the previous/next day from timezone drift.\n- linked?: boolean (default false) — when true, styles the whole card as hoverable/clickable and stretches the title's <a> to fill the card (via a CSS ::after overlay); requires the title slot to contain a real <a>.\n- locale?: string (default 'en') — passed to Intl.DateTimeFormat, e.g. 'ar' for Arabic date formatting.\n- size?: 'sm' | 'md' | 'lg' (default 'md')\n\nEmits: none.\n\nSlots:\n- title — the entry heading; wrap it in an <a> when using `linked`.\n- default — the entry body content.\n- thumbnail — an image shown beside the body (hidden under 600px viewport width).\n\nIMPORTANT: `linked` only changes hover styling and stretches an <a> found inside the `title` slot to cover the card — put a real <a> in `title` yourself, the component does not create one for you.\n\nUsage:\n<I9kTimelineCard date=\"2026-01-25\" linked><template #title><a href=\"#\">Are AI coding tools ready to replace programmers?</a></template><p>A practical discussion of what today's tools can do and what still needs engineering judgement.</p></I9kTimelineCard>"
    },
    {
      "name": "I9kProfileCard",
      "section": "content",
      "summary": "Card pairing an avatar with a name, bio, and optional action links. Use it for an author byline, a team member card, or a speaker bio.",
      "props": [
        {
          "name": "name",
          "type": "string",
          "required": true,
          "default": null
        },
        {
          "name": "alias",
          "type": "string | null",
          "required": false,
          "default": "null"
        },
        {
          "name": "namePrefix",
          "type": "string | null",
          "required": false,
          "default": "null"
        },
        {
          "name": "avatarSrc",
          "type": "string | null",
          "required": false,
          "default": "null"
        },
        {
          "name": "avatarAlt",
          "type": "string",
          "required": false,
          "default": "''"
        },
        {
          "name": "size",
          "type": "'sm' | 'md' | 'lg'",
          "required": false,
          "default": "'md'"
        }
      ],
      "emits": [],
      "slots": [
        "avatar",
        "default",
        "actions"
      ],
      "referencedTypes": {},
      "gotchas": [
        "The avatar column renders only when `avatarSrc` is set or the `avatar` slot is filled — there is no empty placeholder avatar.",
        "`namePrefix` and `alias` are plain text rendered inline with `name` — they cannot hold markup or links."
      ],
      "prompt": "Use I9kProfileCard from @9klabs/design for a person's avatar, name, and bio.\n\nimport { I9kProfileCard } from '@9klabs/design';\n\nProps:\n- name: string (required)\n- alias?: string | null (default null) — rendered after the name, separated by \" · \" (e.g. a handle).\n- namePrefix?: string | null (default null) — rendered before the name (e.g. \"Written by\").\n- avatarSrc?: string | null (default null)\n- avatarAlt?: string (default '')\n- size?: 'sm' | 'md' | 'lg' (default 'md')\n\nEmits: none.\n\nSlots:\n- avatar — overrides the built-in <img> rendered from `avatarSrc`; use this for a custom avatar element.\n- default — the bio content, rendered under the name.\n- actions — a row of action links/buttons under the bio.\n\nBehavior: the avatar column only renders at all when either the `avatar` slot is filled or `avatarSrc` is set — with neither, the card is name/bio/actions only, no empty avatar space.\n\nIMPORTANT: `namePrefix` and `alias` are plain strings rendered inline around `name` in one paragraph — they cannot contain markup or links; put any linked text in the `default` or `actions` slot instead.\n\nUsage:\n<I9kProfileCard name=\"Abdelrahman Ismail\" alias=\"Ismail9k\" name-prefix=\"Written by\" avatar-src=\"https://avatars.githubusercontent.com/u/20756985?s=120&v=4\">Software engineer sharing how AI is changing the way software gets built.<template #actions><a href=\"#instagram\">Instagram</a><a href=\"#github\">GitHub</a></template></I9kProfileCard>"
    },
    {
      "name": "I9kFaqList",
      "section": "content",
      "summary": "List of collapsible question/answer pairs built on native <details>/<summary>. Use it for an FAQ section without wiring any open/close state yourself.",
      "props": [
        {
          "name": "items",
          "type": "I9kFaqItem[]",
          "required": true,
          "default": null
        }
      ],
      "emits": [],
      "slots": [],
      "referencedTypes": {
        "I9kFaqItem": "export interface I9kFaqItem {\n  question: string;\n  answer: string;\n}"
      },
      "gotchas": [
        "Each item's `question` string is used as its list key — keep questions unique within a single `items` array.",
        "question and answer are plain text only; there is no slot for markup or links inside an item.",
        "Items expand independently via native <details> — opening one never closes another."
      ],
      "prompt": "Use I9kFaqList from @9klabs/design to render a list of collapsible FAQ entries.\n\nimport { I9kFaqList } from '@9klabs/design';\n// Item shape: { question: string; answer: string }\n// This type (I9kFaqItem) is not exported from the package; inline the object shape or declare\n// your own local type.\n\nProps:\n- items: I9kFaqItem[] (required) — each item renders as a native <details>/<summary> pair; each item's `question` is used as its Vue :key, so keep questions unique within one list.\n\nEmits: none.\n\nSlots: none — question and answer are plain text per item, not slots.\n\nBehavior: open/close state is native browser <details> behavior — no Vue state is involved, and each item opens/closes independently with no \"only one open at a time\" accordion behavior.\n\nIMPORTANT: both `question` and `answer` render as plain text — there is no way to pass markup or links into an item; keep answers to plain sentences.\n\nUsage:\n<I9kFaqList :items=\"[{ question: 'Is this library tree-shakeable?', answer: 'Yes — each component is a separate export, so unused ones are dropped at build time.' }]\" />"
    },
    {
      "name": "I9kGithubEmbed",
      "section": "content",
      "summary": "Compact card linking to a GitHub repository by \"owner/repo\" name. Use it to reference a specific repo inline in content, e.g. a blog post or project list.",
      "props": [
        {
          "name": "repo",
          "type": "string",
          "required": true,
          "default": null
        }
      ],
      "emits": [],
      "slots": [],
      "referencedTypes": {},
      "gotchas": [
        "This is a static link, not a live embed — it never fetches stars, description, or any other data from GitHub.",
        "Pass the full \"owner/repo\" string; the component does not validate or prepend an owner, so a bare repo name produces a broken link."
      ],
      "prompt": "Use I9kGithubEmbed from @9klabs/design to link to a GitHub repository.\n\nimport { I9kGithubEmbed } from '@9klabs/design';\n\nProps:\n- repo: string (required) — an \"owner/repo\" string, e.g. 'ismail9k/vue3-carousel'. The component builds the link as `https://github.com/${repo}` and does not validate the format.\n\nEmits: none.\n\nSlots: none.\n\nBehavior: this is a static link card — it makes no network request and fetches no repository data (stars, description, etc.) from GitHub; it only renders the GitHub icon and the `repo` text as a link.\n\nIMPORTANT: pass the full \"owner/repo\" string, not just the repo name — the component does not prepend an owner, so 'vue3-carousel' alone produces a broken link (github.com/vue3-carousel) instead of 'ismail9k/vue3-carousel'.\n\nUsage:\n<I9kGithubEmbed repo=\"ismail9k/vue3-carousel\" />"
    },
    {
      "name": "I9kIcon",
      "section": "content",
      "summary": "SVG icon rendered from the library's built-in icon set by name. Use it anywhere a small inline glyph is needed — social links, nav items, buttons.",
      "props": [
        {
          "name": "name",
          "type": "I9kIconName",
          "required": true,
          "default": null
        },
        {
          "name": "title",
          "type": "string",
          "required": false,
          "default": "''"
        },
        {
          "name": "desc",
          "type": "string",
          "required": false,
          "default": "''"
        },
        {
          "name": "size",
          "type": "string | number",
          "required": false,
          "default": "'1.2em'"
        }
      ],
      "emits": [],
      "slots": [],
      "referencedTypes": {
        "I9kIconName": "export type I9kIconName = (typeof I9K_ICON_NAMES)[number];"
      },
      "gotchas": [
        "Icon names are limited to the fixed set in src/icons/paths.json — add a new icon there, do not inline raw SVG in a component.",
        "I9kIcon is aria-hidden by default; pass `title` or `desc` whenever the icon is the only content of an interactive element."
      ],
      "prompt": "Use I9kIcon from @9klabs/design to render a built-in SVG icon by name.\n\nimport { I9kIcon } from '@9klabs/design';\n\nProps:\n- name: I9kIconName (required) — one of the names in src/icons/paths.json: 'facebook', 'twitter', 'medium', 'linkedin', 'behance', 'github', 'menu', 'mail', 'dev', 'phone', 'landMark', 'home', 'instagram', 'youtube', 'tiktok', 'x', '9klabs', 'linktree'.\n- title?: string (default '') — an accessible name for the icon; setting this (or `desc`) makes the icon exposed to assistive tech as role=\"img\" instead of hidden.\n- desc?: string (default '') — a longer accessible description; same effect as `title` on hiddenness.\n- size?: string | number (default '1.2em') — sets both width and height, e.g. '24px', '2em', 32.\n\nEmits: none.\n\nSlots: none.\n\nBehavior: icon names come from src/icons/paths.json, a fixed lookup table of path data — I9kIcon cannot render an arbitrary SVG path or a name outside that set. The component is aria-hidden=\"true\" by default (a decorative icon); it only gets role=\"img\" and becomes visible to assistive tech when you pass a `title` or `desc`.\n\nIMPORTANT: to add a new icon, add its entry to src/icons/paths.json (as a path string, or { viewBox, path } for a non-24x24 icon) — never inline a raw <svg> in a component in place of I9kIcon.\n\nIMPORTANT: I9kIcon is aria-hidden unless you pass `title` or `desc` — always set one of those when the icon is the only content of a link or button (e.g. an icon-only social link), or it will be invisible to screen readers.\n\nUsage:\n<I9kIcon name=\"github\" title=\"GitHub\" size=\"1.5em\" />"
    },
    {
      "name": "I9kAsciiEmoji",
      "section": "content",
      "summary": "Small text-based emoticon (e.g. \"^_^\") rendered in a monospace face, for a lighter-touch alternative to emoji or icon glyphs.",
      "props": [
        {
          "name": "name",
          "type": "keyof typeof labels",
          "required": true,
          "default": null
        },
        {
          "name": "label",
          "type": "string | null",
          "required": false,
          "default": "null"
        },
        {
          "name": "size",
          "type": "'sm' | 'md' | 'lg'",
          "required": false,
          "default": "'md'"
        },
        {
          "name": "color",
          "type": "'primary' | 'accent' | 'muted' | 'current'",
          "required": false,
          "default": "'primary'"
        }
      ],
      "emits": [],
      "slots": [],
      "referencedTypes": {},
      "gotchas": [
        "TypeScript does not enforce a closed set for `name`: the label map is typed as `Record<string, string>`, so `keyof typeof labels` widens to plain `string` and any value compiles.",
        "Passing a `name` outside the seven known strings without also passing `label` renders `role=\"img\"` with no `aria-label` at all — always pass `label` explicitly for anything but the seven known strings."
      ],
      "prompt": "Use I9kAsciiEmoji from @9klabs/design for a small ASCII-art emoticon.\n\nimport { I9kAsciiEmoji } from '@9klabs/design';\n\nProps:\n- name: keyof typeof labels (required) — the internal label map is typed as `Record<string, string>`, so this declared type widens to plain `string` at compile time; TypeScript accepts any string here, not just the seven below. The seven strings with a built-in label are '^_^', '·ᴗ·', '◡̈', '>‿<', 'x_x', 'o_o', '-_-'.\n- label?: string | null (default null) — overrides the automatic aria-label; when omitted, a matching label is used for the seven known strings ('^_^' → \"happy\", '·ᴗ·' → \"gentle smile\", '◡̈' → \"smiling\", '>‿<' → \"joyful\", 'x_x' → \"exhausted\", 'o_o' → \"surprised\", '-_-' → \"unimpressed\").\n- size?: 'sm' | 'md' | 'lg' (default 'md')\n- color?: 'primary' | 'accent' | 'muted' | 'current' (default 'primary')\n\nEmits: none.\n\nSlots: none — the emoticon text comes only from `name`.\n\nBehavior: always renders role=\"img\". An aria-label is present when `label` is passed, or when `name` is one of the seven known strings; otherwise no aria-label is rendered at all.\n\nIMPORTANT: pass one of the seven known strings, or pass `label` explicitly. TypeScript does not restrict `name` to a closed set — any string compiles — but an unrecognized `name` has no entry in the internal label map, so the element ends up with role=\"img\" and no aria-label, breaking the accessibility contract this component exists to provide.\n\nUsage:\n<I9kAsciiEmoji name=\"^_^\" size=\"lg\" color=\"accent\" />"
    },
    {
      "name": "I9kField",
      "section": "forms",
      "summary": "Field wrapper that renders a label, hint, and error, and provides id/size/validity context to one nested control. It is the provider side of the field composable; I9kInput, I9kTextarea, and I9kSelect are its consumers.",
      "props": [
        {
          "name": "label",
          "type": "string",
          "required": false,
          "default": "''"
        },
        {
          "name": "hint",
          "type": "string",
          "required": false,
          "default": "undefined"
        },
        {
          "name": "error",
          "type": "string",
          "required": false,
          "default": "undefined"
        },
        {
          "name": "required",
          "type": "boolean",
          "required": false,
          "default": "false"
        },
        {
          "name": "size",
          "type": "'sm' | 'md' | 'lg'",
          "required": false,
          "default": "'md'"
        },
        {
          "name": "controlId",
          "type": "string",
          "required": false,
          "default": "undefined"
        }
      ],
      "emits": [],
      "slots": [
        "label",
        "default"
      ],
      "referencedTypes": {},
      "gotchas": [
        "Nest exactly one control — I9kField only tracks and warns (in dev) if more than one registers.",
        "Inside I9kField, do not pass label, hint, error, or uiSize/size to I9kInput, I9kTextarea, or I9kSelect: I9kField supplies them through the field composable.",
        "I9kRadioGroup ignores I9kField entirely — it never calls useI9kField(), so wrapping it here has no effect and produces an orphaned label/hint/error alongside the group's own legend/hint/error. Pass legend, hint, error, and size straight to I9kRadioGroup instead."
      ],
      "prompt": "Use I9kField from @9klabs/design to wrap a single form control with a label, hint, and error.\n\nimport { I9kField } from '@9klabs/design';\n\nProps:\n- label?: string (default '') — ignored if the #label slot is used instead.\n- hint?: string\n- error?: string — a defined value renders the error message instead of the hint and marks the control invalid.\n- required?: boolean (default false) — shows a trailing \"*\" next to the label.\n- size?: 'sm' | 'md' | 'lg' (default 'md')\n- controlId?: string — supply to pin the id instead of the auto-generated one.\n\nEmits: none.\n\nSlots:\n- label — overrides the label prop's content.\n- default — scoped slot exposing { controlId, describedBy, invalid, required, size }; the nested control reads these.\n\nIMPORTANT: nest exactly one form control in the default slot. I9kInput, I9kTextarea, and I9kSelect read this context automatically via useI9kField() — inside I9kField, omit their own label/hint/error/uiSize props, since I9kField owns and renders those. I9kRadioGroup does NOT consume this context (it has its own legend/hint/error/size props) — do not wrap it in I9kField.\n\nFor a raw native control instead of a package component, bind the scoped slot props by hand: :id=\"controlId\", :aria-describedby=\"describedBy\", :aria-invalid=\"invalid\", :required=\"required\".\n\nUsage:\n<I9kField label=\"Email\" hint=\"We never share it.\"><I9kInput v-model=\"email\" /></I9kField>"
    },
    {
      "name": "I9kInput",
      "section": "forms",
      "summary": "Single-line text input with an optional label, hint, and error state. Wires its own accessible ids, and inherits size and error state from a wrapping I9kField when there is one.",
      "props": [
        {
          "name": "modelValue",
          "type": "string",
          "required": true,
          "default": null
        },
        {
          "name": "label",
          "type": "string",
          "required": false,
          "default": "undefined"
        },
        {
          "name": "type",
          "type": "'text' | 'email' | 'password'",
          "required": false,
          "default": "'text'"
        },
        {
          "name": "error",
          "type": "string | null",
          "required": false,
          "default": "null"
        },
        {
          "name": "hint",
          "type": "string",
          "required": false,
          "default": "undefined"
        },
        {
          "name": "required",
          "type": "boolean",
          "required": false,
          "default": "false"
        },
        {
          "name": "uiSize",
          "type": "'sm' | 'md' | 'lg'",
          "required": false,
          "default": "undefined"
        }
      ],
      "emits": [
        {
          "name": "update:modelValue",
          "payload": "[value: string]"
        }
      ],
      "slots": [],
      "referencedTypes": {},
      "gotchas": [
        "The visual scale prop is `uiSize`, not `size` — `size` passes through to the native input attribute.",
        "Inside an I9kField, omit `label`, `hint`, `error`, and `uiSize`: the field supplies them and owns the ids."
      ],
      "prompt": "Use I9kInput from @9klabs/design for a labelled single-line text field.\n\nimport { I9kInput } from '@9klabs/design';\n\nProps:\n- modelValue: string (required) — the v-model target.\n- label?: string\n- type?: 'text' | 'email' | 'password' (default 'text')\n- error?: string | null (default null) — a non-null value renders the error state and wires aria-describedby.\n- hint?: string\n- required?: boolean (default false)\n- uiSize?: 'sm' | 'md' | 'lg' — falls back to a wrapping I9kField's size, then to 'md'.\n\nEmits: update:modelValue with the new string.\n\nIMPORTANT: the visual scale prop is `uiSize`, NOT `size`. `size` is left free for the native HTML attribute and is forwarded to the underlying <input>.\n\nUsage:\n<I9kInput v-model=\"email\" label=\"Email\" type=\"email\" ui-size=\"md\" hint=\"We never share it.\" />"
    },
    {
      "name": "I9kTextarea",
      "section": "forms",
      "summary": "Multi-line text control for longer form input. Wires its own id and ARIA attributes standalone, and inherits id, size, and error state from a wrapping I9kField when there is one.",
      "props": [
        {
          "name": "modelValue",
          "type": "string",
          "required": true,
          "default": null
        },
        {
          "name": "uiSize",
          "type": "'sm' | 'md' | 'lg'",
          "required": false,
          "default": "undefined"
        },
        {
          "name": "resize",
          "type": "'vertical' | 'horizontal' | 'both' | 'none'",
          "required": false,
          "default": "'vertical'"
        }
      ],
      "emits": [
        {
          "name": "update:modelValue",
          "payload": "[value: string]"
        }
      ],
      "slots": [],
      "referencedTypes": {},
      "gotchas": [
        "The visual scale prop is `uiSize`, not `size`.",
        "Inside an I9kField, do not pass id, aria-invalid, or aria-describedby — the field supplies them; a conflicting id logs a dev warning.",
        "Without a wrapping I9kField, supply aria-label or aria-labelledby yourself — there is no visible label otherwise."
      ],
      "prompt": "Use I9kTextarea from @9klabs/design for multi-line text input, typically inside an I9kField.\n\nimport { I9kTextarea } from '@9klabs/design';\n\nProps:\n- modelValue: string (required) — the v-model target.\n- uiSize?: 'sm' | 'md' | 'lg' — falls back to a wrapping I9kField's size, then to 'md'.\n- resize?: 'vertical' | 'horizontal' | 'both' | 'none' (default 'vertical')\n\nEmits: update:modelValue with the new string.\n\nSlots: none — this renders a bare <textarea>.\n\nBehavior: inside an I9kField, I9kTextarea calls useI9kField() and takes its id, described-by ids, invalid state, required state, and size from that context automatically. Standalone (no wrapping I9kField), it generates its own id and expects the caller to provide an accessible name via aria-label or aria-labelledby — omitting both triggers a dev-mode console warning. required, aria-invalid, and aria-describedby also pass through as native attrs when there is no I9kField.\n\nIMPORTANT: the visual scale prop is `uiSize`, not `size`. IMPORTANT: inside I9kField, do not pass id, aria-invalid, or aria-describedby — I9kField supplies them, and a mismatched id triggers a dev-mode warning.\n\nUsage:\n<I9kField label=\"Project details\"><I9kTextarea v-model=\"details\" /></I9kField>"
    },
    {
      "name": "I9kSelect",
      "section": "forms",
      "summary": "Native single-select dropdown that auto-selects the option matching modelValue. Wires its own id and ARIA attributes standalone, and inherits id, size, and error state from a wrapping I9kField when there is one.",
      "props": [
        {
          "name": "modelValue",
          "type": "string",
          "required": true,
          "default": null
        },
        {
          "name": "uiSize",
          "type": "'sm' | 'md' | 'lg'",
          "required": false,
          "default": null
        }
      ],
      "emits": [
        {
          "name": "update:modelValue",
          "payload": "[value: string]"
        }
      ],
      "slots": [],
      "referencedTypes": {},
      "gotchas": [
        "The visual scale prop is `uiSize`, not `size`; the native `size` and `multiple` attributes are stripped and logged as a dev warning.",
        "Pass plain <option>/<optgroup> children without a `selected` attribute — I9kSelect sets it based on modelValue matching the option's value or text.",
        "Inside an I9kField, do not pass id, aria-invalid, or aria-describedby — the field supplies them."
      ],
      "prompt": "Use I9kSelect from @9klabs/design for a single-choice dropdown, typically inside an I9kField.\n\nimport { I9kSelect } from '@9klabs/design';\n\nProps:\n- modelValue: string (required) — the v-model target, matched against each child <option>'s value (or its text content if it has no value attribute).\n- uiSize?: 'sm' | 'md' | 'lg' — falls back to a wrapping I9kField's size, then to 'md'.\n\nEmits: update:modelValue with the new string.\n\nSlots:\n- default — plain <option> and <optgroup> elements. I9kSelect clones them and sets `selected` on the one matching modelValue; do not set `selected` yourself.\n\nBehavior: inside an I9kField, I9kSelect calls useI9kField() and takes its id, described-by ids, invalid state, required state, and size from that context automatically. Standalone, it generates its own id and expects an accessible name via aria-label or aria-labelledby — omitting both triggers a dev-mode console warning. This is a native single-select only: the `multiple` and `size` HTML attributes are stripped and log a dev-mode warning if passed.\n\nIMPORTANT: the visual scale prop is `uiSize`, not `size` — passing the native `size` attribute is rejected with a dev warning. IMPORTANT: inside I9kField, do not pass id, aria-invalid, or aria-describedby — I9kField supplies them.\n\nUsage:\n<I9kField label=\"Service\"><I9kSelect v-model=\"service\"><option value=\"\">Choose one</option><option value=\"audit\">Technical audit</option></I9kSelect></I9kField>"
    },
    {
      "name": "I9kRadioGroup",
      "section": "forms",
      "summary": "Fieldset of mutually exclusive radio options, rendered as a stacked list or a card grid. Unlike I9kInput, I9kTextarea, and I9kSelect, it does not participate in the I9kField composable — it owns its own legend, hint, error, and ids entirely.",
      "props": [
        {
          "name": "modelValue",
          "type": "string",
          "required": true,
          "default": null
        },
        {
          "name": "options",
          "type": "readonly I9kRadioOption[]",
          "required": true,
          "default": null
        },
        {
          "name": "legend",
          "type": "string",
          "required": true,
          "default": null
        },
        {
          "name": "name",
          "type": "string",
          "required": false,
          "default": "undefined"
        },
        {
          "name": "hint",
          "type": "string",
          "required": false,
          "default": "undefined"
        },
        {
          "name": "error",
          "type": "string",
          "required": false,
          "default": "undefined"
        },
        {
          "name": "required",
          "type": "boolean",
          "required": false,
          "default": "false"
        },
        {
          "name": "disabled",
          "type": "boolean",
          "required": false,
          "default": "false"
        },
        {
          "name": "size",
          "type": "'sm' | 'md' | 'lg'",
          "required": false,
          "default": "'md'"
        },
        {
          "name": "variant",
          "type": "'default' | 'card'",
          "required": false,
          "default": "'default'"
        },
        {
          "name": "orientation",
          "type": "'horizontal' | 'vertical'",
          "required": false,
          "default": "'vertical'"
        }
      ],
      "emits": [
        {
          "name": "update:modelValue",
          "payload": "[value: string]"
        }
      ],
      "slots": [],
      "referencedTypes": {
        "I9kRadioOption": "export interface I9kRadioOption {\n  label: string;\n  value: string;\n  description?: string;\n  disabled?: boolean;\n}"
      },
      "gotchas": [
        "Never wrap I9kRadioGroup in I9kField — it does not call useI9kField() and will not inherit label, hint, error, or size from it; pass those as its own props instead.",
        "`options` must match the I9kRadioOption shape exactly: { label, value, description?, disabled? }.",
        "The `card` variant lays out options in a two-column grid (one column under 640px) with the native input visually hidden; `default` renders a plain stacked or wrapped list."
      ],
      "prompt": "Use I9kRadioGroup from @9klabs/design for a single choice among a small, fully visible set of options.\n\nimport { I9kRadioGroup } from '@9klabs/design';\nimport type { I9kRadioOption } from '@9klabs/design';\n\nProps:\n- modelValue: string (required) — the v-model target, matched against each option's value.\n- options: readonly I9kRadioOption[] (required) — each is { label: string; value: string; description?: string; disabled?: boolean }.\n- legend: string (required) — the fieldset's accessible name; always rendered, there is no slot override.\n- name?: string — the radio input group name; defaults to an auto-generated id.\n- hint?: string\n- error?: string — a defined value renders the error message instead of the hint and marks the group invalid.\n- required?: boolean (default false)\n- disabled?: boolean (default false) — disables the whole group.\n- size?: 'sm' | 'md' | 'lg' (default 'md')\n- variant?: 'default' | 'card' (default 'default')\n- orientation?: 'horizontal' | 'vertical' (default 'vertical')\n\nEmits: update:modelValue with the selected option's value.\n\nSlots: none.\n\nIMPORTANT: I9kRadioGroup never calls useI9kField() — do NOT wrap it in I9kField expecting it to inherit label, hint, error, or size the way I9kInput/I9kTextarea/I9kSelect do. It renders its own <fieldset><legend> and its own hint/error paragraphs directly from its own props. Wrapping it in I9kField still renders I9kField's separate label/hint/error around the group, producing duplicated, disconnected markup — pass legend, hint, error, required, and size straight to I9kRadioGroup instead, with no I9kField involved.\n\nUsage:\n<I9kRadioGroup v-model=\"service\" legend=\"Choose a service\" :options=\"[{ label: 'Technical audit', value: 'audit' }, { label: 'Design system', value: 'design' }]\" />"
    },
    {
      "name": "I9kButton",
      "section": "actions",
      "summary": "Polymorphic action trigger: renders a native button by default, an anchor when given a destination, or a caller-supplied component. Six variants cover primary actions, filters, and pagination.",
      "props": [
        {
          "name": "to",
          "type": "string | Record<string, unknown> | null",
          "required": false,
          "default": "null"
        },
        {
          "name": "href",
          "type": "string | null",
          "required": false,
          "default": "null"
        },
        {
          "name": "variant",
          "type": "'default' | 'primary' | 'link' | 'filter' | 'pagination' | 'page'",
          "required": false,
          "default": "'default'"
        },
        {
          "name": "size",
          "type": "'sm' | 'md' | 'lg'",
          "required": false,
          "default": "'md'"
        },
        {
          "name": "active",
          "type": "boolean",
          "required": false,
          "default": "false"
        },
        {
          "name": "type",
          "type": "'button' | 'submit' | 'reset'",
          "required": false,
          "default": "'button'"
        },
        {
          "name": "linkComponent",
          "type": "string | object | null",
          "required": false,
          "default": "null"
        }
      ],
      "emits": [],
      "slots": [
        "default"
      ],
      "referencedTypes": {},
      "gotchas": [
        "The root element depends on props, not a separate mode flag: pass `to` or `href` for a link, omit both for a native button.",
        "`type` is only meaningful on the button form — it is not rendered when the component resolves to `<a>` or a `linkComponent`.",
        "In a Vue Router app, pass `link-component=\"RouterLink\"` alongside `to` so navigation uses the router instead of a full page reload."
      ],
      "prompt": "Use I9kButton from @9klabs/design for any clickable action or link styled as a button.\n\nimport { I9kButton } from '@9klabs/design';\n\nProps:\n- to?: string | Record<string, unknown> | null (default null) — a route-like destination. Setting this makes the root render as `<a>` (or `linkComponent` if given).\n- href?: string | null (default null) — a plain URL. Setting this also makes the root render as `<a>`.\n- variant?: 'default' | 'primary' | 'link' | 'filter' | 'pagination' | 'page' (default 'default')\n- size?: 'sm' | 'md' | 'lg' (default 'md')\n- active?: boolean (default false) — toggles the selected look; used by 'filter' and 'page' variants.\n- type?: 'button' | 'submit' | 'reset' (default 'button') — only applies when the root renders as a native <button>; it is dropped when `to` or `href` is set.\n- linkComponent?: string | object | null (default null) — a component to render instead of `<a>` when `to` is set, e.g. a router link component.\n\nEmits: none. It forwards native events (click, etc.) as ordinary DOM listeners via `v-bind`/attribute fallthrough.\n\nSlots: default — the button's content.\n\nRoot element rule: with no `to`/`href`/`linkComponent`, I9kButton renders `<button>`. Passing `to` or `href` switches it to `<a>`. Passing `linkComponent` renders that component instead, and it receives `to` — use this in a Vue Router app by setting `link-component=\"RouterLink\"` so internal navigation goes through the router instead of a full page load.\n\nIMPORTANT: `type` only takes effect on the native `<button>` form. If `to` or `href` is set, `type` is not rendered — do not rely on it to distinguish submit buttons that are also links.\n\nUsage:\n<I9kButton variant=\"primary\" @click=\"onSave\">Save</I9kButton>\n<I9kButton to=\"/pricing\" link-component=\"RouterLink\">See pricing</I9kButton>"
    },
    {
      "name": "I9kButtonGroup",
      "section": "actions",
      "summary": "Layout wrapper that spaces a row or column of buttons with a consistent gap and groups them as one control for assistive tech.",
      "props": [
        {
          "name": "size",
          "type": "'sm' | 'md' | 'lg'",
          "required": false,
          "default": "'md'"
        },
        {
          "name": "orientation",
          "type": "'horizontal' | 'vertical'",
          "required": false,
          "default": "'horizontal'"
        },
        {
          "name": "label",
          "type": "string",
          "required": false,
          "default": "undefined"
        }
      ],
      "emits": [],
      "slots": [
        "default"
      ],
      "referencedTypes": {},
      "gotchas": [
        "`size` on I9kButtonGroup only changes the gap between children — set the same `size` on each child button to actually resize them.",
        "Give it a `label` when the group has no adjacent visible heading; it becomes the `aria-label` on the `role=\"group\"` wrapper.",
        "`orientation=\"vertical\"` stretches children to fill the group's width (`align-items: stretch`); horizontal (the default) wraps children onto new lines instead of overflowing."
      ],
      "prompt": "Use I9kButtonGroup from @9klabs/design to lay out a cluster of related buttons (e.g. Save/Cancel, or a toolbar of icon buttons) with consistent spacing.\n\nimport { I9kButtonGroup } from '@9klabs/design';\n\nProps:\n- size?: 'sm' | 'md' | 'lg' (default 'md') — sets the gap between children only.\n- orientation?: 'horizontal' | 'vertical' (default 'horizontal') — horizontal wraps onto new lines; vertical stretches children to full width.\n- label?: string — sets the group's accessible name (`aria-label`) via `role=\"group\"`. Omit it and no `aria-label` is rendered.\n\nEmits: none.\n\nSlots: default — the buttons (typically I9kButton and/or I9kIconButton).\n\nIMPORTANT: `size` only controls the gap between children — it does NOT resize the buttons inside. Set `size` on each child button to match if you want them visually smaller or larger, not just on the group.\n\nUsage:\n<I9kButtonGroup label=\"Article actions\" size=\"sm\">\n  <I9kButton size=\"sm\">Save</I9kButton>\n  <I9kIconButton icon=\"mail\" label=\"Email article\" size=\"sm\" />\n</I9kButtonGroup>"
    },
    {
      "name": "I9kIconButton",
      "section": "actions",
      "summary": "Circular icon-only action trigger: renders a native button by default, or an anchor/caller-supplied component when given a destination. Its own variant set is separate from I9kButton.",
      "props": [
        {
          "name": "icon",
          "type": "I9kIconName",
          "required": true,
          "default": null
        },
        {
          "name": "label",
          "type": "string",
          "required": true,
          "default": null
        },
        {
          "name": "to",
          "type": "string | Record<string, unknown> | null",
          "required": false,
          "default": "null"
        },
        {
          "name": "href",
          "type": "string | null",
          "required": false,
          "default": "null"
        },
        {
          "name": "variant",
          "type": "'secondary' | 'primary' | 'ghost'",
          "required": false,
          "default": "'secondary'"
        },
        {
          "name": "size",
          "type": "'sm' | 'md' | 'lg'",
          "required": false,
          "default": "'md'"
        },
        {
          "name": "type",
          "type": "'button' | 'submit' | 'reset'",
          "required": false,
          "default": "'button'"
        },
        {
          "name": "linkComponent",
          "type": "string | object | null",
          "required": false,
          "default": "null"
        }
      ],
      "emits": [],
      "slots": [],
      "referencedTypes": {
        "I9kIconName": "export type I9kIconName = (typeof I9K_ICON_NAMES)[number];"
      },
      "gotchas": [
        "I9kIconButton's variant union ('secondary' | 'primary' | 'ghost') is distinct from I9kButton's — never reuse I9kButton variants like 'default' or 'filter' here.",
        "`label` is required and must be non-empty: it is the sole accessible name since the button renders an icon only, no text.",
        "The root element depends on props, not a mode flag: pass `to` or `href` for a link, omit both for a native button, same convention as I9kButton."
      ],
      "prompt": "Use I9kIconButton from @9klabs/design for a compact, icon-only action (e.g. a toolbar button or a social/contact link) that needs no visible text label.\n\nimport { I9kIconButton } from '@9klabs/design';\n\nProps:\n- icon: I9kIconName (required) — a name from src/icons/paths.json, e.g. 'mail', 'home', 'menu'.\n- label: string (required) — the accessible name, rendered as `aria-label`. There is no visible text, so this must be non-empty and descriptive; the component logs a dev warning if it is blank.\n- to?: string | Record<string, unknown> | null (default null) — a route-like destination. Setting this makes the root render as `<a>` (or `linkComponent` if given).\n- href?: string | null (default null) — a plain URL. Setting this also makes the root render as `<a>`.\n- variant?: 'secondary' | 'primary' | 'ghost' (default 'secondary')\n- size?: 'sm' | 'md' | 'lg' (default 'md') — also scales the inner icon.\n- type?: 'button' | 'submit' | 'reset' (default 'button') — only applies when the root renders as a native <button>; dropped when `to` or `href` is set.\n- linkComponent?: string | object | null (default null) — a component to render instead of `<a>` when `to` is set, e.g. a router link component.\n\nEmits: none. It forwards native events (click, etc.) as ordinary DOM listeners via `v-bind`/attribute fallthrough.\n\nSlots: none — content is always the icon; there is no default slot for text.\n\nIMPORTANT: I9kIconButton has its OWN variant type, 'secondary' | 'primary' | 'ghost' — this is NOT the same union as I9kButton's variant ('default' | 'primary' | 'link' | 'filter' | 'pagination' | 'page'). Do not pass I9kButton variants like 'default' or 'filter' here; only 'secondary', 'primary', or 'ghost' are valid.\n\nIMPORTANT: `label` is required and must be non-empty — it is the button's only accessible name since it renders no visible text.\n\nUsage:\n<I9kIconButton icon=\"mail\" label=\"Email us\" variant=\"primary\" @click=\"onEmail\" />\n<I9kIconButton icon=\"github\" label=\"View on GitHub\" href=\"https://github.com/ismail9k\" variant=\"ghost\" />"
    },
    {
      "name": "I9kToast",
      "section": "feedback",
      "summary": "Styled, accessible message banner for status or error text. It is only the visual/ARIA shell — placement, timing, and dismissal are the caller's responsibility.",
      "props": [
        {
          "name": "variant",
          "type": "'info' | 'success' | 'error'",
          "required": false,
          "default": "'info'"
        },
        {
          "name": "size",
          "type": "'sm' | 'md' | 'lg'",
          "required": false,
          "default": "'md'"
        }
      ],
      "emits": [],
      "slots": [
        "default"
      ],
      "referencedTypes": {},
      "gotchas": [
        "I9kToast renders no dismiss control and sets no timer — show, hide, and stack it yourself; it is a static banner until you remove it from the DOM.",
        "`variant=\"error\"` renders `role=\"alert\"` (assertive); `info` and `success` render `role=\"status\"` (polite) — pick `error` only for genuine failures so screen readers don't interrupt for routine status text.",
        "It has no positioning of its own — wrap it in a container with your own `position: fixed` styling if you want it to float above the page."
      ],
      "prompt": "Use I9kToast from @9klabs/design to display a short status or error message with a live-region role wired in automatically.\n\nimport { I9kToast } from '@9klabs/design';\n\nProps:\n- variant?: 'info' | 'success' | 'error' (default 'info') — also sets the ARIA role: 'error' renders `role=\"alert\"`, 'info' and 'success' render `role=\"status\"`.\n- size?: 'sm' | 'md' | 'lg' (default 'md')\n\nEmits: none.\n\nSlots: default — the message content.\n\nIMPORTANT: I9kToast has no dismiss button, no auto-hide timer, and no fixed/floating positioning built in. It is purely the visual banner and ARIA role — you own showing it, hiding it, stacking multiple toasts, and where on the page it sits (e.g. wrap it in your own fixed-position container to make it float).\n\nUsage:\n<I9kToast variant=\"success\">Changes saved.</I9kToast>"
    },
    {
      "name": "I9kNavigation",
      "section": "chrome",
      "summary": "Sticky site header with a brand slot, a link list, and an actions slot. Tracks scroll position to add a background on scroll and switch the brand into a compact state.",
      "props": [
        {
          "name": "links",
          "type": "I9kNavigationLink[]",
          "required": true,
          "default": null
        },
        {
          "name": "brandHref",
          "type": "string",
          "required": false,
          "default": "'/'"
        },
        {
          "name": "brandLabel",
          "type": "string",
          "required": false,
          "default": "'Home'"
        },
        {
          "name": "compactAt",
          "type": "number",
          "required": false,
          "default": "72"
        },
        {
          "name": "expandAt",
          "type": "number",
          "required": false,
          "default": "24"
        }
      ],
      "emits": [
        {
          "name": "navigate",
          "payload": "[link: I9kNavigationLink, event: MouseEvent]"
        }
      ],
      "slots": [
        "brand",
        "actions"
      ],
      "referencedTypes": {
        "I9kNavigationLink": "export interface I9kNavigationLink {\n  id: string;\n  label: string;\n  href: string;\n}"
      },
      "gotchas": [
        "The `links` prop is a plain array of `{ id, label, href }` objects — the menu is not built from slot content.",
        "The `brand` slot receives a `compact: boolean` slot prop; read it to swap the logo/wordmark for a condensed version.",
        "`compactAt` and `expandAt` are independent pixel thresholds with hysteresis between them, not one toggle point — a scroll position between the two keeps the current compact state."
      ],
      "prompt": "Use I9kNavigation from @9klabs/design for a page's top-level site header.\n\nimport { I9kNavigation } from '@9klabs/design';\n\nProps:\n- links: I9kNavigationLink[] (required) — the nav menu. Each item is { id: string; label: string; href: string }. This type is not exported from the package; inline the object shape or declare your own local type.\n- brandHref?: string (default '/') — the href on the brand link wrapping the `brand` slot.\n- brandLabel?: string (default 'Home') — used as the accessible label for both the brand link and the surrounding <nav>.\n- compactAt?: number (default 72) — scroll offset in pixels past which the header switches into its compact state.\n- expandAt?: number (default 24) — scroll offset in pixels below which the header switches back to its expanded state.\n\nEmits: navigate — [link: I9kNavigationLink, event: MouseEvent], fired when a menu link is clicked (in addition to the link's normal navigation).\n\nSlots:\n- brand — the logo/wordmark content, wrapped in the brand <a>. Receives one slot prop: compact: boolean, true once the scroll position has passed compactAt.\n- actions — content rendered after the menu (e.g. a sign-in button), not wrapped in a link.\n\nCompact/expand behavior: compactAt and expandAt are both scroll-Y pixel thresholds, not a single toggle point. Scrolling past compactAt turns compact on; scrolling back below expandAt turns it off. Because expandAt is lower than compactAt by default, there is a dead zone between them where the current state is kept — this hysteresis stops the header from flickering when the scroll position hovers near one threshold. Set expandAt below compactAt when customizing either.\n\nIMPORTANT: links is an array of plain objects, not slot content — build the menu by passing links, and use the brand and actions slots only for content outside that list.\n\nUsage:\n<I9kNavigation :links=\"[{ id: 'docs', label: 'Docs', href: '/docs' }, { id: 'pricing', label: 'Pricing', href: '/pricing' }]\" @navigate=\"onNavigate\">\n  <template #brand=\"{ compact }\"><span :class=\"{ compact }\">Acme</span></template>\n  <template #actions><I9kButton variant=\"primary\" href=\"/signup\">Sign up</I9kButton></template>\n</I9kNavigation>"
    },
    {
      "name": "I9kFooter",
      "section": "chrome",
      "summary": "A page's bottom chrome: an optional row of social links and an optional tagline, both replaceable via slots.",
      "props": [
        {
          "name": "tagline",
          "type": "string | null",
          "required": false,
          "default": "null"
        },
        {
          "name": "socialLinks",
          "type": "I9kSocialLink[]",
          "required": false,
          "default": "() => []"
        },
        {
          "name": "socialLabels",
          "type": "boolean",
          "required": false,
          "default": "false"
        }
      ],
      "emits": [
        {
          "name": "socialClick",
          "payload": "[item: I9kSocialLink, event: MouseEvent]"
        }
      ],
      "slots": [
        "social-icon",
        "default"
      ],
      "referencedTypes": {},
      "gotchas": [
        "The social links row is conditional on `socialLinks.length` — an empty array (the default) renders no I9kSocialLinks at all.",
        "The default slot fully replaces the tagline paragraph rather than appending to it — use one or the other, not both.",
        "I9kSocialLink is not exported from the package — inline `{ name: string; url: string; label?: string; icon?: I9kIconName }` yourself."
      ],
      "prompt": "Use I9kFooter from @9klabs/design as a page's <footer>, for a social-links row and a short tagline.\n\nimport { I9kFooter } from '@9klabs/design';\n\nProps:\n- tagline?: string | null (default null) — plain text shown under the social links, only when the default slot is not used.\n- socialLinks?: I9kSocialLink[] (default []) — rendered via I9kSocialLinks; the type is { name: string; url: string; label?: string; icon?: I9kIconName }. It is not exported from the package — inline the shape or declare your own local type.\n- socialLabels?: boolean (default false) — forwarded to I9kSocialLinks to show text labels next to each icon.\n\nEmits: socialClick — [item: I9kSocialLink, event: MouseEvent], forwarded from the underlying I9kSocialLinks click.\n\nSlots:\n- social-icon — forwarded straight through to I9kSocialLinks' own `icon` slot; receives one slot prop, item: I9kSocialLink, for the link being rendered.\n- default — replaces the tagline paragraph entirely. Falls back to `<p>{{ tagline }}</p>` (only rendered when `tagline` is set) when no slot content is given.\n\nIMPORTANT: the social links row only renders when `socialLinks` is a non-empty array — pass at least one entry or nothing appears.\n\nIMPORTANT: the default slot and the `tagline` prop are mutually exclusive in effect — providing default slot content replaces the tagline paragraph outright, it is not appended alongside it.\n\nUsage:\n<I9kFooter\n  tagline=\"Built with the 9k design system.\"\n  :social-links=\"[{ name: 'GitHub', url: 'https://github.com/ismail9k', icon: 'github' }]\"\n  @social-click=\"onSocialClick\"\n/>"
    },
    {
      "name": "I9kBrandWordmark",
      "section": "chrome",
      "summary": "Decorative, self-animating logotype that types itself out and swaps between a full and short form — purely visual, and carries no accessible name of its own.",
      "props": [
        {
          "name": "compact",
          "type": "boolean",
          "required": false,
          "default": "false"
        },
        {
          "name": "full",
          "type": "string",
          "required": false,
          "default": "'Ismail9k'"
        },
        {
          "name": "short",
          "type": "string",
          "required": false,
          "default": "'9k'"
        }
      ],
      "emits": [],
      "slots": [],
      "referencedTypes": {},
      "gotchas": [
        "It renders `aria-hidden=\"true\"` on its root — always wrap it in an element that supplies a real accessible name (e.g. an `<a aria-label=\"...\">`), it is purely decorative on its own.",
        "It animates on an internal 15–35 second random timer (an idle \"wink\"), and retypes between `full`/`short` when `compact` changes — there is no prop or event to trigger this manually, so do not build a demo or test that waits for it to visibly happen.",
        "Both the retype-on-`compact`-change animation and the idle wink are skipped when the visitor has `prefers-reduced-motion` set; the text just settles directly to its target."
      ],
      "prompt": "Use I9kBrandWordmark from @9klabs/design for a site's animated logotype, typically inside I9kNavigation's `brand` slot bound to that slot's `compact` prop.\n\nimport { I9kBrandWordmark } from '@9klabs/design';\n\nProps (this is the one component in the package using the runtime `defineProps({...})` form rather than the type-generic form, but the resolved props are the same shape):\n- compact?: boolean (default false) — when true, the wordmark types itself down to `short`; when false, it types back up to `full`.\n- full?: string (default 'Ismail9k') — the expanded text.\n- short?: string (default '9k') — the condensed text.\n\nEmits: none. Slots: none.\n\nIMPORTANT: the whole component renders with `aria-hidden=\"true\"` — it has no accessible name of its own. Wrap it in an element that supplies one, e.g. an `<a aria-label=\"Ismail9k, back to homepage\">`, rather than relying on the wordmark's visible text to be read by assistive tech.\n\nIMPORTANT: do not try to demo or screenshot the animation — it is driven entirely internally. Changing `compact` retypes the text via a timed animation; separately, on an internal 15–35 second random timer, a compact, resting wordmark occasionally \"winks\" into a random face (e.g. '^_^') for two seconds before typing back to `short`. There is no prop or event to trigger, control, or observe the wink — it is purely ambient and non-deterministic, and (like the retype) is skipped entirely when the user has `prefers-reduced-motion` set.\n\nUsage:\n<a href=\"/\" aria-label=\"Ismail9k, back to homepage\">\n  <I9kBrandWordmark :compact=\"isScrolledPastHeader\" />\n</a>"
    },
    {
      "name": "I9kSocialLinks",
      "section": "chrome",
      "summary": "Row of pill-shaped social/contact links, each opening in a new tab, with an icon fallback and a customizable accessible label per item.",
      "props": [
        {
          "name": "items",
          "type": "I9kSocialLink[]",
          "required": true,
          "default": null
        },
        {
          "name": "labels",
          "type": "boolean",
          "required": false,
          "default": "false"
        },
        {
          "name": "followLabel",
          "type": "(platform: string) => string",
          "required": false,
          "default": "(platform: string) => `Follow on ${platform}`"
        }
      ],
      "emits": [
        {
          "name": "click",
          "payload": "[item: I9kSocialLink, event: MouseEvent]"
        }
      ],
      "slots": [
        "icon"
      ],
      "referencedTypes": {
        "I9kSocialLink": "export interface I9kSocialLink {\n  name: string;\n  url: string;\n  label?: string;\n  icon?: I9kIconName;\n}"
      },
      "gotchas": [
        "Every link opens in a new tab (`target=\"_blank\" rel=\"noopener\"`) — this is not configurable per item or overall.",
        "An item with no `icon` falls back to a single-letter avatar (the first character of `name`), not a blank space — supply `icon` for every item that should show a real icon.",
        "`followLabel` builds the accessible name from `item.name`; override it when the links are not \"follow\" actions (e.g. email or phone contact links)."
      ],
      "prompt": "Use I9kSocialLinks from @9klabs/design for a row of social or contact links (also used internally by I9kFooter).\n\nimport { I9kSocialLinks } from '@9klabs/design';\n\nProps:\n- items: I9kSocialLink[] (required) — { name: string; url: string; label?: string; icon?: I9kIconName }. This type is not exported from the package — inline the shape or declare your own local type.\n- labels?: boolean (default false) — shows `item.label ?? item.name` as visible text next to each icon.\n- followLabel?: (platform: string) => string (default `(platform) => \\`Follow on \\${platform}\\``) — builds each link's `aria-label` from `item.name`. Override it for non-social contexts, e.g. a plain \"Email\" or \"Call\" link.\n\nEmits: click — [item: I9kSocialLink, event: MouseEvent].\n\nSlots:\n- icon — one slot prop, item: I9kSocialLink. Replaces the default rendering (an I9kIcon when `item.icon` is set, otherwise the first letter of `item.name`) for every link.\n\nIMPORTANT: every link renders with `target=\"_blank\" rel=\"noopener\"` unconditionally — there is no prop to open a link in the same tab.\n\nUsage:\n<I9kSocialLinks\n  :items=\"[\n    { name: 'GitHub', url: 'https://github.com/ismail9k', icon: 'github' },\n    { name: 'Mail', url: 'mailto:hello@ismail9k.com', icon: 'mail' },\n  ]\"\n  @click=\"onSocialClick\"\n/>"
    },
    {
      "name": "I9kThemeSwitcher",
      "section": "chrome",
      "summary": "Controlled light/dark toggle switch. It renders and emits the toggle only — applying the theme to the page is entirely the caller's job.",
      "props": [
        {
          "name": "modelValue",
          "type": "boolean",
          "required": false,
          "default": "false"
        },
        {
          "name": "lightLabel",
          "type": "string",
          "required": false,
          "default": "'Switch to light mode'"
        },
        {
          "name": "darkLabel",
          "type": "string",
          "required": false,
          "default": "'Switch to dark mode'"
        }
      ],
      "emits": [
        {
          "name": "update:modelValue",
          "payload": "[value: boolean]"
        }
      ],
      "slots": [],
      "referencedTypes": {},
      "gotchas": [
        "It is a controlled component with no side effects of its own — it never touches `document.documentElement` or any global theme state; you must apply the theme yourself in response to `update:modelValue`.",
        "`modelValue` is the single source of truth for which face (sun/moon) and label are shown — there is no internal toggle state, so it will not visually flip unless you update `modelValue` from the emitted event."
      ],
      "prompt": "Use I9kThemeSwitcher from @9klabs/design as the visual control for a light/dark theme toggle.\n\nimport { I9kThemeSwitcher } from '@9klabs/design';\n\nProps:\n- modelValue?: boolean (default false) — true means dark mode is active; the switch is fully controlled by this prop, it holds no state of its own.\n- lightLabel?: string (default 'Switch to light mode') — the accessible label used while `modelValue` is true (i.e. the action the next click performs).\n- darkLabel?: string (default 'Switch to dark mode') — the accessible label used while `modelValue` is false.\n\nEmits: update:modelValue — [value: boolean], fired on click with the flipped value. Use `v-model` to wire it up.\n\nSlots: none.\n\nIMPORTANT: this component does NOT touch `document.documentElement` or any global theme state itself — it only renders a switch and emits the new value. You are responsible for reacting to `update:modelValue` (or a `v-model` watcher) to actually apply the theme, e.g. `document.documentElement.classList.toggle('dark', isDark)`, and for persisting the choice if needed.\n\nUsage:\n<I9kThemeSwitcher v-model=\"isDark\" @update:model-value=\"applyTheme\" />"
    },
    {
      "name": "I9kLanguageSwitcher",
      "section": "chrome",
      "summary": "A styled link to an alternate-language version of the current page. It is only the link's markup — it does not itself switch the page's language or navigate via JS.",
      "props": [
        {
          "name": "label",
          "type": "string",
          "required": true,
          "default": null
        },
        {
          "name": "href",
          "type": "string",
          "required": true,
          "default": null
        },
        {
          "name": "hreflang",
          "type": "string | null",
          "required": false,
          "default": "null"
        },
        {
          "name": "linkComponent",
          "type": "string | object | null",
          "required": false,
          "default": "null"
        }
      ],
      "emits": [],
      "slots": [
        "default"
      ],
      "referencedTypes": {},
      "gotchas": [
        "It never touches `document.documentElement.lang` or `dir`, and does not navigate via JavaScript — it is a plain link; the language actually changes only when the browser follows `href` (or your `linkComponent` routing does).",
        "`label` is required but renders only as the default slot's fallback text — passing slot content hides `label` entirely, though you must still supply it for the type."
      ],
      "prompt": "Use I9kLanguageSwitcher from @9klabs/design for a link to an alternate-language version of the current page.\n\nimport { I9kLanguageSwitcher } from '@9klabs/design';\n\nProps:\n- label: string (required) — fallback text shown only when no default slot content is given.\n- href: string (required) — the destination URL, typically the same page in the other language.\n- hreflang?: string | null (default null) — sets the anchor's `hreflang` attribute, e.g. 'ar'.\n- linkComponent?: string | object | null (default null) — a component to render instead of `<a>`, receiving `to` set to `href`, e.g. a router link component.\n\nEmits: none. Slots: default — replaces the visible text entirely; falls back to `{{ label }}` when empty.\n\nIMPORTANT: `label` is required by its type but is only ever rendered as the default slot's fallback content — if you pass default slot content (e.g. a flag emoji plus text), `label` is never displayed. It is still required, so pass a plain-text equivalent even when you also supply slot content.\n\nIMPORTANT: this component does not change `document.documentElement.lang`/`dir`, and does not navigate via JavaScript — it renders a plain `<a href>` (or your `linkComponent`). Actually switching the visitor's language happens through normal navigation to `href`, or through your own routing logic if you pass `linkComponent`.\n\nUsage:\n<I9kLanguageSwitcher label=\"العربية\" href=\"/ar\" hreflang=\"ar\" />"
    },
    {
      "name": "I9kBlurredCircles",
      "section": "chrome",
      "summary": "Decorative, non-interactive layer of four slow-drifting blurred circles, meant as an ambient page background sitting behind real content.",
      "props": [],
      "emits": [],
      "slots": [],
      "referencedTypes": {},
      "gotchas": [
        "It is `position: fixed` and covers the entire browser viewport by default, regardless of where in the DOM it is mounted or how its parent is sized.",
        "To confine it to one container (as in this demo) instead of the whole page, give an ancestor `transform`/`filter`/`contain: paint` (any of these creates a new containing block for `position: fixed` descendants) plus `overflow: hidden`.",
        "It has no `z-index` of its own — it stays behind your content only via DOM order (mount it before your other content) or your own stacking context."
      ],
      "prompt": "Use I9kBlurredCircles from @9klabs/design as a decorative ambient background layer, mounted once near the root of a page, behind your real content.\n\nimport { I9kBlurredCircles } from '@9klabs/design';\n\nProps: none. Emits: none. Slots: none — it renders its own fixed set of four circles and nothing else.\n\nBehavior: it renders `position: fixed; inset: 0; pointer-events: none;` with `aria-hidden=\"true\"` — by default it covers the ENTIRE VIEWPORT, not just its parent element, sits behind content only because of normal DOM/stacking order (it has no `z-index`), and never intercepts clicks. The four circles drift slowly via CSS animation, which is disabled under `prefers-reduced-motion`.\n\nIMPORTANT: because it is `position: fixed`, mounting it anywhere covers the whole browser viewport by default — it does NOT stay confined to a parent container just because that parent is sized or positioned. To scope it to one section instead of the whole page, wrap it in an ancestor that establishes a new containing block for fixed-position descendants, e.g. one with `transform: translateZ(0)` (or any non-none `transform`/`filter`/`contain: paint`) plus `overflow: hidden` to clip it to that ancestor's bounds.\n\nIMPORTANT: mount it once per page (typically as the first child inside your root layout), not once per section — several instances stack multiple full-viewport layers on top of each other.\n\nUsage:\n<body>\n  <I9kBlurredCircles />\n  <!-- rest of the page, stacked above it by DOM order -->\n</body>"
    }
  ]
}