{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "icon-bar",
  "type": "registry:ui",
  "registryDependencies": [],
  "dependencies": [
    "@base-ui/react/toggle",
    "@base-ui/react/toggle-group",
    "motion",
    "lucide-react"
  ],
  "devDependencies": [],
  "files": [
    {
      "path": "icon-bar.tsx",
      "content": "\"use client\";\n\nimport { Toggle as TogglePrimitive } from \"@base-ui/react/toggle\";\nimport { ToggleGroup as ToggleGroupPrimitive } from \"@base-ui/react/toggle-group\";\nimport type { LucideIcon } from \"lucide-react\";\nimport { motion } from \"motion/react\";\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst componentThemeClassName =\n  \"[--ic-background:#ffffff] [--ic-foreground:#111111] [--ic-primary:#111111] [--ic-secondary:#646b75] [--ic-surface-border:#e9edf2] [--ic-border:#e3e7ec] [--ic-card:#ffffff] [--ic-card-foreground:#111111] [--ic-muted:#f5f7fa] [--ic-muted-foreground:#6d7480] [--ic-accent:#f3f5f8] [--color-accent:var(--ic-accent)] [--color-accent-foreground:var(--ic-accent-foreground)] [--ic-accent-foreground:#111111] [--ic-input:#e3e7ec] [--ic-ring:rgba(17,17,17,0.16)] [--ic-destructive:#dc2626] [--ic-paper:#fcfcfd] [--ic-popover-foreground:#111111] [--ic-brand:#0ea5e9] [--ic-brand-soft:#bae6fd] [--ic-shadow-soft:0_18px_38px_-24px_rgba(15,23,42,0.35)] [--ic-chart-1:oklch(0.52_0.19_254)] [--ic-chart-2:oklch(0.74_0.11_232)] [--ic-chart-3:oklch(0.42_0.16_262)] [--ic-chart-4:oklch(0.84_0.07_228)] [--ic-chart-5:oklch(0.62_0.14_240)] [--color-background:var(--ic-background)] [--color-foreground:var(--ic-foreground)] [--color-primary:var(--ic-primary)] [--color-secondary:var(--ic-secondary)] [--color-border:var(--ic-border)] [--color-card:var(--ic-card)] [--color-card-foreground:var(--ic-card-foreground)] [--color-muted:var(--ic-muted)] [--color-muted-foreground:var(--ic-muted-foreground)] [--color-accent:var(--ic-accent)] [--color-accent-foreground:var(--ic-accent-foreground)] [--color-input:var(--ic-input)] [--color-ring:var(--ic-ring)] [--color-destructive:var(--ic-destructive)] [--color-paper:var(--ic-paper)] [--color-popover-foreground:var(--ic-popover-foreground)] [--color-brand:var(--ic-brand)] [--color-brand-soft:var(--ic-brand-soft)] [--color-chart-1:var(--ic-chart-1)] [--color-chart-2:var(--ic-chart-2)] [--color-chart-3:var(--ic-chart-3)] [--color-chart-4:var(--ic-chart-4)] [--color-chart-5:var(--ic-chart-5)] dark:[--ic-background:#111111] dark:[--ic-foreground:#f6f3ec] dark:[--ic-primary:#f6f3ec] dark:[--ic-secondary:#cbc6bb] dark:[--ic-surface-border:#2a2a25] dark:[--ic-border:#2b2a25] dark:[--ic-card:#111111] dark:[--ic-card-foreground:#f6f3ec] dark:[--ic-muted:#171716] dark:[--ic-muted-foreground:#9a958a] dark:[--ic-accent:#1a1a18] [--color-accent:var(--ic-accent)] [--color-accent-foreground:var(--ic-accent-foreground)] dark:[--ic-accent-foreground:#f6f3ec] dark:[--ic-input:#2b2a25] dark:[--ic-ring:rgba(246,243,236,0.18)] dark:[--ic-destructive:#f87171] dark:[--ic-paper:#171716] dark:[--ic-popover-foreground:#f6f3ec] dark:[--ic-brand:#38bdf8] dark:[--ic-brand-soft:#0c4a6e] dark:[--ic-shadow-soft:0_20px_44px_-28px_rgba(0,0,0,0.6)] dark:[--ic-chart-1:oklch(0.68_0.17_250)] dark:[--ic-chart-2:oklch(0.82_0.09_225)] dark:[--ic-chart-3:oklch(0.58_0.15_260)] dark:[--ic-chart-4:oklch(0.75_0.12_235)] dark:[--ic-chart-5:oklch(0.88_0.06_220)]\";\n\nconst ICON_CELL_PX = 36;\nconst EXPAND_DURATION = 0.62;\nconst COLLAPSE_DURATION = 0.48;\nconst FLUID_EASE = [0.16, 1, 0.3, 1] as const;\n\nfunction setRef<T>(ref: React.Ref<T> | undefined, value: T | null) {\n  if (typeof ref === \"function\") {\n    ref(value);\n    return;\n  }\n\n  if (ref) {\n    (ref as React.MutableRefObject<T | null>).current = value;\n  }\n}\n\ntype IconBarProps = {\n  className?: string;\n  children?: React.ReactNode;\n  defaultValue?: string | null;\n  onValueChange?: (value: string | null) => void;\n  value?: string | null;\n};\n\nfunction IconBar({\n  className,\n  children,\n  defaultValue,\n  onValueChange,\n  value: valueProp,\n}: IconBarProps) {\n  const [uncontrolledValue, setUncontrolledValue] = React.useState<\n    string | null\n  >(defaultValue ?? null);\n  const isControlled = valueProp !== undefined;\n  const selectedValue = isControlled ? (valueProp ?? null) : uncontrolledValue;\n\n  const handleValueChange = React.useCallback(\n    (groupValue: string[]) => {\n      const nextValue = groupValue[0] ?? null;\n\n      if (!isControlled) {\n        setUncontrolledValue(nextValue);\n      }\n\n      onValueChange?.(nextValue);\n    },\n    [isControlled, onValueChange]\n  );\n\n  const groupValue = selectedValue ? [selectedValue] : [];\n\n  return (\n    <ToggleGroupPrimitive\n      aria-orientation=\"horizontal\"\n      className={cn(\n        componentThemeClassName,\n        \"flex flex-wrap items-center gap-2\",\n        className\n      )}\n      multiple={false}\n      onValueChange={handleValueChange}\n      role=\"toolbar\"\n      value={groupValue}\n    >\n      {children}\n    </ToggleGroupPrimitive>\n  );\n}\nIconBar.displayName = \"IconBar\";\n\ntype IconBarItemButtonProps = React.ComponentProps<\"button\"> & {\n  forwardedRef: React.ForwardedRef<HTMLButtonElement>;\n  hoverPreview: boolean;\n  icon: LucideIcon;\n  isSelected: boolean;\n  label: string;\n  labelWidth: number;\n  measureRef: React.RefObject<HTMLSpanElement | null>;\n  renderRef?: React.Ref<HTMLButtonElement>;\n  setHoverPreview: React.Dispatch<React.SetStateAction<boolean>>;\n};\n\nfunction IconBarItemButton({\n  className,\n  disabled,\n  forwardedRef,\n  hoverPreview,\n  icon: Icon,\n  isSelected,\n  label,\n  labelWidth,\n  measureRef,\n  onBlur,\n  onFocus,\n  renderRef,\n  setHoverPreview,\n  ...buttonProps\n}: IconBarItemButtonProps) {\n  const buttonRef = React.useRef<HTMLButtonElement | null>(null);\n  const prevSelectedRef = React.useRef(isSelected);\n  const expanded = !disabled && (isSelected || hoverPreview);\n  const showLabel = expanded && labelWidth > 0;\n\n  React.useEffect(() => {\n    if (prevSelectedRef.current && !isSelected) {\n      setHoverPreview(false);\n      buttonRef.current?.blur();\n    }\n\n    prevSelectedRef.current = isSelected;\n  }, [isSelected, setHoverPreview]);\n\n  const mergeRefs = React.useCallback(\n    (node: HTMLButtonElement | null) => {\n      buttonRef.current = node;\n      setRef(renderRef, node);\n      setRef(forwardedRef, node);\n    },\n    [forwardedRef, renderRef]\n  );\n\n  return (\n    <button\n      {...buttonProps}\n      aria-expanded={showLabel}\n      aria-label={label}\n      aria-pressed={isSelected}\n      className={cn(\n        \"relative inline-flex h-9 shrink-0 items-center overflow-hidden rounded-lg bg-muted text-muted-foreground outline-none\",\n        \"transition-[background-color,color] duration-300 ease-[cubic-bezier(0.22,1,0.36,1)]\",\n        \"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n        \"hover:bg-accent/60 hover:text-foreground\",\n        \"disabled:pointer-events-none disabled:opacity-50\",\n        isSelected && \"bg-accent text-foreground\",\n        className\n      )}\n      disabled={disabled}\n      onBlur={(event) => {\n        setHoverPreview(false);\n        onBlur?.(event);\n      }}\n      onFocus={(event) => {\n        onFocus?.(event);\n\n        if (disabled || event.defaultPrevented) return;\n\n        if (event.currentTarget.matches(\":focus-visible\")) {\n          setHoverPreview(true);\n        }\n      }}\n      onMouseEnter={() => {\n        if (!disabled) setHoverPreview(true);\n      }}\n      onMouseLeave={() => {\n        if (!disabled) setHoverPreview(false);\n      }}\n      ref={mergeRefs}\n      type=\"button\"\n    >\n      <span className=\"flex size-9 shrink-0 items-center justify-center\">\n        <Icon aria-hidden className=\"size-[18px] stroke-[1.5]\" />\n      </span>\n\n      <motion.div\n        animate={{ width: showLabel ? labelWidth : 0 }}\n        className=\"overflow-hidden\"\n        initial={false}\n        transition={{\n          duration: expanded ? EXPAND_DURATION : COLLAPSE_DURATION,\n          ease: FLUID_EASE,\n        }}\n      >\n        <span className=\"block whitespace-nowrap pr-3 font-medium text-[14px] tracking-[-0.01em]\">\n          {label}\n        </span>\n      </motion.div>\n\n      <span\n        aria-hidden\n        className=\"pointer-events-none absolute top-0 whitespace-nowrap pr-3 font-medium text-[14px] tracking-[-0.01em] opacity-0\"\n        ref={measureRef}\n        style={{ left: ICON_CELL_PX }}\n      >\n        {label}\n      </span>\n    </button>\n  );\n}\n\ntype IconBarItemProps = Omit<\n  TogglePrimitive.Props,\n  \"children\" | \"render\" | \"value\"\n> & {\n  icon: LucideIcon;\n  label: string;\n  value?: string;\n};\n\nconst IconBarItem = React.forwardRef<HTMLButtonElement, IconBarItemProps>(\n  (\n    {\n      className,\n      disabled = false,\n      icon,\n      label,\n      onBlur: onBlurProp,\n      onClick,\n      onFocus: onFocusProp,\n      value,\n      ...toggleProps\n    },\n    forwardedRef\n  ) => {\n    const itemValue = value ?? label;\n    const [hoverPreview, setHoverPreview] = React.useState(false);\n    const measureRef = React.useRef<HTMLSpanElement>(null);\n    const [labelWidth, setLabelWidth] = React.useState(0);\n\n    // biome-ignore lint/correctness/useExhaustiveDependencies: remeasure when label copy changes\n    React.useLayoutEffect(() => {\n      const node = measureRef.current;\n      if (!node) return;\n\n      const measure = () => {\n        setLabelWidth(Math.ceil(node.getBoundingClientRect().width));\n      };\n\n      measure();\n\n      const observer = new ResizeObserver(measure);\n      observer.observe(node);\n\n      const fonts = document.fonts;\n      if (fonts?.ready) {\n        fonts.ready.then(measure).catch(() => undefined);\n      }\n\n      return () => observer.disconnect();\n    }, [label]);\n\n    return (\n      <TogglePrimitive\n        {...toggleProps}\n        disabled={disabled}\n        onBlur={onBlurProp}\n        onClick={onClick}\n        onFocus={onFocusProp}\n        render={(renderProps, state) => {\n          const {\n            className: renderClassName,\n            ref: renderRef,\n            ...resolvedRenderProps\n          } = renderProps;\n\n          return (\n            <IconBarItemButton\n              {...resolvedRenderProps}\n              className={cn(renderClassName, className)}\n              disabled={disabled}\n              forwardedRef={forwardedRef}\n              hoverPreview={hoverPreview}\n              icon={icon}\n              isSelected={state.pressed}\n              label={label}\n              labelWidth={labelWidth}\n              measureRef={measureRef}\n              renderRef={renderRef}\n              setHoverPreview={setHoverPreview}\n            />\n          );\n        }}\n        value={itemValue}\n      />\n    );\n  }\n);\nIconBarItem.displayName = \"IconBarItem\";\n\nexport { IconBar, IconBarItem };\nexport type { IconBarItemProps, IconBarProps };\n",
      "type": "registry:ui"
    }
  ],
  "title": "Icon Bar",
  "description": "Row of compact icon chips that expand on hover or focus and stay open when selected. Built with Base UI Toggle Group, Motion, and Lucide."
}
