{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tabs",
  "type": "registry:ui",
  "title": "Tabs",
  "description": "Coordinates the indicator, direction, and panel change.",
  "dependencies": [
    "motion"
  ],
  "categories": [
    "navigation"
  ],
  "docs": "https://motion-lexicon.pages.dev/en/components/tabs/",
  "meta": {
    "engines": [
      "motion"
    ],
    "runtimeCost": "light",
    "signature": "Indicator, direction, and content panel change together",
    "sceneFamily": "product-mono",
    "motionRole": "ui",
    "primaryState": "Active tab with its corresponding panel",
    "assetProvenance": "self-contained"
  },
  "files": [
    {
      "path": "src/registry/components/tabs.tsx",
      "type": "registry:ui",
      "target": "components/motion-lexicon/tabs.tsx",
      "content": "\"use client\";\n\nimport { useCallback, useEffect, useId, useLayoutEffect, useRef, useState } from \"react\";\nimport type { KeyboardEvent, ReactNode } from \"react\";\nimport { motion, useReducedMotion } from \"motion/react\";\n\nconst INDICATOR = { type: \"spring\", stiffness: 620, damping: 42, mass: 0.35 } as const;\n\nconst useIsoLayoutEffect =\n  typeof window === \"undefined\" ? useEffect : useLayoutEffect;\n\nconst PANEL = { type: \"spring\", stiffness: 460, damping: 38, mass: 0.8 } as const;\n\nexport type TabItem = {\n  value: string;\n  label: string;\n  disabled?: boolean;\n};\n\nexport type TabsActivation = \"automatic\" | \"manual\";\nexport type UseTabsOptions = {\n  items: TabItem[];\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (value: string) => void;\n  activation?: TabsActivation;\n};\n\nexport function useTabs({\n  items,\n  value: controlled,\n  defaultValue,\n  onValueChange,\n  activation = \"automatic\",\n}: UseTabsOptions) {\n  const base = useId();\n  const nodes = useRef(new Map<string, HTMLButtonElement>());\n  const direction = useRef(1);\n\n  const [internal, setInternal] = useState(\n    () => defaultValue ?? items.find((i) => !i.disabled)?.value ?? items[0]?.value ?? \"\",\n  );\n\n  const value = controlled ?? internal;\n\n  const emit = useRef(onValueChange);\n  emit.current = onValueChange;\n\n  const select = useCallback(\n    (next: string) => {\n      if (next === value) return;\n      const from = items.findIndex((i) => i.value === value);\n      const to = items.findIndex((i) => i.value === next);\n      direction.current = to < from ? -1 : 1;\n      if (controlled === undefined) setInternal(next);\n      emit.current?.(next);\n    },\n    [controlled, items, value],\n  );\n\n  const focusAt = useCallback(\n    (i: number) => {\n      const item = items[i];\n      if (!item) return;\n      nodes.current.get(item.value)?.focus();\n    },\n    [items],\n  );\n\n  const nextEnabled = useCallback(\n    (from: number, dir: number) => {\n      const n = items.length;\n      let i = from < 0 ? 0 : from;\n      for (let k = 0; k < n; k += 1) {\n        i = (i + dir + n) % n;\n        if (!items[i].disabled) return i;\n      }\n      return from;\n    },\n    [items],\n  );\n\n  const endStop = useCallback(\n    (dir: number) => {\n      const n = items.length;\n      if (dir > 0) {\n        for (let i = 0; i < n; i += 1) if (!items[i].disabled) return i;\n      } else {\n        for (let i = n - 1; i >= 0; i -= 1) if (!items[i].disabled) return i;\n      }\n      return 0;\n    },\n    [items],\n  );\n\n  const getTabProps = useCallback(\n    (item: TabItem, index: number) => ({\n      id: `${base}-tab-${item.value}`,\n      role: \"tab\" as const,\n      type: \"button\" as const,\n      \"aria-selected\": item.value === value,\n      \"aria-controls\": `${base}-panel-${item.value}`,\n      \"aria-disabled\": item.disabled ? (true as const) : undefined,\n      tabIndex: item.value === value ? 0 : -1,\n      ref: (node: HTMLButtonElement | null) => {\n        if (node) nodes.current.set(item.value, node);\n        else nodes.current.delete(item.value);\n      },\n      onClick: () => {\n        if (!item.disabled) select(item.value);\n      },\n      onKeyDown: (e: KeyboardEvent<HTMLButtonElement>) => {\n        if (e.key === \"ArrowRight\" || e.key === \"ArrowLeft\") {\n          e.preventDefault();\n          const to = nextEnabled(index, e.key === \"ArrowRight\" ? 1 : -1);\n          focusAt(to);\n          if (activation === \"automatic\") select(items[to].value);\n          return;\n        }\n        if (e.key === \"Home\" || e.key === \"End\") {\n          e.preventDefault();\n          const to = endStop(e.key === \"Home\" ? 1 : -1);\n          focusAt(to);\n          if (activation === \"automatic\") select(items[to].value);\n          return;\n        }\n        if (e.key === \"Enter\" || e.key === \" \") {\n          e.preventDefault();\n          if (!item.disabled) select(item.value);\n        }\n      },\n    }),\n    [activation, base, endStop, focusAt, items, nextEnabled, select, value],\n  );\n\n  const getPanelProps = useCallback(\n    (panelValue: string) => ({\n      id: `${base}-panel-${panelValue}`,\n      role: \"tabpanel\" as const,\n      \"aria-labelledby\": `${base}-tab-${panelValue}`,\n      tabIndex: 0,\n    }),\n    [base],\n  );\n\n  const tabListProps = {\n    role: \"tablist\" as const,\n    \"aria-orientation\": \"horizontal\" as const,\n  };\n\n  return {\n    value,\n    select,\n    direction: direction.current,\n    tabListProps,\n    getTabProps,\n    getPanelProps,\n  };\n}\n\nexport type UseTabsReturn = ReturnType<typeof useTabs>;\n\nexport type TabsProps = {\n  items: TabItem[];\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (value: string) => void;\n  activation?: TabsActivation;\n  renderPanel?: (value: string) => ReactNode;\n  label?: string;\n  panelClassName?: string;\n  className?: string;\n};\n\nexport function Tabs({\n  items,\n  value,\n  defaultValue,\n  onValueChange,\n  activation = \"automatic\",\n  renderPanel,\n  label = \"Tabs\",\n  panelClassName = \"\",\n  className = \"\",\n}: TabsProps) {\n  const tabs = useTabs({ items, value, defaultValue, onValueChange, activation });\n  const reduced = useReducedMotion();\n\n  const rowRef = useRef<HTMLDivElement | null>(null);\n  const tabRefs = useRef<(HTMLButtonElement | null)[]>([]);\n  const [plateau, setPlateau] = useState({ x: 0, width: 0, ready: false });\n\n  const selectedIndex = items.findIndex((item) => item.value === tabs.value);\n\n  useIsoLayoutEffect(() => {\n    const node = tabRefs.current[selectedIndex];\n    if (!node) return;\n\n    const read = () => {\n      setPlateau((prev) =>\n        prev.x === node.offsetLeft &&\n        prev.width === node.offsetWidth &&\n        prev.ready\n          ? prev\n          : { x: node.offsetLeft, width: node.offsetWidth, ready: true },\n      );\n    };\n\n    read();\n    const row = rowRef.current;\n    if (!row) return;\n    const observer = new ResizeObserver(read);\n    observer.observe(row);\n    return () => observer.disconnect();\n  }, [selectedIndex, items]);\n\n  return (\n    <div\n      className={`w-full overflow-hidden rounded-[12px] border border-stone-200 bg-white shadow-[0_1px_2px_rgba(28,25,23,0.06),0_4px_10px_-8px_rgba(28,25,23,0.45)] dark:border-white/[0.16] dark:bg-[#1D1D1A] dark:shadow-[0_1px_6px_rgba(0,0,0,0.45)] ${className}`}\n    >\n      <div\n        {...tabs.tabListProps}\n        ref={rowRef}\n        aria-label={label}\n        className=\"relative flex w-full gap-1 border-b border-stone-200 bg-stone-50 px-1 pt-1 dark:border-white/[0.16] dark:bg-[#1D1D1A]\"\n      >\n        <motion.span\n          layout\n          aria-hidden\n          style={{\n            borderTopLeftRadius: 8,\n            borderTopRightRadius: 8,\n            borderBottomLeftRadius: 0,\n            borderBottomRightRadius: 0,\n            left: plateau.x,\n            width: plateau.width,\n            opacity: plateau.ready ? 1 : 0,\n          }}\n          className=\"absolute bottom-[-1px] top-1 bg-white dark:bg-[#1D1D1A]\"\n          transition={reduced ? { duration: 0 } : INDICATOR}\n        >\n          <motion.span\n            layout\n            aria-hidden\n            style={{ borderTopLeftRadius: 8, borderTopRightRadius: 8 }}\n            transition={reduced ? { duration: 0 } : INDICATOR}\n            className=\"absolute inset-0 border border-b-0 border-stone-200 dark:border-white/[0.16]\"\n          />\n        </motion.span>\n\n        {items.map((item, index) => {\n          const selected = item.value === tabs.value;\n          const tabProps = tabs.getTabProps(item, index);\n          return (\n            <button\n              key={item.value}\n              {...tabProps}\n              ref={(node) => {\n                tabRefs.current[index] = node;\n                tabProps.ref(node);\n              }}\n              className={`relative flex h-8 shrink-0 items-center justify-center rounded-t-[8px] px-3.5 text-[12.5px] outline-none transition-colors duration-150 after:pointer-events-none after:absolute after:inset-0 after:rounded-t-[8px] after:content-[''] focus-visible:after:shadow-[inset_0_0_0_1px_#4568FF] dark:focus-visible:after:shadow-[inset_0_0_0_1px_#93B0FF] ${\n                item.disabled\n                  ? \"cursor-default text-stone-400 dark:text-stone-500\"\n                  : selected\n                    ? \"text-stone-800 dark:text-stone-100\"\n                    : \"text-stone-500 hover:bg-stone-200/50 hover:text-stone-700 dark:text-stone-400 dark:hover:bg-white/[0.05] dark:hover:text-stone-200\"\n              }`}\n            >\n              <span className=\"relative grid place-items-center leading-[1.4]\">\n                <span aria-hidden className=\"invisible col-start-1 row-start-1 font-medium\">\n                  {item.label}\n                </span>\n                <span\n                  className={`col-start-1 row-start-1 ${selected ? \"font-medium\" : \"\"}`}\n                >\n                  {item.label}\n                </span>\n              </span>\n            </button>\n          );\n        })}\n      </div>\n\n      {renderPanel ? (\n        <motion.div\n          key={tabs.value}\n          custom={tabs.direction}\n          {...tabs.getPanelProps(tabs.value)}\n          initial={reduced ? false : { opacity: 0, x: tabs.direction * 12 }}\n          animate={{ opacity: 1, x: 0 }}\n          transition={reduced ? { duration: 0 } : PANEL}\n          className={`rounded-[11px] text-[13.5px] leading-relaxed text-stone-700 outline-none focus-visible:shadow-[inset_0_0_0_1px_#4568FF] dark:text-stone-200 dark:focus-visible:shadow-[inset_0_0_0_1px_#93B0FF] ${panelClassName}`}\n        >\n          {renderPanel(tabs.value)}\n        </motion.div>\n      ) : null}\n    </div>\n  );\n}\n"
    }
  ]
}
