{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "components-tabs",
  "type": "registry:component",
  "title": "Tabs",
  "description": "A roving-focus tab list and the panels it switches",
  "author": "sudo-kw <https://github.com/sudo-kw>",
  "dependencies": [
    "@gpuix/native@0.11.0",
    "@gpuix/react@0.11.0",
    "effect@4.1.2",
    "react@^19.2.4"
  ],
  "registryDependencies": [
    "https://gpuixui.sudokw.com/gpuix-motion.json",
    "https://gpuixui.sudokw.com/gpuix-tokens.json",
    "https://gpuixui.sudokw.com/gpuix-tw.json"
  ],
  "files": [
    {
      "path": "packages/components/src/internal/collection.ts",
      "target": "@ui/gpuixui/components/internal/collection.ts",
      "type": "registry:lib",
      "content": "import * as Arr from \"effect/Array\";\nimport * as Option from \"effect/Option\";\nimport { pipe } from \"effect\";\n\nexport interface CollectionItem {\n  readonly id: number;\n  readonly disabled: boolean;\n}\n\nexport interface Collection {\n  readonly register: (item: CollectionItem) => () => void;\n\n  readonly items: () => ReadonlyArray<CollectionItem>;\n}\n\nexport interface ValueItem extends CollectionItem {\n  readonly value: string;\n}\n\nexport const upsertValue = <Item extends ValueItem>(\n  items: ReadonlyArray<Item>,\n  item: Item,\n): ReadonlyArray<Item> =>\n  Arr.some(items, (existing) => existing.value === item.value)\n    ? Arr.map(items, (existing) =>\n        existing.value === item.value ? item : existing,\n      )\n    : Arr.append(items, item);\n\nexport const insert = (\n  items: ReadonlyArray<CollectionItem>,\n  item: CollectionItem,\n): ReadonlyArray<CollectionItem> =>\n  Arr.some(items, (existing) => existing.id === item.id)\n    ? Arr.map(items, (existing) => (existing.id === item.id ? item : existing))\n    : Arr.append(items, item);\n\nexport const remove = (\n  items: ReadonlyArray<CollectionItem>,\n  id: number,\n): ReadonlyArray<CollectionItem> =>\n  Arr.filter(items, (existing) => existing.id !== id);\n\nexport const enabledIndexes = (\n  items: ReadonlyArray<CollectionItem>,\n): ReadonlyArray<number> =>\n  Arr.map(\n    Arr.filter(\n      Arr.map(items, (item, index) => ({ index, disabled: item.disabled })),\n      (entry) => !entry.disabled,\n    ),\n    (entry) => entry.index,\n  );\n\nconst nth = (\n  indexes: ReadonlyArray<number>,\n  position: number,\n): Option.Option<number> => Option.fromNullishOr(indexes[position]);\n\nexport const firstEnabled = (\n  items: ReadonlyArray<CollectionItem>,\n): Option.Option<number> => nth(enabledIndexes(items), 0);\n\nexport const lastEnabled = (\n  items: ReadonlyArray<CollectionItem>,\n): Option.Option<number> =>\n  pipe(nth(enabledIndexes(items), Arr.length(enabledIndexes(items)) - 1));\n\nexport const indexOfId = (\n  items: ReadonlyArray<CollectionItem>,\n  id: number,\n): Option.Option<number> => Arr.findFirstIndex(items, (item) => item.id === id);\n\nexport const nextEnabled = (\n  items: ReadonlyArray<CollectionItem>,\n  current: Option.Option<number>,\n  delta: 1 | -1,\n): Option.Option<number> => {\n  const pool = enabledIndexes(items);\n  const size = Arr.length(pool);\n  if (size === 0) return Option.none();\n\n  const step = (position: number) =>\n    nth(pool, (position + delta + size) % size);\n\n  return pipe(\n    current,\n    Option.match({\n      onNone: () => (delta > 0 ? nth(pool, 0) : nth(pool, size - 1)),\n      onSome: (index) =>\n        pipe(\n          Arr.findFirstIndex(pool, (candidate) => candidate === index),\n          Option.match({\n            onSome: step,\n            onNone: () =>\n              delta > 0\n                ? pipe(\n                    Arr.findFirst(pool, (candidate) => candidate > index),\n                    Option.orElse(() => nth(pool, 0)),\n                  )\n                : pipe(\n                    Arr.findLast(pool, (candidate) => candidate < index),\n                    Option.orElse(() => nth(pool, size - 1)),\n                  ),\n          }),\n        ),\n    }),\n  );\n};\n\nexport const createCollection = (): Collection => {\n  let entries: ReadonlyArray<CollectionItem> = [];\n  return {\n    register: (item) => {\n      entries = insert(entries, item);\n      return () => {\n        entries = remove(entries, item.id);\n      };\n    },\n    items: () => entries,\n  };\n};\n"
    },
    {
      "path": "packages/components/src/internal/controllable.ts",
      "target": "@ui/gpuixui/components/internal/controllable.ts",
      "type": "registry:lib",
      "content": "import { useCallback, useState } from \"react\";\n\nexport type ControllableState<Value> = readonly [Value, (next: Value) => void];\n\nexport interface ControllableStateOptions<Value> {\n  readonly value: Value | undefined;\n\n  readonly defaultValue: Value;\n\n  readonly onChange?: (next: Value) => void;\n}\n\nexport const useControllableState = <Value>({\n  value,\n  defaultValue,\n  onChange,\n}: ControllableStateOptions<Value>): ControllableState<Value> => {\n  const [internal, setInternal] = useState(defaultValue);\n  const controlled = value !== undefined;\n\n  const setValue = useCallback(\n    (next: Value) => {\n      if (!controlled) setInternal(next);\n      if (value !== next) onChange?.(next);\n    },\n    [controlled, value, onChange],\n  );\n\n  return [controlled ? value : internal, setValue];\n};\n"
    },
    {
      "path": "packages/components/src/internal/key.ts",
      "target": "@ui/gpuixui/components/internal/key.ts",
      "type": "registry:lib",
      "content": "import type { EventPayload, KeyEvent, Props } from \"@gpuix/react\";\nimport * as Option from \"effect/Option\";\n\nexport type KeyPhase = \"down\" | \"up\";\n\nexport const isActivation = (event: EventPayload, phase: KeyPhase): boolean => {\n  if (event.isHeld === true) return false;\n  if (event.key === \"enter\") return phase === \"down\";\n  return event.key === \"space\" && phase === \"up\";\n};\n\nexport const swallowsDefault = (\n  event: EventPayload,\n  phase: KeyPhase,\n): boolean => event.key === \"space\" && phase === \"down\";\n\nexport const activationHandlers = (\n  activate: (event: KeyEvent) => void,\n  observe: Pick<Props, \"onKeyDown\" | \"onKeyUp\"> = {},\n): Pick<Props, \"onKeyDown\" | \"onKeyUp\"> => ({\n  onKeyDown: (event: KeyEvent) => {\n    if (swallowsDefault(event, \"down\")) event.preventDefault();\n    if (isActivation(event, \"down\")) activate(event);\n    observe.onKeyDown?.(event);\n  },\n  onKeyUp: (event: KeyEvent) => {\n    if (isActivation(event, \"up\")) activate(event);\n    observe.onKeyUp?.(event);\n  },\n});\n\nexport const isEnter = (event: EventPayload): boolean => event.key === \"enter\";\n\nexport const isDown = (event: EventPayload): boolean =>\n  event.key === \"down\" || (event.key === \"n\" && event.modifiers?.ctrl === true);\n\nexport const isUp = (event: EventPayload): boolean =>\n  event.key === \"up\" || (event.key === \"p\" && event.modifiers?.ctrl === true);\n\nexport const isRight = (event: EventPayload): boolean => event.key === \"right\";\n\nexport const isLeft = (event: EventPayload): boolean => event.key === \"left\";\n\nexport const isFirst = (event: EventPayload): boolean => event.key === \"home\";\n\nexport const isLast = (event: EventPayload): boolean => event.key === \"end\";\n\nexport const isPageUp = (event: EventPayload): boolean =>\n  event.key === \"pageup\";\n\nexport const isPageDown = (event: EventPayload): boolean =>\n  event.key === \"pagedown\";\n\nexport const isDismiss = (event: EventPayload): boolean =>\n  event.key === \"escape\";\n\nexport const isTab = (event: EventPayload): boolean => event.key === \"tab\";\n\nexport const isSelectAll = (event: EventPayload): boolean =>\n  event.key === \"a\" &&\n  (event.modifiers?.cmd === true || event.modifiers?.ctrl === true);\n\nexport const isMenuKey = (event: EventPayload): boolean =>\n  (event.key === \"f10\" && event.modifiers?.shift === true) ||\n  event.key === \"contextmenu\";\n\nexport const axisDirection = (\n  event: EventPayload,\n  orientation: \"horizontal\" | \"vertical\",\n): Option.Option<1 | -1> => {\n  if (event.key === \"right\") return Option.some(1);\n  if (event.key === \"left\") return Option.some(-1);\n  if (orientation === \"vertical\") {\n    if (isDown(event)) return Option.some(1);\n    if (isUp(event)) return Option.some(-1);\n  }\n  return Option.none();\n};\n"
    },
    {
      "path": "packages/components/src/internal/measure.ts",
      "target": "@ui/gpuixui/components/internal/measure.ts",
      "type": "registry:lib",
      "content": "import type { EventPayload } from \"@gpuix/react\";\nimport * as Option from \"effect/Option\";\n\nexport interface MeasuredBox {\n  readonly x: number;\n  readonly y: number;\n  readonly width: number;\n  readonly height: number;\n}\n\nconst measurable = (value: number | undefined): Option.Option<number> =>\n  Option.filter(Option.fromNullishOr(value), Number.isFinite);\n\nexport const measuredBox = (event: EventPayload): Option.Option<MeasuredBox> =>\n  Option.all({\n    x: measurable(event.x),\n    y: measurable(event.y),\n    width: measurable(event.width),\n    height: measurable(event.height),\n  });\n\nexport const measuredHeight = (event: EventPayload): Option.Option<number> =>\n  measurable(event.height);\n\nexport interface MeasuredPoint {\n  readonly x: number;\n  readonly y: number;\n}\n\nexport const measuredPoint = (\n  event: EventPayload,\n): Option.Option<MeasuredPoint> =>\n  Option.all({\n    x: measurable(event.x),\n    y: measurable(event.y),\n  });\n"
    },
    {
      "path": "packages/components/src/internal/parts.ts",
      "target": "@ui/gpuixui/components/internal/parts.ts",
      "type": "registry:lib",
      "content": "import * as Arr from \"effect/Array\";\nimport * as P from \"effect/Predicate\";\nimport {\n  Children,\n  Fragment,\n  isValidElement,\n  type ReactElement,\n  type ReactNode,\n} from \"react\";\n\nexport type PartComponent = (props: never) => ReactNode;\n\nexport type PartChildren =\n  | ReactElement<unknown>\n  | boolean\n  | null\n  | undefined\n  | ReadonlyArray<PartChildren>;\n\nexport interface PartsContract {\n  readonly parent: string;\n\n  readonly family: ReadonlyArray<PartComponent>;\n\n  readonly accepts: ReadonlyArray<PartComponent>;\n\n  readonly hint?: string;\n}\n\nconst componentName = (type: unknown): string =>\n  P.isString(type)\n    ? type\n    : P.isFunction(type)\n      ? type.name || \"anonymous\"\n      : \"element\";\n\nconst fragmentChildren = (element: ReactElement<unknown>): ReactNode =>\n  hasChildren(element.props) ? element.props.children : undefined;\n\nconst hasChildren = (\n  value: unknown,\n): value is { readonly children?: ReactNode } =>\n  P.isObject(value) && \"children\" in value;\n\nconst leaves = (children: ReactNode): ReadonlyArray<unknown> =>\n  Arr.flatMap(Children.toArray(children), (child) =>\n    isValidElement<unknown>(child) && child.type === Fragment\n      ? leaves(fragmentChildren(child))\n      : [child],\n  );\n\nexport const assertParts = (\n  contract: PartsContract,\n  children: ReactNode,\n): void => {\n  Arr.forEach(leaves(children), (leaf) => {\n    if (!isValidElement(leaf)) return;\n    if (Arr.some(contract.accepts, (part) => part === leaf.type)) return;\n    if (!Arr.some(contract.family, (part) => part === leaf.type)) return;\n\n    throw new Error(\n      `${componentName(leaf.type)} cannot be used inside ${contract.parent}.` +\n        (contract.hint === undefined ? \"\" : ` ${contract.hint}`),\n    );\n  });\n};\n"
    },
    {
      "path": "packages/components/src/internal/presence.tsx",
      "target": "@ui/gpuixui/components/internal/presence.tsx",
      "type": "registry:component",
      "content": "/** @jsxImportSource @gpuix/react */\nimport type { MotionProps } from \"@gpuix/react\";\nimport {\n  delayedTiming,\n  exitWindowMs,\n  fadeFrom,\n  fadeFromAnchor,\n  fadeFromSide,\n  fadeOnly,\n  presenceMotion as motionOfPresence,\n  presenceStyle,\n  transitionWindowMs,\n  type PresenceFrames,\n  type PresenceHitRule,\n  type PresenceSide,\n  type PresenceTiming,\n  type PresenceTransition,\n} from \"../../motion/presence.js\";\nimport {\n  MotionProvider,\n  useMotionPreference,\n  useSurfacePresence,\n  useSurfaceTiming,\n  type MotionProviderProps,\n  type SurfacePresence,\n} from \"../../motion/react/presence.js\";\nimport {\n  motionTiming,\n  type MotionLevel,\n  type MotionPreference,\n} from \"../../motion/timing.js\";\n\nexport { MotionProvider, useMotionPreference, type MotionProviderProps };\n\nexport type SurfaceFrames = PresenceFrames;\n\nexport type SurfaceTiming = PresenceTiming;\n\nexport type PanelSide = PresenceSide;\n\nexport {\n  delayedTiming,\n  exitWindowMs,\n  fadeFrom,\n  fadeFromAnchor,\n  fadeFromSide,\n  fadeOnly,\n  presenceStyle,\n  transitionWindowMs,\n};\nexport type { PresenceHitRule };\n\nexport { useSurfacePresence, useSurfaceTiming, type SurfacePresence };\n\nexport const moveTiming = (\n  level: MotionLevel,\n  preference: MotionPreference,\n): PresenceTransition => motionTiming(level, { phase: \"standard\", preference });\n\nexport const useMoveTiming = (level: MotionLevel): PresenceTransition =>\n  moveTiming(level, useMotionPreference());\n\nexport const presenceMotion = (\n  frames: SurfaceFrames,\n  presence: SurfacePresence,\n  timing: SurfaceTiming = presence.timing,\n): MotionProps => motionOfPresence(frames, presence.present, timing);\n\nexport const PresenceShield = ({ present }: { readonly present: boolean }) =>\n  present ? null : (\n    <div\n      testId=\"presence-shield\"\n      style={{ position: \"absolute\", top: 0, right: 0, bottom: 0, left: 0 }}\n    />\n  );\n\nexport const presenceTabStop = (\n  present: boolean,\n): { readonly tabIndex?: number } => (present ? {} : { tabIndex: -1 });\n"
    },
    {
      "path": "packages/components/src/internal/recipe.ts",
      "target": "@ui/gpuixui/components/internal/recipe.ts",
      "type": "registry:lib",
      "content": "import type { StyleDesc } from \"@gpuix/react\";\nimport { createRecipeCache } from \"../../tw/index.js\";\nimport type { Theme } from \"./theme.js\";\n\nexport const recipeStyle: (recipe: string, theme: Theme) => StyleDesc =\n  createRecipeCache(512);\n"
    },
    {
      "path": "packages/components/src/internal/ref.ts",
      "target": "@ui/gpuixui/components/internal/ref.ts",
      "type": "registry:lib",
      "content": "import type { Ref, RefCallback } from \"react\";\nimport { useCallback } from \"react\";\n\nconst assign = <T>(ref: Ref<T> | undefined, value: T): void => {\n  if (ref === null || ref === undefined) return;\n  if (typeof ref === \"function\") {\n    ref(value);\n    return;\n  }\n  ref.current = value;\n};\n\nexport const useMergedRef = <T>(\n  internal: Ref<T>,\n  caller: Ref<T> | undefined,\n): RefCallback<T> =>\n  useCallback(\n    (value: T) => {\n      assign(internal, value);\n      assign(caller, value);\n    },\n    [internal, caller],\n  );\n"
    },
    {
      "path": "packages/components/src/internal/theme.ts",
      "target": "@ui/gpuixui/components/internal/theme.ts",
      "type": "registry:lib",
      "content": "import type { StyleDesc } from \"@gpuix/react\";\nimport * as Schema from \"effect/Schema\";\nimport {\n  alpha,\n  avatarPixels,\n  avatarSize,\n  brandTextOn,\n  controlFocusRing,\n  controlHeight,\n  controlPaddingX,\n  currentPlatform,\n  darkTheme,\n  disabledAction,\n  disabledOpacity,\n  elevationShadow,\n  filledHover,\n  filledPressed,\n  findHighlight,\n  focusRing,\n  keyboardFocusRing,\n  lightTheme,\n  nativeTextTheme,\n  pointerWash,\n  selectionOn,\n  statusTone,\n  syntaxColors,\n  targetSize,\n  textStyle,\n  type BoxShadow,\n  type ElevationLevel,\n  type FindHighlightSource,\n  type NativeTextFill,\n  type PlatformName,\n  type PointerWashSurface,\n  type StatusTone,\n  type SyntaxColors,\n  type SyntaxRole,\n  type TextRole,\n  type Theme,\n  type ThemeColors,\n} from \"../../tokens/index.js\";\n\nexport {\n  alpha,\n  avatarPixels,\n  avatarSize,\n  controlFocusRing,\n  controlHeight,\n  controlPaddingX,\n  currentPlatform,\n  darkTheme,\n  disabledAction,\n  disabledOpacity,\n  elevationShadow,\n  filledHover,\n  filledPressed,\n  findHighlight,\n  focusRing,\n  keyboardFocusRing,\n  lightTheme,\n  nativeTextTheme,\n  statusTone,\n  syntaxColors,\n  targetSize,\n  textStyle,\n};\nexport type {\n  BoxShadow,\n  ElevationLevel,\n  FindHighlightSource,\n  NativeTextFill,\n  PlatformName,\n  StatusTone,\n  SyntaxColors,\n  SyntaxRole,\n  TextRole,\n  Theme,\n};\nexport { ThemeProvider, useTheme } from \"../../tokens/react.js\";\n\nexport const keyboardRing = (theme: Theme, invalid = false): StyleDesc =>\n  controlFocusRing(theme, { invalid }) ?? {};\n\nexport interface ControlRingState {\n  readonly disabled?: boolean;\n\n  readonly invalid?: boolean;\n}\n\nexport const controlRing = (\n  theme: Theme,\n  state: ControlRingState = {},\n): StyleDesc => controlFocusRing(theme, state) ?? {};\n\nexport const pressWash = (theme: Theme, replaced: PointerWashSurface): string =>\n  pointerWash(theme, \"active\", replaced) ?? theme.colors.active;\n\nexport const selectionWash = (theme: Theme): StyleDesc => ({\n  selectionColor: theme.colors.selection,\n});\n\nexport const keepContent: StyleDesc = { keyboardFocusDim: false };\n\nexport const disabledFade = (\n  disabled: boolean,\n): { readonly opacity: number } | undefined =>\n  disabled ? { opacity: disabledOpacity } : undefined;\n\nconst Tones = Schema.Literals([\"info\", \"success\", \"warning\", \"danger\"]);\n\nexport type Tone = typeof Tones.Type;\n\nexport const toneOf = (value: unknown): Tone =>\n  Schema.is(Tones)(value) ? value : \"info\";\n\nexport interface ToneStyle {\n  readonly fg: string;\n  readonly bg: string;\n  readonly border: string;\n}\n\nconst TOKEN_TONE: Readonly<Record<Tone, StatusTone>> = {\n  info: \"info\",\n  success: \"success\",\n  warning: \"warning\",\n  danger: \"destructive\",\n};\n\nexport const tone = (theme: Theme, value: Tone): ToneStyle => {\n  const { background, border, foreground } = statusTone(\n    theme,\n    TOKEN_TONE[value],\n  );\n  return { bg: background, border, fg: foreground };\n};\n\nconst softRoles: Readonly<Record<Tone, keyof ThemeColors>> = {\n  info: \"infoSoft\",\n  success: \"successSoft\",\n  warning: \"warningSoft\",\n  danger: \"destructiveSoft\",\n};\n\nexport const messageLink = (theme: Theme, value: Tone): string =>\n  brandTextOn(theme, softRoles[value]);\n\nexport const messageSelection = (theme: Theme, value: Tone): string =>\n  selectionOn(theme, softRoles[value]);\n\nexport const toneSurface = (theme: Theme, value: Tone): StyleDesc => {\n  const { bg, border, fg } = tone(theme, value);\n  return {\n    backgroundColor: bg,\n    color: fg,\n    borderWidth: theme.borderWidth.thin,\n    borderColor: border,\n  };\n};\n"
    },
    {
      "path": "packages/components/src/tabs.tsx",
      "target": "@ui/gpuixui/components/tabs.tsx",
      "type": "registry:component",
      "content": "/** @jsxImportSource @gpuix/react */\nimport type {\n  EventPayload,\n  KeyEvent,\n  Props,\n  PublicInstance,\n} from \"@gpuix/react\";\nimport { useGpuix } from \"@gpuix/react\";\nimport {\n  mergeStyles,\n  resolveStyle,\n  type StateStyle,\n} from \"@gpuix/react/floating\";\nimport {\n  cn,\n  TABS_CONTENT,\n  TABS_LIST,\n  TABS_LIST_ORIENTATION,\n  TABS_ORIENTATION,\n  TABS_ROOT,\n  TABS_TRIGGER,\n} from \"../tw/index.js\";\nimport { Match, pipe } from \"effect\";\nimport * as Arr from \"effect/Array\";\nimport * as Option from \"effect/Option\";\nimport {\n  createContext,\n  useCallback,\n  useContext,\n  useLayoutEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport {\n  firstEnabled,\n  lastEnabled,\n  nextEnabled,\n  type CollectionItem,\n} from \"./internal/collection.js\";\nimport { useControllableState } from \"./internal/controllable.js\";\nimport {\n  isActivation,\n  isDown,\n  isFirst,\n  isLast,\n  isLeft,\n  isRight,\n  isUp,\n  swallowsDefault,\n} from \"./internal/key.js\";\nimport { useMoveTiming } from \"./internal/presence.js\";\nimport { recipeStyle } from \"./internal/recipe.js\";\nimport { useMergedRef } from \"./internal/ref.js\";\nimport {\n  assertParts,\n  type PartChildren,\n  type PartComponent,\n  type PartsContract,\n} from \"./internal/parts.js\";\nimport {\n  controlHeight,\n  controlRing,\n  disabledFade,\n  useTheme,\n} from \"./internal/theme.js\";\nimport {\n  indicatorBox,\n  lineIndicatorBox,\n  measuredBox,\n  type TabBox,\n} from \"./tabs/indicator.js\";\nimport {\n  activationOf,\n  moveOrigin,\n  orientationOf,\n  selectsOnFocus,\n  type TabsActivation,\n  type TabsOrientation,\n} from \"./tabs/logic.js\";\n\nexport type { TabsActivation, TabsOrientation } from \"./tabs/logic.js\";\n\ninterface TabRow {\n  readonly value: string;\n  readonly id: number;\n  readonly disabled: boolean;\n}\n\ninterface MeasuredTab extends TabBox {\n  readonly value: string;\n}\n\ninterface TabsContextValue {\n  readonly selected: string;\n  readonly active: Option.Option<string>;\n  readonly tabbable: Option.Option<string>;\n  readonly orientation: TabsOrientation;\n  readonly activation: TabsActivation;\n  readonly rows: ReadonlyArray<TabRow>;\n  readonly boxes: ReadonlyArray<MeasuredTab>;\n  readonly register: (row: TabRow) => () => void;\n  readonly reportBox: (value: string, box: TabBox) => void;\n  readonly select: (value: string) => void;\n  readonly noteFocus: (value: string) => void;\n  readonly focusRow: (row: TabRow, select: boolean) => void;\n  readonly moveBy: (delta: 1 | -1, keyOrigin?: Option.Option<string>) => void;\n  readonly jumpTo: (edge: \"first\" | \"last\") => void;\n  readonly selectActive: () => void;\n}\n\nconst TabsContext = createContext<TabsContextValue | undefined>(undefined);\n\nconst useTabsContext = (): TabsContextValue => {\n  const value = useContext(TabsContext);\n  if (value === undefined) {\n    throw new Error(\"Tabs parts must be used inside Tabs\");\n  }\n  return value;\n};\n\nconst upsert = (\n  rows: ReadonlyArray<TabRow>,\n  row: TabRow,\n): ReadonlyArray<TabRow> =>\n  Option.match(\n    Arr.findFirst(rows, (candidate) => candidate.value === row.value),\n    {\n      onNone: () => Arr.append(rows, row),\n      onSome: (current) =>\n        current.id === row.id && current.disabled === row.disabled\n          ? rows\n          : Arr.map(rows, (candidate) =>\n              candidate.value === row.value ? row : candidate,\n            ),\n    },\n  );\n\nconst sameBox = (a: TabBox, b: TabBox): boolean =>\n  Math.abs(a.x - b.x) < 0.5 &&\n  Math.abs(a.y - b.y) < 0.5 &&\n  Math.abs(a.width - b.width) < 0.5 &&\n  Math.abs(a.height - b.height) < 0.5;\n\nconst upsertBox = (\n  boxes: ReadonlyArray<MeasuredTab>,\n  box: MeasuredTab,\n): ReadonlyArray<MeasuredTab> =>\n  Option.match(\n    Arr.findFirst(boxes, (candidate) => candidate.value === box.value),\n    {\n      onNone: () => Arr.append(boxes, box),\n      onSome: (current) =>\n        sameBox(current, box)\n          ? boxes\n          : Arr.map(boxes, (candidate) =>\n              candidate.value === box.value ? box : candidate,\n            ),\n    },\n  );\n\nconst collection = (\n  rows: ReadonlyArray<TabRow>,\n): ReadonlyArray<CollectionItem> =>\n  Arr.map(rows, (row) => ({ id: row.id, disabled: row.disabled }));\n\nconst rowAt = (\n  rows: ReadonlyArray<TabRow>,\n  index: Option.Option<number>,\n): Option.Option<TabRow> =>\n  Option.flatMap(index, (position) => Arr.get(rows, position));\n\nexport interface TabsProps extends Omit<Props, \"children\"> {\n  readonly value?: string;\n\n  readonly defaultValue?: string;\n  readonly onValueChange?: (value: string) => void;\n\n  readonly orientation?: TabsOrientation;\n\n  readonly activation?: TabsActivation;\n\n  readonly className?: string;\n  readonly children?: PartChildren;\n}\n\nexport const Tabs = ({\n  value: valueProp,\n  defaultValue = \"\",\n  onValueChange,\n  orientation,\n  activation,\n  className,\n  children,\n  style,\n  ...rest\n}: TabsProps) => {\n  const theme = useTheme();\n  const renderer = useGpuix().renderer;\n\n  const axis = orientationOf(orientation);\n  const policy = activationOf(activation);\n  const [selected, setSelected] = useControllableState({\n    value: valueProp,\n    defaultValue,\n    onChange: onValueChange,\n  });\n  const [active, setActive] = useState<Option.Option<string>>(Option.none());\n  const [rows, setRows] = useState<ReadonlyArray<TabRow>>([]);\n  const [boxes, setBoxes] = useState<ReadonlyArray<MeasuredTab>>([]);\n\n  const reportBox = useCallback((value: string, box: TabBox) => {\n    setBoxes((current) => upsertBox(current, { value, ...box }));\n  }, []);\n\n  const register = useCallback((row: TabRow) => {\n    setRows((current) => upsert(current, row));\n    return () => {\n      setRows((current) =>\n        Arr.filter(current, (candidate) => candidate.value !== row.value),\n      );\n      setBoxes((current) =>\n        Arr.filter(current, (box) => box.value !== row.value),\n      );\n    };\n  }, []);\n\n  const noteFocus = useCallback((value: string) => {\n    setActive(Option.some(value));\n  }, []);\n\n  const focusRow = useCallback(\n    (row: TabRow, select: boolean): void => {\n      setActive(Option.some(row.value));\n      renderer?.focusElement?.(row.id);\n      if (select) setSelected(row.value);\n    },\n    [renderer, setSelected],\n  );\n\n  const indexOf = useCallback(\n    (value: string) => Arr.findFirstIndex(rows, (row) => row.value === value),\n    [rows],\n  );\n\n  const moveBy = useCallback(\n    (delta: 1 | -1, keyOrigin: Option.Option<string> = Option.none()): void => {\n      pipe(\n        rowAt(\n          rows,\n          nextEnabled(\n            collection(rows),\n            moveOrigin({ keyOrigin, active, selected }, indexOf),\n            delta,\n          ),\n        ),\n        Option.match({\n          onNone: () => undefined,\n          onSome: (row) => focusRow(row, selectsOnFocus(policy)),\n        }),\n      );\n    },\n    [active, focusRow, indexOf, policy, rows, selected],\n  );\n\n  const jumpTo = useCallback(\n    (edge: \"first\" | \"last\"): void => {\n      const enabled =\n        edge === \"first\"\n          ? firstEnabled(collection(rows))\n          : lastEnabled(collection(rows));\n      pipe(\n        rowAt(rows, enabled),\n        Option.match({\n          onNone: () => undefined,\n          onSome: (row) => focusRow(row, selectsOnFocus(policy)),\n        }),\n      );\n    },\n    [focusRow, policy, rows],\n  );\n\n  const select = useCallback(\n    (value: string): void => {\n      pipe(\n        Arr.findFirst(rows, (row) => row.value === value),\n        Option.filter((row) => !row.disabled),\n        Option.match({\n          onNone: () => undefined,\n          onSome: (row) => setSelected(row.value),\n        }),\n      );\n    },\n    [rows, setSelected],\n  );\n\n  const selectActive = useCallback((): void => {\n    pipe(\n      active,\n      Option.match({\n        onNone: () => undefined,\n        onSome: select,\n      }),\n    );\n  }, [active, select]);\n\n  const tabbable = useMemo(\n    () =>\n      pipe(\n        Arr.findFirst(rows, (row) => row.value === selected && !row.disabled),\n        Option.orElse(() => Arr.findFirst(rows, (row) => !row.disabled)),\n        Option.map((row) => row.value),\n      ),\n    [rows, selected],\n  );\n\n  const context = useMemo<TabsContextValue>(\n    () => ({\n      selected,\n      active,\n      tabbable,\n      orientation: axis,\n      activation: policy,\n      rows,\n      boxes,\n      register,\n      reportBox,\n      select,\n      noteFocus,\n      focusRow,\n      moveBy,\n      jumpTo,\n      selectActive,\n    }),\n    [\n      axis,\n      active,\n      boxes,\n      focusRow,\n      jumpTo,\n      moveBy,\n      noteFocus,\n      policy,\n      register,\n      reportBox,\n      rows,\n      select,\n      selectActive,\n      selected,\n      tabbable,\n    ],\n  );\n\n  assertParts(tabsRoot, children);\n\n  return (\n    <TabsContext.Provider value={context}>\n      <div\n        {...rest}\n        style={mergeStyles(\n          recipeStyle(cn(TABS_ROOT, TABS_ORIENTATION[axis], className), theme),\n          style,\n        )}\n      >\n        {children}\n      </div>\n    </TabsContext.Provider>\n  );\n};\n\nexport type TabsListVariant = \"pill\" | \"line\";\n\nconst LINE_THICKNESS = 2;\n\nexport interface TabsListProps extends Omit<Props, \"children\"> {\n  readonly label?: string;\n  readonly variant?: TabsListVariant;\n\n  readonly className?: string;\n  readonly children?: PartChildren;\n}\n\nexport const TabsList = ({\n  label,\n  variant = \"pill\",\n  className,\n  onKeyDown,\n  onKeyUp,\n  children,\n  style,\n  ...rest\n}: TabsListProps) => {\n  const theme = useTheme();\n  const renderer = useGpuix().renderer;\n  const {\n    orientation,\n    moveBy,\n    jumpTo,\n    selectActive,\n    selected,\n    boxes,\n    rows,\n    reportBox,\n  } = useTabsContext();\n  const [origin, setOrigin] = useState<Option.Option<TabBox>>(Option.none());\n  const probe = useRef<PublicInstance | null>(null);\n  const move = useMoveTiming(\"normal\");\n  assertParts(tabsList, children);\n\n  const direction = (event: EventPayload): Option.Option<1 | -1> =>\n    orientation === \"horizontal\"\n      ? isRight(event)\n        ? Option.some(1)\n        : isLeft(event)\n          ? Option.some(-1)\n          : Option.none()\n      : isDown(event)\n        ? Option.some(1)\n        : isUp(event)\n          ? Option.some(-1)\n          : Option.none();\n\n  const originOf = (event: EventPayload): Option.Option<string> =>\n    pipe(\n      Arr.findFirst(rows, (row) => row.id === event.elementId),\n      Option.map((row) => row.value),\n    );\n\n  const handleKeyDown = (event: KeyEvent): void => {\n    if (swallowsDefault(event, \"down\")) event.preventDefault();\n    Match.value(event).pipe(\n      Match.when(isFirst, () => jumpTo(\"first\")),\n      Match.when(isLast, () => jumpTo(\"last\")),\n      Match.when((press) => isActivation(press, \"down\"), selectActive),\n      Match.orElse((value) =>\n        pipe(\n          direction(value),\n          Option.match({\n            onNone: () => undefined,\n            onSome: (delta) => moveBy(delta, originOf(value)),\n          }),\n        ),\n      ),\n    );\n  };\n\n  const keepOrigin = (box: TabBox): void =>\n    setOrigin((current) =>\n      Option.match(current, {\n        onNone: () => Option.some(box),\n        onSome: (previous) =>\n          sameBox(previous, box) ? current : Option.some(box),\n      }),\n    );\n\n  const noteOrigin = (event: EventPayload): void =>\n    pipe(\n      measuredBox(event),\n      Option.match({ onNone: () => undefined, onSome: keepOrigin }),\n    );\n\n  useLayoutEffect(() => {\n    const at = (id: number) =>\n      Option.fromNullishOr(renderer?.getElementBounds?.(id));\n    Option.match(\n      Option.flatMap(Option.fromNullishOr(probe.current), (element) =>\n        at(element.id),\n      ),\n      {\n        onNone: () => undefined,\n        onSome: (box) => {\n          keepOrigin(box);\n          Arr.forEach(rows, (row) =>\n            Option.match(at(row.id), {\n              onNone: () => undefined,\n              onSome: (next) => reportBox(row.value, next),\n            }),\n          );\n        },\n      },\n    );\n  }, [boxes, className, origin, renderer, reportBox, rows, style]);\n\n  const surface = Option.all({\n    origin,\n    selected: Arr.findFirst(boxes, (box) => box.value === selected),\n  });\n\n  return (\n    <div\n      role=\"tablist\"\n      {...(label === undefined ? {} : { \"aria-label\": label })}\n      {...rest}\n      onKeyDown={(event) => {\n        handleKeyDown(event);\n        onKeyDown?.(event);\n      }}\n      onKeyUp={(event) => {\n        if (isActivation(event, \"up\")) selectActive();\n        onKeyUp?.(event);\n      }}\n      style={mergeStyles(\n        mergeStyles(\n          recipeStyle(\n            cn(TABS_LIST, TABS_LIST_ORIENTATION[orientation], className),\n            theme,\n          ),\n\n          {\n            position: \"relative\",\n            ...(variant === \"line\"\n              ? { backgroundColor: \"transparent\", borderWidth: 0, padding: 0 }\n              : {}),\n          },\n        ),\n        style,\n      )}\n    >\n      <div\n        testId=\"tabs-origin\"\n        ref={probe}\n        onLayout={noteOrigin}\n        style={{\n          position: \"absolute\",\n\n          left: 0,\n          top: 0,\n          right: 0,\n          bottom: 0,\n          pointerEvents: \"none\",\n        }}\n      />\n      {Option.match(surface, {\n        onNone: () => null,\n        onSome: ({ origin: anchoredAt, selected: box }) => (\n          <div\n            testId=\"tabs-indicator\"\n            motion={{\n              initial: false,\n              animate:\n                variant === \"line\"\n                  ? lineIndicatorBox(anchoredAt, box, LINE_THICKNESS)\n                  : indicatorBox(anchoredAt, box),\n              transition: move,\n            }}\n            style={{\n              position: \"absolute\",\n\n              backgroundColor:\n                variant === \"line\"\n                  ? theme.colors.foreground\n                  : theme.colors.selected,\n              borderRadius: variant === \"line\" ? 0 : theme.radius.sm,\n              pointerEvents: \"none\",\n            }}\n          />\n        ),\n      })}\n      {children}\n    </div>\n  );\n};\n\nexport interface TabsTriggerProps extends Omit<Props, \"style\"> {\n  readonly value: string;\n  readonly disabled?: boolean;\n\n  readonly className?: string;\n\n  readonly style?: StateStyle<TabsTriggerState>;\n}\n\nexport interface TabsTriggerState {\n  readonly active: boolean;\n  readonly disabled: boolean;\n  readonly focused: boolean;\n\n  readonly highlighted: boolean;\n  readonly orientation: TabsOrientation;\n}\n\nexport const TabsTrigger = ({\n  value,\n  disabled = false,\n  className,\n  onClick,\n  onFocus,\n  onBlur,\n  onLayout,\n  style,\n  ref,\n  ...rest\n}: TabsTriggerProps) => {\n  const theme = useTheme();\n  const {\n    selected,\n    active,\n    orientation,\n    tabbable,\n    register,\n    reportBox,\n    select,\n    noteFocus,\n  } = useTabsContext();\n  const [focused, setFocused] = useState(false);\n  const [instance, setInstance] = useState<PublicInstance | undefined>(\n    undefined,\n  );\n\n  const attach = useCallback((next: PublicInstance | null) => {\n    setInstance(next ?? undefined);\n  }, []);\n  const setElement = useMergedRef<PublicInstance | null>(attach, ref);\n\n  useLayoutEffect(() => {\n    if (instance === undefined) return;\n    return register({ value, id: instance.id, disabled });\n  }, [disabled, instance, register, value]);\n\n  const isSelected = selected === value;\n  const isTabbable = Option.contains(tabbable, value);\n  const state: TabsTriggerState = {\n    active: isSelected,\n    disabled,\n    focused,\n    highlighted: Option.contains(active, value),\n    orientation,\n  };\n\n  return (\n    <div\n      role=\"tab\"\n      aria-selected={isSelected}\n      {...rest}\n      ref={setElement}\n      tabIndex={disabled ? -1 : isTabbable ? 0 : -1}\n      onFocus={(event) => {\n        noteFocus(value);\n        setFocused(true);\n        onFocus?.(event);\n      }}\n      onBlur={(event) => {\n        setFocused(false);\n        onBlur?.(event);\n      }}\n      onLayout={(event) => {\n        pipe(\n          measuredBox(event),\n          Option.match({\n            onNone: () => undefined,\n            onSome: (box) => reportBox(value, box),\n          }),\n        );\n        onLayout?.(event);\n      }}\n      onClick={\n        disabled\n          ? undefined\n          : (event) => {\n              onClick?.(event);\n              select(value);\n            }\n      }\n      style={mergeStyles(\n        mergeStyles(recipeStyle(cn(TABS_TRIGGER, className), theme), {\n          height: controlHeight(theme, \"md\"),\n          color: isSelected\n            ? theme.colors.foreground\n            : theme.colors.mutedForeground,\n          cursor: disabled ? \"not-allowed\" : \"pointer\",\n          ...disabledFade(disabled),\n          hover: disabled\n            ? undefined\n            : {\n                backgroundColor: isSelected ? undefined : theme.colors.hover,\n                color: theme.colors.foreground,\n              },\n          ...controlRing(theme, { disabled }),\n        }),\n        resolveStyle(style, state),\n      )}\n    />\n  );\n};\n\nexport interface TabsContentProps extends Props {\n  readonly value: string;\n\n  readonly className?: string;\n}\n\nexport const TabsContent = ({\n  value,\n  className,\n  style,\n  ...rest\n}: TabsContentProps) => {\n  const { selected } = useTabsContext();\n  const theme = useTheme();\n\n  if (selected !== value) return null;\n  return (\n    <div\n      role=\"tabpanel\"\n      aria-label={value}\n      {...rest}\n      style={mergeStyles(\n        recipeStyle(cn(TABS_CONTENT, className), theme),\n        style,\n      )}\n    />\n  );\n};\n\nconst tabsParts: ReadonlyArray<PartComponent> = [\n  Tabs,\n  TabsList,\n  TabsTrigger,\n  TabsContent,\n];\n\nconst tabsRoot: PartsContract = {\n  parent: \"Tabs\",\n  family: tabsParts,\n  accepts: [TabsList, TabsContent],\n  hint: \"Put TabsList and TabsContent directly inside Tabs.\",\n};\n\nconst tabsList: PartsContract = {\n  parent: \"TabsList\",\n  family: tabsParts,\n  accepts: [TabsTrigger],\n  hint: \"Put TabsTrigger directly inside TabsList; put TabsContent beside it.\",\n};\n\nexport const tabsMeta = {\n  name: \"Tabs\",\n  tier: 2,\n  status: \"done\",\n  gpuixSupport: \"partial\",\n  limitations: [\n    \"no aria-orientation, aria-controls or aria-labelledby in 0.11.0; the panel is named by its value\",\n    \"the trigger/panel relationship is positional, not semantic\",\n    \"the selected surface is measured through onLayout and slides by retargeting its left/top/width/height on the timing table's `standard` phase, because it moves between two resting states rather than arriving; the runtime has no transform, so the movement is a box interpolation, and reduced motion is the caller's MotionProvider policy\",\n    \"no zIndex: the surface stays behind the triggers by being their earlier sibling\",\n  ],\n  docs: \"docs/tabs.mdx\",\n  example: \"examples/tabs.example.tsx\",\n  testFile: \"src/behaviour/tabs.test.tsx\",\n} as const;\n"
    },
    {
      "path": "packages/components/src/tabs/indicator.ts",
      "target": "@ui/gpuixui/components/tabs/indicator.ts",
      "type": "registry:lib",
      "content": "import type { MeasuredBox } from \"../internal/measure.js\";\n\nexport type TabBox = MeasuredBox;\n\nexport interface IndicatorBox {\n  readonly left: number;\n  readonly top: number;\n  readonly width: number;\n  readonly height: number;\n}\n\nexport { measuredBox } from \"../internal/measure.js\";\n\nexport const indicatorBox = (\n  origin: TabBox,\n  selected: TabBox,\n): IndicatorBox => ({\n  left: selected.x - origin.x,\n  top: selected.y - origin.y,\n  width: selected.width,\n  height: selected.height,\n});\n\nexport const lineIndicatorBox = (\n  origin: TabBox,\n  selected: TabBox,\n  thickness: number,\n): IndicatorBox => ({\n  left: selected.x - origin.x,\n  top: selected.y - origin.y + selected.height - thickness,\n  width: selected.width,\n  height: thickness,\n});\n"
    },
    {
      "path": "packages/components/src/tabs/logic.ts",
      "target": "@ui/gpuixui/components/tabs/logic.ts",
      "type": "registry:lib",
      "content": "import { pipe } from \"effect\";\nimport * as Option from \"effect/Option\";\nimport * as Schema from \"effect/Schema\";\n\nexport const TabsOrientations = Schema.Literals([\"horizontal\", \"vertical\"]);\n\nexport type TabsOrientation = typeof TabsOrientations.Type;\n\nexport const orientationOf = (value: unknown): TabsOrientation =>\n  Schema.is(TabsOrientations)(value) ? value : \"horizontal\";\n\nexport const TabsActivations = Schema.Literals([\"automatic\", \"manual\"]);\n\nexport type TabsActivation = typeof TabsActivations.Type;\n\nexport const activationOf = (value: unknown): TabsActivation =>\n  Schema.is(TabsActivations)(value) ? value : \"automatic\";\n\nexport const selectsOnFocus = (activation: TabsActivation): boolean =>\n  activation === \"automatic\";\n\nexport interface MoveAnchors {\n  readonly keyOrigin: Option.Option<string>;\n\n  readonly active: Option.Option<string>;\n\n  readonly selected: string;\n}\n\nexport const moveOrigin = (\n  { keyOrigin, active, selected }: MoveAnchors,\n  indexOf: (value: string) => Option.Option<number>,\n): Option.Option<number> =>\n  pipe(\n    keyOrigin,\n    Option.flatMap(indexOf),\n    Option.orElse(() => Option.flatMap(active, indexOf)),\n    Option.orElse(() => indexOf(selected)),\n  );\n"
    }
  ],
  "docs": "[Tabs](https://gpuixui.sudokw.com/components/tabs) — These are GPUix native components: the files render through GPUix's reconciler against `@gpuix/react`, not the DOM, and need the per-platform `@gpuix/native` binding. The CLI installs the `@gpuixui/*` runtime the components import as registry items, so the copied tree resolves on its own; wrap it in `ThemeProvider` before it renders.",
  "meta": {
    "url": "/components/tabs",
    "kind": "component",
    "runtime": "gpuix",
    "registry": "gpuixui"
  }
}