{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "dropdown-menu",
  "title": "Dropdown Menu",
  "description": "A popover action menu with a proximity-driven morphing highlight. The muted hover highlight springs between items as the pointer moves, giving the menu a fluid, alive feel.",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "components/ui/dropdown-menu.tsx",
      "type": "registry:ui",
      "content": "'use client'\n\nimport {\n  createContext,\n  forwardRef,\n  useCallback,\n  useContext,\n  useEffect,\n  useId,\n  useLayoutEffect,\n  useMemo,\n  useRef,\n  useState,\n  type HTMLAttributes,\n  type ReactNode,\n} from 'react'\nimport { createPortal } from 'react-dom'\nimport { useReducedMotion, type MotionValue } from 'motion/react'\nimport { cn } from '@/lib/utils'\nimport { playHoverSound, playClickSound } from '@/lib/sound'\nimport { useProximityHighlight, ProximityHighlight } from '@/lib/hooks/use-proximity-highlight'\ninterface DropdownMenuContextValue {\n  open: boolean\n  setOpen: (next: boolean) => void\n  triggerId: string\n  contentId: string\n  nextIndex: () => number\n\n  itemsWidth: number | null\n  setItemsWidth: (w: number | null) => void\n}\n\nconst DropdownMenuContext = createContext<DropdownMenuContextValue | null>(null)\n\nfunction useDropdownMenuCtx(componentName: string) {\n  const ctx = useContext(DropdownMenuContext)\n  if (!ctx) throw new Error(`${componentName} must be used within DropdownMenu`)\n  return ctx\n}\ninterface DropdownMenuContentContextValue {\n  activeIndex: number | null\n  setActiveIndex: (i: number | null) => void\n  highlightX: ReturnType<typeof import('motion/react').useSpring>\n  highlightSize: MotionValue<number>\n  highlightOpacity: ReturnType<typeof import('motion/react').useSpring>\n  axis: 'x' | 'y'\n  focusedIndex: number\n  setFocusedIndex: (i: number) => void\n  setOpen: (next: boolean) => void\n\n  registerItem: (index: number, el: HTMLElement | null) => void\n}\n\nconst DropdownMenuContentContext = createContext<DropdownMenuContentContextValue | null>(null)\n\nfunction useDropdownMenuContentCtx(componentName: string) {\n  const ctx = useContext(DropdownMenuContentContext)\n  if (!ctx) throw new Error(`${componentName} must be used within DropdownMenuContent`)\n  return ctx\n}\nexport interface DropdownMenuProps {\n  children: ReactNode\n  open?: boolean\n  defaultOpen?: boolean\n  onOpenChange?: (open: boolean) => void\n  className?: string\n}\n\nexport function DropdownMenu({\n  children,\n  open: openProp,\n  defaultOpen = false,\n  onOpenChange,\n  className,\n}: DropdownMenuProps) {\n  const isControlled = openProp !== undefined\n  const [internalOpen, setInternalOpen] = useState(defaultOpen)\n  const open = isControlled ? openProp : internalOpen\n  const [itemsWidth, setItemsWidth] = useState<number | null>(null)\n\n  const setOpen = useCallback(\n    (next: boolean) => {\n      if (!isControlled) setInternalOpen(next)\n      onOpenChange?.(next)\n    },\n    [isControlled, onOpenChange],\n  )\n\n  const reactId = useId()\n  const triggerId = `${reactId}-trigger`\n  const contentId = `${reactId}-content`\n  const indexRef = useRef(0)\n  const nextIndex = useCallback(() => {\n    const idx = indexRef.current\n    indexRef.current += 1\n    return idx\n  }, [])\n  useEffect(() => {\n    if (!open) indexRef.current = 0\n  }, [open])\n  useEffect(() => {\n    if (!open) return\n    const handlePointerDown = (e: PointerEvent) => {\n      const target = e.target as Node\n      if ((target as HTMLElement).closest?.('[data-dropdown-trigger]')) return\n      if ((target as HTMLElement).closest?.('[data-dropdown-content]')) return\n      setOpen(false)\n    }\n    document.addEventListener('pointerdown', handlePointerDown)\n    return () => document.removeEventListener('pointerdown', handlePointerDown)\n  }, [open, setOpen])\n  useEffect(() => {\n    if (!open) return\n    const handleKeyDown = (e: KeyboardEvent) => {\n      if (e.key === 'Escape') {\n        e.preventDefault()\n        setOpen(false)\n        document.getElementById(triggerId)?.focus()\n      }\n    }\n    document.addEventListener('keydown', handleKeyDown)\n    return () => document.removeEventListener('keydown', handleKeyDown)\n  }, [open, setOpen, triggerId])\n\n  const ctx = useMemo<DropdownMenuContextValue>(\n    () => ({\n      open,\n      setOpen,\n      triggerId,\n      contentId,\n      nextIndex,\n      itemsWidth,\n      setItemsWidth,\n    }),\n    [open, setOpen, triggerId, contentId, nextIndex, itemsWidth],\n  )\n\n  return (\n    <DropdownMenuContext.Provider value={ctx}>\n      <div className={cn('relative inline-block', className)}>{children}</div>\n    </DropdownMenuContext.Provider>\n  )\n}\nexport interface DropdownMenuTriggerProps extends Omit<\n  HTMLAttributes<HTMLButtonElement>,\n  'children'\n> {\n  children: ReactNode\n  showChevron?: boolean\n}\n\nexport const DropdownMenuTrigger = forwardRef<HTMLButtonElement, DropdownMenuTriggerProps>(\n  ({ children, showChevron = true, className, onClick, onMouseEnter, ...props }, ref) => {\n    const { open, setOpen, triggerId, contentId, itemsWidth } =\n      useDropdownMenuCtx('DropdownMenuTrigger')\n    const reduceMotion = useReducedMotion()\n\n    return (\n      <button\n        ref={ref}\n        id={triggerId}\n        type=\"button\"\n        data-dropdown-trigger\n        aria-haspopup=\"menu\"\n        aria-expanded={open}\n        aria-controls={open ? contentId : undefined}\n        style={itemsWidth ? { width: itemsWidth } : undefined}\n        className={cn(\n          'flex min-h-11 w-full items-center justify-between gap-2 rounded-lg squircle-corners border border-(--color-border) bg-(--color-surface) px-4 py-3 text-left text-sm font-medium text-(--color-fg) transition-colors duration-(--motion-dur-fast) motion-reduce:transition-none',\n          'hover:bg-(--color-surface-2)',\n          'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-2 focus-visible:ring-offset-(--color-bg)',\n          className,\n        )}\n        onClick={(e) => {\n          onClick?.(e)\n          playClickSound()\n          setOpen(!open)\n        }}\n        onMouseEnter={(e) => {\n          onMouseEnter?.(e)\n          playHoverSound()\n        }}\n        {...props}\n      >\n        <span className=\"flex min-w-0 flex-1 items-center gap-2\">{children}</span>\n        {showChevron && (\n          <span\n            className=\"shrink-0 text-(--color-muted)\"\n            aria-hidden=\"true\"\n            style={{\n              display: 'inline-flex',\n              transform: `rotate(${open ? 180 : 0}deg)`,\n              transition: reduceMotion ? 'none' : 'transform 200ms var(--motion-ease-out)',\n            }}\n          >\n            <svg\n              width=\"16\"\n              height=\"16\"\n              viewBox=\"0 0 16 16\"\n              fill=\"none\"\n              stroke=\"currentColor\"\n              strokeWidth=\"1.5\"\n              strokeLinecap=\"round\"\n              strokeLinejoin=\"round\"\n            >\n              <path d=\"M4 6l4 4 4-4\" />\n            </svg>\n          </span>\n        )}\n      </button>\n    )\n  },\n)\nDropdownMenuTrigger.displayName = 'DropdownMenuTrigger'\nexport interface DropdownMenuContentProps extends HTMLAttributes<HTMLDivElement> {\n  align?: 'start' | 'center' | 'end'\n  side?: 'bottom' | 'top'\n  sideOffset?: number\n}\n\nexport const DropdownMenuContent = forwardRef<HTMLDivElement, DropdownMenuContentProps>(\n  ({ align = 'start', side = 'bottom', sideOffset = 8, children, className, ...props }, ref) => {\n    const { open, setOpen, triggerId, contentId, itemsWidth, setItemsWidth } =\n      useDropdownMenuCtx('DropdownMenuContent')\n\n    const outerCtx = useContext(DropdownMenuContext)\n    const probeIndexRef = useRef(0)\n    const probeNextIndex = useCallback(() => probeIndexRef.current++, [])\n    const panelRef = useRef<HTMLDivElement | null>(null)\n    const [position, setPosition] = useState({ left: 0, top: 0 })\n\n    const maxWidthRef = useRef(0)\n    const {\n      activeIndex,\n      setActiveIndex,\n      registerItem,\n      handlers,\n      highlightX,\n      highlightSize,\n      highlightOpacity,\n      axis,\n    } = useProximityHighlight(panelRef, { axis: 'y' })\n\n    const [focusedIndex, setFocusedIndex] = useState(0)\n    useEffect(() => {\n      if (!open) return\n      const measure = () => {\n        const trigger = document.getElementById(triggerId)\n        const content = panelRef.current\n        if (!trigger || !content) return\n        const triggerRect = trigger.getBoundingClientRect()\n        const contentRect = content.getBoundingClientRect()\n        const vw = window.innerWidth\n        const vh = window.innerHeight\n\n        let left = align === 'end' ? triggerRect.right - contentRect.width : triggerRect.left\n        if (align === 'center')\n          left = triggerRect.left + (triggerRect.width - contentRect.width) / 2\n        left = Math.max(12, Math.min(left, vw - contentRect.width - 12))\n\n        let top: number\n        const spaceBelow = vh - triggerRect.bottom - sideOffset\n        if (side === 'top' || (side === 'bottom' && spaceBelow < contentRect.height)) {\n          top = triggerRect.top - contentRect.height - sideOffset\n        } else {\n          top = triggerRect.bottom + sideOffset\n        }\n        setPosition({ left, top })\n\n        const natural = Math.max(triggerRect.width, contentRect.width)\n        maxWidthRef.current = Math.max(maxWidthRef.current, natural)\n        setItemsWidth(maxWidthRef.current)\n      }\n\n      const frame = requestAnimationFrame(measure)\n      window.addEventListener('resize', measure)\n      window.addEventListener('scroll', measure, true)\n      return () => {\n        cancelAnimationFrame(frame)\n        window.removeEventListener('resize', measure)\n        window.removeEventListener('scroll', measure, true)\n      }\n    }, [open, align, side, sideOffset, triggerId, setItemsWidth])\n    useEffect(() => {\n      if (!open) return\n      const handleKeyDown = (e: KeyboardEvent) => {\n        if (!panelRef.current?.contains(document.activeElement)) return\n        const items = panelRef.current.querySelectorAll<HTMLElement>(\n          '[role=\"menuitem\"]:not([data-disabled])',\n        )\n        if (!items.length) return\n        const count = items.length\n\n        if (e.key === 'ArrowDown') {\n          e.preventDefault()\n          const next = (focusedIndex + 1) % count\n          setFocusedIndex(next)\n          items[next]?.focus()\n        } else if (e.key === 'ArrowUp') {\n          e.preventDefault()\n          const next = (focusedIndex - 1 + count) % count\n          setFocusedIndex(next)\n          items[next]?.focus()\n        } else if (e.key === 'Home') {\n          e.preventDefault()\n          setFocusedIndex(0)\n          items[0]?.focus()\n        } else if (e.key === 'End') {\n          e.preventDefault()\n          setFocusedIndex(count - 1)\n          items[count - 1]?.focus()\n        } else if (e.key === 'Tab') {\n          setOpen(false)\n        }\n      }\n      document.addEventListener('keydown', handleKeyDown)\n      return () => document.removeEventListener('keydown', handleKeyDown)\n    }, [open, focusedIndex, setOpen])\n    useEffect(() => {\n      if (!open) return\n      const frame = requestAnimationFrame(() => {\n        setFocusedIndex(0)\n        panelRef.current\n          ?.querySelectorAll<HTMLElement>('[role=\"menuitem\"]:not([data-disabled])')?.[0]\n          ?.focus()\n      })\n      return () => cancelAnimationFrame(frame)\n    }, [open])\n\n    const setRefs = useCallback(\n      (el: HTMLDivElement | null) => {\n        ;(panelRef as React.MutableRefObject<HTMLDivElement | null>).current = el\n        if (typeof ref === 'function') ref(el)\n        else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = el\n      },\n      [ref],\n    )\n\n    const contentCtx = useMemo<DropdownMenuContentContextValue>(\n      () => ({\n        activeIndex,\n        setActiveIndex,\n        highlightX,\n        highlightSize,\n        highlightOpacity,\n        axis,\n        focusedIndex,\n        setFocusedIndex,\n        setOpen,\n        registerItem,\n      }),\n      [\n        activeIndex,\n        setActiveIndex,\n        highlightX,\n        highlightSize,\n        highlightOpacity,\n        axis,\n        focusedIndex,\n        setOpen,\n        registerItem,\n      ],\n    )\n\n    const measureRef = useRef<HTMLDivElement | null>(null)\n    useLayoutEffect(() => {\n      const el = measureRef.current\n      if (!el || typeof ResizeObserver === 'undefined') return\n      const update = () => {\n        const w = el.getBoundingClientRect().width\n        maxWidthRef.current = Math.max(maxWidthRef.current, w)\n        setItemsWidth(maxWidthRef.current)\n      }\n      update()\n      const ro = new ResizeObserver(update)\n      ro.observe(el)\n      return () => ro.disconnect()\n    }, [children, setItemsWidth])\n\n    if (!open) {\n      if (!outerCtx) return null\n\n      return (\n        <DropdownMenuContext.Provider value={{ ...outerCtx, nextIndex: probeNextIndex }}>\n          <DropdownMenuContentContext.Provider value={contentCtx}>\n            <div\n              ref={measureRef}\n              aria-hidden=\"true\"\n              className={cn(\n                'pointer-events-none invisible fixed top-0 left-0 min-w-48 border border-(--color-border) bg-(--color-bg) p-1.5 **:min-w-max',\n                'rounded-lg squircle-corners',\n                className,\n              )}\n            >\n              {children}\n            </div>\n          </DropdownMenuContentContext.Provider>\n        </DropdownMenuContext.Provider>\n      )\n    }\n\n    const content = (\n      <DropdownMenuContentContext.Provider value={contentCtx}>\n        <div\n          ref={setRefs}\n          id={contentId}\n          role=\"menu\"\n          aria-labelledby={triggerId}\n          data-dropdown-content\n          className={cn(\n            'relative z-300 min-w-48 max-w-[calc(100vw-1.5rem)] overflow-hidden rounded-lg squircle-corners border border-(--color-border) bg-(--color-bg) p-1.5 outline-none',\n            'shadow-[0_14px_34px_-22px_rgba(0,0,0,0.15)]',\n            className,\n          )}\n          style={{\n            position: 'fixed',\n            left: position.left,\n            top: position.top,\n            width: itemsWidth ?? undefined,\n          }}\n          tabIndex={-1}\n          {...handlers}\n          {...props}\n        >\n          <ProximityHighlight\n            highlightX={highlightX}\n            highlightSize={highlightSize}\n            highlightOpacity={highlightOpacity}\n            axis={axis}\n            className=\"mx-1.5 rounded-md squircle-corners bg-(--color-surface-2)\"\n          />\n          {children}\n        </div>\n      </DropdownMenuContentContext.Provider>\n    )\n\n    return createPortal(content, document.body)\n  },\n)\nDropdownMenuContent.displayName = 'DropdownMenuContent'\nexport interface DropdownMenuItemProps extends HTMLAttributes<HTMLDivElement> {\n  children: ReactNode\n  disabled?: boolean\n  destructive?: boolean\n  textValue?: string\n  onClick?: () => void\n}\n\nexport const DropdownMenuItem = forwardRef<HTMLDivElement, DropdownMenuItemProps>(\n  (\n    { children, disabled, destructive, textValue, onClick, onMouseEnter, className, ...props },\n    ref,\n  ) => {\n    const { nextIndex } = useDropdownMenuCtx('DropdownMenuItem')\n    const { setActiveIndex, focusedIndex, setFocusedIndex, setOpen, registerItem } =\n      useDropdownMenuContentCtx('DropdownMenuItem')\n\n    const itemRef = useRef<HTMLDivElement | null>(null)\n\n    const indexRef = useRef<number | null>(null)\n    if (indexRef.current === null) indexRef.current = nextIndex()\n    // eslint-disable-next-line react-hooks/refs\n    const index = indexRef.current\n    useEffect(() => {\n      const el = itemRef.current\n      if (index < 0 || !el) return\n      registerItem(index, el)\n      return () => registerItem(index, null)\n    }, [index, registerItem])\n\n    const setItemRef = useCallback(\n      (node: HTMLDivElement | null) => {\n        itemRef.current = node\n        if (typeof ref === 'function') ref(node)\n        else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = node\n      },\n      [ref],\n    )\n\n    return (\n      <div\n        ref={setItemRef}\n        role=\"menuitem\"\n        tabIndex={focusedIndex === index ? 0 : -1}\n        data-disabled={disabled ? '' : undefined}\n        data-proximity-index={index}\n        aria-disabled={disabled || undefined}\n        aria-label={textValue}\n        className={cn(\n          'relative flex min-h-11 w-full cursor-default select-none scroll-m-1 items-center justify-between gap-3 rounded-md squircle-corners px-3 py-2.5 text-left text-sm outline-none transition-colors duration-(--motion-dur-fast) motion-reduce:transition-none',\n          'data-disabled:pointer-events-none data-disabled:opacity-50',\n          destructive\n            ? 'text-(--color-error) focus-visible:text-(--color-error)'\n            : 'text-(--color-fg)',\n          className,\n        )}\n        onPointerDown={(e) => {\n          e.preventDefault()\n        }}\n        onFocus={() => {\n          setFocusedIndex(index)\n          setActiveIndex(index)\n        }}\n        onClick={() => {\n          if (disabled) return\n          playClickSound()\n          onClick?.()\n          setOpen(false)\n        }}\n        onKeyDown={(e) => {\n          if (e.key === 'Enter' || e.key === ' ') {\n            e.preventDefault()\n            if (!disabled) {\n              playClickSound()\n              onClick?.()\n              setOpen(false)\n            }\n          }\n        }}\n        onMouseEnter={(e) => {\n          onMouseEnter?.(e)\n          if (!disabled) playHoverSound()\n        }}\n        {...props}\n      >\n        <span className=\"relative z-10 flex min-w-0 flex-1 items-center gap-2 truncate\">\n          {children}\n        </span>\n      </div>\n    )\n  },\n)\nDropdownMenuItem.displayName = 'DropdownMenuItem'\nexport interface DropdownMenuGroupProps extends HTMLAttributes<HTMLDivElement> {\n  label?: ReactNode\n  labelClassName?: string\n}\n\nexport const DropdownMenuGroup = forwardRef<HTMLDivElement, DropdownMenuGroupProps>(\n  ({ label, labelClassName, children, className, ...props }, ref) => (\n    <div ref={ref} role=\"group\" className={cn(label && 'mt-2 first:mt-0', className)} {...props}>\n      {label && (\n        <div\n          className={cn(\n            'px-3 pt-1 pb-1 text-[11px] font-medium uppercase tracking-[0.12em] text-(--color-subtle)',\n            labelClassName,\n          )}\n        >\n          {label}\n        </div>\n      )}\n      {children}\n    </div>\n  ),\n)\nDropdownMenuGroup.displayName = 'DropdownMenuGroup'\nexport type DropdownMenuLabelProps = HTMLAttributes<HTMLDivElement>\n\nexport const DropdownMenuLabel = forwardRef<HTMLDivElement, DropdownMenuLabelProps>(\n  ({ className, ...props }, ref) => (\n    <div\n      ref={ref}\n      className={cn(\n        'px-3 pt-1 pb-1 text-[11px] font-medium uppercase tracking-[0.12em] text-(--color-subtle)',\n        className,\n      )}\n      {...props}\n    />\n  ),\n)\nDropdownMenuLabel.displayName = 'DropdownMenuLabel'\nexport type DropdownMenuSeparatorProps = HTMLAttributes<HTMLDivElement>\n\nexport const DropdownMenuSeparator = forwardRef<HTMLDivElement, DropdownMenuSeparatorProps>(\n  ({ className, ...props }, ref) => (\n    <div\n      ref={ref}\n      role=\"separator\"\n      aria-hidden=\"true\"\n      className={cn('my-1 h-px bg-(--color-border)', className)}\n      {...props}\n    />\n  ),\n)\nDropdownMenuSeparator.displayName = 'DropdownMenuSeparator'\nexport function DropdownMenuPreview() {\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      <div className=\"w-56\">\n        <DropdownMenu>\n          <DropdownMenuTrigger>Actions</DropdownMenuTrigger>\n          <DropdownMenuContent>\n            <DropdownMenuItem>Edit</DropdownMenuItem>\n            <DropdownMenuItem>Duplicate</DropdownMenuItem>\n            <DropdownMenuSeparator />\n            <DropdownMenuItem destructive>Delete</DropdownMenuItem>\n          </DropdownMenuContent>\n        </DropdownMenu>\n      </div>\n    </div>\n  )\n}\n"
    },
    {
      "path": "lib/hooks/use-proximity-highlight.tsx",
      "type": "registry:lib",
      "content": "'use client'\n\nimport { useRef, useState, useCallback, useEffect, type RefObject } from 'react'\nimport {\n  motion,\n  useMotionValue,\n  useReducedMotion,\n  useSpring,\n  type MotionValue,\n  type MotionStyle,\n} from 'motion/react'\nimport { springs } from '@/lib/motion-tokens'\n\nexport interface ItemRect {\n  top: number\n  height: number\n  left: number\n  width: number\n}\n\ninterface UseProximityHighlightOptions {\n  axis?: 'x' | 'y'\n  spring?: { stiffness: number; damping: number; mass: number }\n}\n\ninterface UseProximityHighlightReturn {\n  activeIndex: number | null\n  setActiveIndex: (index: number | null) => void\n  itemRects: ItemRect[]\n  sessionRef: RefObject<number>\n  handlers: {\n    onMouseMove: (e: React.MouseEvent) => void\n    onMouseEnter: () => void\n    onMouseLeave: () => void\n  }\n  registerItem: (index: number, element: HTMLElement | null) => void\n  measureItems: () => void\n  highlightX: ReturnType<typeof useSpring>\n  highlightSize: MotionValue<number>\n  highlightOpacity: ReturnType<typeof useSpring>\n  axis: 'x' | 'y'\n}\n\nexport function useProximityHighlight<T extends HTMLElement>(\n  containerRef: RefObject<T | null>,\n  options: UseProximityHighlightOptions = {},\n): UseProximityHighlightReturn {\n  const { axis = 'y', spring: springConfig } = options\n  const reduceMotion = useReducedMotion()\n\n  const itemsRef = useRef(new Map<number, HTMLElement>())\n  const [activeIndex, setActiveIndexRaw] = useState<number | null>(null)\n  const activeIndexRef = useRef<number | null>(null)\n  const [itemRects, setItemRects] = useState<ItemRect[]>([])\n  const itemRectsRef = useRef<ItemRect[]>([])\n  const sessionRef = useRef(0)\n  const rafIdRef = useRef<number | null>(null)\n  const remeasureRafIdRef = useRef<number | null>(null)\n  const updateHighlightRef = useRef<(index: number | null) => void>(() => {})\n\n  const rawX = useMotionValue(0)\n  const highlightSize = useMotionValue(0)\n  const rawOpacity = useMotionValue(0)\n\n  const springOpts = reduceMotion ? { duration: 0 } : (springConfig ?? springs.settle)\n\n  const highlightX = useSpring(rawX, springOpts)\n  const highlightOpacity = useSpring(rawOpacity, springOpts)\n\n  const measureItems = useCallback(() => {\n    const container = containerRef.current\n    if (!container) return\n    const rects: ItemRect[] = []\n    itemsRef.current.forEach((element, index) => {\n      rects[index] = {\n        top: element.offsetTop,\n        height: element.offsetHeight,\n        left: element.offsetLeft,\n        width: element.offsetWidth,\n      }\n    })\n    itemRectsRef.current = rects\n    setItemRects(rects)\n  }, [containerRef])\n\n  const registerItem = useCallback(\n    (index: number, element: HTMLElement | null) => {\n      if (element) {\n        itemsRef.current.set(index, element)\n      } else {\n        itemsRef.current.delete(index)\n      }\n      if (remeasureRafIdRef.current !== null) {\n        cancelAnimationFrame(remeasureRafIdRef.current)\n      }\n      remeasureRafIdRef.current = requestAnimationFrame(() => {\n        remeasureRafIdRef.current = null\n        measureItems()\n      })\n    },\n    [measureItems],\n  )\n\n  const updateHighlight = useCallback(\n    (index: number | null) => {\n      if (index === null || !itemRectsRef.current[index]) {\n        if (reduceMotion) {\n          rawOpacity.jump(0)\n        } else {\n          rawOpacity.set(0)\n        }\n        return\n      }\n      const r = itemRectsRef.current[index]\n      if (axis === 'y') {\n        if (reduceMotion) {\n          rawX.jump(r.top)\n          highlightSize.jump(r.height)\n          rawOpacity.jump(1)\n        } else {\n          rawX.set(r.top)\n          highlightSize.jump(r.height)\n          rawOpacity.set(1)\n        }\n      } else {\n        if (reduceMotion) {\n          rawX.jump(r.left)\n          highlightSize.jump(r.width)\n          rawOpacity.jump(1)\n        } else {\n          rawX.set(r.left)\n          highlightSize.jump(r.width)\n          rawOpacity.set(1)\n        }\n      }\n    },\n    [axis, reduceMotion, rawX, highlightSize, rawOpacity],\n  )\n\n  useEffect(() => {\n    updateHighlightRef.current = updateHighlight\n  })\n\n  const setActiveIndex = useCallback((index: number | null) => {\n    setActiveIndexRaw(index)\n    updateHighlightRef.current(index)\n  }, [])\n\n  const handleMouseMove = useCallback(\n    (e: React.MouseEvent) => {\n      const mouseX = e.clientX\n      const mouseY = e.clientY\n\n      if (rafIdRef.current !== null) {\n        cancelAnimationFrame(rafIdRef.current)\n      }\n\n      rafIdRef.current = requestAnimationFrame(() => {\n        rafIdRef.current = null\n        const container = containerRef.current\n        if (!container) return\n\n        const containerRect = container.getBoundingClientRect()\n        const mousePos = axis === 'x' ? mouseX : mouseY\n\n        let closestIndex: number | null = null\n        let closestDistance = Infinity\n        let containingIndex: number | null = null\n\n        const rects = itemRectsRef.current\n        const scrollOffset = axis === 'x' ? container.scrollLeft : container.scrollTop\n        const borderOffset = axis === 'x' ? container.clientLeft : container.clientTop\n        const containerEdge = axis === 'x' ? containerRect.left : containerRect.top\n\n        const layoutSize = axis === 'x' ? container.offsetWidth : container.offsetHeight\n        const visualSize = axis === 'x' ? containerRect.width : containerRect.height\n        const scale = layoutSize > 0 ? visualSize / layoutSize : 1\n\n        for (let index = 0; index < rects.length; index++) {\n          const r = rects[index]\n          if (!r) continue\n\n          const contentPos = axis === 'x' ? r.left : r.top\n          const itemStart = containerEdge + (borderOffset + contentPos - scrollOffset) * scale\n          const itemSize = (axis === 'x' ? r.width : r.height) * scale\n          const itemEnd = itemStart + itemSize\n\n          if (mousePos >= itemStart && mousePos <= itemEnd) {\n            containingIndex = index\n          }\n\n          const itemCenter = itemStart + itemSize / 2\n          const distance = Math.abs(mousePos - itemCenter)\n\n          if (distance < closestDistance) {\n            closestDistance = distance\n            closestIndex = index\n          }\n        }\n\n        const next = containingIndex ?? closestIndex\n        if (next !== activeIndexRef.current) {\n          activeIndexRef.current = next\n          setActiveIndex(next)\n        }\n      })\n    },\n    [axis, containerRef, setActiveIndex],\n  )\n\n  const handleMouseEnter = useCallback(() => {\n    sessionRef.current += 1\n  }, [])\n\n  const handleMouseLeave = useCallback(() => {\n    if (rafIdRef.current !== null) {\n      cancelAnimationFrame(rafIdRef.current)\n      rafIdRef.current = null\n    }\n    activeIndexRef.current = null\n    setActiveIndex(null)\n  }, [setActiveIndex])\n\n  useEffect(() => {\n    return () => {\n      if (rafIdRef.current !== null) {\n        cancelAnimationFrame(rafIdRef.current)\n      }\n      if (remeasureRafIdRef.current !== null) {\n        cancelAnimationFrame(remeasureRafIdRef.current)\n      }\n    }\n  }, [])\n\n  return {\n    activeIndex,\n    setActiveIndex,\n    itemRects,\n    sessionRef,\n    handlers: {\n      onMouseMove: handleMouseMove,\n      onMouseEnter: handleMouseEnter,\n      onMouseLeave: handleMouseLeave,\n    },\n    registerItem,\n    measureItems,\n    highlightX,\n    highlightSize,\n    highlightOpacity,\n    axis,\n  }\n}\n\nexport interface ProximityHighlightProps {\n  highlightX: ReturnType<typeof useSpring>\n  highlightSize: MotionValue<number>\n  highlightOpacity: ReturnType<typeof useSpring>\n  axis: 'x' | 'y'\n  className?: string\n  style?: MotionStyle\n}\n\nexport function ProximityHighlight({\n  highlightX,\n  highlightSize,\n  highlightOpacity,\n  axis,\n  className,\n  style,\n}: ProximityHighlightProps) {\n  return (\n    <motion.div\n      aria-hidden=\"true\"\n      className={className}\n      style={{\n        ...style,\n        position: 'absolute',\n        inset: 0,\n        pointerEvents: 'none',\n        ...(axis === 'y'\n          ? { y: highlightX, height: highlightSize }\n          : { x: highlightX, width: highlightSize }),\n        opacity: highlightOpacity,\n      }}\n    />\n  )\n}\n\nexport function useRegisterProximityItem(\n  registerItem: (index: number, element: HTMLElement | null) => void,\n  index: number,\n  ref: RefObject<HTMLElement | null>,\n) {\n  useEffect(() => {\n    registerItem(index, ref.current)\n    return () => registerItem(index, null)\n  }, [index, registerItem, ref])\n}\n"
    },
    {
      "path": "lib/sound.ts",
      "type": "registry:lib",
      "content": "'use client'\n\nconst MUTE_KEY = 'sound-muted'\nconst MUTE_EVENT = 'sound-mute-change'\n\nlet muted = false\nif (typeof window !== 'undefined') {\n  try {\n    muted = localStorage.getItem(MUTE_KEY) === '1'\n  } catch {}\n}\n\nexport function isSoundMuted(): boolean {\n  return muted\n}\n\nexport function setSoundMuted(value: boolean) {\n  muted = value\n  if (typeof window !== 'undefined') {\n    try {\n      localStorage.setItem(MUTE_KEY, value ? '1' : '0')\n    } catch {}\n    window.dispatchEvent(new CustomEvent(MUTE_EVENT))\n  }\n}\n\nexport function toggleSoundMuted(): boolean {\n  setSoundMuted(!muted)\n  return muted\n}\n\nexport function subscribeSoundMuted(callback: () => void) {\n  if (typeof window === 'undefined') return () => {}\n  window.addEventListener(MUTE_EVENT, callback)\n  const onStorage = (e: StorageEvent) => {\n    if (e.key === MUTE_KEY) callback()\n  }\n  window.addEventListener('storage', onStorage)\n  return () => {\n    window.removeEventListener(MUTE_EVENT, callback)\n    window.removeEventListener('storage', onStorage)\n  }\n}\n\nlet audioCtx: AudioContext | null = null\n\nfunction initAudio(): AudioContext | null {\n  if (typeof window === 'undefined') return null\n  if (!audioCtx) {\n    const AudioContextClass =\n      window.AudioContext ||\n      (window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext\n    if (AudioContextClass) {\n      audioCtx = new AudioContextClass()\n    }\n  }\n  if (audioCtx && audioCtx.state === 'suspended') {\n    audioCtx.resume().catch(() => {})\n  }\n  return audioCtx\n}\n\nif (typeof window !== 'undefined') {\n  const unlock = () => {\n    initAudio()\n    window.removeEventListener('pointerdown', unlock)\n    window.removeEventListener('keydown', unlock)\n    window.removeEventListener('touchstart', unlock)\n  }\n  window.addEventListener('pointerdown', unlock, { passive: true })\n  window.addEventListener('keydown', unlock, { passive: true })\n  window.addEventListener('touchstart', unlock, { passive: true })\n}\n\nexport function getAudioContext(): AudioContext | null {\n  return initAudio()\n}\n\nexport function playHoverSound(volume = 0.12, pitch = 1.2) {\n  if (muted) return\n  try {\n    const ctx = getAudioContext()\n    if (!ctx) return\n    const now = ctx.currentTime\n\n    const osc = ctx.createOscillator()\n    const gain = ctx.createGain()\n\n    osc.type = 'sine'\n    osc.frequency.setValueAtTime(1400 * pitch, now)\n    osc.frequency.exponentialRampToValueAtTime(350 * pitch, now + 0.012)\n\n    gain.gain.setValueAtTime(volume, now)\n    gain.gain.exponentialRampToValueAtTime(0.0001, now + 0.012)\n\n    osc.connect(gain)\n    gain.connect(ctx.destination)\n\n    osc.start(now)\n    osc.stop(now + 0.012)\n  } catch {}\n}\n\nexport function playClickSound(volume = 0.2, pitch = 1.0) {\n  if (muted) return\n  try {\n    const ctx = getAudioContext()\n    if (!ctx) return\n    const now = ctx.currentTime\n\n    const osc = ctx.createOscillator()\n    const gain = ctx.createGain()\n\n    osc.type = 'triangle'\n    osc.frequency.setValueAtTime(850 * pitch, now)\n    osc.frequency.exponentialRampToValueAtTime(160 * pitch, now + 0.02)\n\n    gain.gain.setValueAtTime(volume, now)\n    gain.gain.exponentialRampToValueAtTime(0.0001, now + 0.02)\n\n    osc.connect(gain)\n    gain.connect(ctx.destination)\n\n    osc.start(now)\n    osc.stop(now + 0.02)\n  } catch {}\n}\n\nexport function playTickSound(volume = 0.1, pitch = 1.5) {\n  if (muted) return\n  try {\n    const ctx = getAudioContext()\n    if (!ctx) return\n    const now = ctx.currentTime\n\n    const osc = ctx.createOscillator()\n    const gain = ctx.createGain()\n\n    osc.type = 'sine'\n    osc.frequency.setValueAtTime(1800 * pitch, now)\n    osc.frequency.exponentialRampToValueAtTime(450 * pitch, now + 0.01)\n\n    gain.gain.setValueAtTime(volume, now)\n    gain.gain.exponentialRampToValueAtTime(0.0001, now + 0.01)\n\n    osc.connect(gain)\n    gain.connect(ctx.destination)\n\n    osc.start(now)\n    osc.stop(now + 0.01)\n  } catch {}\n}\n\nexport function playBounceSound(volume = 0.3, pitch = 1.0) {\n  if (muted) return\n  try {\n    const ctx = getAudioContext()\n    if (!ctx) return\n    const now = ctx.currentTime\n\n    const osc = ctx.createOscillator()\n    const gain = ctx.createGain()\n\n    osc.type = 'sine'\n    osc.frequency.setValueAtTime(260 * pitch, now)\n    osc.frequency.exponentialRampToValueAtTime(650 * pitch, now + 0.04)\n    osc.frequency.exponentialRampToValueAtTime(190 * pitch, now + 0.14)\n\n    gain.gain.setValueAtTime(volume, now)\n    gain.gain.exponentialRampToValueAtTime(0.0001, now + 0.14)\n\n    osc.connect(gain)\n    gain.connect(ctx.destination)\n\n    osc.start(now)\n    osc.stop(now + 0.14)\n\n    const subOsc = ctx.createOscillator()\n    const subGain = ctx.createGain()\n\n    subOsc.type = 'triangle'\n    subOsc.frequency.setValueAtTime(420 * pitch, now)\n    subOsc.frequency.exponentialRampToValueAtTime(120 * pitch, now + 0.05)\n\n    subGain.gain.setValueAtTime(volume * 0.7, now)\n    subGain.gain.exponentialRampToValueAtTime(0.0001, now + 0.05)\n\n    subOsc.connect(subGain)\n    subGain.connect(ctx.destination)\n\n    subOsc.start(now)\n    subOsc.stop(now + 0.05)\n  } catch {}\n}\n"
    }
  ],
  "type": "registry:ui"
}
