{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "icon-bar",
  "title": "Icon Bar",
  "description": "A horizontal toolbar of icon buttons with bloom-open label reveal on hover/selection, accent dot indicator, and roving tabindex.",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "components/ui/icon-bar.tsx",
      "type": "registry:ui",
      "content": "'use client'\n\nimport {\n  createContext,\n  forwardRef,\n  useCallback,\n  useContext,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n  type HTMLAttributes,\n  type ReactNode,\n} from 'react'\nimport { useReducedMotion } from 'motion/react'\nimport { cn } from '@/lib/utils'\nimport { durations } from '@/lib/motion-tokens'\nimport { WeightShiftText } from '@/components/ui/weight-shift-text'\n\ninterface IconBarContextValue {\n  value: string | null\n  setValue: (v: string | null) => void\n\n  rovingIndex: number\n  setRovingIndex: (i: number) => void\n}\n\nconst IconBarContext = createContext<IconBarContextValue | null>(null)\n\nfunction useIconBarCtx(componentName: string) {\n  const ctx = useContext(IconBarContext)\n  if (!ctx) throw new Error(`${componentName} must be used within IconBar`)\n  return ctx\n}\n\nexport interface IconBarProps extends Omit<HTMLAttributes<HTMLDivElement>, 'defaultValue'> {\n  children: ReactNode\n  value?: string | null\n  defaultValue?: string | null\n  onValueChange?: (v: string | null) => void\n  'aria-label'?: string\n}\n\nexport const IconBar = forwardRef<HTMLDivElement, IconBarProps>(\n  (\n    {\n      children,\n      value: valueProp,\n      defaultValue = null,\n      onValueChange,\n      'aria-label': ariaLabel = 'Toolbar',\n      className,\n      ...props\n    },\n    ref,\n  ) => {\n    const isControlled = valueProp !== undefined\n    const [internalValue, setInternalValue] = useState(defaultValue)\n    const value = isControlled ? valueProp : internalValue\n\n    const setValue = useCallback(\n      (v: string | null) => {\n        if (!isControlled) setInternalValue(v)\n        onValueChange?.(v)\n      },\n      [isControlled, onValueChange],\n    )\n\n    const [rovingIndex, setRovingIndex] = useState(0)\n    const barRef = useRef<HTMLDivElement>(null)\n\n    const handleKeyDown = useCallback((e: React.KeyboardEvent<HTMLDivElement>) => {\n      const bar = barRef.current\n      if (!bar) return\n      const buttons = Array.from(\n        bar.querySelectorAll<HTMLElement>('[role=\"toolbar\"] > button:not([disabled])'),\n      )\n      if (!buttons.length) return\n\n      const isRTL = bar.ownerDocument.defaultView?.getComputedStyle(bar).direction === 'rtl'\n      const count = buttons.length\n      const currentIdx = buttons.indexOf(document.activeElement as HTMLElement)\n\n      let nextIdx = currentIdx\n\n      if (e.key === 'ArrowRight') {\n        e.preventDefault()\n        nextIdx = isRTL ? (currentIdx <= 0 ? count - 1 : currentIdx - 1) : (currentIdx + 1) % count\n      } else if (e.key === 'ArrowLeft') {\n        e.preventDefault()\n        nextIdx = isRTL ? (currentIdx + 1) % count : currentIdx <= 0 ? count - 1 : currentIdx - 1\n      } else if (e.key === 'Home') {\n        e.preventDefault()\n        nextIdx = 0\n      } else if (e.key === 'End') {\n        e.preventDefault()\n        nextIdx = count - 1\n      }\n\n      if (nextIdx !== currentIdx && nextIdx >= 0 && nextIdx < count) {\n        setRovingIndex(nextIdx)\n        buttons[nextIdx]?.focus()\n      }\n    }, [])\n\n    const ctx = useMemo<IconBarContextValue>(\n      () => ({ value, setValue, rovingIndex, setRovingIndex }),\n      [value, setValue, rovingIndex],\n    )\n\n    return (\n      <IconBarContext.Provider value={ctx}>\n        <div\n          ref={(node) => {\n            ;(barRef as React.MutableRefObject<HTMLDivElement | null>).current = node\n            if (typeof ref === 'function') ref(node)\n            else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = node\n          }}\n          role=\"toolbar\"\n          aria-orientation=\"horizontal\"\n          aria-label={ariaLabel}\n          className={cn('inline-flex items-center gap-1', className)}\n          onKeyDown={handleKeyDown}\n          {...props}\n        >\n          {children}\n        </div>\n      </IconBarContext.Provider>\n    )\n  },\n)\nIconBar.displayName = 'IconBar'\n\nexport interface IconBarItemProps extends HTMLAttributes<HTMLButtonElement> {\n  value: string\n  label: string\n  icon: ReactNode\n  disabled?: boolean\n}\n\nexport const IconBarItem = forwardRef<HTMLButtonElement, IconBarItemProps>(\n  ({ value: itemValue, label, icon, disabled, className, onClick, ...props }, ref) => {\n    const { value, setValue, rovingIndex, setRovingIndex } = useIconBarCtx('IconBarItem')\n    const reduceMotion = useReducedMotion()\n    const isSelected = value === itemValue\n\n    const measureRef = useRef<HTMLSpanElement>(null)\n    const [labelWidth, setLabelWidth] = useState(0)\n    const [hovered, setHovered] = useState(false)\n    const itemIndexRef = useRef(-1)\n    const [itemIdx, setItemIdx] = useState(-1)\n\n    useEffect(() => {\n      const bar = measureRef.current?.closest('[role=\"toolbar\"]')\n      if (!bar) return\n      const buttons = Array.from(bar.querySelectorAll<HTMLElement>('[role=\"toolbar\"] > button'))\n      const idx = buttons.indexOf(measureRef.current?.closest('button') as HTMLElement)\n      if (idx >= 0) {\n        itemIndexRef.current = idx\n        setItemIdx(idx)\n      }\n    }, [])\n\n    useEffect(() => {\n      const measure = () => {\n        if (measureRef.current) {\n          setLabelWidth(measureRef.current.offsetWidth)\n        }\n      }\n      measure()\n      if (document.fonts?.ready) {\n        document.fonts.ready.then(measure)\n      }\n    }, [label])\n\n    useEffect(() => {\n      const el = measureRef.current\n      if (!el) return\n      const observer = new ResizeObserver(() => {\n        setLabelWidth(el.offsetWidth)\n      })\n      observer.observe(el)\n      return () => observer.disconnect()\n    }, [])\n\n    const bloomed = hovered || isSelected\n\n    const expandDuration = reduceMotion ? '0ms' : durations.slow\n    const collapseDuration = reduceMotion ? '0ms' : durations.base\n    const isActive = bloomed && !disabled\n\n    return (\n      <>\n        <span\n          ref={measureRef}\n          aria-hidden=\"true\"\n          className=\"absolute invisible whitespace-nowrap px-3 py-2 text-sm font-medium\"\n        >\n          {label}\n        </span>\n        <button\n          ref={ref}\n          type=\"button\"\n          tabIndex={rovingIndex === itemIdx ? 0 : -1}\n          aria-pressed={isSelected}\n          aria-label={label}\n          disabled={disabled}\n          className={cn(\n            'relative flex h-11 items-center gap-0 rounded-md squircle-corners outline-none transition-colors duration-(--motion-dur-fast) motion-reduce:transition-none',\n            'focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-2 focus-visible:ring-offset-(--color-bg)',\n            isSelected\n              ? 'bg-(--color-surface-2) text-(--color-fg)'\n              : 'text-(--color-muted) hover:text-(--color-fg)',\n            disabled && 'cursor-not-allowed opacity-50',\n            className,\n          )}\n          onMouseEnter={() => !disabled && setHovered(true)}\n          onMouseLeave={() => setHovered(false)}\n          onFocus={() => !disabled && setHovered(true)}\n          onBlur={() => setHovered(false)}\n          onPointerDown={(e) => e.preventDefault()}\n          onClick={(e) => {\n            if (disabled) return\n            onClick?.(e)\n            setRovingIndex(itemIndexRef.current)\n            setValue(isSelected ? null : itemValue)\n          }}\n          {...props}\n        >\n          <span\n            className=\"relative z-10 flex size-9 shrink-0 items-center justify-center\"\n            aria-hidden=\"true\"\n          >\n            {icon}\n          </span>\n\n          <span\n            className=\"overflow-hidden\"\n            style={{\n              width: isActive ? labelWidth : 0,\n              transition: `width ${isActive ? expandDuration : collapseDuration} var(--motion-ease-out)`,\n            }}\n          >\n            <span className=\"flex items-center pe-3\">\n              <WeightShiftText\n                baseWeight={400}\n                activeWeight={500}\n                active={bloomed && !reduceMotion}\n                duration=\"200ms\"\n              >\n                {label}\n              </WeightShiftText>\n            </span>\n          </span>\n\n          {isSelected && (\n            <span\n              className=\"absolute top-1.5 end-1.5 size-1 rounded-full bg-(--color-accent)\"\n              aria-hidden=\"true\"\n            />\n          )}\n        </button>\n      </>\n    )\n  },\n)\nIconBarItem.displayName = 'IconBarItem'\n\nexport function IconBarPreview() {\n  const [value, setValue] = useState<string | null>('pen')\n  return (\n    <div\n      className=\"flex h-full w-full items-center justify-center p-6\"\n      style={{ backgroundColor: 'var(--color-surface)' }}\n    >\n      <IconBar value={value} onValueChange={setValue} aria-label=\"Tools\">\n        <IconBarItem\n          value=\"pen\"\n          label=\"Pen\"\n          icon={\n            <svg\n              width=\"18\"\n              height=\"18\"\n              viewBox=\"0 0 24 24\"\n              fill=\"none\"\n              stroke=\"currentColor\"\n              strokeWidth=\"1.5\"\n              strokeLinecap=\"round\"\n              strokeLinejoin=\"round\"\n            >\n              <path d=\"M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z\" />\n              <path d=\"m15 5 4 4\" />\n            </svg>\n          }\n        />\n        <IconBarItem\n          value=\"eraser\"\n          label=\"Eraser\"\n          icon={\n            <svg\n              width=\"18\"\n              height=\"18\"\n              viewBox=\"0 0 24 24\"\n              fill=\"none\"\n              stroke=\"currentColor\"\n              strokeWidth=\"1.5\"\n              strokeLinecap=\"round\"\n              strokeLinejoin=\"round\"\n            >\n              <path d=\"m7 21-4.3-4.3c-1-1-1-2.5 0-3.4l9.6-9.6c1-1 2.5-1 3.4 0l5.6 5.6c1 1 1 2.5 0 3.4L13 21\" />\n              <path d=\"M22 21H7\" />\n              <path d=\"m5 11 9 9\" />\n            </svg>\n          }\n        />\n        <IconBarItem\n          value=\"fill\"\n          label=\"Fill\"\n          disabled\n          icon={\n            <svg\n              width=\"18\"\n              height=\"18\"\n              viewBox=\"0 0 24 24\"\n              fill=\"none\"\n              stroke=\"currentColor\"\n              strokeWidth=\"1.5\"\n              strokeLinecap=\"round\"\n              strokeLinejoin=\"round\"\n            >\n              <path d=\"m19 11-8-8-8.6 8.6a2 2 0 0 0 0 2.8l5.2 5.2c.8.8 2 .8 2.8 0L19 11Z\" />\n              <path d=\"m5 2 5 5\" />\n              <path d=\"M2 13h15\" />\n              <path d=\"M22 20a2 2 0 1 1-4 0c0-1.6 1.7-2.8 2-4 .3 1.2 2 2.4 2 4Z\" />\n            </svg>\n          }\n        />\n      </IconBar>\n    </div>\n  )\n}\n"
    },
    {
      "path": "components/ui/weight-shift-text.tsx",
      "type": "registry:ui",
      "content": "'use client'\n\nimport { forwardRef, useState, type HTMLAttributes, type ReactNode } from 'react'\nimport { useReducedMotion } from 'motion/react'\nimport { cn } from '@/lib/utils'\nimport { durations } from '@/lib/motion-tokens'\n\nexport interface WeightShiftTextProps extends Omit<HTMLAttributes<HTMLSpanElement>, 'children'> {\n  children: ReactNode\n  baseWeight?: number\n  activeWeight?: number\n  active?: boolean\n  duration?: string\n}\n\nexport const WeightShiftText = forwardRef<HTMLSpanElement, WeightShiftTextProps>(\n  (\n    {\n      children,\n      baseWeight = 400,\n      activeWeight = 600,\n      active,\n      duration = durations.fast,\n      className,\n      style,\n      onMouseEnter: onMouseEnterProp,\n      onMouseLeave: onMouseLeaveProp,\n      ...props\n    },\n    ref,\n  ) => {\n    const reduceMotion = useReducedMotion()\n    const [isHovered, setIsHovered] = useState(false)\n\n    const isControlledActive = active !== undefined\n    const currentWeight = isControlledActive\n      ? active\n        ? activeWeight\n        : baseWeight\n      : isHovered\n        ? activeWeight\n        : baseWeight\n\n    return (\n      <span\n        ref={ref}\n        className={cn('relative inline-block', className)}\n        onMouseEnter={(e) => {\n          setIsHovered(true)\n          onMouseEnterProp?.(e)\n        }}\n        onMouseLeave={(e) => {\n          setIsHovered(false)\n          onMouseLeaveProp?.(e)\n        }}\n        style={style}\n        {...props}\n      >\n        <span\n          aria-hidden=\"true\"\n          className=\"pointer-events-none invisible absolute inset-0\"\n          style={{\n            fontVariationSettings: `\"wght\" ${activeWeight}`,\n          }}\n        >\n          {children}\n        </span>\n        <span\n          className=\"relative\"\n          style={{\n            fontVariationSettings: `\"wght\" ${currentWeight}`,\n            transition: reduceMotion\n              ? 'none'\n              : `font-variation-settings ${duration} var(--motion-ease-out)`,\n          }}\n        >\n          {children}\n        </span>\n      </span>\n    )\n  },\n)\nWeightShiftText.displayName = 'WeightShiftText'\n\nexport function WeightShiftTextPreview() {\n  return (\n    <div\n      className=\"w-full h-full min-h-50 rounded-lg overflow-hidden flex flex-col items-center justify-center gap-6 p-4\"\n      style={{ backgroundColor: 'var(--color-surface)' }}\n    >\n      <WeightShiftText\n        baseWeight={300}\n        activeWeight={700}\n        className=\"text-lg cursor-pointer select-none\"\n        style={{ color: 'var(--color-fg)' }}\n      >\n        Hover to shift weight\n      </WeightShiftText>\n      <WeightShiftText\n        baseWeight={400}\n        activeWeight={600}\n        active={true}\n        className=\"text-sm\"\n        style={{ color: 'var(--color-muted)' }}\n      >\n        Always active\n      </WeightShiftText>\n    </div>\n  )\n}\n"
    },
    {
      "path": "lib/motion-tokens.ts",
      "type": "registry:lib",
      "content": "export const durations = {\n  instant: '50ms',\n  fast: '120ms',\n  base: '200ms',\n  slow: '320ms',\n} as const\n\nexport const easings = {\n  out: 'cubic-bezier(0.22, 1, 0.36, 1)',\n  inOut: 'cubic-bezier(0.65, 0, 0.35, 1)',\n} as const\n\nexport const springs = {\n  fast: { type: 'spring', stiffness: 400, damping: 30, mass: 0.8 },\n  press: { type: 'spring', stiffness: 700, damping: 32, mass: 1 },\n  moderate: { type: 'spring', stiffness: 300, damping: 24, mass: 1 },\n  settle: { type: 'spring', stiffness: 260, damping: 26, mass: 1 },\n} as const\n"
    }
  ],
  "type": "registry:ui"
}
