{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "animated-tabs",
  "title": "Animated Tabs",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "Radix Tabs with a pill or underline indicator that slides between triggers on a spring, and panels that fade in. Keeps full keyboard support and ARIA; controlled or uncontrolled.",
  "dependencies": [
    "motion@^12",
    "radix-ui"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/motion-presets.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/animated-tabs.tsx",
      "content": "// Ballmac UI: Animated Tabs. https://ui.ballmac.com/components/animated-tabs\n\"use client\"\n\nimport * as React from \"react\"\nimport { motion, useReducedMotion } from \"motion/react\"\nimport { Tabs as TabsPrimitive } from \"radix-ui\"\n\nimport { cn } from \"@/lib/utils\"\nimport { duration, ease, spring } from \"@/lib/ballmac/motion\"\n\ntype AnimatedTabsVariant = \"pill\" | \"underline\"\n\ntype AnimatedTabsContextValue = { value: string; indicatorId: string; variant: AnimatedTabsVariant }\n\nconst AnimatedTabsContext = React.createContext<AnimatedTabsContextValue | null>(null)\n\nfunction useAnimatedTabs(part: string) {\n  const context = React.useContext(AnimatedTabsContext)\n  if (!context) throw new Error(`<${part}> must be used inside <AnimatedTabs>`)\n  return context\n}\n\ntype AnimatedTabsProps = React.ComponentProps<typeof TabsPrimitive.Root> & {\n  /** Indicator style: a sliding pill behind the label, or a sliding underline. */\n  variant?: AnimatedTabsVariant\n}\n\nfunction AnimatedTabs({\n  variant = \"pill\",\n  value: valueProp,\n  defaultValue = \"\",\n  onValueChange,\n  className,\n  ...props\n}: AnimatedTabsProps) {\n  const [internal, setInternal] = React.useState(defaultValue)\n  const value = valueProp ?? internal\n  const indicatorId = `animated-tabs-${React.useId()}`\n  const context = React.useMemo(() => ({ value, indicatorId, variant }), [value, indicatorId, variant])\n\n  return (\n    <AnimatedTabsContext.Provider value={context}>\n      <TabsPrimitive.Root\n        data-slot=\"animated-tabs\"\n        data-variant={variant}\n        value={value}\n        onValueChange={(next) => {\n          if (valueProp === undefined) setInternal(next)\n          onValueChange?.(next)\n        }}\n        className={cn(\"flex flex-col gap-4\", className)}\n        {...props}\n      />\n    </AnimatedTabsContext.Provider>\n  )\n}\n\nfunction AnimatedTabsList({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.List>) {\n  const { variant } = useAnimatedTabs(\"AnimatedTabsList\")\n  return (\n    <TabsPrimitive.List\n      data-slot=\"animated-tabs-list\"\n      className={cn(\n        \"relative inline-flex w-fit max-w-full items-center overflow-x-auto [scrollbar-width:none]\",\n        variant === \"pill\" ? \"h-10 gap-0.5 rounded-full bg-muted p-1\" : \"h-10 gap-5 border-b\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction AnimatedTabsTrigger({\n  value,\n  className,\n  children,\n  ...props\n}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {\n  const { value: selected, indicatorId, variant } = useAnimatedTabs(\"AnimatedTabsTrigger\")\n  const reduceMotion = useReducedMotion()\n  const active = selected === value\n\n  return (\n    <TabsPrimitive.Trigger\n      data-slot=\"animated-tabs-trigger\"\n      value={value}\n      className={cn(\n        \"relative inline-flex h-full shrink-0 items-center justify-center gap-1.5 text-sm font-medium whitespace-nowrap text-muted-foreground outline-none transition-colors duration-150 select-none hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n        variant === \"pill\" ? \"rounded-full px-3.5\" : \"rounded-sm px-0.5\",\n        className\n      )}\n      {...props}\n    >\n      {active && (\n        <motion.span\n          aria-hidden=\"true\"\n          data-slot=\"animated-tabs-indicator\"\n          layoutId={indicatorId}\n          transition={reduceMotion ? { duration: 0 } : spring.snappy}\n          className={cn(\n            \"pointer-events-none absolute\",\n            variant === \"pill\"\n              ? \"inset-0 rounded-full bg-background shadow-[0_1px_2px_rgb(0_0_0/0.08),0_0_0_1px_color-mix(in_oklch,var(--foreground)_6%,transparent)] dark:bg-accent\"\n              : \"inset-x-0 bottom-0 h-0.5 rounded-full bg-foreground\"\n          )}\n        />\n      )}\n      <span className=\"relative z-10 inline-flex items-center gap-1.5\">{children}</span>\n    </TabsPrimitive.Trigger>\n  )\n}\n\nfunction AnimatedTabsContent({ className, children, ...props }: React.ComponentProps<typeof TabsPrimitive.Content>) {\n  const reduceMotion = useReducedMotion()\n  return (\n    <TabsPrimitive.Content\n      data-slot=\"animated-tabs-content\"\n      className={cn(\"outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 rounded-md\", className)}\n      {...props}\n    >\n      {/* Radix mounts only the selected panel, so each panel fades in as it arrives. */}\n      <motion.div\n        initial={reduceMotion ? false : { opacity: 0, y: 6, filter: \"blur(4px)\" }}\n        animate={{ opacity: 1, y: 0, filter: \"blur(0px)\" }}\n        transition={{ duration: duration.slow, ease: ease.out }}\n      >\n        {children}\n      </motion.div>\n    </TabsPrimitive.Content>\n  )\n}\n\nexport {\n  AnimatedTabs,\n  AnimatedTabsList,\n  AnimatedTabsTrigger,\n  AnimatedTabsContent,\n  type AnimatedTabsProps,\n  type AnimatedTabsVariant,\n}\n",
      "type": "registry:ui",
      "target": "@components/ballmac/animated-tabs.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "tabs",
      "segmented",
      "navigation",
      "indicator",
      "layout animation",
      "radix"
    ],
    "version": "1.0.0",
    "updated": "2026-09-29",
    "ai": {
      "summary": "Same API as Radix Tabs: <AnimatedTabs defaultValue variant=\"pill\" | \"underline\"> with AnimatedTabsList, AnimatedTabsTrigger value and AnimatedTabsContent value. The indicator is a Motion layoutId element inside the active trigger.",
      "whenToUse": [
        "Switching between views of the same object (overview, activity, settings)",
        "Settings pages with a few sections",
        "Pricing toggles such as monthly and yearly"
      ],
      "whenNotToUse": [
        "Page-level navigation between routes (use links)",
        "More than about six options (use a select)",
        "Package-manager command tabs (use install-tabs)"
      ],
      "composesWith": [
        "install-tabs",
        "code-block"
      ],
      "a11y": [
        {
          "keys": "Left / Right",
          "action": "Moves focus between tabs and selects them"
        },
        {
          "keys": "Home / End",
          "action": "Jumps to the first or last tab"
        },
        {
          "keys": "Tab",
          "action": "Moves from the tab list into the selected panel"
        }
      ],
      "customization": [
        "variant: pill (default) | underline",
        "value / onValueChange for controlled use, defaultValue otherwise",
        "Icon-only triggers need aria-label",
        "Style parts via [data-slot=animated-tabs-indicator], -list, -trigger, -content",
        "Reduced motion: the indicator jumps and panels appear without a fade"
      ]
    },
    "examples": [
      "animated-tabs-demo",
      "animated-tabs-underline"
    ],
    "url": "https://ui.ballmac.com/components/animated-tabs"
  },
  "categories": [
    "navigation"
  ],
  "type": "registry:ui"
}