{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tabs-motion",
  "title": "Motion tabs",
  "description": "Styled tabs: the Motion set. 5 decorative tabs variations (SlideTabs, FadeTabs, PopTabs, MorphTabs, SpringTabs) on the ai2 token system, powered by framer-motion, reduced-motion aware and sized sm to xl. Part of the free styled layer.",
  "dependencies": [
    "motion@^12.42.2"
  ],
  "registryDependencies": [
    "@ai2/tokens"
  ],
  "files": [
    {
      "path": "registry/ai2/styled/tabs-motion.tsx",
      "content": "\"use client\"\r\n\r\nimport * as React from \"react\"\r\nimport { motion, useReducedMotion } from \"motion/react\"\r\nimport type { Transition } from \"motion/react\"\r\n\r\nimport { cn } from \"@/lib/utils\"\r\n\r\n/* Motion tabs family: 5 tab strips differing ONLY in how the active indicator moves\n   between tabs. The indicator is carried with a framer-motion layoutId; because the\n   layoutId derives from React.useId() there is no teleporting between two examples\n   on the same page. Under reduced motion the indicator switches instantly and the\n   transform and scale animations do not run. Each export is a complete tabs\n   component - internal active-tab state, role=\"tablist\"/role=\"tab\" and a\n   role=\"tabpanel\" panel. Color comes ONLY from tokens, via alpha color-mix. */\r\n\r\nexport type StyledSize = \"sm\" | \"md\" | \"lg\" | \"xl\"\r\n\r\ntype TabItem = { value: string; label: React.ReactNode; panel: React.ReactNode }\r\n\r\ntype TabsProps = {\r\n  className?: string\r\n  size?: StyledSize\r\n  tabs?: TabItem[]\r\n  value?: string\r\n  defaultValue?: string\r\n  onValueChange?: (v: string) => void\r\n}\r\n\r\nconst DEFAULT_TABS: TabItem[] = [\r\n  { value: \"overview\", label: \"Overview\", panel: \"A summary of the workspace: recent activity, open items and the team.\" },\r\n  { value: \"activity\", label: \"Activity\", panel: \"Every event from the last seven days, newest first.\" },\r\n  { value: \"settings\", label: \"Settings\", panel: \"Workspace name, members and notification preferences.\" },\r\n]\r\n\r\nconst sizeTab: Record<StyledSize, string> = {\r\n  sm: \"h-8 px-3 text-xs\",\r\n  md: \"h-9 px-4 text-sm\",\r\n  lg: \"h-10 px-5 text-sm\",\r\n  xl: \"h-11 px-6 text-base\",\r\n}\r\n\r\nconst tabBase =\r\n  \"relative z-10 inline-flex select-none items-center justify-center gap-2 whitespace-nowrap font-medium outline-none transition-colors focus-visible:ring-[3px] focus-visible:ring-ring/50 [&_svg]:size-4 [&_svg]:shrink-0 [&>svg]:shrink-0 [&_i]:text-base [&_i]:leading-none [&>i]:shrink-0\"\r\n\r\nconst panelBase =\r\n  \"rounded-lg border border-border bg-card p-4 text-sm text-muted-foreground outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50\"\r\n\r\nfunction useTabsState(props: TabsProps, fallback: TabItem[]) {\r\n  const { tabs, value, defaultValue, onValueChange } = props\r\n  const list = React.useMemo(() => (tabs && tabs.length ? tabs : fallback), [tabs, fallback])\r\n  const [internal, setInternal] = React.useState<string>(() => defaultValue ?? list[0]?.value ?? \"\")\r\n  const active = value ?? internal\r\n  const refs = React.useRef<(HTMLButtonElement | null)[]>([])\r\n  const select = React.useCallback(\r\n    (v: string) => {\r\n      if (value === undefined) setInternal(v)\r\n      onValueChange?.(v)\r\n    },\r\n    [value, onValueChange]\r\n  )\r\n  const onKeyDown = (e: React.KeyboardEvent) => {\r\n    if (e.key !== \"ArrowLeft\" && e.key !== \"ArrowRight\") return\r\n    e.preventDefault()\r\n    const idx = list.findIndex((t) => t.value === active)\r\n    const dir = e.key === \"ArrowRight\" ? 1 : -1\r\n    const ni = (idx + dir + list.length) % list.length\r\n    select(list[ni].value)\r\n    refs.current[ni]?.focus()\r\n  }\r\n  return { list, active, select, refs, onKeyDown }\r\n}\r\n\r\ntype MotionConfig = {\r\n  bar: string\r\n  tab: string\r\n  indicator: string\r\n  active: string\r\n  inactive: string\r\n  /** Gostergenin sekmeler arasinda tasinma gecisi. */\r\n  transition: Transition\r\n  /** Reduced motion kapali iken gostergenin giris animasyonu. */\r\n  enter?: { initial: Record<string, number>; animate: Record<string, number> }\r\n}\r\n\r\n/* Shared shell: bar plus a layoutId indicator plus panel. */\r\nfunction MotionTabsShell({ props, config }: { props: TabsProps; config: MotionConfig }) {\r\n  const { className, size = \"md\" } = props\r\n  const { list, active, select, refs, onKeyDown } = useTabsState(props, DEFAULT_TABS)\r\n  const uid = React.useId()\r\n  const reduce = useReducedMotion()\r\n  const transition: Transition = reduce ? { duration: 0 } : config.transition\r\n  const enter = reduce ? undefined : config.enter\r\n  const current = list.find((t) => t.value === active) ?? list[0]\r\n\r\n  return (\r\n    <div data-slot=\"styled-tabs\" className={cn(\"flex flex-col gap-3\", className)}>\r\n      {/* 200% text (WCAG 1.4.4): the strip is `w-fit`, so growing text widened\n          the page. Scrolling was moved inside the strip itself; the wrapper is\n          REQUIRED because `overflow-x-auto` clips vertically too and the focus\n          ring would be cut off. The `-m-1 p-1` pair opens room for the ring and\n          preserves the outer measurements. The same fix is documented in detail\n          in registry/ai2/ui/tabs.tsx. */}\r\n      <div\r\n        data-slot=\"styled-tabs-viewport\"\r\n        className=\"-m-1 flex max-w-full overflow-x-auto p-1 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden\"\r\n      >\r\n        <div\r\n          role=\"tablist\"\r\n          aria-orientation=\"horizontal\"\r\n          onKeyDown={onKeyDown}\r\n          className={cn(\"inline-flex w-fit items-center gap-1\", config.bar)}\r\n        >\r\n          {list.map((tab, i) => {\r\n            const on = tab.value === active\r\n            return (\r\n              <button\r\n                key={tab.value}\r\n                ref={(el) => {\r\n                  refs.current[i] = el\r\n                }}\r\n                type=\"button\"\r\n                role=\"tab\"\r\n                id={`${uid}-tab-${tab.value}`}\r\n                aria-selected={on}\r\n                aria-controls={`${uid}-panel-${tab.value}`}\r\n                tabIndex={on ? 0 : -1}\r\n                onClick={() => select(tab.value)}\r\n                className={cn(tabBase, sizeTab[size], config.tab, on ? config.active : config.inactive)}\r\n              >\r\n                {on ? (\r\n                  <motion.span\r\n                    layoutId={`${uid}-motion-indicator`}\r\n                    transition={transition}\r\n                    initial={enter?.initial}\r\n                    animate={enter?.animate}\r\n                    className={cn(\"absolute -z-10\", config.indicator)}\r\n                  />\r\n                ) : null}\r\n                {tab.label}\r\n              </button>\r\n            )\r\n          })}\r\n        </div>\r\n      </div>\r\n      {current ? (\r\n        <div\r\n          role=\"tabpanel\"\r\n          id={`${uid}-panel-${current.value}`}\r\n          aria-labelledby={`${uid}-tab-${current.value}`}\r\n          tabIndex={0}\r\n          className={panelBase}\r\n        >\r\n          {current.panel}\r\n        </div>\r\n      ) : null}\r\n    </div>\r\n  )\r\n}\r\n\r\n/* Slide: the indicator slides sideways with a constant-speed tween. */\r\nexport function SlideTabs(props: TabsProps) {\r\n  return (\r\n    <MotionTabsShell\r\n      props={props}\r\n      config={{\r\n        bar: \"rounded-lg bg-muted p-1\",\r\n        tab: \"rounded-md\",\r\n        indicator: \"inset-0 rounded-md bg-primary\",\r\n        active: \"text-primary-foreground\",\r\n        inactive: \"text-muted-foreground hover:text-foreground\",\r\n        transition: { type: \"tween\" as const, duration: 0.28, ease: \"easeInOut\" },\r\n      }}\r\n    />\r\n  )\r\n}\r\n\r\n/* Fade: gosterge kayarken opaklik uzerinden erir, yumusak gecis. */\r\nexport function FadeTabs(props: TabsProps) {\r\n  return (\r\n    <MotionTabsShell\r\n      props={props}\r\n      config={{\r\n        bar: \"border-b border-border\",\r\n        tab: \"-mb-px rounded-t-md\",\r\n        indicator: \"inset-x-0 -bottom-px h-0.5 rounded-full bg-primary\",\r\n        active: \"text-foreground\",\r\n        inactive: \"text-muted-foreground hover:text-foreground\",\r\n        transition: { type: \"tween\" as const, duration: 0.35, ease: \"easeOut\" },\r\n        enter: { initial: { opacity: 0 }, animate: { opacity: 1 } },\r\n      }}\r\n    />\r\n  )\r\n}\r\n\r\n/* Pop: the indicator opens with a scale on the new tab, fast and crisp. */\r\nexport function PopTabs(props: TabsProps) {\r\n  return (\r\n    <MotionTabsShell\r\n      props={props}\r\n      config={{\r\n        bar: \"rounded-full bg-muted p-1\",\r\n        tab: \"rounded-full\",\r\n        indicator: \"inset-0 rounded-full bg-primary\",\r\n        active: \"text-primary-foreground\",\r\n        inactive: \"text-muted-foreground hover:text-foreground\",\r\n        transition: { type: \"spring\" as const, stiffness: 700, damping: 32 },\r\n        enter: { initial: { scale: 0.7, opacity: 0 }, animate: { scale: 1, opacity: 1 } },\r\n      }}\r\n    />\r\n  )\r\n}\r\n\r\n/* Morph: the indicator morphs to the tab along with its width and corner radius. */\r\nexport function MorphTabs(props: TabsProps) {\r\n  return (\r\n    <MotionTabsShell\r\n      props={props}\r\n      config={{\r\n        bar: \"rounded-xl border border-border bg-card p-1\",\r\n        tab: \"rounded-lg\",\r\n        indicator: \"inset-0 rounded-lg border border-[color-mix(in_oklab,var(--color-primary)_30%,transparent)] bg-[color-mix(in_oklab,var(--color-primary)_14%,transparent)]\",\r\n        active: \"text-primary\",\r\n        inactive: \"text-muted-foreground hover:text-foreground\",\r\n        transition: { type: \"spring\" as const, stiffness: 260, damping: 26 },\r\n      }}\r\n    />\r\n  )\r\n}\r\n\r\n/* Spring: dusuk sonumlu yay, gosterge hedefte hafifce salinir. */\r\nexport function SpringTabs(props: TabsProps) {\r\n  return (\r\n    <MotionTabsShell\r\n      props={props}\r\n      config={{\r\n        bar: \"rounded-lg border border-border bg-muted p-1\",\r\n        tab: \"rounded-md\",\r\n        indicator: \"inset-0 rounded-md border border-border bg-background shadow-sm\",\r\n        active: \"text-foreground\",\r\n        inactive: \"text-muted-foreground hover:text-foreground\",\r\n        transition: { type: \"spring\" as const, stiffness: 520, damping: 18, mass: 0.9 },\r\n      }}\r\n    />\r\n  )\r\n}\r\n",
      "type": "registry:component",
      "target": "components/ui/tabs-motion.tsx"
    }
  ],
  "categories": [
    "styled",
    "tabs"
  ],
  "type": "registry:component"
}