{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tabs",
  "title": "Tabs",
  "description": "WAI-ARIA tablist, tab, and tabpanel primitives with roving focus, automatic or manual keyboard activation, and default quiet panel context.",
  "registryDependencies": [
    "https://frame12.dev/r/frame12-styles.json",
    "https://frame12.dev/r/panel.json"
  ],
  "files": [
    {
      "path": "ui/tabs.tsx",
      "content": "'use client';\n\nimport * as React from 'react';\n\nexport type TabsOrientation = 'horizontal' | 'vertical';\nexport type TabsDensity = 'comfortable' | 'compact';\nexport type TabsActivationMode = 'automatic' | 'manual';\n\nconst PANEL_VARIANT_CTX_KEY = Symbol.for('frame12.PanelVariantContext');\nconst globalRegistry = globalThis as unknown as Record<\n  symbol,\n  React.Context<'expressive' | 'quiet' | undefined> | undefined\n>;\nconst PanelVariantContext = (globalRegistry[PANEL_VARIANT_CTX_KEY] ??= React.createContext<\n  'expressive' | 'quiet' | undefined\n>(undefined));\n\nconst useIsomorphicLayoutEffect =\n  typeof window !== 'undefined' ? React.useLayoutEffect : React.useEffect;\n\n/**\n * Injective (1-to-1) encoding of arbitrary string values into valid HTML ID tokens.\n * Standard [a-zA-Z0-9-] characters stay readable; '_' becomes '__'; all other\n * code points become '_<hex>_', preventing collisions like \"a b\" vs \"a-b\".\n */\nexport function encodeTabValueForId(value: string): string {\n  if (value.length === 0) return '_empty_';\n  let out = '';\n  for (let i = 0; i < value.length; i++) {\n    const ch = value[i];\n    if (/[a-zA-Z0-9-]/.test(ch)) {\n      out += ch;\n    } else if (ch === '_') {\n      out += '__';\n    } else {\n      const cp = value.codePointAt(i)!;\n      out += `_${cp.toString(16)}_`;\n      if (cp > 0xffff) i++;\n    }\n  }\n  return out;\n}\n\ninterface StaticTabsInfo {\n  enabledValues: string[];\n  triggerIds: Map<string, string>;\n  panelIds: Map<string, string>;\n}\n\nfunction scanStaticTabs(children: React.ReactNode): StaticTabsInfo {\n  const info: StaticTabsInfo = {\n    enabledValues: [],\n    triggerIds: new Map(),\n    panelIds: new Map(),\n  };\n\n  const walk = (node: React.ReactNode) => {\n    React.Children.forEach(node, (child) => {\n      if (!React.isValidElement<Record<string, unknown>>(child)) return;\n      const fnType = child.type as {\n        f12IsTabsTrigger?: boolean;\n        f12IsTabsContent?: boolean;\n      };\n      if (fnType?.f12IsTabsTrigger && typeof child.props.value === 'string') {\n        const val = child.props.value;\n        if (!child.props.disabled) {\n          info.enabledValues.push(val);\n        }\n        if (typeof child.props.id === 'string' && child.props.id.length > 0) {\n          info.triggerIds.set(val, child.props.id);\n        }\n        return;\n      }\n      if (fnType?.f12IsTabsContent && typeof child.props.value === 'string') {\n        const val = child.props.value;\n        if (typeof child.props.id === 'string' && child.props.id.length > 0) {\n          info.panelIds.set(val, child.props.id);\n        }\n      }\n      if (child.props && child.props.children) {\n        walk(child.props.children as React.ReactNode);\n      }\n    });\n  };\n\n  walk(children);\n  return info;\n}\n\ninterface RegisteredTrigger {\n  value: string;\n  disabled: boolean;\n  id: string;\n}\n\ninterface TabsContextValue {\n  baseId: string;\n  value: string;\n  focusedValue: string;\n  setFocusedValue: (val: string) => void;\n  onSelect: (val: string) => void;\n  orientation: TabsOrientation;\n  density: TabsDensity;\n  activationMode: TabsActivationMode;\n  getTabId: (val: string, explicitId?: string) => string;\n  getPanelId: (val: string, explicitId?: string) => string;\n  registerTrigger: (\n    el: HTMLButtonElement,\n    val: string,\n    disabled: boolean,\n    resolvedId: string,\n  ) => void;\n  unregisterTrigger: (el: HTMLButtonElement) => void;\n  registerPanel: (val: string, resolvedId: string) => void;\n  unregisterPanel: (val: string, resolvedId: string) => void;\n}\n\nconst TabsContext = React.createContext<TabsContextValue | null>(null);\n\nfunction useTabsContext(componentName: string): TabsContextValue {\n  const ctx = React.useContext(TabsContext);\n  if (!ctx) {\n    throw new Error(`[FRAME/12 ${componentName}] must be used within <Tabs>.`);\n  }\n  return ctx;\n}\n\nexport interface TabsProps extends React.HTMLAttributes<HTMLDivElement> {\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (value: string) => void;\n  orientation?: TabsOrientation;\n  density?: TabsDensity;\n  activationMode?: TabsActivationMode;\n  idPrefix?: string;\n}\n\nexport const Tabs = React.forwardRef<HTMLDivElement, TabsProps>(function Tabs(\n  {\n    value: controlledValue,\n    defaultValue,\n    onValueChange,\n    orientation = 'horizontal',\n    density = 'comfortable',\n    activationMode = 'automatic',\n    idPrefix,\n    className = '',\n    children,\n    ...rest\n  },\n  ref,\n) {\n  const autoId = React.useId().replace(/:/g, '');\n  const baseId = idPrefix ?? `f12-tabs-${autoId}`;\n  const staticInfo = scanStaticTabs(children);\n  const firstStaticEnabled = staticInfo.enabledValues[0] ?? '';\n\n  const isControlled = controlledValue !== undefined;\n  const hasUserSelectedRef = React.useRef(false);\n  const [uncontrolledValue, setUncontrolledValue] = React.useState<string>(\n    () => defaultValue ?? firstStaticEnabled,\n  );\n  const activeValue = isControlled ? controlledValue : uncontrolledValue;\n  const [focusedValue, setFocusedValue] = React.useState<string>(\n    () => activeValue || firstStaticEnabled,\n  );\n\n  const triggersMapRef = React.useRef<Map<HTMLButtonElement, RegisteredTrigger>>(new Map());\n  const triggersKeyRef = React.useRef<string>('');\n  const [registeredTriggers, setRegisteredTriggers] = React.useState<RegisteredTrigger[]>([]);\n  const [registeredTriggerIds, setRegisteredTriggerIds] = React.useState<Record<string, string>>(\n    {},\n  );\n  const [registeredPanelIds, setRegisteredPanelIds] = React.useState<Record<string, string>>({});\n\n  const syncRegisteredTriggers = React.useCallback(() => {\n    const entries = Array.from(triggersMapRef.current.entries());\n    entries.sort(([a], [b]) => {\n      if (a === b) return 0;\n      const pos = a.compareDocumentPosition(b);\n      return pos & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1;\n    });\n    const ordered = entries.map(([, info]) => info);\n    const nextKey = ordered\n      .map((t) => `${t.value}\\0${t.disabled ? '1' : '0'}\\0${t.id}`)\n      .join('\\n');\n    if (nextKey !== triggersKeyRef.current) {\n      triggersKeyRef.current = nextKey;\n      setRegisteredTriggers(ordered);\n      const idMap: Record<string, string> = {};\n      for (const t of ordered) {\n        idMap[t.value] = t.id;\n      }\n      setRegisteredTriggerIds(idMap);\n    }\n  }, []);\n\n  const registerTrigger = React.useCallback(\n    (el: HTMLButtonElement, val: string, disabled: boolean, resolvedId: string) => {\n      triggersMapRef.current.set(el, { value: val, disabled, id: resolvedId });\n      syncRegisteredTriggers();\n    },\n    [syncRegisteredTriggers],\n  );\n\n  const unregisterTrigger = React.useCallback(\n    (el: HTMLButtonElement) => {\n      if (triggersMapRef.current.delete(el)) {\n        syncRegisteredTriggers();\n      }\n    },\n    [syncRegisteredTriggers],\n  );\n\n  const registerPanel = React.useCallback((val: string, resolvedId: string) => {\n    setRegisteredPanelIds((prev) =>\n      prev[val] === resolvedId ? prev : { ...prev, [val]: resolvedId },\n    );\n  }, []);\n\n  const unregisterPanel = React.useCallback((val: string, resolvedId: string) => {\n    setRegisteredPanelIds((prev) => {\n      if (prev[val] !== resolvedId) return prev;\n      const next = { ...prev };\n      delete next[val];\n      return next;\n    });\n  }, []);\n\n  useIsomorphicLayoutEffect(() => {\n    if (registeredTriggers.length === 0) return;\n    const enabledList = registeredTriggers.filter((t) => !t.disabled);\n    const firstEnabled = enabledList[0]?.value ?? '';\n    if (!isControlled && firstEnabled !== '') {\n      if (\n        uncontrolledValue === '' ||\n        (defaultValue === undefined &&\n          !hasUserSelectedRef.current &&\n          uncontrolledValue !== firstEnabled) ||\n        !registeredTriggers.some((t) => t.value === uncontrolledValue && !t.disabled)\n      ) {\n        setUncontrolledValue(firstEnabled);\n        setFocusedValue(firstEnabled);\n      }\n    }\n  }, [registeredTriggers, isControlled, uncontrolledValue, defaultValue]);\n\n  React.useEffect(() => {\n    setFocusedValue(activeValue);\n  }, [activeValue]);\n\n  const hasMountedTriggers = registeredTriggers.length > 0;\n  const enabledMountedValues = React.useMemo(\n    () => registeredTriggers.filter((t) => !t.disabled).map((t) => t.value),\n    [registeredTriggers],\n  );\n\n  const effectiveFocusedValue = hasMountedTriggers\n    ? enabledMountedValues.includes(focusedValue)\n      ? focusedValue\n      : enabledMountedValues.includes(activeValue)\n        ? activeValue\n        : (enabledMountedValues[0] ?? '')\n    : focusedValue || activeValue || firstStaticEnabled;\n\n  const handleSelect = React.useCallback(\n    (nextValue: string) => {\n      hasUserSelectedRef.current = true;\n      if (!isControlled) {\n        setUncontrolledValue(nextValue);\n      }\n      setFocusedValue(nextValue);\n      onValueChange?.(nextValue);\n    },\n    [isControlled, onValueChange],\n  );\n\n  const getTabId = React.useCallback(\n    (val: string, explicitId?: string) =>\n      explicitId ||\n      registeredTriggerIds[val] ||\n      staticInfo.triggerIds.get(val) ||\n      `${baseId}-tab-${encodeTabValueForId(val)}`,\n    [baseId, registeredTriggerIds, staticInfo.triggerIds],\n  );\n\n  const getPanelId = React.useCallback(\n    (val: string, explicitId?: string) =>\n      explicitId ||\n      registeredPanelIds[val] ||\n      staticInfo.panelIds.get(val) ||\n      `${baseId}-panel-${encodeTabValueForId(val)}`,\n    [baseId, registeredPanelIds, staticInfo.panelIds],\n  );\n\n  const ctxValue = React.useMemo<TabsContextValue>(\n    () => ({\n      baseId,\n      value: activeValue,\n      focusedValue: effectiveFocusedValue,\n      setFocusedValue,\n      onSelect: handleSelect,\n      orientation,\n      density,\n      activationMode,\n      getTabId,\n      getPanelId,\n      registerTrigger,\n      unregisterTrigger,\n      registerPanel,\n      unregisterPanel,\n    }),\n    [\n      baseId,\n      activeValue,\n      effectiveFocusedValue,\n      handleSelect,\n      orientation,\n      density,\n      activationMode,\n      getTabId,\n      getPanelId,\n      registerTrigger,\n      unregisterTrigger,\n      registerPanel,\n      unregisterPanel,\n    ],\n  );\n\n  const isCompact = density === 'compact';\n\n  return (\n    <TabsContext.Provider value={ctxValue}>\n      <div\n        ref={ref}\n        className={`f12-tabs${isCompact ? ' f12-tabs-compact' : ''}${orientation === 'vertical' ? ' f12-tabs-vertical' : ''}${className ? ` ${className}` : ''}`}\n        data-orientation={orientation}\n        data-density={isCompact ? 'compact' : undefined}\n        {...rest}\n      >\n        {children}\n      </div>\n    </TabsContext.Provider>\n  );\n});\n\nexport interface TabsListProps extends React.HTMLAttributes<HTMLDivElement> {\n  ariaLabel?: string;\n}\n\nexport const TabsList = React.forwardRef<HTMLDivElement, TabsListProps>(function TabsList(\n  { ariaLabel, 'aria-label': userAriaLabel, className = '', onBlur, children, ...rest },\n  forwardedRef,\n) {\n  const { value, setFocusedValue, orientation, density } = useTabsContext('TabsList');\n  const listRef = React.useRef<HTMLDivElement | null>(null);\n  const isCompact = density === 'compact';\n\n  const setRefs = React.useCallback(\n    (node: HTMLDivElement | null) => {\n      listRef.current = node;\n      if (typeof forwardedRef === 'function') {\n        forwardedRef(node);\n      } else if (forwardedRef) {\n        (forwardedRef as React.MutableRefObject<HTMLDivElement | null>).current = node;\n      }\n    },\n    [forwardedRef],\n  );\n\n  return (\n    <div\n      ref={setRefs}\n      role=\"tablist\"\n      aria-orientation={orientation}\n      aria-label={userAriaLabel ?? ariaLabel}\n      className={`f12-tabs-list${isCompact ? ' f12-tabs-list-compact' : ''}${className ? ` ${className}` : ''}`}\n      onBlur={(e) => {\n        onBlur?.(e);\n        if (!listRef.current?.contains(e.relatedTarget as Node | null)) {\n          setFocusedValue(value);\n        }\n      }}\n      {...rest}\n    >\n      {children}\n    </div>\n  );\n});\n\nexport interface TabsTriggerProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {\n  value: string;\n}\n\nexport const TabsTrigger = React.forwardRef<HTMLButtonElement, TabsTriggerProps>(\n  function TabsTrigger(\n    { id, value, disabled, className = '', onFocus, onClick, onKeyDown, children, ...rest },\n    forwardedRef,\n  ) {\n    const {\n      baseId,\n      value: activeValue,\n      focusedValue,\n      setFocusedValue,\n      onSelect,\n      orientation,\n      activationMode,\n      getTabId,\n      getPanelId,\n      registerTrigger,\n      unregisterTrigger,\n    } = useTabsContext('TabsTrigger');\n    const btnRef = React.useRef<HTMLButtonElement | null>(null);\n    const isSelected = activeValue === value;\n    const isFocused = (focusedValue || activeValue) === value;\n    const defaultTabId = `${baseId}-tab-${encodeTabValueForId(value)}`;\n    const tabId = id || getTabId(value, id) || defaultTabId;\n    const panelId = getPanelId(value);\n\n    const setRefs = React.useCallback(\n      (node: HTMLButtonElement | null) => {\n        btnRef.current = node;\n        if (typeof forwardedRef === 'function') {\n          forwardedRef(node);\n        } else if (forwardedRef) {\n          (forwardedRef as React.MutableRefObject<HTMLButtonElement | null>).current = node;\n        }\n      },\n      [forwardedRef],\n    );\n\n    const resolvedOwnTabId = id || defaultTabId;\n    useIsomorphicLayoutEffect(() => {\n      const node = btnRef.current;\n      if (!node) return;\n      registerTrigger(node, value, Boolean(disabled), resolvedOwnTabId);\n      return () => {\n        unregisterTrigger(node);\n      };\n    }, [registerTrigger, unregisterTrigger, value, disabled, resolvedOwnTabId]);\n\n    return (\n      <button\n        ref={setRefs}\n        id={tabId}\n        type=\"button\"\n        role=\"tab\"\n        aria-selected={isSelected}\n        aria-controls={panelId}\n        data-state={isSelected ? 'active' : 'inactive'}\n        data-value={value}\n        disabled={disabled}\n        tabIndex={disabled ? -1 : isFocused ? 0 : -1}\n        className={`f12-tabs-trigger${className ? ` ${className}` : ''}`}\n        onFocus={(e) => {\n          onFocus?.(e);\n          setFocusedValue(value);\n        }}\n        onClick={(e) => {\n          onClick?.(e);\n          if (!e.defaultPrevented && !disabled) {\n            onSelect(value);\n          }\n        }}\n        onKeyDown={(e) => {\n          onKeyDown?.(e);\n          if (\n            e.defaultPrevented ||\n            e.altKey ||\n            e.ctrlKey ||\n            e.metaKey ||\n            e.shiftKey\n          ) {\n            return;\n          }\n          const listEl = e.currentTarget.closest('[role=\"tablist\"]');\n          if (!listEl) return;\n          const enabledTabs = Array.from(\n            listEl.querySelectorAll<HTMLButtonElement>('[role=\"tab\"]:not(:disabled)'),\n          ).filter((el) => el.closest('[role=\"tablist\"]') === listEl);\n          if (enabledTabs.length === 0) return;\n          const currentIdx = enabledTabs.indexOf(e.currentTarget);\n          if (currentIdx === -1) return;\n\n          const isRtl =\n            orientation === 'horizontal' &&\n            typeof window !== 'undefined' &&\n            window.getComputedStyle(listEl).direction === 'rtl';\n\n          let nextIdx = -1;\n          const isForward =\n            orientation === 'vertical'\n              ? e.key === 'ArrowDown' || e.key === 'ArrowRight'\n              : (isRtl ? e.key === 'ArrowLeft' : e.key === 'ArrowRight') || e.key === 'ArrowDown';\n          const isBackward =\n            orientation === 'vertical'\n              ? e.key === 'ArrowUp' || e.key === 'ArrowLeft'\n              : (isRtl ? e.key === 'ArrowRight' : e.key === 'ArrowLeft') || e.key === 'ArrowUp';\n\n          if (isForward) {\n            nextIdx = (currentIdx + 1) % enabledTabs.length;\n          } else if (isBackward) {\n            nextIdx = (currentIdx - 1 + enabledTabs.length) % enabledTabs.length;\n          } else if (e.key === 'Home') {\n            nextIdx = 0;\n          } else if (e.key === 'End') {\n            nextIdx = enabledTabs.length - 1;\n          }\n\n          if (nextIdx !== -1) {\n            e.preventDefault();\n            const targetBtn = enabledTabs[nextIdx];\n            const targetValue = targetBtn.getAttribute('data-value');\n            if (targetValue !== null) {\n              setFocusedValue(targetValue);\n              if (activationMode === 'automatic') {\n                onSelect(targetValue);\n              }\n            }\n            targetBtn.focus();\n          }\n        }}\n        {...rest}\n      >\n        {children}\n      </button>\n    );\n  },\n);\n\n(TabsTrigger as unknown as { f12IsTabsTrigger?: boolean }).f12IsTabsTrigger = true;\n\nexport interface TabsContentProps extends React.HTMLAttributes<HTMLDivElement> {\n  value: string;\n  forceMount?: boolean;\n}\n\nexport const TabsContent = React.forwardRef<HTMLDivElement, TabsContentProps>(\n  function TabsContent({ id, value, forceMount = false, className = '', children, ...rest }, ref) {\n    const {\n      baseId,\n      value: activeValue,\n      getTabId,\n      getPanelId,\n      registerPanel,\n      unregisterPanel,\n    } = useTabsContext('TabsContent');\n    const isSelected = activeValue === value;\n    const defaultPanelId = `${baseId}-panel-${encodeTabValueForId(value)}`;\n    const resolvedOwnPanelId = id || defaultPanelId;\n    const panelId = id || getPanelId(value, id) || defaultPanelId;\n    const tabId = getTabId(value);\n\n    useIsomorphicLayoutEffect(() => {\n      registerPanel(value, resolvedOwnPanelId);\n      return () => {\n        unregisterPanel(value, resolvedOwnPanelId);\n      };\n    }, [registerPanel, unregisterPanel, value, resolvedOwnPanelId]);\n\n    return (\n      <PanelVariantContext.Provider value=\"quiet\">\n        <div\n          ref={ref}\n          id={panelId}\n          role=\"tabpanel\"\n          aria-labelledby={tabId}\n          hidden={!isSelected}\n          tabIndex={isSelected ? 0 : undefined}\n          data-state={isSelected ? 'active' : 'inactive'}\n          className={`f12-tabs-content${className ? ` ${className}` : ''}`}\n          {...rest}\n        >\n          {isSelected || forceMount ? children : null}\n        </div>\n      </PanelVariantContext.Provider>\n    );\n  },\n);\n\n(TabsContent as unknown as { f12IsTabsContent?: boolean }).f12IsTabsContent = true;\n",
      "type": "registry:ui",
      "target": "components/frame12/tabs.tsx"
    }
  ],
  "type": "registry:ui"
}