{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "docs-utils",
  "title": "Docs utilities",
  "description": "Shared helpers used by docscn components: URL matching, ref merging, hotkeys, copy-button state and tab groups that share a selection.",
  "files": [
    {
      "path": "registry/base/docs/utils/urls.ts",
      "content": "// Adapted from Fumadocs UI (https://github.com/fuma-nama/fumadocs)\n// Copyright (c) 2023 Fuma, MIT License\n\nexport function normalize(urlOrPath: string) {\n  if (urlOrPath.length > 1 && urlOrPath.endsWith(\"/\"))\n    return urlOrPath.slice(0, -1);\n  return urlOrPath;\n}\n\n/**\n * @returns if `href` is matching the given pathname\n */\nexport function isActive(\n  href: string,\n  pathname: string,\n  nested = false,\n): boolean {\n  href = normalize(href);\n  pathname = normalize(pathname);\n\n  return href === pathname || (nested && pathname.startsWith(`${href}/`));\n}\n",
      "type": "registry:lib",
      "target": "@components/docs/utils/urls.ts"
    },
    {
      "path": "registry/base/docs/utils/merge-refs.ts",
      "content": "// Adapted from Fumadocs UI (https://github.com/fuma-nama/fumadocs)\n// Copyright (c) 2023 Fuma, MIT License\n\nimport type * as React from \"react\";\n\nexport function mergeRefs<T>(\n  ...refs: (React.Ref<T> | undefined)[]\n): React.RefCallback<T> {\n  return (value) => {\n    refs.forEach((ref) => {\n      if (typeof ref === \"function\") {\n        ref(value);\n      } else if (ref) {\n        ref.current = value;\n      }\n    });\n  };\n}\n",
      "type": "registry:lib",
      "target": "@components/docs/utils/merge-refs.ts"
    },
    {
      "path": "registry/base/docs/utils/hotkey.ts",
      "content": "// Adapted from Fumadocs UI (https://github.com/fuma-nama/fumadocs)\n// Copyright (c) 2023 Fuma, MIT License\n\nimport { useEffect, useEffectEvent } from \"react\";\n\n/**\n * Listen to `keydown` on window, skipping keys that are already handled or composing text.\n */\nexport function useHotKey(\n  onKeyDown: (e: KeyboardEvent) => void,\n  {\n    enabled = true,\n    ignoreTyping = false,\n  }: {\n    enabled?: boolean;\n    /** ignore it while typing in an editable element (e.g. `<input />`), or when a dialog is opened */\n    ignoreTyping?: boolean;\n  } = {},\n) {\n  const listener = useEffectEvent((e: KeyboardEvent) => {\n    if (e.defaultPrevented || e.isComposing || e.keyCode === 229) return;\n    if (ignoreTyping && isTypingTarget(e.target)) return;\n    onKeyDown(e);\n  });\n\n  useEffect(() => {\n    if (!enabled) return;\n    window.addEventListener(\"keydown\", listener);\n    return () => window.removeEventListener(\"keydown\", listener);\n  }, [enabled]);\n}\n\nfunction isTypingTarget(target: EventTarget | null): boolean {\n  if (!(target instanceof HTMLElement)) return false;\n  if (target.isContentEditable) return true;\n  if ([\"INPUT\", \"TEXTAREA\", \"SELECT\"].includes(target.tagName)) return true;\n\n  return target.closest('[role=\"dialog\"]') !== null;\n}\n",
      "type": "registry:lib",
      "target": "@components/docs/utils/hotkey.ts"
    },
    {
      "path": "registry/base/docs/utils/use-copy-button.ts",
      "content": "// Adapted from Fumadocs UI (https://github.com/fuma-nama/fumadocs)\n// Copyright (c) 2023 Fuma, MIT License\n\n\"use client\";\nimport {\n  type MouseEventHandler,\n  useCallback,\n  useEffect,\n  useLayoutEffect,\n  useRef,\n  useState,\n} from \"react\";\n\nexport function useCopyButton(\n  onCopy: () => void | Promise<void>,\n): [checked: boolean, onClick: MouseEventHandler] {\n  const [checked, setChecked] = useState(false);\n  const callbackRef = useRef(onCopy);\n  const timeoutRef = useRef<number | null>(null);\n\n  useLayoutEffect(() => {\n    callbackRef.current = onCopy;\n  });\n\n  const onClick: MouseEventHandler = useCallback(() => {\n    if (timeoutRef.current) window.clearTimeout(timeoutRef.current);\n    const res = Promise.resolve(callbackRef.current());\n\n    void res.then(() => {\n      setChecked(true);\n      timeoutRef.current = window.setTimeout(() => {\n        setChecked(false);\n      }, 1500);\n    });\n  }, []);\n\n  // Avoid updates after being unmounted\n  useEffect(() => {\n    return () => {\n      if (timeoutRef.current) window.clearTimeout(timeoutRef.current);\n    };\n  }, []);\n\n  return [checked, onClick];\n}\n",
      "type": "registry:lib",
      "target": "@components/docs/utils/use-copy-button.ts"
    },
    {
      "path": "registry/base/docs/utils/use-is-scroll-top.ts",
      "content": "// Adapted from Fumadocs UI (https://github.com/fuma-nama/fumadocs)\n// Copyright (c) 2023 Fuma, MIT License\n\n\"use client\";\nimport { useSyncExternalStore } from \"react\";\n\nconst subscribe = (onChange: () => void) => {\n  window.addEventListener(\"scroll\", onChange, { passive: true });\n  return () => window.removeEventListener(\"scroll\", onChange);\n};\nconst noop = () => () => {};\nconst getServerSnapshot = () => undefined;\n\n/**\n * @returns `undefined` on the server, during hydration, or when disabled.\n */\nexport function useIsScrollTop({\n  enabled = true,\n}: {\n  enabled?: boolean;\n}): boolean | undefined {\n  return useSyncExternalStore(\n    enabled ? subscribe : noop,\n    () => (enabled ? window.scrollY < 10 : undefined),\n    getServerSnapshot,\n  );\n}\n",
      "type": "registry:lib",
      "target": "@components/docs/utils/use-is-scroll-top.ts"
    },
    {
      "path": "registry/base/docs/utils/tabs-group.ts",
      "content": "// Adapted from Fumadocs UI (https://github.com/fuma-nama/fumadocs)\n// Copyright (c) 2023 Fuma, MIT License\n\"use client\";\n\nimport { useCallback, useMemo, useSyncExternalStore } from \"react\";\n\nconst listeners = new Map<string, Set<() => void>>();\n\nfunction subscribe(groupId: string | undefined) {\n  return (onChange: () => void) => {\n    if (!groupId) return () => {};\n    const groupListeners = listeners.get(groupId) ?? new Set();\n    groupListeners.add(onChange);\n    listeners.set(groupId, groupListeners);\n    return () => {\n      groupListeners.delete(onChange);\n    };\n  };\n}\n\n/**\n * The tab selected in a group of tabs that share a `groupId`, kept in\n * `sessionStorage` (and `localStorage` with `persist`), and a setter that\n * updates every tabs of the group.\n *\n * The value is `null` without a `groupId`, on the server, and before a tab of\n * the group has been selected.\n */\nexport function useTabsGroup(\n  groupId: string | undefined,\n  persist = false,\n): [value: string | null, setValue: (value: string) => void] {\n  const value = useSyncExternalStore(\n    useMemo(() => subscribe(groupId), [groupId]),\n    () => {\n      if (!groupId) return null;\n      const value = sessionStorage.getItem(groupId);\n      return persist ? (value ?? localStorage.getItem(groupId)) : value;\n    },\n    () => null,\n  );\n\n  const setValue = useCallback(\n    (value: string) => {\n      if (!groupId) return;\n      sessionStorage.setItem(groupId, value);\n      if (persist) localStorage.setItem(groupId, value);\n      for (const listener of listeners.get(groupId) ?? []) listener();\n    },\n    [groupId, persist],\n  );\n\n  return [value, setValue];\n}\n",
      "type": "registry:lib",
      "target": "@components/docs/utils/tabs-group.ts"
    },
    {
      "path": "registry/base/docs/utils/array.ts",
      "content": "// Array.prototype.findLast and findLastIndex are ES2023, which TypeScript\n// configs targeting ES2022 (such as shadcn/ui's monorepo template) don't\n// include, so docscn uses these instead.\n\nexport function findLastIndex<T>(\n  items: readonly T[],\n  predicate: (item: T) => unknown,\n): number {\n  for (let i = items.length - 1; i >= 0; i--) {\n    if (predicate(items[i]!)) return i;\n  }\n  return -1;\n}\n\nexport function findLast<T, S extends T>(\n  items: readonly T[],\n  predicate: (item: T) => item is S,\n): S | undefined;\nexport function findLast<T>(\n  items: readonly T[],\n  predicate: (item: T) => unknown,\n): T | undefined;\nexport function findLast<T>(\n  items: readonly T[],\n  predicate: (item: T) => unknown,\n): T | undefined {\n  const index = findLastIndex(items, predicate);\n  return index === -1 ? undefined : items[index];\n}\n",
      "type": "registry:lib",
      "target": "@components/docs/utils/array.ts"
    }
  ],
  "type": "registry:lib"
}