[
  {
    "path": ".github/workflows/ci.yml",
    "content": "on:\n  pull_request:\n  push:\n    branches: [master]\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v6\n      - uses: oven-sh/setup-bun@v2\n\n      - name: Install dependencies\n        run: bun ci\n\n      - name: Run unit tests\n        run: bun run test\n"
  },
  {
    "path": ".github/workflows/github-pages.yml",
    "content": "on:\n  push:\n    branches: [master]\n\njobs:\n  deploy:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v6\n      - uses: oven-sh/setup-bun@v2\n\n      - name: Install dependencies\n        run: bun ci\n\n      - name: Build app\n        run: bun run rollup -c\n\n      - name: Deploy to GitHub Pages\n        uses: peaceiris/actions-gh-pages@v4\n        with:\n          github_token: ${{ secrets.GITHUB_TOKEN }}\n          publish_dir: dist\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "on:\n  push:\n    tags:\n      - \"v*\"\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n      id-token: write\n    steps:\n      - uses: actions/checkout@v6\n      - uses: actions/setup-node@v6\n        with:\n          node-version: 22\n          registry-url: \"https://registry.npmjs.org\"\n\n      - name: Prune package.json\n        run: npx culls --preserve=svelte\n\n      - name: Publish package\n        env:\n          NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }}\n        run: npm publish --provenance --access public\n"
  },
  {
    "path": ".gitignore",
    "content": ".DS_Store\n*.tgz\n/dist\n/lib\n/node_modules\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),\nand this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n\n## [5.0.2](https://github.com/metonym/svelte-typeahead/releases/tag/v5.0.2) - 2025-11-24\n\n**Fixes**\n\n- close dropdown for keyboard navigation\n\n## [5.0.1](https://github.com/metonym/svelte-typeahead/releases/tag/v5.0.1) - 2025-09-11\n\n**Fixes**\n\n- patch `svelte-search` to v2.1.2\n\n## [5.0.0](https://github.com/metonym/svelte-typeahead/releases/tag/v5.0.0) - 2025-01-20\n\n**Breaking Changes**\n\n- set `package.json#type` to `module` with `exports` field\n- drop bundled ESM/UMD builds (only Svelte source code is distributed)\n- colocate TypeScript definitions with Svelte source code\n\n**Fixes**\n\n- patch `svelte-search` to v2.1.1\n- run `npm pkg fix` to fix `package.json` metadata\n\n## [4.4.2](https://github.com/metonym/svelte-typeahead/releases/tag/v4.4.2) - 2024-11-20\n\n- fix `$$restProps` type errors by upgrading `svelte-search`\n\n## [4.4.1](https://github.com/metonym/svelte-typeahead/releases/tag/v4.4.1) - 2022-11-12\n\n- replace `aria-owns` with `aria-controls` to resolve `a11y-role-has-required-aria-props` warning\n\n## [4.4.0](https://github.com/metonym/svelte-typeahead/releases/tag/v4.4.0) - 2022-11-08\n\n**Features**\n\n- add `showAllResultsOnFocus` prop to display all results when focusing an empty input\n\n**Fixes**\n\n- silence a11y warnings emitted by Svelte@3.52\n- event type for dispatched `clear` event should be `null`\n- unset `ul` margin\n\n## [4.3.2](https://github.com/metonym/svelte-typeahead/releases/tag/v4.3.2) - 2022-09-01\n\n**Fixes**\n\n- allow results to be selected if using `showDropdownOnFocus`\n\n## [4.3.1](https://github.com/metonym/svelte-typeahead/releases/tag/v4.3.1) - 2022-08-29\n\n**Fixes**\n\n- skip disabled results when auto-selecting a result\n\n## [4.3.0](https://github.com/metonym/svelte-typeahead/releases/tag/v4.3.0) - 2022-08-29\n\n**Features**\n\n- add `showDropdownOnFocus` prop to only show dropdown if the search input is focused\n\n## [4.2.4](https://github.com/metonym/svelte-typeahead/releases/tag/v4.2.4) - 2022-07-05\n\n**Fixes**\n\n- set dropdown menu `z-index: 1` when expanded\n\n## [4.2.3](https://github.com/metonym/svelte-typeahead/releases/tag/v4.2.3) - 2022-07-04\n\n**Fixes**\n\n- update selected index when hovering over a result\n\n## [4.2.2](https://github.com/metonym/svelte-typeahead/releases/tag/v4.2.2) - 2022-03-20\n\n**Fixes**\n\n- pressing \"ArrowUp\" on last item should not reset highlighted index\n\n## [4.2.1](https://github.com/metonym/svelte-typeahead/releases/tag/v4.2.1) - 2021-10-30\n\n**Fixes**\n\n- prevent keyboard selection and navigation of disabled results\n- add missing annotation for `TItem.disabled` to `results` prop\n\n**Documentation**\n\n- add `pnpm` installation command\n- remove \"disabling items after selection\" example\n- remove `id` fields from `data` array\n- simplify dispatched events example\n\n## [4.2.0](https://github.com/metonym/svelte-typeahead/releases/tag/v4.2.0) - 2021-10-26\n\n**Features**\n\n- use TypeScript generics so that the item type can be inferred from `data` value\n\n## [4.1.0](https://github.com/metonym/svelte-typeahead/releases/tag/v4.1.0) - 2021-10-03\n\n**Features**\n\n- render a \"no-results\" slot when the search value does not yield results\n- add `value` to the default slot props\n\n## [4.0.0](https://github.com/metonym/svelte-typeahead/releases/tag/v4.0.0) - 2021-09-05\n\n**Breaking Changes**\n\n- use `.svelte.d.ts` extension for component TypeScript definition\n\n## [3.0.0](https://github.com/metonym/svelte-typeahead/releases/tag/v3.0.0) - 2021-03-16\n\n**Breaking Changes**\n\n- the `ul` element is rendered even without results to preserve the accessibility label\n\n**Fixes**\n\n- disable form ARIA attributes in `svelte-search`\n- pass the correct `aria-activedescendant` item to `svelte-search`\n\n## [2.4.1](https://github.com/metonym/svelte-typeahead/releases/tag/v2.4.1) - 2021-03-14\n\n**Fixes**\n\n- pass `id` to `Search` to fix `aria-labelledby` references\n\n## [2.4.0](https://github.com/metonym/svelte-typeahead/releases/tag/v2.4.0) - 2021-03-04\n\n**Features**\n\n- add `limit` prop to limit number of results; default value is `Infinity`\n\n## [2.3.0](https://github.com/metonym/svelte-typeahead/releases/tag/v2.3.0) - 2021-02-21\n\n**Features**\n\n- add `disable`, `filter` props to disable and filter items from the result set\n\n**Fixes**\n\n- bind the input element reference correctly to fix focusing behavior\n- don't pass the Typeahead id to Search\n\n## [2.2.0](https://github.com/metonym/svelte-typeahead/releases/tag/v2.2.0) - 2021-02-20\n\n**Features**\n\n- add `inputAfterSelect` prop to allow user to preserve or clear the input field after selecting a result; possible values are `\"update\" | \"clear\" | \"keep\"` (default is `\"update\"`)\n- add searched value to dispatched \"select\" event detail (`e.detail.searched`)\n\n## [2.1.0](https://github.com/metonym/svelte-typeahead/releases/tag/v2.1.0) - 2021-02-20\n\n**Features**\n\n- include `original` item and `originalIndex` in dispatched \"select\" event\n\n## [2.0.0](https://github.com/metonym/svelte-typeahead/releases/tag/v2.0.0) - 2020-12-31\n\n**Breaking Changes**\n\n- upgrade `svelte-search` to version 1.0.0\n- defer to default `label`, `placeholder` props from `search-svelte`\n- use `SvelteComponentTyped` interface in TypeScript definitions\n\n## [1.0.0](https://github.com/metonym/svelte-typeahead/releases/tag/v1.0.0) - 2020-11-28\n\n**Features**\n\n- export reactive `results` array containing fuzzy results\n- add `focusAfterSelect` to opt in to focusing input after selecting a result\n- keydown default behavior is preventing if pressing \"ArrowUp\", \"ArrowDown\", or \"Escape\"\n\n**Breaking changes**\n\n- `focusAfterSelect` is `false` by default\n- redesigned default styles\n- if using TS, Svelte version >=3.30 is required\n\n## [0.2.0](https://github.com/metonym/svelte-typeahead/releases/tag/v0.2.0) - 2020-11-17\n\n- Add TypeScript definitions\n\n## [0.1.1](https://github.com/metonym/svelte-typeahead/releases/tag/v0.1.1) - 2020-08-06\n\n- Remove `filter` named export\n\n## [0.1.0](https://github.com/metonym/svelte-typeahead/releases/tag/v0.1.0) - 2020-04-15\n\n- Initial release\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2020-present Eric Liu\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# svelte-typeahead\n\n[![NPM][npm]][npm-url]\n\n> Accessible, fuzzy search typeahead component.\n\n<!-- REPO_URL -->\n\nThis component uses the lightweight [fuzzy](https://github.com/mattyork/fuzzy) library for client-side, fuzzy search and follows [WAI-ARIA guidelines](https://www.w3.org/TR/wai-aria-practices/examples/combobox/aria1.1pattern/listbox-combo.html).\n\nTry it in the [Svelte REPL](https://svelte.dev/repl/a1b828d80de24f7e995b2365782c8d04).\n\n---\n\n<!-- TOC -->\n\n## Installation\n\n```bash\n# npm\nnpm i svelte-typeahead\n\n# pnpm\npnpm i svelte-typeahead\n\n# Yarn\nyarn add svelte-typeahead\n\n# Bun\nbun add svelte-typeahead\n```\n\n## Usage\n\n### Basic\n\nPass an array of objects to the `data` prop. Use the `extract` prop to specify the value to search on.\n\n```svelte\n<script>\n  import Typeahead from \"svelte-typeahead\";\n\n  const data = [\n    { state: \"California\" },\n    { state: \"North Carolina\" },\n    { state: \"North Dakota\" },\n    { state: \"South Carolina\" },\n    { state: \"South Dakota\" },\n    { state: \"Michigan\" },\n    { state: \"Tennessee\" },\n    { state: \"Nevada\" },\n    { state: \"New Hampshire\" },\n    { state: \"New Jersey\" },\n  ];\n\n  const extract = (item) => item.state;\n</script>\n\n<Typeahead {data} {extract} />\n```\n\n### Custom label\n\n`$$restProps` are forwarded to [svelte-search](https://github.com/metonym/svelte-search).\n\nUse the `label` prop to specify a custom label.\n\n```svelte\n<Typeahead label=\"U.S. States\" {data} {extract} />\n```\n\n### Hidden label\n\nSet `hideLabel` to `true` to visually hide the label.\n\nIt's recommended that you set the `label` – even if hidden – for accessibility.\n\n```svelte\n<Typeahead label=\"U.S. States\" hideLabel {data} {extract} />\n```\n\n### Custom-styled results\n\nThis component uses the [fuzzy](https://github.com/mattyork/fuzzy) library to highlight matching characters with the [mark](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/mark) element.\n\nUse the default slot to render custom results.\n\n```svelte\n<Typeahead {data} {extract} let:result let:index>\n  <strong>{@html result.string}</strong>\n  {index}\n</Typeahead>\n```\n\n### No results\n\nUse the \"no-results\" slot to render a message if the search value does not yield results.\n\n```svelte\n<Typeahead value=\"abcd\" {data} {extract} let:value>\n  <svelte:fragment slot=\"no-results\">\n    No results found for \"{value}\"\n  </svelte:fragment>\n</Typeahead>\n```\n\n### Limit the number of results\n\nUse the `limit` prop to specify the maximum number of results to display. The default is `Infinity`.\n\n```svelte\n<Typeahead limit={2} {data} {extract} />\n```\n\n### Disabled items\n\nDisable items using the `disable` filter. Disabled items are not selectable or navigable by keyboard.\n\nIn the following example, items with a `state` value containing \"Carolina\" are disabled.\n\n```svelte\n<Typeahead\n  {data}\n  value=\"ca\"\n  extract={(item) => item.state}\n  disable={(item) => /Carolina/.test(item.state)}\n/>\n```\n\n### Focus after select\n\nSet `focusAfterSelect` to `true` to re-focus the search input after selecting a result.\n\n```svelte\n<Typeahead focusAfterSelect {data} {extract} />\n```\n\n### Show dropdown on focus\n\nBy default, the dropdown will be shown if the `value` has results.\n\nSet `showDropdownOnFocus` to `true` to only show the dropdown when the search input is focused.\n\n```svelte\n<Typeahead value=\"ca\" showDropdownOnFocus {data} {extract} />\n```\n\n### Show all results on focus\n\nBy default, no results are shown if an empty input (i.e., `value=\"\"`) is focused.\n\nSet `showAllResultsOnFocus` to `true` for all results to be shown when an empty input is focused.\n\n```svelte\n<Typeahead showAllResultsOnFocus {data} {extract} />\n```\n\n### Styling\n\n**Note:** this component is minimally styled by design. You can target the component using the `[data-svelte-typeahead]` selector.\n\n```css\n:global([data-svelte-typeahead]) {\n  margin: 1rem;\n}\n```\n\n## API\n\n### Props\n\n| Name                  | Type                                                                       | Default value     | Description                                                                                                                           |\n| :-------------------- | :------------------------------------------------------------------------- | :---------------- | :------------------------------------------------------------------------------------------------------------------------------------ |\n| value                 | `string`                                                                   | `\"\"`              | Input search value.                                                                                                                   |\n| data                  | `TItem[]`                                                                  | `[]`              | Items to search.                                                                                                                      |\n| extract               | `(TItem) => any`                                                           | `(item) => item`  | Target the value if `TItem` is an object.                                                                                             |\n| disable               | `(TItem) => boolean`                                                       | `(item) => false` | Disabled items are shown in results but are not selectable.                                                                           |\n| filter                | `(TItem) => boolean`                                                       | `(item) => false` | Filtered out items will not be displayed in the results.                                                                              |\n| autoselect            | `boolean`                                                                  | `true`            | Whether to automatically select the first result.                                                                                     |\n| inputAfterSelect      | `\"update\" \\| \"clear\" \\| \"keep\"`                                            | `\"update\"`        | Set to `\"clear\"` to clear the `value` after selecting a result. Set to `\"keep\"` to keep the search field unchanged after a selection. |\n| results               | `FuzzyResult[]`                                                            | `[]`              | Raw fuzzy results from the [fuzzy](https://github.com/mattyork/fuzzy) module                                                          |\n| focusAfterSelect      | `boolean`                                                                  | `false`           | Set to `true` to re-focus the input after selecting a result.                                                                         |\n| showDropdownOnFocus   | `boolean`                                                                  | `false`           | Set to `true` to only show results when the input is focused.                                                                         |\n| showAllResultsOnFocus | `boolean`                                                                  | `false`           | Set to `true` for all results to be shown when an empty input is focused.                                                             |\n| limit                 | `number`                                                                   | `Infinity`        | Specify the maximum number of results to display.                                                                                     |\n| `...$$restProps`      | (forwarded to [`svelte-search`](https://github.com/metonym/svelte-search)) | N/A               | All other props are forwarded to the input element.                                                                                   |\n\n### Dispatched events\n\n- **on:select**: dispatched when selecting a result\n- **on:clear**: dispatched when clearing the input field\n\n```svelte\n<script>\n  import Typeahead from \"svelte-typeahead\";\n\n  let e = [];\n</script>\n\n<Typeahead\n  {data}\n  {extract}\n  on:select={({ detail }) => (e = [...e, { event: \"select\", detail }])}\n  on:clear={() => (e = [...e, { event: \"clear\" }])}\n/>\n\n<pre>{JSON.stringify(e, null, 2)}</pre>\n```\n\n### Forwarded events\n\nThe following events are forwarded to the [svelte-search](https://github.com/metonym/svelte-search) component.\n\n- on:type\n- on:input\n- on:change\n- on:focus\n- on:clear\n- on:blur\n- on:keydown\n\n## Changelog\n\n[Changelog](CHANGELOG.md)\n\n## License\n\n[MIT](LICENSE)\n\n[npm]: https://img.shields.io/npm/v/svelte-typeahead.svg?color=%23ff3e00&style=for-the-badge\n[npm-url]: https://npmjs.com/package/svelte-typeahead\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"svelte-typeahead\",\n  \"version\": \"5.0.2\",\n  \"license\": \"MIT\",\n  \"description\": \"Accessible, fuzzy search typeahead component\",\n  \"author\": \"Eric Liu (https://github.com/metonym)\",\n  \"type\": \"module\",\n  \"svelte\": \"./src/index.js\",\n  \"main\": \"./src/index.js\",\n  \"types\": \"./src/index.d.ts\",\n  \"exports\": {\n    \"./*.svelte\": {\n      \"types\": \"./src/*.svelte.d.ts\",\n      \"import\": \"./src/*.svelte\"\n    },\n    \"./*.js\": {\n      \"types\": \"./src/*.d.ts\",\n      \"import\": \"./src/*.js\"\n    },\n    \".\": {\n      \"types\": \"./src/index.d.ts\",\n      \"import\": \"./src/index.js\",\n      \"svelte\": \"./src/index.js\"\n    }\n  },\n  \"scripts\": {\n    \"dev\": \"rollup -cw\",\n    \"test\": \"svelte-check --workspace tests\"\n  },\n  \"dependencies\": {\n    \"fuzzy\": \"0.1.3\",\n    \"svelte-search\": \"^2.1.2\"\n  },\n  \"devDependencies\": {\n    \"@rollup/plugin-commonjs\": \"^21.1.0\",\n    \"@types/bun\": \"^1.3.3\",\n    \"culls\": \"^0.1.2\",\n    \"svelte\": \"^3.59.1\",\n    \"svelte-check\": \"^4.1.4\",\n    \"svelte-readme\": \"^3.6.3\",\n    \"typescript\": \"^5.9.3\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/metonym/svelte-typeahead.git\"\n  },\n  \"homepage\": \"https://github.com/metonym/svelte-typeahead\",\n  \"bugs\": \"https://github.com/metonym/svelte-typeahead/issues\",\n  \"keywords\": [\n    \"svelte\",\n    \"svelte component\",\n    \"typeahead\",\n    \"fuzzy\",\n    \"highlight\",\n    \"search\",\n    \"filter\",\n    \"WAI-ARIA\",\n    \"accessibility\"\n  ],\n  \"files\": [\n    \"src\"\n  ]\n}\n"
  },
  {
    "path": "rollup.config.ts",
    "content": "import commonjs from \"@rollup/plugin-commonjs\";\nimport svelteReadme from \"svelte-readme\";\n\nexport default svelteReadme({\n  plugins: [commonjs()],\n  style: `\n    .code-fence li + li { margin: 0; }\n    .code-fence { min-height: 16rem; }\n    .code-fence pre { margin-bottom: 0; margin-top: 12px; }\n  `,\n});\n"
  },
  {
    "path": "src/Typeahead.svelte",
    "content": "<script>\n  /**\n   * @template TItem = string | number | Record<string, any>\n   */\n\n  export let id = \"typeahead-\" + Math.random().toString(36);\n  export let value = \"\";\n\n  /** @type {TItem[]} */\n  export let data = [];\n\n  /** @type {(item: TItem) => any} */\n  export let extract = (item) => item;\n\n  /** @type {(item: TItem) => boolean} */\n  export let disable = (item) => false;\n\n  /** @type {(item: TItem) => boolean} */\n  export let filter = (item) => false;\n\n  /** Set to `false` to prevent the first result from being selected */\n  export let autoselect = true;\n\n  /**\n   * Set to `keep` to keep the search field unchanged after select, set to `clear` to auto-clear search field\n   * @type {\"update\" | \"clear\" | \"keep\"}\n   */\n  export let inputAfterSelect = \"update\";\n\n  /** @type {{ original: TItem; index: number; score: number; string: string; disabled?: boolean; }[]} */\n  export let results = [];\n\n  /** Set to `true` to re-focus the input after selecting a result */\n  export let focusAfterSelect = false;\n\n  /** Set to `true` to only show results when the input is focused */\n  export let showDropdownOnFocus = false;\n\n  /** Set to `true` for all results to be shown when an empty input is focused */\n  export let showAllResultsOnFocus = false;\n\n  /**\n   * Specify the maximum number of results to return\n   * @type {number}\n   */\n  export let limit = Infinity;\n\n  import fuzzy from \"fuzzy\";\n  import Search from \"svelte-search\";\n  import { tick, createEventDispatcher, afterUpdate } from \"svelte\";\n\n  const dispatch = createEventDispatcher();\n\n  let comboboxRef = null;\n  let searchRef = null;\n  let hideDropdown = false;\n  let selectedIndex = -1;\n  let prevResults = \"\";\n  let isFocused = false;\n\n  afterUpdate(() => {\n    if (prevResults !== resultsId && autoselect) {\n      selectedIndex = getNextNonDisabledIndex();\n    }\n\n    if (prevResults !== resultsId && !$$slots[\"no-results\"]) {\n      hideDropdown = results.length === 0;\n    }\n\n    prevResults = resultsId;\n  });\n\n  async function select() {\n    const result = results[selectedIndex];\n\n    if (result.disabled) return;\n\n    const selectedValue = extract(result.original);\n    const searchedValue = value;\n\n    if (inputAfterSelect == \"clear\") value = \"\";\n    if (inputAfterSelect == \"update\") value = selectedValue;\n\n    dispatch(\"select\", {\n      selectedIndex,\n      searched: searchedValue,\n      selected: selectedValue,\n      original: result.original,\n      originalIndex: result.index,\n    });\n\n    await tick();\n\n    if (focusAfterSelect) searchRef.focus();\n    close();\n  }\n\n  /** @type {() => number} */\n  function getNextNonDisabledIndex() {\n    let index = 0;\n    let disabled = results[index]?.disabled ?? false;\n\n    while (disabled) {\n      if (index === results.length) {\n        index = 0;\n      } else {\n        index += 1;\n      }\n\n      disabled = results[index]?.disabled ?? false;\n    }\n\n    return index;\n  }\n\n  /** @type {(direction: -1 | 1) => void} */\n  function change(direction) {\n    let index =\n      direction === 1 && selectedIndex === results.length - 1\n        ? 0\n        : selectedIndex + direction;\n    if (index < 0) index = results.length - 1;\n\n    let disabled = results[index].disabled;\n\n    while (disabled) {\n      if (index === results.length) {\n        index = 0;\n      } else {\n        index += direction;\n      }\n\n      disabled = results[index].disabled;\n    }\n\n    selectedIndex = index;\n  }\n\n  const open = () => (hideDropdown = false);\n  const close = () => {\n    hideDropdown = true;\n    isFocused = false;\n  };\n\n  $: options = { pre: \"<mark>\", post: \"</mark>\", extract };\n  $: results = fuzzy\n    .filter(value, data, options)\n    .filter(({ score }) => score > 0)\n    .slice(0, limit)\n    .filter((result) => !filter(result.original))\n    .map((result) => ({ ...result, disabled: disable(result.original) }));\n  $: resultsId = results.map((result) => extract(result.original)).join(\"\");\n  $: showResults = !hideDropdown && results.length > 0;\n  $: if (showDropdownOnFocus) {\n    showResults = showResults && isFocused;\n  }\n  $: if (isFocused && showAllResultsOnFocus && value.length === 0) {\n    results = data\n      .filter((datum) => !filter(datum))\n      .map((original, index) => ({\n        disabled: disable(original),\n        index,\n        original,\n        score: 0,\n        string: extract(original),\n      }));\n  }\n</script>\n\n<svelte:window\n  on:click={({ target }) => {\n    if (!hideDropdown && !comboboxRef?.contains(target)) {\n      close();\n    }\n  }}\n/>\n\n<div\n  data-svelte-typeahead\n  bind:this={comboboxRef}\n  role=\"combobox\"\n  aria-haspopup=\"listbox\"\n  aria-controls=\"{id}-listbox\"\n  class:dropdown={results.length > 0}\n  aria-expanded={showResults}\n  id=\"{id}-typeahead\"\n>\n  <Search\n    {id}\n    removeFormAriaAttributes={true}\n    {...$$restProps}\n    bind:ref={searchRef}\n    aria-autocomplete=\"list\"\n    aria-controls=\"{id}-listbox\"\n    aria-labelledby=\"{id}-label\"\n    aria-activedescendant={selectedIndex >= 0 &&\n    !hideDropdown &&\n    results.length > 0\n      ? `${id}-result-${selectedIndex}`\n      : null}\n    bind:value\n    on:type\n    on:input\n    on:change\n    on:focus\n    on:focus={() => {\n      open();\n      if (showDropdownOnFocus || showAllResultsOnFocus) {\n        showResults = true;\n        isFocused = true;\n      }\n    }}\n    on:clear\n    on:clear={open}\n    on:blur={(e) => {\n      // Check if focus is moving to an element within the combobox\n      const relatedTarget = e.relatedTarget;\n      if (relatedTarget && comboboxRef?.contains(relatedTarget)) {\n        return;\n      }\n      // Close immediately for keyboard navigation (Tab, Shift+Tab)\n      close();\n    }}\n    on:keydown\n    on:keydown={(e) => {\n      if (results.length === 0) return;\n\n      switch (e.key) {\n        case \"Enter\":\n          select();\n          break;\n        case \"ArrowDown\":\n          e.preventDefault();\n          change(1);\n          break;\n        case \"ArrowUp\":\n          e.preventDefault();\n          change(-1);\n          break;\n        case \"Escape\":\n          e.preventDefault();\n          value = \"\";\n          searchRef?.focus();\n          close();\n          break;\n      }\n    }}\n  />\n  <ul\n    class:svelte-typeahead-list={true}\n    role=\"listbox\"\n    aria-labelledby=\"{id}-label\"\n    id=\"{id}-listbox\"\n  >\n    {#if showResults}\n      {#each results as result, index}\n        <!-- svelte-ignore a11y-click-events-have-key-events -->\n        <li\n          role=\"option\"\n          id=\"{id}-result-{index}\"\n          class:selected={selectedIndex === index}\n          class:disabled={result.disabled}\n          aria-selected={selectedIndex === index}\n          on:mousedown={(e) => {\n            if (result.disabled) return;\n            e.preventDefault(); // Prevent input from losing focus\n            selectedIndex = index;\n            select();\n          }}\n          on:mouseenter={() => {\n            if (result.disabled) return;\n            selectedIndex = index;\n          }}\n        >\n          <slot {result} {index} {value}>\n            {@html result.string}\n          </slot>\n        </li>\n      {/each}\n    {/if}\n    {#if $$slots[\"no-results\"] && !hideDropdown && value.length > 0 && results.length === 0}\n      <div class:no-results={true}>\n        <slot name=\"no-results\" {value} />\n      </div>\n    {/if}\n  </ul>\n</div>\n\n<style>\n  [data-svelte-typeahead] {\n    position: relative;\n    background-color: #fff;\n  }\n\n  ul {\n    position: absolute;\n    top: 100%;\n    left: 0;\n    width: 100%;\n    margin: 0;\n    padding: 0;\n    list-style: none;\n    background-color: inherit;\n    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);\n  }\n\n  [aria-expanded=\"true\"] ul {\n    z-index: 1;\n  }\n\n  li,\n  .no-results {\n    padding: 0.25rem 1rem;\n  }\n\n  li {\n    cursor: pointer;\n  }\n\n  li:not(:last-of-type) {\n    border-bottom: 1px solid #e0e0e0;\n  }\n\n  li:hover {\n    background-color: #e5e5e5;\n  }\n\n  .selected {\n    background-color: #e5e5e5;\n  }\n\n  .selected:hover {\n    background-color: #cacaca;\n  }\n\n  .disabled {\n    opacity: 0.4;\n    cursor: not-allowed;\n  }\n\n  :global([data-svelte-search] label) {\n    margin-bottom: 0.25rem;\n    display: inline-flex;\n    font-size: 0.875rem;\n  }\n\n  :global([data-svelte-search] input) {\n    width: 100%;\n    padding: 0.5rem 0.75rem;\n    background: none;\n    font-size: 1rem;\n    border: 0;\n    border-radius: 0;\n    border: 1px solid #e5e5e5;\n  }\n\n  :global([data-svelte-search] input:focus) {\n    outline-color: #0f62fe;\n    outline-offset: 2px;\n    outline-width: 1px;\n  }\n</style>\n"
  },
  {
    "path": "src/Typeahead.svelte.d.ts",
    "content": "import type { SvelteComponentTyped } from \"svelte\";\nimport type { SearchProps } from \"svelte-search/src/Search.svelte\";\n\nexport interface TypeaheadProps<TItem> extends Omit<SearchProps, \"results\"> {\n  /**\n   * @default \"typeahead-\" + Math.random().toString(36)\n   */\n  id?: string;\n\n  /**\n   * @default \"\"\n   */\n  value?: string;\n\n  /**\n   * @default []\n   */\n  data?: TItem[];\n\n  /**\n   * @default (item) => item\n   */\n  extract?: (item: TItem) => any;\n\n  /**\n   * @default (item) => false\n   */\n  disable?: (item: TItem) => boolean;\n\n  /**\n   * @default (item) => false\n   */\n  filter?: (item: TItem) => boolean;\n\n  /**\n   * Set to `false` to prevent the first result from being selected\n   * @default true\n   */\n  autoselect?: boolean;\n\n  /**\n   * Set to `keep` to keep the search field unchanged after select, set to `clear` to auto-clear search field\n   * @default \"update\"\n   */\n  inputAfterSelect?: \"update\" | \"clear\" | \"keep\";\n\n  /**\n   * @default []\n   */\n  results?: {\n    original: TItem;\n    index: number;\n    score: number;\n    string: string;\n    disabled?: boolean;\n  }[];\n\n  /**\n   * Set to `true` to re-focus the input after selecting a result\n   * @default false\n   */\n  focusAfterSelect?: boolean;\n\n  /**\n   * Set to `true` to only show results when the input is focused\n   * @default false\n   */\n  showDropdownOnFocus?: boolean;\n\n  /**\n   * Set to `true` for all results to be shown when an empty input is focused\n   * @default false\n   */\n  showAllResultsOnFocus?: boolean;\n\n  /**\n   * Specify the maximum number of results to return\n   * @default Infinity\n   */\n  limit?: number;\n}\n\nexport default class Typeahead<\n  TItem = string | number | Record<string, any>\n> extends SvelteComponentTyped<\n  TypeaheadProps<TItem>,\n  {\n    select: CustomEvent<{\n      searched: string;\n      selected: TItem;\n      selectedIndex: number;\n      original: TItem;\n      originalIndex: number;\n    }>;\n    type: CustomEvent<string>;\n    clear: CustomEvent<null>;\n    input: WindowEventMap[\"input\"];\n    change: WindowEventMap[\"change\"];\n    focus: WindowEventMap[\"focus\"];\n    blur: WindowEventMap[\"blur\"];\n    keydown: WindowEventMap[\"keydown\"];\n  },\n  {\n    default: {\n      result: {\n        original: TItem;\n        index: number;\n        score: number;\n        string: string;\n      };\n      index: number;\n      value: string;\n    };\n    \"no-results\": {\n      value: string;\n    };\n  }\n> {}\n"
  },
  {
    "path": "src/index.d.ts",
    "content": "export { default } from \"./Typeahead.svelte\";\n"
  },
  {
    "path": "src/index.js",
    "content": "export { default } from \"./Typeahead.svelte\";\n"
  },
  {
    "path": "tests/Typeahead.test.svelte",
    "content": "<script lang=\"ts\">\n  import Typeahead from \"svelte-typeahead\";\n  import type { TypeaheadProps } from \"svelte-typeahead/Typeahead.svelte\";\n  import T from \"svelte-typeahead\";\n\n  type Item = (typeof data)[number];\n\n  let results: TypeaheadProps<Item>[\"results\"] = [];\n\n  $: console.log(results?.[0]?.disabled);\n\n  const data = [\n    { id: 0, state: \"California\" },\n    { id: 1, state: \"North Carolina\" },\n    { id: 2, state: \"North Dakota\" },\n    { id: 3, state: \"South Carolina\" },\n    { id: 4, state: \"South Dakota\" },\n    { id: 5, state: \"Michigan\" },\n    { id: 6, state: \"Tennessee\" },\n    { id: 7, state: \"Nevada\" },\n    { id: 8, state: \"New Hampshire\" },\n    { id: 9, state: \"New Jersey\" },\n  ];\n\n  const extract = (item: Item) => item.state;\n  const disable = (item: Item) => item.state.length > 10;\n  const filter = (item: Item) => item.id < 4;\n</script>\n\n<Typeahead\n  {extract}\n  {disable}\n  {filter}\n  limit={1}\n  autocapitalize={false + \"\"}\n  placeholder=\"#{4}\"\n  autofocus\n  hideLabel\n  focusAfterSelect\n  inputAfterSelect=\"keep\"\n  debounce={800}\n  showAllResultsOnFocus\n  {data}\n  on:select={(e) => {\n    console.log(\"select\", e.detail);\n  }}\n  on:clear\n  on:type={(e) => {\n    console.log(e.detail);\n  }}\n  bind:results\n  let:result\n  let:index\n  let:value={searchedValue}\n>\n  {result.original}\n  {@html result.string}\n  {index}\n  {result.score}\n  {searchedValue}\n  <svelte:fragment slot=\"no-results\" let:value>\n    No results {value}\n  </svelte:fragment>\n</Typeahead>\n\n<T debounce={300} />\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"verbatimModuleSyntax\": true,\n    \"isolatedModules\": true,\n    \"target\": \"ESNext\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"node\",\n    \"strict\": true,\n    \"skipLibCheck\": true,\n    \"paths\": {\n      \"svelte-typeahead\": [\"./src\"],\n      \"svelte-typeahead/*\": [\"./src/*\"]\n    }\n  },\n  \"include\": [\"src\", \"tests\"]\n}\n"
  }
]