{
  "name": "badge-overflow",
  "type": "registry:ui",
  "dependencies": [
    "cn",
    "radix-ui"
  ],
  "files": [
    {
      "path": "ui/badge-overflow.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport { cn } from \"cn\";\nimport { Slot as SlotPrimitive } from \"radix-ui\";\nimport { useComposedRefs } from \"radix-ui/internal\";\nimport * as React from \"react\";\n\ninterface GetBadgeLabel<T> {\n  /**\n   * Callback that returns a label string for each badge item.\n   * Optional for primitive arrays (strings, numbers), required for object arrays.\n   * @example getBadgeLabel={(item) => item.name}\n   */\n  getBadgeLabel: (item: T) => string;\n}\n\ntype BadgeOverflowElement = React.ComponentRef<typeof BadgeOverflow>;\n\ntype BadgeOverflowProps<T = string> = React.ComponentProps<\"div\"> &\n  (T extends object ? GetBadgeLabel<T> : Partial<GetBadgeLabel<T>>) & {\n    items: T[];\n    lineCount?: number;\n    renderBadge: (item: T, label: string) => React.ReactNode;\n    renderOverflow?: (count: number) => React.ReactNode;\n    asChild?: boolean;\n  };\n\nfunction BadgeOverflow<T = string>(props: BadgeOverflowProps<T>) {\n  const {\n    items,\n    getBadgeLabel: getBadgeLabelProp,\n    lineCount = 1,\n    renderBadge,\n    renderOverflow,\n    asChild,\n    className,\n    style,\n    ref,\n    ...rootProps\n  } = props;\n\n  const getBadgeLabel = React.useCallback(\n    (item: T): string => {\n      if (typeof item === \"object\" && !getBadgeLabelProp) {\n        throw new Error(\n          \"`getBadgeLabel` is required when using array of objects\",\n        );\n      }\n      return getBadgeLabelProp ? getBadgeLabelProp(item) : (item as string);\n    },\n    [getBadgeLabelProp],\n  );\n\n  const rootRef = React.useRef<BadgeOverflowElement | null>(null);\n  const composedRef = useComposedRefs(ref, rootRef);\n  const measureRef = React.useRef<HTMLDivElement | null>(null);\n  const [containerWidth, setContainerWidth] = React.useState(0);\n  const [badgeGap, setBadgeGap] = React.useState(4);\n  const [badgeHeight, setBadgeHeight] = React.useState(20);\n  const [overflowBadgeWidth, setOverflowBadgeWidth] = React.useState(40);\n  const [isMeasured, setIsMeasured] = React.useState(false);\n  const [badgeWidths, setBadgeWidths] = React.useState<Map<string, number>>(\n    new Map(),\n  );\n\n  React.useLayoutEffect(() => {\n    if (!rootRef.current || !measureRef.current) return;\n\n    function measureContainer() {\n      if (!rootRef.current || !measureRef.current) return;\n\n      const computedStyle = getComputedStyle(rootRef.current);\n\n      const gapValue = computedStyle.gap;\n      const gap = gapValue ? parseFloat(gapValue) : 4;\n      setBadgeGap(gap);\n\n      const paddingLeft = parseFloat(computedStyle.paddingLeft) || 0;\n      const paddingRight = parseFloat(computedStyle.paddingRight) || 0;\n      const totalPadding = paddingLeft + paddingRight;\n\n      const widthMap = new Map<string, number>();\n      const measureChildren = measureRef.current.children;\n\n      for (let i = 0; i < items.length; i++) {\n        const child = measureChildren[i] as HTMLElement | undefined;\n        if (child) {\n          const label = getBadgeLabel(items[i] as T);\n          widthMap.set(label, child.offsetWidth);\n        }\n      }\n      setBadgeWidths(widthMap);\n\n      const firstBadge = measureChildren[0] as HTMLElement | undefined;\n      if (firstBadge) {\n        setBadgeHeight(firstBadge.offsetHeight || 20);\n      }\n\n      const overflowChild = measureChildren[items.length] as\n        | HTMLElement\n        | undefined;\n\n      if (overflowChild) {\n        setOverflowBadgeWidth(overflowChild.offsetWidth || 40);\n      }\n\n      const width = rootRef.current.clientWidth - totalPadding;\n      setContainerWidth(width);\n      setIsMeasured(true);\n    }\n\n    measureContainer();\n\n    const resizeObserver = new ResizeObserver(measureContainer);\n    resizeObserver.observe(rootRef.current);\n\n    return () => {\n      resizeObserver.disconnect();\n    };\n  }, [items, getBadgeLabel]);\n\n  const placeholderHeight = React.useMemo(\n    () => badgeHeight * lineCount + badgeGap * (lineCount - 1),\n    [badgeHeight, badgeGap, lineCount],\n  );\n\n  const { visibleItems, hiddenCount } = React.useMemo(() => {\n    if (!containerWidth || items.length === 0 || badgeWidths.size === 0) {\n      return { visibleItems: items, hiddenCount: 0 };\n    }\n\n    let currentLineWidth = 0;\n    let currentLine = 1;\n    const visible: T[] = [];\n\n    for (let i = 0; i < items.length; i++) {\n      const item = items[i];\n      if (!item) continue;\n\n      const label = getBadgeLabel(item);\n      const badgeWidth = badgeWidths.get(label);\n\n      if (!badgeWidth) {\n        // Skip items that haven't been measured yet\n        continue;\n      }\n\n      const widthWithGap = badgeWidth + badgeGap;\n      const isLastLine = currentLine === lineCount;\n      const hasMoreItems = i < items.length - 1;\n\n      const availableWidth =\n        isLastLine && hasMoreItems\n          ? containerWidth - overflowBadgeWidth - badgeGap\n          : containerWidth;\n\n      if (currentLineWidth + widthWithGap <= availableWidth) {\n        currentLineWidth += widthWithGap;\n        visible.push(item);\n      } else if (currentLine < lineCount) {\n        currentLine++;\n        currentLineWidth = widthWithGap;\n        visible.push(item);\n      } else {\n        // We're on the last line and this badge doesn't fit\n        break;\n      }\n    }\n\n    return {\n      visibleItems: visible,\n      hiddenCount: Math.max(0, items.length - visible.length),\n    };\n  }, [\n    items,\n    getBadgeLabel,\n    containerWidth,\n    lineCount,\n    badgeGap,\n    overflowBadgeWidth,\n    badgeWidths,\n  ]);\n\n  const Comp = asChild ? SlotPrimitive.Slot : \"div\";\n\n  return (\n    <>\n      <div\n        ref={measureRef}\n        className=\"pointer-events-none invisible absolute flex flex-wrap\"\n        style={{ gap: badgeGap }}\n      >\n        {items.map((item, index) => (\n          <React.Fragment key={index}>\n            {renderBadge(item, getBadgeLabel(item))}\n          </React.Fragment>\n        ))}\n        {renderOverflow ? (\n          renderOverflow(99)\n        ) : (\n          <div className=\"inline-flex h-5 shrink-0 items-center rounded-md border px-1.5 text-xs font-medium\">+99</div>\n        )}\n      </div>\n      {isMeasured ? (\n        <Comp\n          data-slot=\"badge-overflow\"\n          {...rootProps}\n          ref={composedRef}\n          className={cn(\"flex flex-wrap\", className)}\n          style={{\n            gap: badgeGap,\n            ...style,\n          }}\n        >\n          {visibleItems.map((item, index) => (\n            <React.Fragment key={index}>\n              {renderBadge(item, getBadgeLabel(item))}\n            </React.Fragment>\n          ))}\n          {hiddenCount > 0 &&\n            (renderOverflow ? (\n              renderOverflow(hiddenCount)\n            ) : (\n              <div className=\"inline-flex h-5 shrink-0 items-center rounded-md border px-1.5 text-xs font-medium\">+{hiddenCount}</div>\n            ))}\n        </Comp>\n      ) : (\n        <Comp\n          data-slot=\"badge-overflow\"\n          {...rootProps}\n          ref={composedRef}\n          className={cn(\"flex flex-wrap\", className)}\n          style={{\n            gap: badgeGap,\n            minHeight: placeholderHeight,\n            ...style,\n          }}\n        >\n          {items\n            .slice(\n              0,\n              Math.min(items.length, lineCount * 3 - (lineCount > 1 ? 1 : 0)),\n            )\n            .map((item, index) => (\n              <React.Fragment key={index}>\n                {renderBadge(item, getBadgeLabel(item))}\n              </React.Fragment>\n            ))}\n        </Comp>\n      )}\n    </>\n  );\n}\n\nexport { BadgeOverflow };\n",
      "target": ""
    }
  ]
}