{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "breadcrumbs",
  "title": "Breadcrumbs",
  "description": "A horizontal trail of links that auto-collapses into an ellipsis button when the trail exceeds maxItems.",
  "dependencies": [
    "class-variance-authority"
  ],
  "files": [
    {
      "path": "components/ui/breadcrumbs.tsx",
      "type": "registry:ui",
      "content": "'use client'\n\nimport { forwardRef, useMemo, type HTMLAttributes, type ReactNode } from 'react'\nimport { cva, type VariantProps } from 'class-variance-authority'\nimport { cn } from '@/lib/utils'\nimport { playHoverSound } from '@/lib/sound'\n\nexport interface BreadcrumbItem {\n  label: ReactNode\n  href?: string\n  icon?: ReactNode\n  title?: string\n}\n\nconst breadcrumbVariants = cva('flex items-center', {\n  variants: {\n    variant: {\n      default: '',\n      muted: '',\n    },\n    size: {\n      sm: 'text-[11px] gap-1',\n      md: 'text-sm gap-1.5',\n      lg: 'text-base gap-2',\n    },\n  },\n  defaultVariants: {\n    variant: 'default',\n    size: 'md',\n  },\n})\n\nexport interface BreadcrumbsProps\n  extends Omit<HTMLAttributes<HTMLElement>, 'color'>, VariantProps<typeof breadcrumbVariants> {\n  items: BreadcrumbItem[]\n  maxItems?: number\n  separator?: ReactNode\n  siteUrl?: string\n  'aria-label'?: string\n  listClassName?: string\n}\n\nconst DefaultSeparator = () => (\n  <span\n    className=\"inline-flex translate-y-px shrink-0 items-center justify-center text-(--color-subtle)\"\n    aria-hidden=\"true\"\n  >\n    <svg\n      width=\"12\"\n      height=\"12\"\n      viewBox=\"0 0 12 12\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth=\"1.5\"\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n    >\n      <path d=\"M4.5 2.5l3 3.5-3 3.5\" />\n    </svg>\n  </span>\n)\n\nexport const Breadcrumbs = forwardRef<HTMLElement, BreadcrumbsProps>(\n  (\n    {\n      items,\n      maxItems = 4,\n      separator,\n      siteUrl,\n      'aria-label': ariaLabel = 'Breadcrumb',\n      variant,\n      size,\n      className,\n      listClassName,\n      ...props\n    },\n    ref,\n  ) => {\n    const sep = separator ?? <DefaultSeparator />\n\n    const { visibleItems, collapsedItems } = useMemo(() => {\n      if (!maxItems || items.length <= maxItems) {\n        return { visibleItems: items, collapsedItems: [] }\n      }\n      const keep = maxItems - 2\n      return {\n        visibleItems: [items[0], ...items.slice(-keep)],\n        collapsedItems: items.slice(1, items.length - keep),\n      }\n    }, [items, maxItems])\n\n    const hasEllipsis = collapsedItems.length > 0\n\n    const jsonLd = useMemo(() => {\n      if (!siteUrl) return null\n      return {\n        '@context': 'https://schema.org',\n        '@type': 'BreadcrumbList',\n        itemListElement: items.map((item, i) => ({\n          '@type': 'ListItem',\n          position: i + 1,\n          name: typeof item.label === 'string' ? item.label : '',\n          item: item.href ? `${siteUrl}${item.href}` : undefined,\n        })),\n      }\n    }, [items, siteUrl])\n\n    const renderItem = (item: BreadcrumbItem) => {\n      const isLast = !item.href\n      const content = (\n        <span\n          className={cn(\n            breadcrumbVariants({ variant, size }),\n            isLast\n              ? 'font-medium text-(--color-fg)'\n              : 'text-(--color-muted) transition-colors duration-(--motion-dur-fast) motion-reduce:transition-none hover:text-(--color-fg)',\n          )}\n        >\n          {item.icon && (\n            <span className=\"shrink-0 [&_svg]:size-3.5 me-1.5\" aria-hidden=\"true\">\n              {item.icon}\n            </span>\n          )}\n          {item.label}\n        </span>\n      )\n\n      if (isLast) {\n        return (\n          <span aria-current=\"page\" title={item.title}>\n            {content}\n          </span>\n        )\n      }\n\n      return (\n        <a\n          href={item.href}\n          title={item.title}\n          className=\"outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 focus-visible:ring-offset-(--color-bg) rounded-sm\"\n          onMouseEnter={() => playHoverSound()}\n        >\n          {content}\n        </a>\n      )\n    }\n\n    return (\n      <>\n        {jsonLd && (\n          <script\n            type=\"application/ld+json\"\n            dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}\n          />\n        )}\n        <nav ref={ref} aria-label={ariaLabel} className={className} {...props}>\n          <ol className={cn('flex flex-wrap items-center', listClassName)}>\n            {visibleItems.map((item, i) => {\n              const elements: ReactNode[] = []\n              if (hasEllipsis && i === 1) {\n                elements.push(\n                  <li key=\"ellipsis\" className=\"flex items-center\">\n                    <button\n                      type=\"button\"\n                      aria-label={`Show ${collapsedItems.length} hidden pages`}\n                      className={cn(\n                        'inline-flex items-center justify-center rounded-md squircle-corners text-(--color-muted) transition-colors duration-(--motion-dur-fast) motion-reduce:transition-none hover:text-(--color-fg) focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent)',\n                        size === 'sm' && 'size-7',\n                        size === 'lg' && 'size-10',\n                      )}\n                      onMouseEnter={() => playHoverSound()}\n                    >\n                      <svg\n                        width=\"16\"\n                        height=\"16\"\n                        viewBox=\"0 0 16 16\"\n                        fill=\"currentColor\"\n                        aria-hidden=\"true\"\n                      >\n                        <circle cx=\"3\" cy=\"8\" r=\"1.5\" />\n                        <circle cx=\"8\" cy=\"8\" r=\"1.5\" />\n                        <circle cx=\"13\" cy=\"8\" r=\"1.5\" />\n                      </svg>\n                    </button>\n                  </li>,\n                )\n                elements.push(\n                  <li\n                    key=\"sep-ellipsis\"\n                    role=\"presentation\"\n                    aria-hidden=\"true\"\n                    className=\"flex items-center\"\n                  >\n                    {sep}\n                  </li>,\n                )\n              }\n              elements.push(\n                <li key={`item-${i}`} className=\"flex items-center\">\n                  {renderItem(item)}\n                </li>,\n              )\n              if (i < visibleItems.length - 1) {\n                elements.push(\n                  <li\n                    key={`sep-${i}`}\n                    role=\"presentation\"\n                    aria-hidden=\"true\"\n                    className=\"flex items-center\"\n                  >\n                    {sep}\n                  </li>,\n                )\n              }\n              return elements\n            })}\n          </ol>\n        </nav>\n      </>\n    )\n  },\n)\nBreadcrumbs.displayName = 'Breadcrumbs'\n\nexport { breadcrumbVariants }\n\nexport function BreadcrumbsPreview() {\n  return (\n    <div className=\"flex items-center justify-center p-6\">\n      <div className=\"w-full max-w-md\">\n        <Breadcrumbs\n          items={[\n            { label: 'Home', href: '/' },\n            { label: 'Components', href: '/components' },\n            { label: 'Breadcrumbs' },\n          ]}\n        />\n      </div>\n    </div>\n  )\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"
}
