// src/web-components/components.config.ts
// Single source of truth for dual-framework (Vue + React) component generation.
// Add one entry here to add a new component to the generator pipeline.

// ---------------------------------------------------------------------------
// Schema types
// ---------------------------------------------------------------------------

export interface PropConfig {
  /** Canonical defineProps name, e.g. "color", "fill" */
  name: string
  /**
   * The TS type EXPRESSION for this prop:
   * primitive keyword (`string`/`boolean`) used inline; otherwise a union body
   * literal from which the generator emits `export type <Component><Prop> = <this>`.
   */
  tsType: string
  /** Optional JSDoc line for the generated prop */
  jsdoc?: string
  /** INFRA-F59 appearance-axis tag: 'fill'|'kind'|'color'|'state'|'size'. When set,
   *  audit-prop-naming asserts `name` === prop-naming-conventions.json conventions[axis]. */
  axis?: 'fill' | 'kind' | 'color' | 'state' | 'size'
}

export interface VModelConfig {
  /** React controlled-value prop name, e.g. "value" */
  reactValueProp: string
  /** React change-handler prop name, e.g. "onChange" */
  reactChangeProp: string
  /** Custom-element DOM property name for the bound value, e.g. "modelValue" */
  elementProperty: string
  /** Vue emit event name, e.g. "update:modelValue" */
  updateEvent: string
  /** CustomEvent.detail index holding the new value, e.g. 0 */
  detailIndex: number
  /** TypeScript value type string, e.g. "string" */
  valueType: string
}

export interface EventConfig {
  /** React handler prop name, e.g. "onClick" */
  reactHandlerProp: string
  /** Native/custom DOM event name to listen to, e.g. "click" */
  domEvent: string
  /** TypeScript handler signature string, e.g. "(e: Event) => void" */
  handlerType: string
  /**
   * CustomEvent.detail index holding the emitted value, e.g. 0.
   * present → Vue custom emit: unwrap CustomEvent.detail[detailIndex] and call the
   *           handler with that value (handlerType is e.g. `(value: number) => void`);
   * absent  → native DOM event: pass the Event object directly to the handler
   *           (handlerType is e.g. `(e: Event) => void`).
   */
  detailIndex?: number
}

export interface ComponentConfig {
  /** React export name, e.g. "Button" */
  name: string
  /** Custom element tag, e.g. "tvu-button" */
  tag: string
  /** SFC import name used in the generated Vue binding, e.g. "ButtonBridge" */
  canonicalImport: string
  /** Relative path from the generated file to the canonical SFC, e.g. "../canonical/ButtonBridge.vue" */
  canonicalPath: string
  /** Figma Code Connect manifest filter key, e.g. "Button" */
  figmaCodeComponent: string
  /** Simple value props set via element property (excludes v-model prop) */
  props: PropConfig[]
  /** Native/custom events mapped to React handler props */
  events: EventConfig[]
  /** v-model binding config, or null if the component has no v-model */
  vModel: VModelConfig | null
  /** Whether the component accepts React children via its default slot */
  hasDefaultSlot: boolean
  /** Named slots other than the default slot, e.g. ["label"] */
  namedSlots: string[]
  /**
   * Custom-element shadow-DOM mode (Vue 3.5 `defineCustomElement` option).
   * Omitted or `true` → default shadow DOM (styles encapsulated in the shadow root).
   * `false` → LIGHT DOM: the SFC's styles are injected into the document head, so
   *   (a) content teleported out of the element via `<Teleport to="body">` is still
   *       styled (scoped CSS no longer trapped in the shadow root), and
   *   (b) document-level ancestor selectors like `[data-theme='light']` match.
   * Required for overlay/teleport components (PopupBox, DropDownListSelect).
   */
  shadowRoot?: boolean
  /**
   * For shadowRoot:false components whose `<style>` lives in a base/child SFC (not the
   * canonical wrapper itself), list those extra style-source modules. Their compiled
   * `styles` are ALSO injected into the document head. Vue 3.5 injects NO styles in
   * light-DOM mode (it warns instead — see runtime-dom `_applyStyles` guard), so the
   * generated register.ts injects them manually via `injectLightDomStyles`. A missing
   * source surfaces as A_TRUE_DRIFT in the render gate (unstyled teleported content).
   * Only meaningful when `shadowRoot === false`.
   */
  lightDomStyleSources?: { importName: string; path: string }[]
}

// ---------------------------------------------------------------------------
// Pilot configs — Button, InputBoxLine, FormItem
// Props mirrored verbatim from src/canonical/*.vue defineProps (order preserved).
// ---------------------------------------------------------------------------

export const COMPONENT_CONFIGS: ComponentConfig[] = [
  // -------------------------------------------------------------------------
  // Button
  // Source: src/canonical/ButtonBridge.vue
  // defineProps: color, fixedWidth, icon, radius, size, status, fill, theme
  // -------------------------------------------------------------------------
  {
    name: 'Button',
    tag: 'tvu-button',
    canonicalImport: 'ButtonBridge',
    canonicalPath: '../canonical/ButtonBridge.vue',
    figmaCodeComponent: 'Button',
    props: [
      { name: 'color',      tsType: `'gray 1' | 'green' | 'orange' | 'red'` },
      { name: 'fixedWidth', tsType: `'no' | 'yes'` },
      { name: 'icon',       tsType: `'left' | 'light' | 'loading' | 'loading button' | 'no' | 'right'` },
      { name: 'radius',     tsType: `'round' | 'square'` },
      { name: 'size',       tsType: `'XS' | 'S' | 'M' | 'L'` },
      { name: 'status',     tsType: `'default' | 'disable' | 'hover' | 'loading'` },
      {
        name: 'fill',
        tsType: `'filling' | 'ghost' | 'rimless'`,
        jsdoc: 'Appearance/fill variant enum (filling/ghost/rimless).',
        axis: 'fill',
      },
      { name: 'theme', tsType: `'dark' | 'light'` },
    ],
    events: [
      { reactHandlerProp: 'onClick', domEvent: 'click', handlerType: '(e: Event) => void' },
    ],
    vModel: null,
    hasDefaultSlot: true,
    namedSlots: [],
  },

  // -------------------------------------------------------------------------
  // InputBoxLine
  // Source: src/canonical/InputBoxLine.vue
  // defineProps: modelValue, placeholder, darkTheme, status, enable, ux, size, feature, readonly
  // modelValue is the v-model prop — moved to vModel config.
  // Remaining props: placeholder, darkTheme, status, enable, ux, size, feature, readonly
  // -------------------------------------------------------------------------
  {
    name: 'Input',
    tag: 'tvu-input',
    canonicalImport: 'InputBoxLine',
    canonicalPath: '../canonical/InputBoxLine.vue',
    figmaCodeComponent: 'InputBoxLine',
    props: [
      { name: 'placeholder', tsType: 'string' },
      { name: 'darkTheme',   tsType: `'on' | 'off'` },
      { name: 'status',      tsType: `'Placeholder' | 'Value' | 'Filled'` },
      { name: 'enable',      tsType: `'on' | 'off'` },
      { name: 'ux',          tsType: `'default' | 'error' | 'click' | 'hover'` },
      { name: 'size',        tsType: `'M' | 'L' | 'XL'` },
      { name: 'feature',     tsType: `'no' | 'yes' | 'text count'` },
      { name: 'readonly',    tsType: 'boolean' },
    ],
    events: [],
    vModel: {
      reactValueProp:  'value',
      reactChangeProp: 'onChange',
      elementProperty: 'modelValue',
      updateEvent:     'update:modelValue',
      detailIndex:     0,
      valueType:       'string',
    },
    hasDefaultSlot: false,
    namedSlots: [],
  },

  // -------------------------------------------------------------------------
  // FormItem
  // Source: src/canonical/FormItem.vue
  // defineProps: label, required, error, hint, labelWidth, layout, status, theme, type
  // -------------------------------------------------------------------------
  {
    name: 'FormItem',
    tag: 'tvu-form-item',
    canonicalImport: 'FormItem',
    canonicalPath: '../canonical/FormItem.vue',
    figmaCodeComponent: 'FormItem',
    props: [
      { name: 'label',      tsType: 'string' },
      { name: 'required',   tsType: 'boolean' },
      { name: 'error',      tsType: 'string' },
      { name: 'hint',       tsType: 'string' },
      { name: 'labelWidth', tsType: `'120 px' | '200 px' | 'Dynamic'` },
      { name: 'layout',     tsType: `'1 line' | '1 line & Right' | '2 lines'` },
      { name: 'status',     tsType: `'Error' | 'Normal'` },
      { name: 'theme',      tsType: `'Dark' | 'Light'` },
      { name: 'type',       tsType: `'Label & checkbox' | 'Label & Input' | 'Label & Radio' | 'Label & Selector' | 'Label & Switch' | 'Label & Textarea'` },
    ],
    events: [],
    vModel: null,
    hasDefaultSlot: true,
    namedSlots: ['label'],
  },

  // =========================================================================
  // Batch A — Phase 1 additions (2026-06-30)
  // =========================================================================

  // -------------------------------------------------------------------------
  // Badge
  // Source: src/canonical/Badge.vue
  // defineProps: color, fill, type — no emits — default slot
  // -------------------------------------------------------------------------
  {
    name: 'Badge',
    tag: 'tvu-badge',
    canonicalImport: 'Badge',
    canonicalPath: '../canonical/Badge.vue',
    figmaCodeComponent: 'Badge',
    props: [
      { name: 'color', tsType: `'Neutral' | 'Blue' | 'Green' | 'Orange' | 'Red'` },
      { name: 'fill',  tsType: `'Filled' | 'Line'`, axis: 'fill' },
      { name: 'type',  tsType: `'Circle' | 'Rectangle'`, axis: 'kind' },
    ],
    events: [],
    vModel: null,
    hasDefaultSlot: true,
    namedSlots: [],
  },

  // -------------------------------------------------------------------------
  // PillStatus
  // Source: src/canonical/PillStatus.vue
  // defineProps: status, active, count — no emits — default slot + named "count" slot
  // -------------------------------------------------------------------------
  {
    name: 'PillStatus',
    tag: 'tvu-pill-status',
    canonicalImport: 'PillStatus',
    canonicalPath: '../canonical/PillStatus.vue',
    figmaCodeComponent: 'PillStatus',
    props: [
      { name: 'status', tsType: `'On-air' | 'Preview' | 'Analyzing' | 'Inactive'` },
      { name: 'active', tsType: 'boolean' },
      { name: 'count',  tsType: 'number | string' },
    ],
    events: [],
    vModel: null,
    hasDefaultSlot: true,
    namedSlots: ['count'],
  },

  // -------------------------------------------------------------------------
  // Progress
  // Source: src/canonical/Progress.vue
  // defineProps: size, status, theme, value, showLabel — no emits — no slots
  // -------------------------------------------------------------------------
  {
    name: 'Progress',
    tag: 'tvu-progress',
    canonicalImport: 'Progress',
    canonicalPath: '../canonical/Progress.vue',
    figmaCodeComponent: 'Progress',
    props: [
      { name: 'size',      tsType: `'M' | 'S'` },
      { name: 'status',    tsType: `'default' | 'error' | 'success' | 'warning'` },
      { name: 'theme',     tsType: `'dark' | 'light'` },
      { name: 'value',     tsType: 'number' },
      { name: 'showLabel', tsType: 'boolean' },
    ],
    events: [],
    vModel: null,
    hasDefaultSlot: false,
    namedSlots: [],
  },

  // -------------------------------------------------------------------------
  // Rating
  // Source: src/canonical/Rating.vue
  // defineProps: value ('1'|'2'|'3'|'4'|'5'), readonly — emits update:value → vModel
  // -------------------------------------------------------------------------
  {
    name: 'Rating',
    tag: 'tvu-rating',
    canonicalImport: 'Rating',
    canonicalPath: '../canonical/Rating.vue',
    figmaCodeComponent: 'Rating',
    props: [
      { name: 'readonly', tsType: 'boolean' },
    ],
    events: [],
    vModel: {
      reactValueProp:  'value',
      reactChangeProp: 'onChange',
      elementProperty: 'value',
      updateEvent:     'update:value',
      detailIndex:     0,
      valueType:       `'1' | '2' | '3' | '4' | '5'`,
    },
    hasDefaultSlot: false,
    namedSlots: [],
  },

  // -------------------------------------------------------------------------
  // BreadcrumbItem
  // Source: src/canonical/BreadcrumbItem.vue
  // defineProps: href, showSeparator, separatorType, state — no emits — default slot
  // -------------------------------------------------------------------------
  {
    name: 'BreadcrumbItem',
    tag: 'tvu-breadcrumb-item',
    canonicalImport: 'BreadcrumbItem',
    canonicalPath: '../canonical/BreadcrumbItem.vue',
    figmaCodeComponent: 'BreadcrumbItem',
    props: [
      { name: 'href',          tsType: 'string' },
      { name: 'showSeparator', tsType: 'boolean' },
      { name: 'separatorType', tsType: `'arrow' | 'slash'` },
      { name: 'state',         tsType: `'current' | 'Default' | 'disabled' | 'hover'` },
    ],
    events: [],
    vModel: null,
    hasDefaultSlot: true,
    namedSlots: [],
  },

  // -------------------------------------------------------------------------
  // TopBar
  // Source: src/canonical/TopBar.vue
  // defineProps: tag, title, showMenu, showSearchBox — no emits
  // Named slots: logo, left, search, menu, right-content (no default slot)
  // -------------------------------------------------------------------------
  {
    name: 'TopBar',
    tag: 'tvu-top-bar',
    canonicalImport: 'TopBar',
    canonicalPath: '../canonical/TopBar.vue',
    figmaCodeComponent: 'TopBar',
    props: [
      { name: 'tag',           tsType: `'After Login' | 'Before Login'` },
      { name: 'title',         tsType: 'string' },
      { name: 'showMenu',      tsType: 'boolean' },
      { name: 'showSearchBox', tsType: 'boolean' },
    ],
    events: [],
    vModel: null,
    hasDefaultSlot: false,
    namedSlots: ['logo', 'left', 'search', 'menu', 'right-content'],
  },

  // =========================================================================
  // Batch B — Phase 1 additions (2026-06-30)
  // =========================================================================

  // -------------------------------------------------------------------------
  // Switch
  // Source: src/canonical/Switch.vue
  // defineProps: darkTheme, status (v-model), enable, loading
  // emits: update:status — status: 'off' | 'on' | 'live'
  // No slots.
  // -------------------------------------------------------------------------
  {
    name: 'Switch',
    tag: 'tvu-switch',
    canonicalImport: 'Switch',
    canonicalPath: '../canonical/Switch.vue',
    figmaCodeComponent: 'Switch',
    props: [
      { name: 'darkTheme', tsType: `'on' | 'off'` },
      { name: 'enable',    tsType: `'yes' | 'no'` },
      { name: 'loading',   tsType: `'yes' | 'no'` },
    ],
    events: [],
    vModel: {
      reactValueProp:  'status',
      reactChangeProp: 'onStatusChange',
      elementProperty: 'status',
      updateEvent:     'update:status',
      detailIndex:     0,
      valueType:       `'off' | 'on' | 'live'`,
    },
    hasDefaultSlot: false,
    namedSlots: [],
  },

  // -------------------------------------------------------------------------
  // CheckBox
  // Source: src/canonical/CheckBox.vue
  // defineProps: darkTheme, status (v-model), enable, readonly
  // emits: update:status — status: 'off' | 'on' | 'some'
  // Default slot (used to pass label text).
  // -------------------------------------------------------------------------
  {
    name: 'CheckBox',
    tag: 'tvu-check-box',
    canonicalImport: 'CheckBox',
    canonicalPath: '../canonical/CheckBox.vue',
    figmaCodeComponent: 'CheckBox',
    props: [
      { name: 'darkTheme', tsType: `'on' | 'off'` },
      { name: 'enable',    tsType: `'yes' | 'no'` },
      { name: 'readonly',  tsType: 'boolean' },
    ],
    events: [],
    vModel: {
      reactValueProp:  'status',
      reactChangeProp: 'onStatusChange',
      elementProperty: 'status',
      updateEvent:     'update:status',
      detailIndex:     0,
      valueType:       `'off' | 'on' | 'some'`,
    },
    hasDefaultSlot: true,
    namedSlots: [],
  },

  // -------------------------------------------------------------------------
  // Radio
  // Source: src/canonical/Radio.vue
  // defineProps: darkTheme, status (v-model), enable
  // emits: update:status — status: 'off' | 'on'
  // Default slot (used to pass label text).
  // -------------------------------------------------------------------------
  {
    name: 'Radio',
    tag: 'tvu-radio',
    canonicalImport: 'Radio',
    canonicalPath: '../canonical/Radio.vue',
    figmaCodeComponent: 'Radio',
    props: [
      { name: 'darkTheme', tsType: `'on' | 'off'` },
      { name: 'enable',    tsType: `'yes' | 'no'` },
    ],
    events: [],
    vModel: {
      reactValueProp:  'status',
      reactChangeProp: 'onStatusChange',
      elementProperty: 'status',
      updateEvent:     'update:status',
      detailIndex:     0,
      valueType:       `'off' | 'on'`,
    },
    hasDefaultSlot: true,
    namedSlots: [],
  },

  // =========================================================================
  // Batch D — Phase 1 additions (2026-06-30)
  // render-gate-unverified: PillCounter + Breadcrumb have 0 manifest entries
  // (shared Vue/React coverage gap, owner-acknowledged). Bindings generated;
  // gate wiring deferred until manifest entries are added.
  // =========================================================================

  // -------------------------------------------------------------------------
  // PillCounter
  // Source: src/canonical/PillCounter.vue
  // defineProps: kind — no emits — default slot
  // -------------------------------------------------------------------------
  {
    name: 'PillCounter',
    tag: 'tvu-pill-counter',
    canonicalImport: 'PillCounter',
    canonicalPath: '../canonical/PillCounter.vue',
    figmaCodeComponent: 'PillCounter',
    props: [
      { name: 'kind', tsType: `'success' | 'warning' | 'info' | 'neutral'` },
    ],
    events: [],
    vModel: null,
    hasDefaultSlot: true,
    namedSlots: [],
  },

  // -------------------------------------------------------------------------
  // Breadcrumb
  // Source: src/canonical/Breadcrumb.vue
  // defineProps: none (uses useAttrs — passthrough) — no emits — default slot
  // -------------------------------------------------------------------------
  {
    name: 'Breadcrumb',
    tag: 'tvu-breadcrumb',
    canonicalImport: 'Breadcrumb',
    canonicalPath: '../canonical/Breadcrumb.vue',
    figmaCodeComponent: 'Breadcrumb',
    props: [],
    events: [],
    vModel: null,
    hasDefaultSlot: true,
    namedSlots: [],
  },

  // =========================================================================
  // Batch C — Phase 1 additions (2026-06-30)
  // =========================================================================

  // -------------------------------------------------------------------------
  // InputBoxFilled
  // Source: src/canonical/InputBoxFilled.vue
  // defineProps: modelValue (v-model string), placeholder, darkTheme, status,
  //             enable, ux, size, feature, readonly
  // emits: update:modelValue — value: string
  // No slots.
  // -------------------------------------------------------------------------
  {
    name: 'InputBoxFilled',
    tag: 'tvu-input-box-filled',
    canonicalImport: 'InputBoxFilled',
    canonicalPath: '../canonical/InputBoxFilled.vue',
    figmaCodeComponent: 'InputBoxFilled',
    props: [
      { name: 'placeholder', tsType: 'string' },
      { name: 'darkTheme',   tsType: `'on' | 'off'` },
      { name: 'status',      tsType: `'Placeholder' | 'Value'` },
      { name: 'enable',      tsType: `'on' | 'off'` },
      { name: 'ux',          tsType: `'default' | 'error' | 'click' | 'hover'` },
      { name: 'size',        tsType: `'M' | 'L' | 'XL'` },
      { name: 'feature',     tsType: `'no' | 'yes' | 'text count'` },
      { name: 'readonly',    tsType: 'boolean' },
    ],
    events: [],
    vModel: {
      reactValueProp:  'value',
      reactChangeProp: 'onChange',
      elementProperty: 'modelValue',
      updateEvent:     'update:modelValue',
      detailIndex:     0,
      valueType:       'string',
    },
    hasDefaultSlot: false,
    namedSlots: [],
  },

  // -------------------------------------------------------------------------
  // InputNumber
  // Source: src/canonical/InputNumber.vue
  // defineProps: modelValue (v-model number), type, min, max, step, disabled
  // emits: update:modelValue — value: number
  // No slots.
  // -------------------------------------------------------------------------
  {
    name: 'InputNumber',
    tag: 'tvu-input-number',
    canonicalImport: 'InputNumber',
    canonicalPath: '../canonical/InputNumber.vue',
    figmaCodeComponent: 'InputNumber',
    props: [
      { name: 'type', tsType: `'Default' | 'Only Add' | 'Only Reduce' | 'Readonly'`, axis: 'kind' },
      { name: 'min',       tsType: 'number' },
      { name: 'max',       tsType: 'number' },
      { name: 'step',      tsType: 'number' },
      { name: 'disabled',  tsType: 'boolean' },
    ],
    events: [],
    vModel: {
      reactValueProp:  'value',
      reactChangeProp: 'onChange',
      elementProperty: 'modelValue',
      updateEvent:     'update:modelValue',
      detailIndex:     0,
      valueType:       'number',
    },
    hasDefaultSlot: false,
    namedSlots: [],
  },

  // -------------------------------------------------------------------------
  // Slider
  // Source: src/canonical/Slider.vue
  // defineProps: modelValue (v-model number), size, theme, min, max, step,
  //             disabled, showValue
  // emits: update:modelValue — value: number
  // No slots.
  // -------------------------------------------------------------------------
  {
    name: 'Slider',
    tag: 'tvu-slider',
    canonicalImport: 'Slider',
    canonicalPath: '../canonical/Slider.vue',
    figmaCodeComponent: 'Slider',
    props: [
      { name: 'size',      tsType: `'M' | 'S'` },
      { name: 'theme',     tsType: `'dark' | 'light'` },
      { name: 'min',       tsType: 'number' },
      { name: 'max',       tsType: 'number' },
      { name: 'step',      tsType: 'number' },
      { name: 'disabled',  tsType: 'boolean' },
      { name: 'showValue', tsType: 'boolean' },
    ],
    events: [],
    vModel: {
      reactValueProp:  'value',
      reactChangeProp: 'onChange',
      elementProperty: 'modelValue',
      updateEvent:     'update:modelValue',
      detailIndex:     0,
      valueType:       'number',
    },
    hasDefaultSlot: false,
    namedSlots: [],
  },

  // -------------------------------------------------------------------------
  // Pagination
  // Source: src/canonical/Pagination.vue
  // defineProps: modelValue (v-model number), type, total, pageSize
  // emits: update:modelValue — value: number
  //        update:pageSize   — value: number  (extra, added to events[])
  // No slots.
  // -------------------------------------------------------------------------
  {
    name: 'Pagination',
    tag: 'tvu-pagination',
    canonicalImport: 'Pagination',
    canonicalPath: '../canonical/Pagination.vue',
    figmaCodeComponent: 'Pagination',
    props: [
      { name: 'type',     tsType: `'Classic' | 'Simple' | 'Small'`, axis: 'kind' },
      { name: 'total',    tsType: 'number' },
      { name: 'pageSize', tsType: 'number' },
    ],
    events: [
      // update:pageSize is a Vue custom emit carrying the new page size in detail[0].
      { reactHandlerProp: 'onPageSizeChange', domEvent: 'update:pageSize', handlerType: '(value: number) => void', detailIndex: 0 },
    ],
    vModel: {
      reactValueProp:  'value',
      reactChangeProp: 'onChange',
      elementProperty: 'modelValue',
      updateEvent:     'update:modelValue',
      detailIndex:     0,
      valueType:       'number',
    },
    hasDefaultSlot: false,
    namedSlots: [],
    // INFRA-F50: the page-size picker is now the DS SelectBox (SelectBoxBase), whose
    // dropdown `<Teleport to="body">`s out of the element. As a shadow-DOM CE the portal's
    // scoped styles would be trapped in the shadow root → unstyled dropdown in document.body
    // (same reason SelectBoxLine/Filled/PopupBox are light DOM). Light DOM + head injection
    // styles the teleported dropdown; nested SelectBoxBase/DropDownListSelect/Icon/Button/Input
    // styles are auto-collected by the generator's populateNestedStyleSources import-graph walk.
    shadowRoot: false,
  },

  // =========================================================================
  // Batch E — Phase 1 additions (2026-06-30)
  // provide/inject family: Tab→TabItem / Steps→StepItem composition.
  // Vue 3.5 _inheritParentContext: spike-proven to work across CE boundaries.
  // For React consumers: <TvuTab><TvuTabItem/></TvuTab> compose correctly.
  // Tab + Steps have 0 manifest entries → generate + smoke, gate-UNVERIFIED.
  // =========================================================================

  // -------------------------------------------------------------------------
  // Tab
  // Source: src/canonical/Tab.vue
  // defineProps: modelValue (v-model, string | number), type, property2
  // emits: update:modelValue
  // Default slot (for TabItem children).
  // NOTE: 0 manifest entries → render-gate-UNVERIFIED (like PillCounter/Breadcrumb).
  //       Real React consumers compose: <TvuTab><TvuTabItem/></TvuTab>.
  //       The harness renders Tab in isolation; inject falls to defaults.
  // -------------------------------------------------------------------------
  {
    name: 'Tab',
    tag: 'tvu-tab',
    canonicalImport: 'Tab',
    canonicalPath: '../canonical/Tab.vue',
    figmaCodeComponent: 'Tab',
    props: [
      { name: 'fill',  tsType: `'Line' | 'Text' | 'Filled' | 'line' | 'text' | 'filled'`, axis: 'fill' },
      { name: 'color', tsType: `'White' | 'Green' | 'white' | 'green'`, axis: 'color' },
    ],
    events: [],
    vModel: {
      reactValueProp:  'value',
      reactChangeProp: 'onChange',
      elementProperty: 'modelValue',
      updateEvent:     'update:modelValue',
      detailIndex:     0,
      valueType:       'string | number',
    },
    hasDefaultSlot: true,
    namedSlots: [],
  },

  // -------------------------------------------------------------------------
  // TabList
  // Source: src/canonical/TabList.vue
  // defineProps: items, modelValue (v-model, string | number), type
  // emits: update:modelValue
  // Default slot.
  // -------------------------------------------------------------------------
  {
    name: 'TabList',
    tag: 'tvu-tab-list',
    canonicalImport: 'TabList',
    canonicalPath: '../canonical/TabList.vue',
    figmaCodeComponent: 'TabList',
    props: [
      { name: 'items', tsType: '{ label: string; value: string | number; disabled?: boolean }[]' },
      { name: 'fill',  tsType: `'Line' | 'Text' | 'Filled' | 'line' | 'text' | 'filled'`, axis: 'fill' },
    ],
    events: [],
    vModel: {
      reactValueProp:  'value',
      reactChangeProp: 'onChange',
      elementProperty: 'modelValue',
      updateEvent:     'update:modelValue',
      detailIndex:     0,
      valueType:       'string | number',
    },
    hasDefaultSlot: true,
    namedSlots: [],
  },

  // -------------------------------------------------------------------------
  // TabItem
  // Source: src/canonical/TabItem.vue
  // defineProps: disabled, property1, property2, type, value
  // No emits (interactive state is injected from parent Tab via provide/inject).
  // Default slot (tab label content).
  // NOTE: In isolation (no Tab ancestor) inject falls to defaults — passes gate.
  //       Real React consumers: <TvuTab><TvuTabItem/></TvuTab>.
  // -------------------------------------------------------------------------
  {
    name: 'TabItem',
    tag: 'tvu-tab-item',
    canonicalImport: 'TabItem',
    canonicalPath: '../canonical/TabItem.vue',
    figmaCodeComponent: 'TabItem',
    props: [
      { name: 'disabled', tsType: 'boolean' },
      { name: 'state',    tsType: `'Normal' | 'Active' | 'normal' | 'active'`, axis: 'state' },
      { name: 'color',    tsType: `'White' | 'Green' | 'white' | 'green'`, axis: 'color' },
      { name: 'fill',     tsType: `'Line' | 'Text' | 'Filled' | 'line' | 'text' | 'filled'`, axis: 'fill' },
      { name: 'value',    tsType: 'string | number' },
    ],
    events: [],
    vModel: null,
    hasDefaultSlot: true,
    namedSlots: [],
  },

  // -------------------------------------------------------------------------
  // Steps
  // Source: src/canonical/Steps.vue
  // defineProps: active, current, direction, type
  // No emits.
  // Default slot (for StepItem children).
  // NOTE: 0 manifest entries → render-gate-UNVERIFIED (like PillCounter/Breadcrumb).
  //       Real React consumers compose: <TvuSteps><TvuStepItem/></TvuSteps>.
  // -------------------------------------------------------------------------
  {
    name: 'Steps',
    tag: 'tvu-steps',
    canonicalImport: 'Steps',
    canonicalPath: '../canonical/Steps.vue',
    figmaCodeComponent: 'Steps',
    props: [
      { name: 'active',     tsType: 'number' },
      { name: 'current',    tsType: 'number' },
      { name: 'direction',  tsType: `'horizontal' | 'vertical'` },
      { name: 'type',  tsType: `'number' | 'icon'`, axis: 'kind' },
    ],
    events: [],
    vModel: null,
    hasDefaultSlot: true,
    namedSlots: [],
  },

  // -------------------------------------------------------------------------
  // StepItem
  // Source: src/canonical/StepItem.vue
  // defineProps: description, direction, iconName, index, showLeadingConnector,
  //             showTrailingConnector, state, type, title
  // No emits (state flows in from Steps parent via provide/inject).
  // No default slot (StepItem renders title/description via props only).
  // NOTE: In isolation (no Steps ancestor) inject falls to defaults — passes gate.
  //       Real React consumers: <TvuSteps><TvuStepItem/></TvuSteps>.
  // -------------------------------------------------------------------------
  {
    name: 'StepItem',
    tag: 'tvu-step-item',
    canonicalImport: 'StepItem',
    canonicalPath: '../canonical/StepItem.vue',
    figmaCodeComponent: 'StepItem',
    props: [
      { name: 'description',           tsType: 'string' },
      { name: 'direction',             tsType: `'horizontal' | 'vertical'` },
      { name: 'iconName',              tsType: 'string' },
      { name: 'index',                 tsType: 'number' },
      { name: 'showLeadingConnector',  tsType: 'boolean' },
      { name: 'showTrailingConnector', tsType: 'boolean' },
      { name: 'state',                 tsType: `'pending' | 'active' | 'completed'` },
      { name: 'type',                  tsType: `'number' | 'icon'`, axis: 'kind' },
      { name: 'title',                 tsType: 'string' },
    ],
    events: [],
    vModel: null,
    hasDefaultSlot: false,
    namedSlots: [],
  },

  // =========================================================================
  // Batch F — Phase 0 additions (2026-06-30)
  // OVERLAY/TELEPORT family — highest-risk batch.
  // PopupBox + SelectBoxLine + SelectBoxFilled + DropDownListSelect + Tooltip.
  // SelectBoxBase is internal (used by Line/Filled only); NOT registered as CE.
  // =========================================================================

  // -------------------------------------------------------------------------
  // PopupBox
  // Source: src/canonical/PopupBox.vue
  // defineProps: theme, visible (v-model boolean), title, width, closable,
  //             closeOnBackdrop, closeOnEscape, showFooter, cancelText, confirmText
  // emits: update:visible (v-model), close, cancel, confirm
  // Slots: default + footer
  // Overlay mechanism: <Teleport to="body"> fixed modal (base PopupBox.vue)
  // -------------------------------------------------------------------------
  {
    name: 'PopupBox',
    tag: 'tvu-popup-box',
    canonicalImport: 'PopupBox',
    canonicalPath: '../canonical/PopupBox.vue',
    figmaCodeComponent: 'PopupBox',
    props: [
      { name: 'theme',           tsType: `'dark' | 'light'` },
      { name: 'title',           tsType: 'string' },
      { name: 'width',           tsType: 'string | number' },
      { name: 'closable',        tsType: 'boolean' },
      { name: 'closeOnBackdrop', tsType: 'boolean' },
      { name: 'closeOnEscape',   tsType: 'boolean' },
      { name: 'showFooter',      tsType: 'boolean' },
      { name: 'cancelText',      tsType: 'string' },
      { name: 'confirmText',     tsType: 'string' },
    ],
    events: [
      { reactHandlerProp: 'onClose',   domEvent: 'close',   handlerType: '() => void' },
      { reactHandlerProp: 'onCancel',  domEvent: 'cancel',  handlerType: '() => void' },
      { reactHandlerProp: 'onConfirm', domEvent: 'confirm', handlerType: '() => void' },
    ],
    vModel: {
      reactValueProp:  'visible',
      reactChangeProp: 'onVisibleChange',
      elementProperty: 'visible',
      updateEvent:     'update:visible',
      detailIndex:     0,
      valueType:       'boolean',
    },
    hasDefaultSlot: true,
    namedSlots: ['footer'],
    // PopupBox `<Teleport to="body">`s its modal; scoped CSS trapped in a shadow
    // root never reaches the teleported .popup-box in document.body. Light DOM +
    // manual head style injection puts the SFC's styles in document.head so the
    // teleported content is styled. The canonical PopupBox is a pass-through wrapper
    // with NO <style> of its own — the modal markup, Teleport and scoped CSS all live
    // in BasePopupBox, so its styles must be injected too (lightDomStyleSources).
    shadowRoot: false,
    lightDomStyleSources: [
      { importName: 'BasePopupBox', path: '../components/PopupBox/PopupBox.vue' },
    ],
  },

  // -------------------------------------------------------------------------
  // SelectBoxLine
  // Source: src/canonical/SelectBoxLine.vue
  // defineProps: modelValue (v-model), placeholder, options, darkTheme, status,
  //             enable, ux, size, feature, multiple, editable
  // emits: update:modelValue
  // No slots.
  // Overlay mechanism: delegates to SelectBoxBase which <Teleport to="body">s dropdown.
  // SelectBoxBase is internal only — NOT registered as its own CE.
  // -------------------------------------------------------------------------
  {
    name: 'SelectBoxLine',
    tag: 'tvu-select-box-line',
    canonicalImport: 'SelectBoxLine',
    canonicalPath: '../canonical/SelectBoxLine.vue',
    figmaCodeComponent: 'SelectBoxLine',
    props: [
      { name: 'placeholder', tsType: 'string' },
      { name: 'options',     tsType: '{ label: string; value: string | number }[]' },
      { name: 'darkTheme',   tsType: `'on' | 'off'` },
      { name: 'status',      tsType: `'Placeholder' | 'Value' | 'multi select'` },
      { name: 'enable',      tsType: `'on' | 'off'` },
      { name: 'ux',          tsType: `'default' | 'error' | 'hover' | 'click' | 'editable'` },
      { name: 'size',        tsType: `'M' | 'L'` },
      { name: 'feature',     tsType: `'default' | 'time' | 'date'` },
      { name: 'multiple',    tsType: 'boolean' },
      { name: 'editable',    tsType: 'boolean' },
    ],
    events: [],
    vModel: {
      reactValueProp:  'value',
      reactChangeProp: 'onChange',
      elementProperty: 'modelValue',
      updateEvent:     'update:modelValue',
      detailIndex:     0,
      valueType:       'string | number | Array<string | number>',
    },
    hasDefaultSlot: false,
    namedSlots: [],
    // SelectBoxBase `<Teleport to="body">`s its dropdown portal; the portal's own
    // positioning/size styles (`.select-dropdown-portal { position: fixed }`, driven by
    // an inline trigger-rect top/left) live in SelectBoxBase's scoped <style>. Trapped
    // in a shadow root they never reach the teleported portal in document.body, so the
    // dropdown falls to `position: static` and renders full-width at the page bottom
    // instead of anchored under the trigger. Light DOM + head injection styles the
    // teleported portal (matches the Vue app, where scoped CSS is global). Nested
    // SelectBoxBase / DropDownListSelect / Icon / CheckBox styles are auto-collected by
    // the generator's populateNestedStyleSources import-graph walk.
    shadowRoot: false,
  },

  // -------------------------------------------------------------------------
  // SelectBoxFilled
  // Source: src/canonical/SelectBoxFilled.vue
  // defineProps: identical to SelectBoxLine (same base, different variant prop)
  // emits: update:modelValue
  // No slots.
  // Overlay mechanism: delegates to SelectBoxBase <Teleport to="body"> dropdown.
  // -------------------------------------------------------------------------
  {
    name: 'SelectBoxFilled',
    tag: 'tvu-select-box-filled',
    canonicalImport: 'SelectBoxFilled',
    canonicalPath: '../canonical/SelectBoxFilled.vue',
    figmaCodeComponent: 'SelectBoxFilled',
    props: [
      { name: 'placeholder', tsType: 'string' },
      { name: 'options',     tsType: '{ label: string; value: string | number }[]' },
      { name: 'darkTheme',   tsType: `'on' | 'off'` },
      { name: 'status',      tsType: `'Placeholder' | 'Value' | 'multi select'` },
      { name: 'enable',      tsType: `'on' | 'off'` },
      { name: 'ux',          tsType: `'default' | 'error' | 'hover' | 'click' | 'editable'` },
      { name: 'size',        tsType: `'M' | 'L'` },
      { name: 'feature',     tsType: `'default' | 'time' | 'date'` },
      { name: 'multiple',    tsType: 'boolean' },
      { name: 'editable',    tsType: 'boolean' },
    ],
    events: [],
    vModel: {
      reactValueProp:  'value',
      reactChangeProp: 'onChange',
      elementProperty: 'modelValue',
      updateEvent:     'update:modelValue',
      detailIndex:     0,
      valueType:       'string | number | Array<string | number>',
    },
    hasDefaultSlot: false,
    namedSlots: [],
    // Same overlay/teleport reason as SelectBoxLine: the SelectBoxBase dropdown portal's
    // scoped positioning styles must reach the teleported portal in document.body.
    shadowRoot: false,
  },

  // -------------------------------------------------------------------------
  // DropDownListSelect
  // Source: src/canonical/DropDownListSelect.vue
  // defineProps: darkTheme, type, items
  // emits: select (value: string | number | undefined), close (no args)
  // No v-model. No slots (renders items from prop).
  // Overlay mechanism: rendered inside SelectBoxBase's <Teleport to="body"> portal.
  //   As a standalone CE it renders inline (no teleport of its own).
  // -------------------------------------------------------------------------
  {
    name: 'DropDownListSelect',
    tag: 'tvu-drop-down-list-select',
    canonicalImport: 'DropDownListSelect',
    canonicalPath: '../canonical/DropDownListSelect.vue',
    figmaCodeComponent: 'DropDownListSelect',
    props: [
      { name: 'darkTheme', tsType: `'on' | 'off'` },
      { name: 'type',      tsType: `'Radio' | 'Multi' | 'Operation List' | 'Sort By'` },
      // items is a structural type (readonly Item[] alias in SFC — multi-line, not resolved by audit's
      // single-line regex). No `|` in tsType so audit skips union-body check (correct: it's an object
      // type, not a simple union). Make optional fields concrete: runtime Vue CE accepts partial objects.
      { name: 'items',     tsType: '{ label: string; value: string; checked: boolean; active: boolean; disabled: boolean }[]' },
    ],
    events: [
      { reactHandlerProp: 'onSelect', domEvent: 'select', handlerType: '(value: string | number | undefined) => void', detailIndex: 0 },
      { reactHandlerProp: 'onClose',  domEvent: 'close',  handlerType: '() => void' },
    ],
    vModel: null,
    hasDefaultSlot: false,
    namedSlots: [],
    // Uses a document-level `[data-theme='light']` ancestor selector (disabled+checked
    // leaf text color) that can't match across a shadow boundary. Light DOM lets the
    // document-level data-theme attribute resolve for the component's styles.
    shadowRoot: false,
  },

  // -------------------------------------------------------------------------
  // Tooltip
  // Source: src/canonical/Tooltip.vue
  // defineProps: darkTheme, pointing, content, disabled, open
  // No emits. No v-model.
  // Slots: default (trigger) + named "content"
  // Overlay mechanism: position:fixed anchored (NO teleport — stays in CE shadow root).
  // -------------------------------------------------------------------------
  {
    name: 'Tooltip',
    tag: 'tvu-tooltip',
    canonicalImport: 'Tooltip',
    canonicalPath: '../canonical/Tooltip.vue',
    figmaCodeComponent: 'Tooltip',
    props: [
      { name: 'darkTheme', tsType: `'off' | 'on'` },
      { name: 'pointing',  tsType: `'Center down' | 'Center up' | 'left down' | 'left up' | 'right down' | 'right up'` },
      { name: 'content',   tsType: 'string' },
      { name: 'disabled',  tsType: 'boolean' },
      { name: 'open',      tsType: 'boolean' },
    ],
    events: [],
    vModel: null,
    hasDefaultSlot: true,
    namedSlots: ['content'],
  },

  // =========================================================================
  // Batch G — Phase 1 additions (2026-06-30)
  // Notification / Message (inline content) · Table (composite) · Chart (canvas).
  // ALL default shadow DOM — none use Teleport or html[data-theme] ancestor selectors
  // (scoped CSS + CSS custom properties, which pierce the shadow boundary fine), so no
  // shadowRoot:false / light-DOM injection is needed (unlike Batch F overlays).
  // =========================================================================

  // -------------------------------------------------------------------------
  // Notification
  // Source: src/canonical/Notification.vue
  // defineProps: form, type, theme, title, description, closable, cancelText, confirmText, okText
  // emits: close / cancel / confirm — all zero-payload (handlerType '() => void', no detailIndex;
  //   handler ignores the CustomEvent, same shape as PopupBox close/cancel/confirm — gate-proven).
  // No v-model. No slots (prop-driven content). 36 manifest entries → gate-verifiable.
  // Theme via `theme` prop (notif--dark/light), not html[data-theme] → default shadow DOM safe.
  // -------------------------------------------------------------------------
  {
    name: 'Notification',
    tag: 'tvu-notification',
    canonicalImport: 'Notification',
    canonicalPath: '../canonical/Notification.vue',
    figmaCodeComponent: 'Notification',
    props: [
      { name: 'form',        tsType: `'dialog' | 'alert' | 'pop confirm' | 'slide'` },
      { name: 'type',        tsType: `'warning' | 'danger' | 'error' | 'info' | 'success' | 'default'` },
      { name: 'theme',       tsType: `'dark' | 'light'` },
      { name: 'title',       tsType: 'string' },
      { name: 'description', tsType: 'string' },
      { name: 'closable',    tsType: 'boolean' },
      { name: 'cancelText',  tsType: 'string' },
      { name: 'confirmText', tsType: 'string' },
      { name: 'okText',      tsType: 'string' },
    ],
    events: [
      { reactHandlerProp: 'onClose',   domEvent: 'close',   handlerType: '() => void' },
      { reactHandlerProp: 'onCancel',  domEvent: 'cancel',  handlerType: '() => void' },
      { reactHandlerProp: 'onConfirm', domEvent: 'confirm', handlerType: '() => void' },
    ],
    vModel: null,
    hasDefaultSlot: false,
    namedSlots: [],
  },

  // -------------------------------------------------------------------------
  // Message
  // Source: src/canonical/Message.vue
  // defineProps: status, size, closable. The canonical bridge forwards NO emit (base
  //   Message.vue has a no-payload `close` + autoDismissMs, but the canonical wrapper
  //   does not relay them — mirror the canonical surface: events []).
  // No v-model, no slots. Inline content (no Teleport, no html[data-theme]) → shadow DOM.
  // 16 manifest entries → gate-verifiable.
  // -------------------------------------------------------------------------
  {
    name: 'Message',
    tag: 'tvu-message',
    canonicalImport: 'Message',
    canonicalPath: '../canonical/Message.vue',
    figmaCodeComponent: 'Message',
    props: [
      { name: 'status',   tsType: `'success' | 'info' | 'error' | 'warning'` },
      { name: 'size',     tsType: `'M' | 'L'` },
      { name: 'closable', tsType: 'boolean' },
    ],
    events: [],
    vModel: null,
    hasDefaultSlot: false,
    namedSlots: [],
  },

  // -------------------------------------------------------------------------
  // Table  (COMPOSITE — prop-driven, no v-model / emits / slots)
  // Source: src/canonical/Table.vue
  // defineProps: align, type, columns, data, striped.
  // columns/data are STRUCTURAL (no `|`) → parity audit skips union-body check (correct:
  //   they're object/array shapes, not simple unions). align/type unions ARE checked.
  // No Teleport, no html[data-theme] ancestor selectors → default shadow DOM OK.
  // 12 manifest entries (type×align×theme full cross-product); manifest supplies columns+data.
  // -------------------------------------------------------------------------
  {
    name: 'Table',
    tag: 'tvu-table',
    canonicalImport: 'Table',
    canonicalPath: '../canonical/Table.vue',
    figmaCodeComponent: 'Table',
    props: [
      { name: 'align',   tsType: `'Center' | 'Left' | 'Right'` },
      { name: 'type',    tsType: `'Header' | 'Tbody'` },
      { name: 'columns', tsType: `readonly { key: string; title: string; align?: 'left' | 'center' | 'right'; width?: string | number; showLeftIcon?: boolean; leftIconName?: string; showRightIcon?: boolean; rightIconName?: string }[]` },
      { name: 'data',    tsType: 'Record<string, unknown>[]' },
      { name: 'striped', tsType: 'boolean' },
    ],
    events: [],
    vModel: null,
    hasDefaultSlot: false,
    namedSlots: [],
  },

  // -------------------------------------------------------------------------
  // Chart  (SVG — echarts/core; ECharts migration 2026-07-01)
  // Source: src/canonical/Chart.vue (thin wrapper) → src/components/Chart/Chart.vue
  // defineProps: type, datasets, labels, height, width — no emits — no v-model — no slots.
  // Theme via JS (use-chart-tokens reads :root + MutationObserver on data-theme) → shadow-safe;
  //   no html[data-theme] CSS selector, no Teleport → default shadow DOM.
  // RENDER-GATE SCOPE (honest): echarts draws to <svg> (SVGRenderer); the gate reads ONLY the
  //   .tvu-chart wrapper computed style (box/padding/radius/bg/legend-text). The SVG chart body
  //   (slice/bar/line colors, data geometry) is left un-asserted for now —
  //   the manifest's expectedNodes ellipse/frame fills are aspirational for Chart. 12 entries
  //   = a container/theme-token gate, not a chart-rendering gate.
  // -------------------------------------------------------------------------
  {
    name: 'Chart',
    tag: 'tvu-chart',
    canonicalImport: 'Chart',
    canonicalPath: '../canonical/Chart.vue',
    figmaCodeComponent: 'Chart',
    props: [
      { name: 'type',     tsType: `'pie' | 'donut' | 'line' | 'bar' | 'bar-horizontal' | 'line-bar'` },
      { name: 'datasets', tsType: `{ label: string; data: number[]; type?: 'line' | 'bar'; color?: string }[]` },
      { name: 'labels',   tsType: 'string[]' },
      { name: 'height',   tsType: 'number' },
      { name: 'width',    tsType: 'number' },
    ],
    events: [],
    vModel: null,
    hasDefaultSlot: false,
    namedSlots: [],
  },

  // =========================================================================
  // META-02 — code-first composites (owner approved 2026-07-02).
  // Figma has NO standalone Logo / MenuList / UserMenu component-set; they exist
  // only as internal layers of the `Top bar` component + brand icon assets. These
  // promote/extract them into the dual-framework library so CLAUDE_DESIGN_RULES
  // §2-3 (Logo not hand-drawn / after-login UserMenu / Menu uses MenuList) are
  // satisfiable in the design tool. Registered as divergence
  // `logo-menulist-usermenu-code-first-2026-07-02`.
  // =========================================================================

  // -------------------------------------------------------------------------
  // Logo — brand mark wrapper (registry assets icon/logo/TVU + icon/logo/ts).
  // Source: src/canonical/Logo.vue
  {
    name: 'Logo',
    tag: 'tvu-logo',
    canonicalImport: 'Logo',
    canonicalPath: '../canonical/Logo.vue',
    figmaCodeComponent: 'Logo',
    props: [
      { name: 'type', tsType: `'tvu' | 'ts'` },
      { name: 'size', tsType: 'number' },
    ],
    events: [],
    vModel: null,
    hasDefaultSlot: false,
    namedSlots: [],
  },

  // -------------------------------------------------------------------------
  // MenuList — menu-item group (topbar nav / dropdown / panel body).
  // Source: src/canonical/MenuList.vue. `select` is a Vue custom emit (value in detail[0]).
  {
    name: 'MenuList',
    tag: 'tvu-menu-list',
    canonicalImport: 'MenuList',
    canonicalPath: '../canonical/MenuList.vue',
    figmaCodeComponent: 'MenuList',
    props: [
      // items is a structural object array (contains `{` → audit skips union-body check).
      { name: 'items',       tsType: '{ label: string; value?: string | number; icon?: string; active?: boolean; disabled?: boolean }[]' },
      { name: 'orientation', tsType: `'horizontal' | 'vertical'` },
    ],
    events: [
      { reactHandlerProp: 'onSelect', domEvent: 'select', handlerType: '(value: string | number) => void', detailIndex: 0 },
    ],
    vModel: null,
    hasDefaultSlot: false,
    namedSlots: [],
  },

  // -------------------------------------------------------------------------
  // UserMenu — after-login account menu (avatar → panel; body is a MenuList).
  // Source: src/canonical/UserMenu.vue. action/languageChange carry a value (detail[0]);
  // signOut has no payload. Vue 3.5 dispatches both camelCase + hyphenated event names,
  // so the hyphenated domEvent (`language-change` / `sign-out`) is the safe DOM form.
  {
    name: 'UserMenu',
    tag: 'tvu-user-menu',
    canonicalImport: 'UserMenu',
    canonicalPath: '../canonical/UserMenu.vue',
    figmaCodeComponent: 'UserMenu',
    props: [
      { name: 'name',      tsType: 'string' },
      { name: 'email',     tsType: 'string' },
      { name: 'role',      tsType: 'string' },
      { name: 'image',     tsType: 'string' },
      { name: 'color',     tsType: 'string' },
      { name: 'actions',   tsType: '{ label: string; value?: string | number; icon?: string; active?: boolean; disabled?: boolean }[]' },
      { name: 'languages', tsType: '{ label: string; value: string }[]' },
      { name: 'lang',      tsType: 'string' },
    ],
    events: [
      { reactHandlerProp: 'onAction',         domEvent: 'action',          handlerType: '(value: string | number) => void', detailIndex: 0 },
      { reactHandlerProp: 'onLanguageChange', domEvent: 'language-change', handlerType: '(value: string) => void', detailIndex: 0 },
      { reactHandlerProp: 'onSignOut',        domEvent: 'sign-out',        handlerType: '() => void' },
    ],
    vModel: null,
    hasDefaultSlot: false,
    namedSlots: [],
  },
]
