{
  "$schema": "https://shadcn-svelte.com/schema/registry-item.json",
  "name": "animated-tabs",
  "type": "registry:ui",
  "title": "AnimatedTabs",
  "description": "Animated shadcn Tabs with spring-based sliding indicator via svelte-motion layoutId.",
  "dependencies": [
    "@humanspeak/svelte-motion",
    "bits-ui"
  ],
  "devDependencies": [],
  "registryDependencies": [],
  "files": [
    {
      "content": "import Content, { type TabsContentProps } from './animated-tabs-content.svelte'\nimport List, {\n    tabsListVariants,\n    type TabsListProps,\n    type TabsListVariant\n} from './animated-tabs-list.svelte'\nimport Trigger, { type TabsTriggerProps } from './animated-tabs-trigger.svelte'\nimport Root, { type TabsProps } from './animated-tabs.svelte'\n\nexport {\n    Content,\n    List,\n    Root,\n    //\n    Root as AnimatedTabs,\n    Content as TabsContent,\n    List as TabsList,\n    tabsListVariants,\n    Trigger as TabsTrigger,\n    Trigger,\n    type TabsContentProps,\n    type TabsListProps,\n    type TabsListVariant,\n    type TabsProps,\n    type TabsTriggerProps\n}\n",
      "type": "registry:file",
      "target": "animated-tabs/index.ts"
    },
    {
      "content": "<!--\n  @component\n  Animated shadcn Tabs content panel.\n\n  Wraps bits-ui Tabs.Content. When animated, uses the child render prop\n  to keep the panel element always in the DOM (never `hidden`). Inactive\n  panels use `display:none` so they collapse. The active panel's children\n  are wrapped in a MotionDiv that replays a fade+slide entrance on each\n  tab switch — the panel container (and any styling on it) stays stable.\n-->\n<script lang=\"ts\">\n    import { cn, type WithoutChildrenOrChild } from '$UTILS$'\n    import {\n        MotionDiv,\n        type MotionAnimate,\n        type MotionInitial,\n        type MotionTransition\n    } from '@humanspeak/svelte-motion'\n    import { Tabs as TabsPrimitive, type TabsContentProps as BitsTabsContentProps } from 'bits-ui'\n    import { getContext } from 'svelte'\n    import { TABS_CTX, type TabsContext } from './animated-tabs.svelte'\n\n    export type TabsContentProps = WithoutChildrenOrChild<BitsTabsContentProps> & {\n        /** Override animation for this panel. Inherits from Root when unset. */\n        animated?: boolean\n        /** Override the content entrance initial state. Default: `{ opacity: 0, y: 8 }` */\n        initial?: MotionInitial\n        /** Override the content entrance animate target. Default: `{ opacity: 1, y: 0 }` */\n        animate?: MotionAnimate\n        /** Override the content entrance transition. Default: `{ duration: 0.3, ease: 'easeOut' }` */\n        transition?: MotionTransition\n    }\n\n    let {\n        class: className,\n        value,\n        animated,\n        initial,\n        animate,\n        transition,\n        ref = $bindable(null),\n        children,\n        ...restProps\n    }: TabsContentProps & { children?: import('svelte').Snippet } = $props()\n\n    const ctx = getContext<TabsContext>(TABS_CTX)\n    const isActive = $derived(ctx.value() === value)\n    const isAnimated = $derived(animated ?? ctx.animated)\n\n    const defaultInitial: MotionInitial = { opacity: 0, y: 8 }\n    const defaultAnimate: MotionAnimate = { opacity: 1, y: 0 }\n    const defaultTransition: MotionTransition = { duration: 0.3, ease: 'easeOut' }\n</script>\n\n{#if isAnimated}\n    <TabsPrimitive.Content bind:ref {value} {...restProps}>\n        {#snippet child({ props: panelProps })}\n            <!-- eslint-disable-next-line @typescript-eslint/no-unused-vars -- hidden must be stripped so the panel stays in the DOM -->\n            {@const { hidden: _hidden, style: panelStyle, ...safeProps } = panelProps}\n            <div\n                {...safeProps}\n                data-slot=\"tabs-content\"\n                class={cn('flex-1 text-sm outline-none', className)}\n                style=\"{panelStyle ?? ''}{isActive ? '' : ';display:none'}\"\n            >\n                {#key ctx.value()}\n                    <MotionDiv\n                        initial={initial ?? defaultInitial}\n                        animate={animate ?? defaultAnimate}\n                        transition={transition ?? defaultTransition}\n                    >\n                        {@render children?.()}\n                    </MotionDiv>\n                {/key}\n            </div>\n        {/snippet}\n    </TabsPrimitive.Content>\n{:else}\n    <TabsPrimitive.Content\n        bind:ref\n        {value}\n        data-slot=\"tabs-content\"\n        class={cn('flex-1 text-sm outline-none', className)}\n        {...restProps}\n    >\n        {@render children?.()}\n    </TabsPrimitive.Content>\n{/if}\n",
      "type": "registry:file",
      "target": "animated-tabs/animated-tabs-content.svelte"
    },
    {
      "content": "<!--\n  @component\n  Animated shadcn Tabs list component.\n\n  Wraps bits-ui Tabs.List for ARIA-compliant keyboard navigation\n  (arrow keys, Home/End, roving tabindex). Exposes shadcn's `default`\n  (filled pill bar) and `line` (underline) list styles via the\n  `tabsListVariants` / `variant` API.\n-->\n<script lang=\"ts\" module>\n    import { tv, type VariantProps } from 'tailwind-variants'\n\n    /** Symbol key for the list-level context (propagates the active `variant`). */\n    export const TABS_LIST_CTX = Symbol('animated-tabs-list')\n\n    /** Context shape exposed by the list to its triggers. */\n    export type TabsListContext = {\n        variant: () => TabsListVariant\n    }\n\n    /**\n     * shadcn Tabs list style variants. Orientation hooks are keyed off the\n     * root's `data-orientation` (emitted by bits-ui) so they resolve in this\n     * fork; `data-variant` drives the per-trigger `line`/`default` chrome.\n     */\n    export const tabsListVariants = tv({\n        base: 'group/tabs-list text-muted-foreground inline-flex w-fit items-center justify-center rounded-lg p-[3px] group-data-[orientation=horizontal]/tabs:h-9 group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col data-[variant=line]:rounded-none',\n        variants: {\n            variant: {\n                default: 'bg-muted',\n                line: 'gap-1 bg-transparent'\n            }\n        },\n        defaultVariants: {\n            variant: 'default'\n        }\n    })\n\n    export type TabsListVariant = VariantProps<typeof tabsListVariants>['variant']\n</script>\n\n<script lang=\"ts\">\n    import { cn, type WithoutChildrenOrChild } from '$UTILS$'\n    import { Tabs as TabsPrimitive, type TabsListProps as BitsTabsListProps } from 'bits-ui'\n    import { setContext } from 'svelte'\n\n    export type TabsListProps = WithoutChildrenOrChild<BitsTabsListProps> & {\n        /** shadcn list style. `default` is the filled pill bar; `line` is underline tabs. */\n        variant?: TabsListVariant\n    }\n\n    let {\n        class: className,\n        variant = 'default',\n        ref = $bindable(null),\n        children,\n        ...restProps\n    }: TabsListProps & { children?: import('svelte').Snippet } = $props()\n\n    setContext<TabsListContext>(TABS_LIST_CTX, {\n        variant: () => variant\n    })\n</script>\n\n<TabsPrimitive.List\n    bind:ref\n    data-slot=\"tabs-list\"\n    data-variant={variant}\n    class={cn(tabsListVariants({ variant }), className)}\n    {...restProps}\n>\n    {@render children?.()}\n</TabsPrimitive.List>\n",
      "type": "registry:file",
      "target": "animated-tabs/animated-tabs-list.svelte"
    },
    {
      "content": "<!--\n  @component\n  Animated shadcn Tabs trigger component.\n\n  In the `default` list variant with `animated=true` (from context), renders a\n  spring-based sliding indicator via svelte-motion `layoutId`. The `line`\n  variant and `animated=false` fall back to shadcn's CSS active styling\n  (background swap for `default`, underline for `line`).\n-->\n<script lang=\"ts\">\n    import { cn, type WithoutChildrenOrChild } from '$UTILS$'\n    import { AnimatePresence, MotionDiv } from '@humanspeak/svelte-motion'\n    import { Tabs as TabsPrimitive, type TabsTriggerProps as BitsTabsTriggerProps } from 'bits-ui'\n    import { getContext } from 'svelte'\n    import { TABS_CTX, type TabsContext } from './animated-tabs.svelte'\n    import { TABS_LIST_CTX, type TabsListContext } from './animated-tabs-list.svelte'\n\n    export type TabsTriggerProps = WithoutChildrenOrChild<BitsTabsTriggerProps> & {\n        /** Override animation for this trigger. Inherits from Root when unset. */\n        animated?: boolean\n        /** Override the indicator spring transition. Default: `{ type: 'spring', stiffness: 500, damping: 30 }` */\n        transition?: Record<string, unknown>\n    }\n\n    let {\n        class: className,\n        value,\n        animated,\n        transition,\n        ref = $bindable(null),\n        children,\n        ...restProps\n    }: TabsTriggerProps & { children?: import('svelte').Snippet } = $props()\n\n    const ctx = getContext<TabsContext>(TABS_CTX)\n    const listCtx = getContext<TabsListContext>(TABS_LIST_CTX)\n    const isActive = $derived(ctx.value() === value)\n    const variant = $derived(listCtx?.variant() ?? 'default')\n    // The sliding box indicator represents the `default` filled variant; the\n    // `line` variant uses shadcn's CSS underline instead.\n    const showIndicator = $derived((animated ?? ctx.animated) && variant === 'default')\n\n    const defaultTransition = { type: 'spring' as const, stiffness: 500, damping: 30 }\n</script>\n\n<TabsPrimitive.Trigger\n    bind:ref\n    {value}\n    data-slot=\"tabs-trigger\"\n    class={cn(\n        \"relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-1.5 py-0.5 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 has-data-[icon=inline-end]:pr-1 has-data-[icon=inline-start]:pl-1 dark:text-muted-foreground dark:hover:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n        // `line` variant chrome: transparent background + sliding underline,\n        // driven purely by the list's `data-variant` group selector.\n        'group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent',\n        'after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-[state=active]:after:opacity-100',\n        // `default` variant CSS active styling — used whenever the motion\n        // indicator is not handling the active background (line variant or\n        // `animated=false`). Skipped while the indicator paints it.\n        !showIndicator &&\n            'group-data-[variant=default]/tabs-list:data-[state=active]:bg-background group-data-[variant=default]/tabs-list:data-[state=active]:text-foreground group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm dark:group-data-[variant=default]/tabs-list:data-[state=active]:border-input dark:group-data-[variant=default]/tabs-list:data-[state=active]:bg-input/30 dark:group-data-[variant=default]/tabs-list:data-[state=active]:text-foreground',\n        className\n    )}\n    {...restProps}\n>\n    {#if showIndicator}\n        <AnimatePresence>\n            {#if isActive}\n                <MotionDiv\n                    key=\"indicator\"\n                    layoutId={ctx.layoutId}\n                    class=\"absolute inset-0 rounded-md bg-background shadow-sm dark:border dark:border-input dark:bg-input/30\"\n                    transition={transition ?? defaultTransition}\n                />\n            {/if}\n        </AnimatePresence>\n        <span\n            class=\"relative z-10 inline-flex items-center gap-1.5\"\n            class:text-foreground={isActive}\n        >\n            {@render children?.()}\n        </span>\n    {:else}\n        {@render children?.()}\n    {/if}\n</TabsPrimitive.Trigger>\n",
      "type": "registry:file",
      "target": "animated-tabs/animated-tabs-trigger.svelte"
    },
    {
      "content": "<!--\n  @component\n  Animated shadcn Tabs root component powered by svelte-motion.\n\n  Drop-in replacement for standard shadcn tabs with a spring-based sliding\n  indicator (via layoutId) that animates between triggers. Wraps bits-ui\n  Tabs primitives for full ARIA compliance.\n\n  Set `animated={false}` to disable motion and get vanilla shadcn behavior.\n-->\n<script lang=\"ts\" module>\n    /** Module-level counter to generate unique layoutIds per tab group. */\n    let instanceCounter = 0\n\n    /** Symbol key for the tabs context. */\n    export const TABS_CTX = Symbol('animated-tabs')\n\n    /** Context shape propagated to child components. */\n    export type TabsContext = {\n        animated: boolean\n        layoutId: string\n        value: () => string\n    }\n</script>\n\n<script lang=\"ts\">\n    import { cn, type WithoutChildrenOrChild } from '$UTILS$'\n    import { Tabs as TabsPrimitive, type TabsRootProps as BitsTabsRootProps } from 'bits-ui'\n    import { setContext } from 'svelte'\n\n    export type TabsProps = WithoutChildrenOrChild<BitsTabsRootProps> & {\n        /** Set to false to disable motion animations (vanilla shadcn behavior). */\n        animated?: boolean\n    }\n\n    let {\n        class: className,\n        value = $bindable(''),\n        onValueChange,\n        animated = true,\n        ref = $bindable(null),\n        children,\n        ...restProps\n    }: TabsProps & { children?: import('svelte').Snippet } = $props()\n\n    // eslint-disable-next-line no-useless-assignment -- counter is read on next instance mount\n    const layoutId = `animated-tabs-${instanceCounter++}`\n\n    setContext<TabsContext>(TABS_CTX, {\n        animated,\n        layoutId,\n        get value() {\n            return () => value\n        }\n    })\n</script>\n\n<TabsPrimitive.Root\n    bind:ref\n    bind:value\n    {onValueChange}\n    data-slot=\"tabs\"\n    class={cn('group/tabs flex flex-col gap-2 data-[orientation=horizontal]:flex-col', className)}\n    {...restProps}\n>\n    {@render children?.()}\n</TabsPrimitive.Root>\n",
      "type": "registry:file",
      "target": "animated-tabs/animated-tabs.svelte"
    }
  ],
  "categories": [
    "navigation"
  ],
  "author": "Humanspeak, Inc.",
  "docs": "Requires @humanspeak/svelte-motion and bits-ui. Set animated={false} on Root to disable."
}
