{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "docs-tabs",
  "title": "Tabs",
  "description": "Tabs and Tab for MDX content: tabs from a list of items or with explicit values, with selections shared between tab groups, persisted, or linked from the URL hash.",
  "dependencies": [
    "cn"
  ],
  "registryDependencies": [
    "tabs",
    "@docscn/docs-utils"
  ],
  "files": [
    {
      "path": "registry/base/docs/components/tabs.tsx",
      "content": "// Adapted from Fumadocs UI (https://github.com/fuma-nama/fumadocs)\n// Copyright (c) 2023 Fuma, MIT License\n\"use client\";\n\nimport { cn } from \"cn\";\nimport {\n  type ComponentProps,\n  createContext,\n  type ReactNode,\n  use,\n  useEffect,\n  useId,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport {\n  TabsContent as TabsContentPrimitive,\n  TabsList as TabsListPrimitive,\n  Tabs as TabsPrimitive,\n  TabsTrigger as TabsTriggerPrimitive,\n} from \"@/components/ui/tabs\";\nimport { mergeRefs } from \"../utils/merge-refs\";\nimport { useTabsGroup } from \"../utils/tabs-group\";\n\ntype CollectionKey = string | symbol;\ntype WithStringClassName<T> = Omit<T, \"className\"> & { className?: string };\n\nexport interface TabsProps extends WithStringClassName<\n  Omit<ComponentProps<typeof TabsPrimitive>, \"value\" | \"onValueChange\">\n> {\n  /**\n   * The tabs' labels. Renders the tabs list, and lets `<Tab>`s omit their\n   * `value` (they're matched to `items` by their order).\n   */\n  items?: string[];\n\n  /**\n   * Shortcut for `defaultValue` when `items` is provided.\n   *\n   * @defaultValue 0\n   */\n  defaultIndex?: number;\n\n  /**\n   * Additional label in tabs list when `items` is provided.\n   */\n  label?: ReactNode;\n\n  /**\n   * Identifier for sharing the selected tab between tab groups\n   */\n  groupId?: string;\n\n  /**\n   * Persist the selected tab in `localStorage` (requires `groupId`)\n   */\n  persist?: boolean;\n\n  /**\n   * Update the URL hash to the selected tab's `id`\n   */\n  updateAnchor?: boolean;\n\n  value?: string;\n  onValueChange?: (value: string) => void;\n}\n\nconst TabsContext = createContext<{\n  items?: string[];\n  collection: CollectionKey[];\n  /** Mounted panels by value, to open the tab containing a hash target */\n  panels: Map<string, HTMLElement>;\n  /** Tab values by their `id` (URL hash), including panels that aren't mounted */\n  ids: Map<string, string>;\n} | null>(null);\n\nfunction useTabContext() {\n  const ctx = use(TabsContext);\n  if (!ctx) throw new Error(\"You must wrap your component in <Tabs>\");\n  return ctx;\n}\n\nexport function TabsList({\n  className,\n  ...props\n}: WithStringClassName<ComponentProps<typeof TabsListPrimitive>>) {\n  return (\n    <TabsListPrimitive\n      variant=\"line\"\n      {...props}\n      className={cn(\n        \"not-docs-typeset w-full justify-start gap-3.5 overflow-x-auto px-4 group-data-horizontal/tabs:h-auto\",\n        className,\n      )}\n    />\n  );\n}\n\nexport function TabsTrigger({\n  className,\n  ...props\n}: WithStringClassName<ComponentProps<typeof TabsTriggerPrimitive>>) {\n  return (\n    <TabsTriggerPrimitive\n      {...props}\n      className={cn(\n        \"flex-none gap-2 px-0 py-2 text-sm text-muted-foreground group-data-horizontal/tabs:after:bottom-0 group-data-horizontal/tabs:after:h-px hover:text-foreground data-active:text-foreground [&_svg]:size-4\",\n        className,\n      )}\n    />\n  );\n}\n\nexport function Tabs({\n  ref,\n  className,\n  items,\n  label,\n  defaultIndex = 0,\n  defaultValue = items ? escapeValue(items[defaultIndex] ?? \"\") : undefined,\n  groupId,\n  persist = false,\n  updateAnchor = false,\n  value: _value,\n  onValueChange,\n  children,\n  ...props\n}: TabsProps) {\n  const rootRef = useRef<HTMLDivElement>(null);\n  const collection = useMemo<CollectionKey[]>(() => [], []);\n  const panels = useMemo(() => new Map<string, HTMLElement>(), []);\n  const ids = useMemo(() => new Map<string, string>(), []);\n  const [uncontrolledValue, setUncontrolledValue] = useState(\n    defaultValue as string | undefined,\n  );\n  const [groupValue, setGroupValue] = useTabsGroup(groupId, persist);\n  const value =\n    _value ??\n    (groupValue !== null &&\n    (!items || items.map(escapeValue).includes(groupValue))\n      ? groupValue\n      : uncontrolledValue);\n\n  // open the tab whose panel is, or contains, the element the URL hash targets\n  useEffect(() => {\n    function openFromHash() {\n      const hash = decodeURIComponent(window.location.hash.slice(1));\n      if (!hash) return;\n\n      // a tab's own id, whether or not its panel is mounted\n      const tabValue = ids.get(hash);\n      if (tabValue) {\n        setUncontrolledValue(tabValue);\n        rootRef.current?.scrollIntoView();\n        return;\n      }\n\n      // an element inside a mounted panel\n      const target = document.getElementById(hash);\n      if (!target) return;\n\n      for (const [value, panel] of panels) {\n        if (panel !== target && !panel.contains(target)) continue;\n        setUncontrolledValue(value);\n        requestAnimationFrame(() => target.scrollIntoView());\n        return;\n      }\n    }\n\n    const frame = requestAnimationFrame(openFromHash);\n    window.addEventListener(\"hashchange\", openFromHash);\n    return () => {\n      cancelAnimationFrame(frame);\n      window.removeEventListener(\"hashchange\", openFromHash);\n    };\n  }, [ids, panels]);\n\n  return (\n    <TabsPrimitive\n      ref={mergeRefs(ref, rootRef)}\n      value={value}\n      onValueChange={(v) => {\n        if (typeof v !== \"string\") return;\n        if (updateAnchor) {\n          const id = [...ids].find(([, value]) => value === v)?.[0];\n          if (id) window.history.replaceState(null, \"\", `#${id}`);\n        }\n        setGroupValue(v);\n        setUncontrolledValue(v);\n        onValueChange?.(v);\n      }}\n      {...props}\n      className={cn(\n        \"my-4 gap-0 overflow-hidden rounded-xl border bg-muted/50\",\n        className,\n      )}\n    >\n      {items && (\n        <TabsList>\n          {label && (\n            <span className=\"my-auto me-auto text-sm font-medium\">{label}</span>\n          )}\n          {items.map((item) => (\n            <TabsTrigger key={item} value={escapeValue(item)}>\n              {item}\n            </TabsTrigger>\n          ))}\n        </TabsList>\n      )}\n      <TabsContext\n        value={useMemo(\n          () => ({ items, collection, panels, ids }),\n          [collection, items, panels, ids],\n        )}\n      >\n        {children}\n      </TabsContext>\n    </TabsPrimitive>\n  );\n}\n\nexport interface TabProps extends Omit<\n  ComponentProps<typeof TabsContent>,\n  \"value\"\n> {\n  /**\n   * Value of tab, detect from index if unspecified.\n   */\n  value?: string;\n}\n\nexport function Tab({ value, ...props }: TabProps) {\n  const { items } = useTabContext();\n  const resolved =\n    value ??\n    // eslint-disable-next-line react-hooks/rules-of-hooks -- `value` is not supposed to change\n    items?.at(useCollectionIndex());\n  if (!resolved)\n    throw new Error(\n      \"Failed to resolve tab `value`, please pass a `value` prop to the Tab component.\",\n    );\n\n  return (\n    <TabsContent value={escapeValue(resolved)} {...props}>\n      {props.children}\n    </TabsContent>\n  );\n}\n\nexport function TabsContent({\n  ref,\n  value,\n  id,\n  className,\n  ...props\n}: WithStringClassName<ComponentProps<typeof TabsContentPrimitive>>) {\n  const { panels, ids } = useTabContext();\n  // `id` is the tab's URL hash, the panel keeps the primitive's id that triggers point `aria-controls` at\n  if (id) ids.set(id, value as string);\n\n  return (\n    <TabsContentPrimitive\n      ref={mergeRefs(ref, (element: HTMLDivElement | null) => {\n        if (element) panels.set(value as string, element);\n        else panels.delete(value as string);\n      })}\n      value={value}\n      {...props}\n      className={cn(\n        \"rounded-xl bg-background p-4 text-[0.9375rem] *:first:mt-0 *:last:mb-0 [&>figure:only-child]:-m-4 [&>figure:only-child]:rounded-xl [&>figure:only-child]:border-none\",\n        className,\n      )}\n    >\n      {props.children}\n    </TabsContentPrimitive>\n  );\n}\n\n/**\n * Inspired by Headless UI.\n *\n * Return the index of children, this is made possible by registering the order of render from children using React context.\n * This is supposed by work with pre-rendering & pure client-side rendering.\n */\nfunction useCollectionIndex() {\n  const key = useId();\n  const { collection } = useTabContext();\n\n  useEffect(() => {\n    return () => {\n      const idx = collection.indexOf(key);\n      if (idx !== -1) collection.splice(idx, 1);\n    };\n  }, [key, collection]);\n\n  if (!collection.includes(key)) collection.push(key);\n  return collection.indexOf(key);\n}\n\n/**\n * only escape whitespaces in values in simple mode\n */\nfunction escapeValue(v: string): string {\n  return v.toLowerCase().replace(/\\s+/g, \"-\");\n}\n",
      "type": "registry:component",
      "target": "@components/docs/components/tabs.tsx"
    }
  ],
  "type": "registry:component"
}