{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "px-tabs",
  "title": "Tabs",
  "description": "Tabs whose underline is forty pixels that stream from the old tab to the new one, with panels that slide in from the side.",
  "registryDependencies": [
    "https://elements.lumesec.ai/r/lumesec-theme.json",
    "https://elements.lumesec.ai/r/lumesec-math.json",
    "https://elements.lumesec.ai/r/lumesec-motion.json",
    "https://elements.lumesec.ai/r/lumesec-pixel-canvas.json",
    "https://elements.lumesec.ai/r/lumesec-use-controllable-state.json",
    "utils"
  ],
  "files": [
    {
      "path": "registry/components/lumesec/px-tabs.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { lerp, mixRgb } from \"@/lib/lumesec/math\";\nimport { prefersReducedMotion, springEuler } from \"@/lib/lumesec/motion\";\nimport { usePixelCanvas } from \"@/lib/lumesec/pixel-canvas\";\nimport { useControllableState } from \"@/lib/lumesec/use-controllable-state\";\nimport { cn } from \"@/lib/utils\";\n\n/** One tab and its panel. */\nexport interface PxTabsItem {\n  /** Identifies the tab in `value`. Defaults to `label`. */\n  value?: string;\n  /** Tab label. */\n  label: string;\n  /** Small count badge after the label. */\n  count?: string | number;\n  /** Panel content. */\n  content: React.ReactNode;\n}\n\n/** Props of `PxTabs`. Other `div` props go to the root. */\nexport interface PxTabsProps extends Omit<React.ComponentProps<\"div\">, \"children\" | \"defaultValue\" | \"onChange\"> {\n  /** The tabs, in order. */\n  items: readonly PxTabsItem[];\n  /** Controlled selected tab. Use with `onValueChange`. */\n  value?: string;\n  /** Initially selected tab when uncontrolled. Defaults to the first tab. */\n  defaultValue?: string;\n  /** Called when the user selects another tab. */\n  onValueChange?: (value: string) => void;\n}\n\n/** Number of dots in the underline. */\nconst DOTS = 40;\n\ninterface UnderlineDot {\n  position: number;\n  velocity: number;\n  /** Time in seconds when this dot starts moving, so the line streams across. */\n  start: number;\n}\n\n/**\n * Tabs whose underline is forty pixels that stream from the old tab to the new one; the new panel slides\n * in from the side it was selected from.\n *\n * @example\n * <PxTabs\n *   items={[\n *     { label: \"Overview\", content: \"Usage is steady this week.\" },\n *     { label: \"Activity\", count: 12, content: \"12 events since Monday.\" },\n *   ]}\n * />\n */\nexport function PxTabs({ items, value: valueProp, defaultValue, onValueChange, className, ...props }: PxTabsProps) {\n  const baseId = React.useId();\n  const values = items.map((item) => item.value ?? item.label);\n  const [value, setValue] = useControllableState({\n    value: valueProp,\n    defaultValue: defaultValue ?? values[0] ?? \"\",\n    onChange: onValueChange,\n  });\n  const index = Math.max(0, values.indexOf(value));\n\n  const tablistRef = React.useRef<HTMLDivElement>(null);\n  const tabRefs = React.useRef<(HTMLButtonElement | null)[]>([]);\n  const panelRefs = React.useRef<(HTMLDivElement | null)[]>([]);\n  const dots = React.useRef<UnderlineDot[]>([]);\n  const laidOutWidth = React.useRef(0);\n  const indexRef = React.useRef(index);\n  const shownIndex = React.useRef(index);\n\n  /** Left and right end of the underline under tab `i`, inset 8px, in canvas coordinates. */\n  const span = React.useCallback((i: number): [number, number] => {\n    const tablist = tablistRef.current;\n    const tab = tabRefs.current[i];\n    if (!tablist || !tab) return [0, 0];\n    const left = tab.offsetLeft - tablist.offsetLeft - tablist.scrollLeft;\n    return [left + 8, left + tab.offsetWidth - 8];\n  }, []);\n\n  const { ref: canvasRef, redraw } = usePixelCanvas((frame) => {\n    const { palette, time, delta } = frame;\n    frame.clear();\n    // no tabs: no underline\n    if (!tabRefs.current[indexRef.current]) return false;\n    const [a, b] = span(indexRef.current);\n    if (frame.width !== laidOutWidth.current) {\n      // a new size snaps the line under the current tab\n      laidOutWidth.current = frame.width;\n      dots.current = Array.from({ length: DOTS }, (_, k) => ({\n        position: lerp(a, b, k / (DOTS - 1)),\n        velocity: 0,\n        start: 0,\n      }));\n    }\n    const reduced = prefersReducedMotion();\n    let moving = false;\n    dots.current.forEach((dot, k) => {\n      const target = lerp(a, b, k / (DOTS - 1));\n      if (reduced) dot.position = target;\n      else if (time >= dot.start) springEuler(dot, target, delta, 240, 0.7);\n      const speed = Math.min(1, Math.abs(dot.velocity) / 500);\n      if (Math.abs(target - dot.position) > 0.3) moving = true;\n      const color = mixRgb(palette.accent, palette.shine, speed);\n      frame.dot(dot.position - 1.2, 1, color, 1, 2.4);\n      frame.dot(dot.position - 1.2, 4.2, color, 0.6 + speed * 0.4, 2.4);\n    });\n    return moving;\n  });\n\n  React.useLayoutEffect(() => {\n    indexRef.current = index;\n    const previous = shownIndex.current;\n    shownIndex.current = index;\n    if (previous === index) return;\n    const panel = panelRefs.current[index];\n    if (panel && !prefersReducedMotion() && typeof panel.animate === \"function\") {\n      panel.animate(\n        [\n          { opacity: 0, transform: `translateX(${index > previous ? 10 : -10}px)` },\n          { opacity: 1, transform: \"none\" },\n        ],\n        {\n          duration: 260,\n          easing: \"cubic-bezier(.2,.8,.2,1)\",\n        },\n      );\n    }\n    // the dots leave one after another, starting with the ones nearest the new tab\n    const now = performance.now() / 1000;\n    const forward = index > previous;\n    dots.current.forEach((dot, k) => {\n      dot.start = now + ((forward ? DOTS - 1 - k : k) / (DOTS - 1)) * 0.14;\n    });\n    redraw();\n  }, [index, redraw]);\n\n  React.useEffect(redraw, [items, redraw]);\n\n  const select = (i: number) => {\n    const next = values[i];\n    if (next !== undefined) setValue(next);\n  };\n\n  const onKeyDown = (event: React.KeyboardEvent<HTMLButtonElement>) => {\n    const n = items.length;\n    const keys: Record<string, number> = {\n      ArrowRight: (index + 1) % n,\n      ArrowLeft: (index - 1 + n) % n,\n      Home: 0,\n      End: n - 1,\n    };\n    const next = keys[event.key];\n    if (next === undefined) return;\n    event.preventDefault();\n    select(next);\n    tabRefs.current[next]?.focus();\n  };\n\n  return (\n    <div data-slot=\"px-tabs\" className={cn(\"block text-foreground\", className)} {...props}>\n      <div\n        ref={tablistRef}\n        role=\"tablist\"\n        // the underline follows the tabs when a narrow tab row scrolls sideways\n        onScroll={redraw}\n        className=\"flex [scrollbar-width:none] gap-0.5 overflow-x-auto [&::-webkit-scrollbar]:hidden\"\n      >\n        {items.map((item, i) => (\n          <button\n            key={values[i]}\n            ref={(node) => {\n              tabRefs.current[i] = node;\n            }}\n            type=\"button\"\n            role=\"tab\"\n            id={`${baseId}-tab-${i}`}\n            aria-selected={i === index}\n            aria-controls={`${baseId}-panel-${i}`}\n            tabIndex={i === index ? 0 : -1}\n            className=\"cursor-pointer rounded-t-lg border-0 bg-transparent px-3 py-2 text-[14px] leading-normal font-medium whitespace-nowrap text-muted-foreground outline-none select-none hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-lumesec focus-visible:outline-solid aria-selected:text-foreground\"\n            onClick={() => select(i)}\n            onKeyDown={onKeyDown}\n          >\n            {item.label}\n            {item.count !== undefined && (\n              <span className=\"ml-1.5 rounded-[5px] bg-foreground/8 px-[5px] py-px font-mono text-[11px]\">\n                {item.count}\n              </span>\n            )}\n          </button>\n        ))}\n      </div>\n      <canvas ref={canvasRef} aria-hidden=\"true\" className=\"block h-2 w-full [image-rendering:pixelated]\" />\n      <div className=\"-mt-[3px] h-px bg-border\" />\n      <div className=\"pt-3.5\">\n        {items.map((item, i) => (\n          <div\n            key={values[i]}\n            ref={(node) => {\n              panelRefs.current[i] = node;\n            }}\n            role=\"tabpanel\"\n            id={`${baseId}-panel-${i}`}\n            aria-labelledby={`${baseId}-tab-${i}`}\n            tabIndex={0}\n            hidden={i !== index}\n          >\n            {item.content}\n          </div>\n        ))}\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/lumesec/px-tabs.tsx"
    }
  ],
  "meta": {
    "tag": "px-tabs",
    "collection": "pixel-ui",
    "react": "PxTabs",
    "kind": "react"
  },
  "docs": "Tabs docs: https://elements.lumesec.ai/components/pixel-ui/tabs",
  "categories": [
    "pixel-ui",
    "navigation-data"
  ],
  "type": "registry:component"
}