{
  "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 * as React from \"react\";\n\nimport { useComposedRefs } from \"@/lib/compose-refs\";\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-semibold\">\n            +99\n          </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-semibold\">\n                +{hiddenCount}\n              </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": ""
    },
    {
      "path": "lib/compose-refs.ts",
      "type": "registry:lib",
      "content": "/**\n * @see https://github.com/radix-ui/primitives/blob/main/packages/react/compose-refs/src/compose-refs.tsx\n */\n\nimport * as React from \"react\";\n\ntype PossibleRef<T> = React.Ref<T> | undefined;\n\n/**\n * Set a given ref to a given value\n * This utility takes care of different types of refs: callback refs and RefObject(s)\n */\nfunction setRef<T>(ref: PossibleRef<T>, value: T) {\n  if (typeof ref === \"function\") {\n    return ref(value);\n  }\n\n  if (ref !== null && ref !== undefined) {\n    ref.current = value;\n  }\n}\n\n/**\n * A utility to compose multiple refs together\n * Accepts callback refs and RefObject(s)\n */\nfunction composeRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {\n  return (node) => {\n    let hasCleanup = false;\n    const cleanups = refs.map((ref) => {\n      const cleanup = setRef(ref, node);\n      if (!hasCleanup && typeof cleanup === \"function\") {\n        hasCleanup = true;\n      }\n      return cleanup;\n    });\n\n    // React <19 will log an error to the console if a callback ref returns a\n    // value. We don't use ref cleanups internally so this will only happen if a\n    // user's ref callback returns a value, which we only expect if they are\n    // using the cleanup functionality added in React 19.\n    if (hasCleanup) {\n      return () => {\n        for (let i = 0; i < cleanups.length; i++) {\n          const cleanup = cleanups[i];\n          if (typeof cleanup === \"function\") {\n            cleanup();\n          } else {\n            setRef(refs[i], null);\n          }\n        }\n      };\n    }\n  };\n}\n\n/**\n * A custom hook that composes multiple refs\n * Accepts callback refs and RefObject(s)\n */\nfunction useComposedRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {\n  // oxlint-disable-next-line react/exhaustive-deps -- we want to memoize by all values\n  return React.useCallback(composeRefs(...refs), refs);\n}\n\nexport { composeRefs, useComposedRefs };\n",
      "target": ""
    }
  ]
}