{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tabs",
  "dependencies": [
    "@radix-ui/react-tabs"
  ],
  "files": [
    {
      "path": "components/ui/tabs.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\ntype TabVariant = \"default\" | \"underline\" | \"ghost\"\ntype TabSize = \"sm\" | \"small\" | \"default\" | \"lg\" | \"large\"\n\ninterface TabItem {\n  id: string\n  label: React.ReactNode\n  icon?: React.ReactNode\n  disabled?: boolean\n}\n\ninterface TabsContextValue {\n  activeTab: string\n  setActiveTab: (id: string) => void\n  variant: TabVariant\n  size: TabSize\n  indicatorThickness?: string\n  indicatorClassName?: string\n  concentric?: boolean\n  hoveredIndex: number | null\n  setHoveredIndex: (index: number | null) => void\n  activeIndex: number\n  setActiveIndex: (index: number) => void\n  tabRefs: React.MutableRefObject<(HTMLButtonElement | null)[]>\n}\n\nconst TabsContext = React.createContext<TabsContextValue | null>(null)\n\nfunction useTabs() {\n  const context = React.useContext(TabsContext)\n  if (!context) {\n    throw new Error(\"useTabs must be used within an Tabs component\")\n  }\n  return context\n}\n\ninterface TabsProps {\n  defaultValue?: string\n  value?: string\n  onValueChange?: (value: string) => void\n  children: React.ReactNode\n  className?: string\n  variant?: TabVariant\n  size?: TabSize\n  /** Override the underline indicator thickness (e.g. \"2px\", \"4px\") */\n  indicatorThickness?: string\n  /** Override the active indicator className (e.g. \"bg-muted\") */\n  indicatorClassName?: string\n  /** Use concentric border radius (outer = inner + padding) for default variant */\n  concentric?: boolean\n}\n\nfunction Tabs({\n  defaultValue,\n  value,\n  onValueChange,\n  children,\n  className,\n  variant = \"default\",\n  size = \"default\",\n  indicatorThickness,\n  indicatorClassName,\n  concentric = false,\n}: TabsProps) {\n  const [internalValue, setInternalValue] = React.useState(defaultValue ?? \"\")\n  const [hoveredIndex, setHoveredIndex] = React.useState<number | null>(null)\n  const [activeIndex, setActiveIndex] = React.useState<number>(0)\n  const tabRefs = React.useRef<(HTMLButtonElement | null)[]>([])\n\n  const activeTab = value ?? internalValue\n  const setActiveTab = React.useCallback(\n    (id: string) => {\n      if (value === undefined) {\n        setInternalValue(id)\n      }\n      onValueChange?.(id)\n    },\n    [value, onValueChange]\n  )\n\n  return (\n    <TabsContext.Provider\n      value={{\n        activeTab,\n        setActiveTab,\n        variant,\n        size,\n        indicatorThickness,\n        indicatorClassName,\n        concentric,\n        hoveredIndex,\n        setHoveredIndex,\n        activeIndex,\n        setActiveIndex,\n        tabRefs,\n      }}\n    >\n      <div\n        data-slot=\"animated-tabs\"\n        className={cn(\"flex flex-col gap-2\", className)}\n      >\n        {children}\n      </div>\n    </TabsContext.Provider>\n  )\n}\n\ninterface TabsListProps {\n  children: React.ReactNode\n  className?: string\n}\n\nfunction TabsList({ children, className }: TabsListProps) {\n  const {\n    variant,\n    size,\n    hoveredIndex,\n    activeIndex,\n    tabRefs,\n    concentric,\n    indicatorThickness,\n    indicatorClassName,\n  } = useTabs()\n  const [hoverStyle, setHoverStyle] = React.useState({\n    left: \"0px\",\n    width: \"0px\",\n  })\n  const [activeStyle, setActiveStyle] = React.useState({\n    left: \"0px\",\n    width: \"0px\",\n  })\n  const [isInitialized, setIsInitialized] = React.useState(false)\n\n  const normalizedSize =\n    size === \"small\" ? \"sm\" : size === \"large\" ? \"lg\" : size\n\n  const listHeightClasses = {\n    sm: variant === \"default\" ? \"h-8\" : \"h-8\",\n    default: variant === \"default\" ? \"h-10\" : \"h-10\",\n    lg: variant === \"default\" ? \"h-12\" : \"h-12\",\n  }\n\n  const hoverHeightClasses = {\n    sm: \"h-6\",\n    default: \"h-7\",\n    lg: \"h-9\",\n  }\n\n  const hoverOffsetMap = {\n    sm: \"-1px\",\n    default: \"-2px\",\n    lg: \"-3px\",\n  }\n\n  const underlineThicknessClasses = {\n    sm: \"h-[2px]\",\n    default: \"h-[3px]\",\n    lg: \"h-[4px]\",\n  }\n\n  const defaultIndicatorHeightClasses = {\n    sm: \"h-6\",\n    default: \"h-8\",\n    lg: \"h-10\",\n  }\n\n  const concentricRadii = {\n    sm: { outer: \"8px\", inner: \"4px\" },\n    default: { outer: \"10px\", inner: \"6px\" },\n    lg: { outer: \"12px\", inner: \"8px\" },\n  }\n\n  React.useEffect(() => {\n    if (hoveredIndex !== null && variant === \"underline\") {\n      const hoveredElement = tabRefs.current[hoveredIndex]\n      if (hoveredElement) {\n        const { offsetLeft, offsetWidth } = hoveredElement\n        setHoverStyle({\n          left: `${offsetLeft}px`,\n          width: `${offsetWidth}px`,\n        })\n      }\n    }\n  }, [hoveredIndex, variant, tabRefs])\n\n  const updateActiveIndicator = React.useCallback(() => {\n    if (activeIndex >= 0) {\n      const activeElement = tabRefs.current[activeIndex]\n      if (activeElement) {\n        const { offsetLeft, offsetWidth } = activeElement\n        setActiveStyle({\n          left: `${offsetLeft}px`,\n          width: `${offsetWidth}px`,\n        })\n      }\n    }\n  }, [activeIndex, tabRefs])\n\n  React.useEffect(() => {\n    updateActiveIndicator()\n  }, [activeIndex, updateActiveIndicator])\n\n  React.useEffect(() => {\n    requestAnimationFrame(() => {\n      updateActiveIndicator()\n      requestAnimationFrame(() => {\n        setIsInitialized(true)\n      })\n    })\n  }, [updateActiveIndicator])\n\n  return (\n    <div\n      data-slot=\"animated-tabs-list\"\n      role=\"tablist\"\n      className={cn(\n        \"text-muted-foreground relative inline-flex items-center\",\n        listHeightClasses[normalizedSize],\n        variant === \"default\" && [\n          \"bg-muted w-fit justify-center p-1\",\n          !concentric && \"rounded-lg\",\n        ],\n        variant === \"ghost\" && \"w-fit justify-center gap-1 bg-transparent p-0\",\n        variant === \"underline\" &&\n          \"border-border w-full justify-start gap-0 border-b\",\n        className\n      )}\n      style={\n        variant === \"default\" && concentric\n          ? { borderRadius: concentricRadii[normalizedSize].outer }\n          : undefined\n      }\n    >\n      {variant === \"underline\" && (\n        <div\n          className={cn(\n            \"bg-muted absolute z-0 rounded-sm transition-all duration-300 ease-out\",\n            hoverHeightClasses[normalizedSize]\n          )}\n          style={{\n            ...hoverStyle,\n            opacity: hoveredIndex !== null ? 1 : 0,\n            top: `calc(50% + ${hoverOffsetMap[normalizedSize]})`,\n            transform: \"translateY(-50%)\",\n          }}\n          aria-hidden=\"true\"\n        />\n      )}\n\n      {(variant === \"default\" || variant === \"ghost\") && (\n        <div\n          className={cn(\n            \"absolute z-0\",\n            indicatorClassName ||\n              (variant === \"ghost\" ? \"bg-muted\" : \"bg-background\"),\n            defaultIndicatorHeightClasses[normalizedSize],\n            !concentric && \"rounded-md\",\n            isInitialized && \"transition-all duration-300 ease-out\",\n            variant === \"default\" && \"shadow-sm\"\n          )}\n          style={{\n            ...activeStyle,\n            borderRadius: concentric\n              ? concentricRadii[normalizedSize].inner\n              : undefined,\n          }}\n          aria-hidden=\"true\"\n        />\n      )}\n\n      {variant === \"underline\" && (\n        <div\n          className={cn(\n            \"bg-foreground absolute bottom-0 z-10\",\n            !indicatorThickness && underlineThicknessClasses[normalizedSize],\n            isInitialized && \"transition-all duration-300 ease-out\"\n          )}\n          style={{\n            ...activeStyle,\n            height: indicatorThickness || undefined,\n          }}\n          aria-hidden=\"true\"\n        />\n      )}\n\n      {children}\n    </div>\n  )\n}\n\ninterface TabsTriggerProps {\n  value: string\n  children: React.ReactNode\n  className?: string\n  disabled?: boolean\n  icon?: React.ReactNode\n}\n\nconst TabsTrigger = React.forwardRef<HTMLButtonElement, TabsTriggerProps>(\n  ({ value, children, className, disabled = false, icon }, forwardedRef) => {\n    const {\n      activeTab,\n      setActiveTab,\n      variant,\n      size,\n      concentric,\n      setHoveredIndex,\n      setActiveIndex,\n      tabRefs,\n    } = useTabs()\n    const isActive = activeTab === value\n    const indexRef = React.useRef<number>(-1)\n\n    const normalizedSize =\n      size === \"small\" ? \"sm\" : size === \"large\" ? \"lg\" : size\n\n    const defaultSizeClasses = {\n      sm: \"h-7 px-2.5 py-1 text-sm\",\n      default: \"h-8 px-2.5 py-1.5 text-sm\",\n      lg: \"h-10 px-3 py-2 text-base\",\n    }\n\n    const underlineSizeClasses = {\n      sm: \"h-9 px-2.5 pb-2.5 pt-2 text-sm\",\n      default: \"h-10 px-3 pb-3 pt-2 text-sm\",\n      lg: \"h-12 px-4 pb-4 pt-2.5 text-base\",\n    }\n\n    const concentricInnerRadii = {\n      sm: \"4px\",\n      default: \"6px\",\n      lg: \"8px\",\n    }\n\n    const setTabRef = React.useCallback(\n      (el: HTMLButtonElement | null) => {\n        if (el) {\n          const currentIndex = tabRefs.current.indexOf(el)\n          if (currentIndex === -1) {\n            indexRef.current = tabRefs.current.length\n            tabRefs.current.push(el)\n          } else {\n            indexRef.current = currentIndex\n          }\n        }\n\n        if (typeof forwardedRef === \"function\") {\n          forwardedRef(el)\n        } else if (forwardedRef) {\n          forwardedRef.current = el\n        }\n      },\n      [tabRefs, forwardedRef]\n    )\n\n    React.useEffect(() => {\n      if (isActive && indexRef.current >= 0) {\n        setActiveIndex(indexRef.current)\n      }\n    }, [isActive, setActiveIndex])\n\n    return (\n      <button\n        ref={setTabRef}\n        type=\"button\"\n        role=\"tab\"\n        aria-selected={isActive}\n        aria-disabled={disabled}\n        disabled={disabled}\n        data-state={isActive ? \"active\" : \"inactive\"}\n        data-slot=\"animated-tabs-trigger\"\n        data-value={value}\n        onClick={() => {\n          if (!disabled) {\n            setActiveTab(value)\n            setActiveIndex(indexRef.current)\n          }\n        }}\n        onMouseEnter={() =>\n          variant === \"underline\" && setHoveredIndex(indexRef.current)\n        }\n        onMouseLeave={() => variant === \"underline\" && setHoveredIndex(null)}\n        className={cn(\n          \"ring-offset-background relative z-10 inline-flex items-center justify-center gap-1.5 font-medium whitespace-nowrap\",\n          \"transition-colors duration-200\",\n          \"focus-visible:ring-ring focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none\",\n          \"disabled:pointer-events-none disabled:opacity-50\",\n          (variant === \"default\" || variant === \"ghost\") && [\n            !concentric && \"rounded-md\",\n            defaultSizeClasses[normalizedSize],\n            isActive\n              ? \"text-foreground\"\n              : \"text-muted-foreground hover:text-foreground/80\",\n          ],\n          variant === \"underline\" && [\n            \"rounded-md\",\n            underlineSizeClasses[normalizedSize],\n            isActive\n              ? \"text-foreground\"\n              : \"text-muted-foreground hover:text-foreground\",\n          ],\n          className\n        )}\n        style={\n          (variant === \"default\" || variant === \"ghost\") && concentric\n            ? { borderRadius: concentricInnerRadii[normalizedSize] }\n            : undefined\n        }\n      >\n        <span className=\"relative z-10 flex items-center gap-1.5\">\n          {icon && (\n            <span className=\"shrink-0 [&_svg]:pointer-events-none [&_svg]:size-4\">\n              {icon}\n            </span>\n          )}\n          {children}\n        </span>\n      </button>\n    )\n  }\n)\n\nTabsTrigger.displayName = \"TabsTrigger\"\n\ninterface TabsContentProps {\n  value: string\n  children: React.ReactNode\n  className?: string\n  forceMount?: boolean\n  /** Enable default opacity animation with ease in/out (default: false) */\n  animate?: boolean\n  /** Set Y translation offset in pixels for entrance animation (default: no animation) */\n  animateY?: number\n  /** Set opacity animation - overrides animate when explicitly set (default: false) */\n  animateOpacity?: boolean\n  /** Animation duration in ms (default: 250) */\n  animationDuration?: number\n  /** Animation easing function (default: \"ease-out\" for animateY, \"ease-in-out\" for animate) */\n  animationEasing?:\n    | \"ease\"\n    | \"ease-in\"\n    | \"ease-out\"\n    | \"ease-in-out\"\n    | \"linear\"\n    | string\n}\n\nfunction TabsContent({\n  value,\n  children,\n  className,\n  forceMount = false,\n  animate = false,\n  animateY,\n  animateOpacity,\n  animationDuration = 250,\n  animationEasing,\n}: TabsContentProps) {\n  const { activeTab } = useTabs()\n  const isActive = activeTab === value\n  const [shouldRender, setShouldRender] = React.useState(isActive)\n  const [hasEntered, setHasEntered] = React.useState(false)\n\n  const shouldAnimateOpacity = animateOpacity ?? animate\n  const hasAnimation = animateY !== undefined || shouldAnimateOpacity\n\n  const resolvedEasing =\n    animationEasing ??\n    (animateY !== undefined ? \"cubic-bezier(0.16, 1, 0.3, 1)\" : \"ease-in-out\")\n\n  React.useEffect(() => {\n    if (isActive) {\n      setShouldRender(true)\n      setHasEntered(false)\n      if (hasAnimation) {\n        requestAnimationFrame(() => {\n          requestAnimationFrame(() => {\n            setHasEntered(true)\n          })\n        })\n      } else {\n        setHasEntered(true)\n      }\n    } else {\n      setHasEntered(false)\n      if (!forceMount) {\n        if (hasAnimation) {\n          const timer = setTimeout(\n            () => setShouldRender(false),\n            animationDuration\n          )\n          return () => clearTimeout(timer)\n        } else {\n          setShouldRender(false)\n        }\n      }\n    }\n  }, [isActive, forceMount, animationDuration, hasAnimation])\n\n  if (!forceMount && !shouldRender) {\n    return null\n  }\n\n  const showContent = isActive && hasEntered\n\n  return (\n    <div\n      role=\"tabpanel\"\n      data-state={isActive ? \"active\" : \"inactive\"}\n      data-slot=\"animated-tabs-content\"\n      className={cn(\n        \"ring-offset-background focus-visible:ring-ring mt-2 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none\",\n        !isActive && forceMount && \"hidden\",\n        className\n      )}\n      style={{\n        transition: hasAnimation\n          ? `opacity ${animationDuration}ms ${resolvedEasing}, transform ${animationDuration}ms ${resolvedEasing}`\n          : undefined,\n        opacity: shouldAnimateOpacity ? (showContent ? 1 : 0) : undefined,\n        transform:\n          animateY !== undefined\n            ? `translateY(${showContent ? 0 : animateY}px)`\n            : undefined,\n      }}\n      tabIndex={0}\n    >\n      {children}\n    </div>\n  )\n}\n\ninterface TabsFromArrayProps {\n  tabs: TabItem[]\n  defaultValue?: string\n  value?: string\n  onValueChange?: (value: string) => void\n  className?: string\n  listClassName?: string\n  triggerClassName?: string\n  contentClassName?: string\n  children?: (tab: TabItem) => React.ReactNode\n}\n\nfunction TabsFromArray({\n  tabs,\n  defaultValue,\n  value,\n  onValueChange,\n  className,\n  listClassName,\n  triggerClassName,\n  contentClassName,\n  children,\n}: TabsFromArrayProps) {\n  const initialValue = defaultValue ?? tabs[0]?.id\n\n  return (\n    <Tabs\n      defaultValue={initialValue}\n      value={value}\n      onValueChange={onValueChange}\n      className={className}\n    >\n      <TabsList className={listClassName}>\n        {tabs.map((tab) => (\n          <TabsTrigger\n            key={tab.id}\n            value={tab.id}\n            disabled={tab.disabled}\n            icon={tab.icon}\n            className={triggerClassName}\n          >\n            {tab.label}\n          </TabsTrigger>\n        ))}\n      </TabsList>\n      {children &&\n        tabs.map((tab) => (\n          <TabsContent key={tab.id} value={tab.id} className={contentClassName}>\n            {children(tab)}\n          </TabsContent>\n        ))}\n    </Tabs>\n  )\n}\n\nexport {\n  Tabs,\n  TabsList,\n  TabsTrigger,\n  TabsContent,\n  TabsFromArray,\n  type TabItem,\n  type TabsProps,\n  type TabsListProps,\n  type TabsTriggerProps,\n  type TabsContentProps,\n  type TabsFromArrayProps,\n}\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}