{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "vertical-tabs",
  "title": "Vertical Tabs",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-svelte/components/vertical-tabs/VerticalTabs.svelte",
      "content": "<script lang=\"ts\" module>\n  import { getContext, setContext } from 'svelte'\n  import type { Snippet } from 'svelte'\n  import type { HTMLAttributes } from 'svelte/elements'\n\n  export interface VerticalTabsProps extends HTMLAttributes<HTMLDivElement> {\n    children?: Snippet\n    /** Controlled active value. Omit for uncontrolled mode with `defaultValue`. */\n    value?: string\n    /** Initial active value in uncontrolled mode. */\n    defaultValue?: string\n    onValueChange?: (value: string) => void\n  }\n\n  export interface VerticalTabsContext {\n    getValue: () => string | undefined\n    select: (value: string) => void\n    rootId: string\n  }\n\n  const VERTICAL_TABS_KEY = Symbol('vertical-tabs')\n\n  export function setVerticalTabsContext(ctx: VerticalTabsContext) {\n    setContext(VERTICAL_TABS_KEY, ctx)\n  }\n\n  export function getVerticalTabsContext(component: string): VerticalTabsContext {\n    const ctx = getContext<VerticalTabsContext | undefined>(VERTICAL_TABS_KEY)\n    if (!ctx) throw new Error(`<${component}> must be used inside <VerticalTabs>`)\n    return ctx\n  }\n\n  /** Keep value-derived element ids valid when values contain spaces or symbols. */\n  export function sanitizeId(value: string): string {\n    return value.replace(/[^a-zA-Z0-9_-]/g, '-')\n  }\n</script>\n\n<script lang=\"ts\">\n  import { cn } from '$lib/utils'\n\n  let { children, value = $bindable(), defaultValue, onValueChange, class: className, ...restProps }: VerticalTabsProps =\n    $props()\n\n  const rootId = $props.id()\n\n  let internal = $state(defaultValue)\n  const current = $derived(value ?? internal)\n\n  function select(v: string) {\n    if (value === undefined) internal = v\n    onValueChange?.(v)\n  }\n\n  setVerticalTabsContext({\n    getValue: () => current,\n    select,\n    rootId,\n  })\n</script>\n\n<div\n  data-uipkge\n  data-slot=\"vertical-tabs\"\n  data-orientation=\"vertical\"\n  class={cn('flex w-full gap-6', className)}\n  {...restProps}\n>\n  {@render children?.()}\n</div>\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/vertical-tabs/VerticalTabs.svelte"
    },
    {
      "path": "packages/registry-svelte/components/vertical-tabs/VerticalTabsList.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { Snippet } from 'svelte'\n  import type { HTMLAttributes } from 'svelte/elements'\n\n  export interface VerticalTabsListProps extends HTMLAttributes<HTMLDivElement> {\n    children?: Snippet\n    /** Wrap keyboard focus from last trigger to first (and vice versa). Default true. */\n    loop?: boolean\n    /** Enable sliding active indicator (default true). When false, active chrome paints on the trigger. */\n    animated?: boolean\n  }\n</script>\n\n<script lang=\"ts\">\n  import { cn } from '$lib/utils'\n  import { getVerticalTabsContext } from './VerticalTabs.svelte'\n\n  let { children, loop = true, animated = true, class: className, ...restProps }: VerticalTabsListProps = $props()\n\n  const ctx = getVerticalTabsContext('VerticalTabsList')\n\n  let listEl: HTMLDivElement | null = $state(null)\n  let indicatorStyle = $state<Record<string, string>>({ opacity: '0' })\n  let firstPosition = true\n\n  function motionSafeTransition() {\n    if (typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches) {\n      return 'none'\n    }\n    return firstPosition\n      ? 'none'\n      : 'transform 220ms cubic-bezier(0.22, 1, 0.36, 1), width 220ms cubic-bezier(0.22, 1, 0.36, 1), height 220ms cubic-bezier(0.22, 1, 0.36, 1)'\n  }\n\n  function updateIndicator() {\n    if (!animated) return\n    const root = listEl\n    if (!root) return\n    const active = root.querySelector<HTMLElement>('[data-slot=\"vertical-tabs-trigger\"][data-state=\"active\"]')\n    if (!active) {\n      indicatorStyle = { opacity: '0' }\n      return\n    }\n\n    const listRect = root.getBoundingClientRect()\n    const activeRect = active.getBoundingClientRect()\n    const left = activeRect.left - listRect.left + root.scrollLeft\n    const top = activeRect.top - listRect.top + root.scrollTop\n    const transition = motionSafeTransition()\n\n    // Full active surface slides (muted pill); primary rail is nested absolute so it stays inset-y-1.\n    indicatorStyle = {\n      width: `${activeRect.width}px`,\n      height: `${activeRect.height}px`,\n      transform: `translate3d(${left}px, ${top}px, 0)`,\n      opacity: '1',\n      transition,\n    }\n    firstPosition = false\n  }\n\n  function styleAttr(style: Record<string, string>): string {\n    return Object.entries(style)\n      .map(([k, v]) => `${k.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)}: ${v}`)\n      .join('; ')\n  }\n\n  $effect(() => {\n    const root = listEl\n    const on = animated\n    firstPosition = true\n    if (!root || !on) {\n      indicatorStyle = { opacity: '0' }\n      return\n    }\n\n    const ro = new ResizeObserver(() => updateIndicator())\n    ro.observe(root)\n    root.querySelectorAll('[data-slot=\"vertical-tabs-trigger\"]').forEach((el) => ro.observe(el))\n\n    const mo = new MutationObserver((mutations) => {\n      // Re-observe new triggers without treating parent re-renders as first paint.\n      for (const m of mutations) {\n        if (m.type === 'childList') {\n          root.querySelectorAll('[data-slot=\"vertical-tabs-trigger\"]').forEach((el) => ro.observe(el))\n        }\n      }\n      queueMicrotask(updateIndicator)\n    })\n    mo.observe(root, {\n      attributes: true,\n      attributeFilter: ['data-state'],\n      subtree: true,\n      childList: true,\n    })\n\n    updateIndicator()\n    // The scroll container may settle a frame after mount (fonts, layout).\n    const raf = requestAnimationFrame(() => updateIndicator())\n\n    return () => {\n      ro.disconnect()\n      mo.disconnect()\n      cancelAnimationFrame(raf)\n    }\n  })\n\n  function enabledTriggers(root: HTMLElement): HTMLElement[] {\n    return Array.from(\n      root.querySelectorAll<HTMLElement>('[data-slot=\"vertical-tabs-trigger\"]:not([data-disabled])'),\n    ).filter((el) => !el.hasAttribute('disabled'))\n  }\n\n  function handleKeydown(e: KeyboardEvent) {\n    const root = listEl\n    if (!root) return\n    const target = e.target as HTMLElement | null\n    if (!target?.closest('[data-slot=\"vertical-tabs-trigger\"]')) return\n\n    const triggers = enabledTriggers(root)\n    const idx = triggers.indexOf(target.closest('[data-slot=\"vertical-tabs-trigger\"]') as HTMLElement)\n    if (idx < 0) return\n\n    let next = -1\n    if (e.key === 'ArrowDown' || e.key === 'ArrowRight') next = idx + 1\n    else if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') next = idx - 1\n    else if (e.key === 'Home') next = 0\n    else if (e.key === 'End') next = triggers.length - 1\n    else return\n\n    e.preventDefault()\n    if (next < 0) next = loop ? triggers.length - 1 : 0\n    if (next >= triggers.length) next = loop ? 0 : triggers.length - 1\n    const el = triggers[next]\n    if (!el) return\n    el.focus()\n    // Automatic activation, like reka-ui tabs: focus selects.\n    const v = el.getAttribute('data-value')\n    if (v) ctx.select(v)\n  }\n</script>\n\n<div\n  bind:this={listEl}\n  data-uipkge\n  data-slot=\"vertical-tabs-list\"\n  data-animated={animated ? 'true' : 'false'}\n  role=\"tablist\"\n  aria-orientation=\"vertical\"\n  class={cn('group/list border-border relative flex w-56 shrink-0 flex-col gap-0.5 border-r pr-3', className)}\n  onkeydown={handleKeydown}\n  {...restProps}\n>\n  {#if animated}\n    <span\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={styleAttr(indicatorStyle)}\n    >\n      <!-- Primary rail stays inset relative to the sliding surface (matches prior trigger chrome). -->\n      <span class=\"bg-primary absolute inset-y-1 left-0 w-0.5 rounded-full\"></span>\n    </span>\n  {/if}\n  {@render children?.()}\n</div>\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/vertical-tabs/VerticalTabsList.svelte"
    },
    {
      "path": "packages/registry-svelte/components/vertical-tabs/VerticalTabsSection.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { HTMLAttributes } from 'svelte/elements'\n\n  export interface VerticalTabsSectionProps extends HTMLAttributes<HTMLDivElement> {\n    label: string\n  }\n</script>\n\n<script lang=\"ts\">\n  import { cn } from '$lib/utils'\n\n  let { label, class: className, ...restProps }: VerticalTabsSectionProps = $props()\n</script>\n\n<div\n  data-uipkge\n  data-slot=\"vertical-tabs-section\"\n  class={cn('text-muted-foreground mt-3 mb-1 px-2 text-xs font-medium tracking-wider uppercase first:mt-0', className)}\n  {...restProps}\n>\n  {label}\n</div>\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/vertical-tabs/VerticalTabsSection.svelte"
    },
    {
      "path": "packages/registry-svelte/components/vertical-tabs/VerticalTabsTrigger.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { Snippet } from 'svelte'\n  import type { HTMLButtonAttributes } from 'svelte/elements'\n\n  export interface VerticalTabsTriggerProps extends HTMLButtonAttributes {\n    children?: Snippet\n    value: string\n    disabled?: boolean\n  }\n</script>\n\n<script lang=\"ts\">\n  import { cn } from '$lib/utils'\n  import { getVerticalTabsContext, sanitizeId } from './VerticalTabs.svelte'\n\n  let { children, value, disabled = false, class: className, ...restProps }: VerticalTabsTriggerProps = $props()\n\n  const ctx = getVerticalTabsContext('VerticalTabsTrigger')\n\n  const active = $derived(ctx.getValue() === value)\n  const triggerId = $derived(`${ctx.rootId}-trigger-${sanitizeId(value)}`)\n  const contentId = $derived(`${ctx.rootId}-content-${sanitizeId(value)}`)\n\n  function handleClick() {\n    if (!disabled) ctx.select(value)\n  }\n</script>\n\n<button\n  type=\"button\"\n  data-uipkge\n  data-slot=\"vertical-tabs-trigger\"\n  data-value={value}\n  data-state={active ? 'active' : 'inactive'}\n  data-disabled={disabled ? '' : undefined}\n  id={triggerId}\n  role=\"tab\"\n  aria-selected={active}\n  aria-controls={contentId}\n  tabindex={active ? 0 : -1}\n  {disabled}\n  class={cn(\n    // z-10 keeps label above the sliding indicator; active surface paints on the list\n    // indicator when the parent list has data-animated=true. Static active chrome\n    // restores when data-animated=false (group-data variants below).\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',\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',\n    'data-[state=active]:text-foreground',\n    'group-data-[animated=false]/list:data-[state=active]:bg-muted',\n    // Static primary rail (only when list animation is off).\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    'group-data-[animated=false]/list:data-[state=active]:before:opacity-100',\n    '[&>svg]:size-4 [&>svg]:shrink-0',\n    className,\n  )}\n  onclick={handleClick}\n  {...restProps}\n>\n  {@render children?.()}\n</button>\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/vertical-tabs/VerticalTabsTrigger.svelte"
    },
    {
      "path": "packages/registry-svelte/components/vertical-tabs/VerticalTabsContent.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { Snippet } from 'svelte'\n  import type { HTMLAttributes } from 'svelte/elements'\n\n  export interface VerticalTabsContentProps extends HTMLAttributes<HTMLDivElement> {\n    children?: Snippet\n    value: string\n    forceMount?: boolean\n  }\n</script>\n\n<script lang=\"ts\">\n  import { cn } from '$lib/utils'\n  import { getVerticalTabsContext, sanitizeId } from './VerticalTabs.svelte'\n\n  let { children, value, forceMount = false, class: className, ...restProps }: VerticalTabsContentProps = $props()\n\n  const ctx = getVerticalTabsContext('VerticalTabsContent')\n\n  const active = $derived(ctx.getValue() === value)\n  const triggerId = $derived(`${ctx.rootId}-trigger-${sanitizeId(value)}`)\n  const contentId = $derived(`${ctx.rootId}-content-${sanitizeId(value)}`)\n</script>\n\n{#if forceMount || active}\n  <div\n    data-uipkge\n    data-slot=\"vertical-tabs-content\"\n    data-state={active ? 'active' : 'inactive'}\n    id={contentId}\n    role=\"tabpanel\"\n    aria-labelledby={triggerId}\n    tabindex=\"0\"\n    hidden={forceMount && !active ? true : undefined}\n    class={cn(\n      'ring-offset-background focus-visible:ring-ring/50 flex-1 focus-visible:ring-2 focus-visible:outline-none',\n      className,\n    )}\n    {...restProps}\n  >\n    {@render children?.()}\n  </div>\n{/if}\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/vertical-tabs/VerticalTabsContent.svelte"
    },
    {
      "path": "packages/registry-svelte/components/vertical-tabs/index.ts",
      "content": "export { default as VerticalTabs, type VerticalTabsProps } from './VerticalTabs.svelte'\nexport { default as VerticalTabsList, type VerticalTabsListProps } from './VerticalTabsList.svelte'\nexport { default as VerticalTabsSection, type VerticalTabsSectionProps } from './VerticalTabsSection.svelte'\nexport { default as VerticalTabsTrigger, type VerticalTabsTriggerProps } from './VerticalTabsTrigger.svelte'\nexport { default as VerticalTabsContent, type VerticalTabsContentProps } from './VerticalTabsContent.svelte'\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/vertical-tabs/index.ts"
    }
  ],
  "dependencies": [],
  "devDependencies": [],
  "registryDependencies": [],
  "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"
  ]
}