{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "vertical-tabs",
  "title": "Vertical Tabs",
  "type": "registry:ui",
  "description": "Settings-page navigation pattern — labels stack on the left, content panel on the right. Same API as Tabs but with a vertical orientation. Use for dense, multi-section settings UIs.",
  "categories": [
    "navigation"
  ],
  "files": [
    {
      "path": "packages/elements-lit/src/components/vertical-tabs/vertical-tabs.ts",
      "content": "import { LitElement, css, html, nothing } from 'lit'\nimport { styleMap } from 'lit/directives/style-map.js'\nimport { cn } from '../../lib/utils'\nimport { tailwind } from '../../lib/styles'\nimport { ThemeController } from '../../lib/theme'\n\nconst trueByDefault = { fromAttribute: (v: string | null) => v !== 'false', toAttribute: () => null }\n\n/**\n * <uip-vertical-tabs> — Root container for vertical tabs layout.\n */\nexport class UipVerticalTabs extends LitElement {\n  static styles = [tailwind, css`:host { display: block; width: 100%; }`]\n\n  static properties = {\n    value: { reflect: true },\n    defaultValue: { attribute: 'default-value' },\n  }\n\n  value = ''\n  defaultValue = ''\n\n  constructor() {\n    super()\n    new ThemeController(this)\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'vertical-tabs')\n    if (!this.value && this.defaultValue) {\n      this.value = this.defaultValue\n    }\n  }\n\n  protected firstUpdated() {\n    if (!this.value) {\n      const firstTrigger = this.querySelector('uip-vertical-tabs-trigger') as UipVerticalTabsTrigger | null\n      if (firstTrigger && firstTrigger.value) {\n        this.value = firstTrigger.value\n      }\n    }\n    this.notifyChildren()\n  }\n\n  setValue(val: string) {\n    if (this.value === val) return\n    this.value = val\n    this.dispatchEvent(new CustomEvent('value-change', { detail: { value: val }, bubbles: true, composed: true }))\n    this.notifyChildren()\n  }\n\n  private notifyChildren() {\n    const triggers = this.querySelectorAll('uip-vertical-tabs-trigger') as NodeListOf<UipVerticalTabsTrigger>\n    triggers.forEach((t) => t.syncActive(this.value))\n\n    const contents = this.querySelectorAll('uip-vertical-tabs-content') as NodeListOf<UipVerticalTabsContent>\n    contents.forEach((c) => c.syncActive(this.value))\n\n    const list = this.querySelector('uip-vertical-tabs-list') as UipVerticalTabsList | null\n    list?.requestIndicatorUpdate()\n  }\n\n  render() {\n    return html`\n      <div part=\"base\" class=\"flex w-full gap-6\">\n        <slot></slot>\n      </div>\n    `\n  }\n}\n\n/**\n * <uip-vertical-tabs-list> — Left sidebar list of triggers with sliding indicator.\n */\nexport class UipVerticalTabsList extends LitElement {\n  static styles = [tailwind, css`:host { display: block; flex-shrink: 0; }`]\n\n  static properties = {\n    animated: { converter: trueByDefault },\n    indicatorStyle: { state: true },\n  }\n\n  animated = true\n  private indicatorStyle: Record<string, string> = { opacity: '0' }\n  private firstPosition = true\n\n  constructor() {\n    super()\n    new ThemeController(this)\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'vertical-tabs-list')\n    this.setAttribute('data-animated', this.animated ? 'true' : 'false')\n    this.setAttribute('role', 'tablist')\n    this.setAttribute('aria-orientation', 'vertical')\n  }\n\n  protected updated(changed: Map<string, unknown>) {\n    if (changed.has('animated')) {\n      this.setAttribute('data-animated', this.animated ? 'true' : 'false')\n      this.updateIndicator()\n    }\n  }\n\n  requestIndicatorUpdate() {\n    setTimeout(() => this.updateIndicator(), 0)\n  }\n\n  private updateIndicator() {\n    if (!this.animated) {\n      this.indicatorStyle = { opacity: '0' }\n      return\n    }\n\n    const activeTrigger = this.querySelector('uip-vertical-tabs-trigger[data-state=\"active\"]') as HTMLElement | null\n    if (!activeTrigger) {\n      this.indicatorStyle = { opacity: '0' }\n      return\n    }\n\n    const listRect = this.getBoundingClientRect()\n    const activeRect = activeTrigger.getBoundingClientRect()\n    const left = activeRect.left - listRect.left\n    const top = activeRect.top - listRect.top\n\n    this.indicatorStyle = {\n      width: `${activeRect.width}px`,\n      height: `${activeRect.height}px`,\n      transform: `translate3d(${left}px, ${top}px, 0)`,\n      opacity: '1',\n      transition: this.firstPosition ? 'none' : 'transform 220ms cubic-bezier(0.22, 1, 0.36, 1), height 220ms cubic-bezier(0.22, 1, 0.36, 1)',\n    }\n    this.firstPosition = false\n  }\n\n  render() {\n    return html`\n      <div\n        part=\"list\"\n        class=\"group/list border-border relative flex w-56 shrink-0 flex-col gap-0.5 border-r pr-3\"\n      >\n        ${this.animated\n          ? html`\n              <span\n                part=\"indicator\"\n                data-slot=\"vertical-tabs-indicator\"\n                aria-hidden=\"true\"\n                class=\"bg-muted pointer-events-none absolute top-0 left-0 z-0 rounded-md will-change-transform\"\n                style=${styleMap(this.indicatorStyle)}\n              >\n                <span class=\"bg-primary absolute inset-y-1 left-0 w-0.5 rounded-full\"></span>\n              </span>\n            `\n          : nothing}\n        <slot @slotchange=${() => this.requestIndicatorUpdate()}></slot>\n      </div>\n    `\n  }\n}\n\n/**\n * <uip-vertical-tabs-section> — Category heading between tabs.\n */\nexport class UipVerticalTabsSection extends LitElement {\n  static styles = [tailwind, css`:host { display: block; }`]\n\n  static properties = {\n    label: { type: String },\n  }\n\n  label = ''\n\n  constructor() {\n    super()\n    new ThemeController(this)\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'vertical-tabs-section')\n  }\n\n  render() {\n    return html`\n      <div\n        part=\"section\"\n        class=\"text-muted-foreground mt-3 mb-1 px-2 text-xs font-medium tracking-wider uppercase first:mt-0\"\n      >\n        ${this.label}<slot></slot>\n      </div>\n    `\n  }\n}\n\n/**\n * <uip-vertical-tabs-trigger> — Tab trigger button.\n */\nexport class UipVerticalTabsTrigger extends LitElement {\n  static styles = [tailwind, css`:host { display: block; width: 100%; }`]\n\n  static properties = {\n    value: { reflect: true },\n    disabled: { type: Boolean, reflect: true },\n    active: { state: true },\n  }\n\n  value = ''\n  disabled = false\n  private active = false\n\n  constructor() {\n    super()\n    new ThemeController(this)\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'vertical-tabs-trigger')\n    this.setAttribute('role', 'tab')\n    this.setAttribute('tabindex', this.disabled ? '-1' : '0')\n    this.addEventListener('click', this.handleClick.bind(this))\n  }\n\n  syncActive(parentValue: string) {\n    this.active = this.value === parentValue\n    this.setAttribute('data-state', this.active ? 'active' : 'inactive')\n    this.setAttribute('aria-selected', this.active ? 'true' : 'false')\n  }\n\n  private handleClick(e: MouseEvent) {\n    if (this.disabled) {\n      e.preventDefault()\n      return\n    }\n    const root = this.closest('uip-vertical-tabs') as UipVerticalTabs | null\n    root?.setValue(this.value)\n  }\n\n  render() {\n    const list = this.closest('uip-vertical-tabs-list') as UipVerticalTabsList | null\n    const isAnimated = list?.animated !== false\n\n    return html`\n      <button\n        type=\"button\"\n        part=\"button\"\n        ?disabled=${this.disabled}\n        data-state=${this.active ? 'active' : 'inactive'}\n        class=${cn(\n          'group/trigger text-muted-foreground relative z-10 flex w-full items-center gap-2 rounded-md px-3 py-2 text-left text-sm font-medium transition-[color,background-color] duration-150 outline-none',\n          'hover:bg-muted/60 hover:text-foreground',\n          'focus-visible:ring-ring/50 focus-visible:ring-2 focus-visible:outline-none',\n          'disabled:pointer-events-none disabled:opacity-50 cursor-pointer disabled:cursor-not-allowed',\n          this.active && 'text-foreground font-semibold',\n          !isAnimated && this.active && 'bg-muted',\n          !isAnimated &&\n            \"before:bg-primary before:pointer-events-none before:absolute before:inset-y-1 before:left-0 before:w-0.5 before:rounded-full before:opacity-0 before:content-['']\",\n          !isAnimated && this.active && 'before:opacity-100',\n          '[&_svg]:size-4 [&_svg]:shrink-0',\n        )}\n      >\n        <slot></slot>\n      </button>\n    `\n  }\n}\n\n/**\n * <uip-vertical-tabs-content> — Tab panel content.\n */\nexport class UipVerticalTabsContent extends LitElement {\n  static styles = [tailwind, css`:host { display: block; flex: 1 1 0%; }`]\n\n  static properties = {\n    value: { reflect: true },\n    active: { state: true },\n  }\n\n  value = ''\n  private active = false\n\n  constructor() {\n    super()\n    new ThemeController(this)\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'vertical-tabs-content')\n    this.setAttribute('role', 'tabpanel')\n  }\n\n  syncActive(parentValue: string) {\n    this.active = this.value === parentValue\n    this.setAttribute('data-state', this.active ? 'active' : 'inactive')\n    this.style.display = this.active ? 'block' : 'none'\n  }\n\n  render() {\n    return html`\n      <div\n        part=\"content\"\n        class=\"ring-offset-background focus-visible:ring-ring/50 flex-1 focus-visible:ring-2 focus-visible:outline-none\"\n      >\n        <slot></slot>\n      </div>\n    `\n  }\n}\n\ncustomElements.get('uip-vertical-tabs') || customElements.define('uip-vertical-tabs', UipVerticalTabs)\ncustomElements.get('uip-vertical-tabs-list') || customElements.define('uip-vertical-tabs-list', UipVerticalTabsList)\ncustomElements.get('uip-vertical-tabs-section') || customElements.define('uip-vertical-tabs-section', UipVerticalTabsSection)\ncustomElements.get('uip-vertical-tabs-trigger') || customElements.define('uip-vertical-tabs-trigger', UipVerticalTabsTrigger)\ncustomElements.get('uip-vertical-tabs-content') || customElements.define('uip-vertical-tabs-content', UipVerticalTabsContent)\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'uip-vertical-tabs': UipVerticalTabs\n    'uip-vertical-tabs-list': UipVerticalTabsList\n    'uip-vertical-tabs-section': UipVerticalTabsSection\n    'uip-vertical-tabs-trigger': UipVerticalTabsTrigger\n    'uip-vertical-tabs-content': UipVerticalTabsContent\n  }\n}\n",
      "type": "registry:file",
      "target": "src/uip/components/vertical-tabs/vertical-tabs.ts"
    }
  ],
  "dependencies": [
    "lit"
  ],
  "registryDependencies": [
    "https://uipkge.dev/r/lit/lit-core.json"
  ]
}
