{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "search-suggest",
  "type": "registry:block",
  "title": "Search Suggest",
  "description": "A search field whose card grows into live suggestions: recent searches you can remove, a debounced fetch with a shimmer, results that stagger in with the match underlined, and an active bar that springs between rows. Follows the ARIA combobox pattern, with / and Cmd+K to focus.",
  "docs": "Search Suggest was added to public/vividsites/search-suggest.html. Paste its markup, <style> and <script> where you want it on the page (or open the file to see it alone). Customise it with the CSS variables at the top. Docs and live demo: https://vividsites.app/c/search-suggest",
  "categories": [
    "ui",
    "forms"
  ],
  "files": [
    {
      "path": "components/vividsites/search-suggest.html",
      "type": "registry:file",
      "target": "public/vividsites/search-suggest.html",
      "content": "<!--\nvs-search-suggest: a search field whose card grows into live suggestions. It follows the\n  WAI-ARIA combobox pattern (input role=\"combobox\" + grouped role=\"listbox\" through\n  aria-activedescendant). With an empty query it shows Recent searches (remove one with its\n  ×, or Shift+Delete, or Clear them all) and a Popular list; as you type, a debounced fetch\n  shows a shimmer, then results stagger in with the matched text underlined, rows that\n  survive a keystroke glide to their new place (FLIP), the card's height springs to fit, and\n  an active bar springs between rows. Enter opens a result and remembers it in Recents.\n\n  Markup:\n    <div class=\"vs-search-suggest\" data-label=\"Search recipes\" data-hotkey>\n      <script type=\"application/json\" class=\"vs-search-suggest-data\">{\n        \"items\":   [{\"id\":\"orzo\",\"title\":\"Charred leek & brown butter orzo\",\"tag\":\"Main\",\"meta\":\"35 min\",\"color\":\"#9aa86a\",\"terms\":\"leek butter parmesan\"}],\n        \"recent\":  [\"miso\", \"rhubarb galette\"],\n        \"popular\": [\"orzo\"],\n        \"tryInstead\": [\"leek\", \"miso\"]\n      }</script>\n    </div>\n  Root attributes:\n    data-label        accessible name (and the placeholder unless data-placeholder is set)\n    data-placeholder  placeholder text\n    data-hotkey       \"/\" and Cmd/Ctrl+K focus this field from anywhere on the page\n    data-latency      simulated fetch time in ms for the built-in local search (default 260)\n    data-storage-key  if set, Recents persist in localStorage under this key (safely ignored\n                      when storage is blocked)\n    data-noun         word for the result count (\"recipes\")\n  Remote search: set el.vsSource = async (query, signal) => [{ id, title, tag, meta, color, sub }]\n    and the component handles debounce, stale responses, shimmer and highlighting.\n  Variables (on .vs-search-suggest): --vs-search-suggest-scale (everything is in em), -width,\n    -font, -accent (caret, match underline, active bar, progress), -ink, -dim, -card, -line,\n    -hl (active row), -chip, -radius, -max (list height before scrolling), -stiffness.\n  Keys: ↓/↑ move (and open), Home/End, Enter opens the active row (a Recent row re-runs that\n    search), Esc clears the query, then closes, Shift+Delete removes the active Recent,\n    Tab closes; \"/\" or Cmd/Ctrl+K focus with data-hotkey.\n  Events: \"vs-search-suggest-commit\" { item, query } and \"vs-search-suggest-query\" { query }.\n  Reduced motion: springs snap, no stagger, no shimmer sweep.\n-->\n<style>\n.vs-search-suggest {\n  --vs-search-suggest-scale: 1;\n  --vs-search-suggest-width: 34em;\n  --vs-search-suggest-font: inherit;\n  --vs-search-suggest-accent: #3f7d3a;            /* caret, match underline, active bar */\n  --vs-search-suggest-ink: #1b1b19;\n  --vs-search-suggest-dim: #7b7a74;\n  --vs-search-suggest-card: #ffffff;\n  --vs-search-suggest-line: rgba(27,27,25,.1);\n  --vs-search-suggest-hl: #f3f2ee;                /* the springing active row */\n  --vs-search-suggest-chip: #f1f0eb;\n  --vs-search-suggest-radius: 1.15em;\n  --vs-search-suggest-max: 25em;\n  --vs-search-suggest-stiffness: 560;\n\n  position: relative; display: block; width: min(100%, var(--vs-search-suggest-width)); height: 3.75em;\n  font-family: var(--vs-search-suggest-font); font-size: calc(14px * var(--vs-search-suggest-scale)); line-height: 1.3;\n  color: var(--vs-search-suggest-ink); -webkit-font-smoothing: antialiased; -webkit-tap-highlight-color: transparent; text-align: left;\n}\n.vs-search-suggest *, .vs-search-suggest *::before, .vs-search-suggest *::after { box-sizing: border-box; }\n.vs-search-suggest-box {\n  position: absolute; left: 0; right: 0; top: 0; height: 3.75em; overflow: hidden; border-radius: var(--vs-search-suggest-radius);\n  background: var(--vs-search-suggest-card);\n  box-shadow: 0 0 0 .5px var(--vs-search-suggest-line), inset 0 .5px 0 rgba(255,255,255,.9), 0 1px 2px rgba(27,27,25,.05), 0 6px 16px -8px rgba(27,27,25,.12);\n  transition: box-shadow .35s cubic-bezier(.2,0,0,1); will-change: height;\n}\n.vs-search-suggest:hover .vs-search-suggest-box { box-shadow: 0 0 0 .5px rgba(27,27,25,.16), inset 0 .5px 0 rgba(255,255,255,.9), 0 1px 2px rgba(27,27,25,.06), 0 8px 20px -8px rgba(27,27,25,.16); }\n.vs-search-suggest.is-focus .vs-search-suggest-box {\n  box-shadow: 0 0 0 .5px rgba(27,27,25,.14), 0 0 0 4px rgba(27,27,25,.04), inset 0 .5px 0 #fff, 0 2px 4px rgba(27,27,25,.04), 0 16px 34px -12px rgba(27,27,25,.2), 0 40px 80px -30px rgba(27,27,25,.22);\n}\n.vs-search-suggest.is-open { z-index: 50; }\n\n/* field row */\n.vs-search-suggest-field { position: relative; display: flex; align-items: center; gap: .7em; height: 3.75em; padding: 0 .75em 0 1.15em; cursor: text; }\n.vs-search-suggest-icon { position: relative; flex: none; width: 1.2em; height: 1.2em; color: var(--vs-search-suggest-dim); transition: color .2s; }\n.vs-search-suggest.is-focus .vs-search-suggest-icon { color: var(--vs-search-suggest-ink); }\n.vs-search-suggest-icon svg { position: absolute; inset: 0; width: 100%; height: 100%; }\n.vs-search-suggest-spin { opacity: 0; transform: scale(.6); transition: opacity .2s, transform .3s cubic-bezier(.34,1.56,.64,1); }\n.vs-search-suggest-spin circle:last-child { stroke: var(--vs-search-suggest-accent); stroke-dasharray: 14 60; transform-origin: 12px 12px; animation: vs-search-suggest-rot .7s linear infinite; }\n.vs-search-suggest-glass { transition: opacity .2s, transform .3s cubic-bezier(.34,1.56,.64,1); }\n.vs-search-suggest.is-loading .vs-search-suggest-spin { opacity: 1; transform: none; }\n.vs-search-suggest.is-loading .vs-search-suggest-glass { opacity: 0; transform: scale(.6); }\n@keyframes vs-search-suggest-rot { to { transform: rotate(360deg); } }\n.vs-search-suggest-input {\n  flex: 1; min-width: 0; height: 100%; border: 0; outline: 0; padding: 0; margin: 0; background: none; font: inherit; font-size: 1.08em;\n  font-weight: 400; letter-spacing: -.01em; color: inherit; caret-color: var(--vs-search-suggest-accent);\n}\n.vs-search-suggest-input::placeholder { color: color-mix(in srgb, var(--vs-search-suggest-dim) 80%, transparent); }\n.vs-search-suggest-input::-webkit-search-cancel-button { -webkit-appearance: none; appearance: none; }\n.vs-search-suggest-input::selection { background: color-mix(in srgb, var(--vs-search-suggest-accent) 20%, transparent); }\n.vs-search-suggest-keys { display: flex; gap: .25em; transition: opacity .2s, transform .3s cubic-bezier(.34,1.56,.64,1); }\n.vs-search-suggest.is-focus .vs-search-suggest-keys, .vs-search-suggest.has-q .vs-search-suggest-keys { opacity: 0; transform: scale(.85); pointer-events: none; }\n.vs-search-suggest kbd {\n  display: inline-grid; place-items: center; min-width: 1.75em; height: 1.75em; padding: 0 .4em; border-radius: .45em;\n  font: 500 .76em/1 var(--vs-search-suggest-font); color: var(--vs-search-suggest-dim); background: var(--vs-search-suggest-chip);\n  box-shadow: inset 0 0 0 .5px rgba(27,27,25,.08), inset 0 -1px 0 rgba(27,27,25,.08);\n}\n.vs-search-suggest-clear {\n  position: absolute; right: .75em; top: 50%; width: 1.9em; height: 1.9em; margin-top: -.95em; display: grid; place-items: center; padding: 0; border: 0; border-radius: 50%;\n  background: var(--vs-search-suggest-chip); color: var(--vs-search-suggest-dim); cursor: pointer; opacity: 0; transform: scale(.6); pointer-events: none;\n  transition: opacity .18s, transform .35s cubic-bezier(.34,1.56,.64,1), background .15s, color .15s;\n}\n.vs-search-suggest-clear svg { width: .72em; height: .72em; }\n.vs-search-suggest.has-q .vs-search-suggest-clear { opacity: 1; transform: none; pointer-events: auto; }\n.vs-search-suggest-clear:hover { background: color-mix(in srgb, var(--vs-search-suggest-chip), var(--vs-search-suggest-ink) 8%); color: var(--vs-search-suggest-ink); }\n.vs-search-suggest-clear:active { transform: scale(.88); }\n.vs-search-suggest-clear:focus-visible { outline: 1.5px solid var(--vs-search-suggest-accent); outline-offset: 2px; }\n.vs-search-suggest-prog { position: absolute; left: 0; right: 0; bottom: 0; height: 1.5px; overflow: hidden; opacity: 0; transition: opacity .25s; pointer-events: none; }\n.vs-search-suggest-prog::before { content: \"\"; position: absolute; top: 0; bottom: 0; width: 34%; background: linear-gradient(90deg, transparent, var(--vs-search-suggest-accent), transparent); animation: vs-search-suggest-prog 1s cubic-bezier(.45,0,.55,1) infinite; }\n.vs-search-suggest.is-loading.is-open .vs-search-suggest-prog { opacity: 1; }\n@keyframes vs-search-suggest-prog { from { transform: translateX(-100%); } to { transform: translateX(300%); } }\n\n/* panel */\n.vs-search-suggest-panel { position: relative; max-height: var(--vs-search-suggest-max); overflow-y: auto; overscroll-behavior: contain; padding: .3em .45em .55em; box-shadow: inset 0 .5px 0 var(--vs-search-suggest-line); opacity: 0; transition: opacity .2s; scrollbar-width: thin; }\n.vs-search-suggest.is-open .vs-search-suggest-panel { opacity: 1; }\n.vs-search-suggest-list { position: relative; outline: none; }\n.vs-search-suggest-pill {\n  position: absolute; left: 0; right: 0; top: 0; height: 3.2em; border-radius: calc(var(--vs-search-suggest-radius) - .35em); background: var(--vs-search-suggest-hl);\n  pointer-events: none; opacity: 0; will-change: transform, opacity;\n}\n.vs-search-suggest-pill::before { content: \"\"; position: absolute; left: -.45em; top: 25%; bottom: 25%; width: 2px; border-radius: 0 3px 3px 0; background: var(--vs-search-suggest-accent); }\n.vs-search-suggest-gh { display: flex; align-items: center; justify-content: space-between; height: 2.55em; padding: .45em .75em 0; font-size: .78em; font-weight: 500; color: var(--vs-search-suggest-dim); letter-spacing: .01em; user-select: none; }\n.vs-search-suggest-gh button { font: inherit; color: var(--vs-search-suggest-dim); background: none; border: 0; padding: .2em .45em; margin-right: -.45em; border-radius: .4em; cursor: pointer; transition: color .15s, background .15s; }\n.vs-search-suggest-gh button:hover { color: var(--vs-search-suggest-ink); background: var(--vs-search-suggest-chip); }\n.vs-search-suggest-opt {\n  position: relative; display: grid; grid-template-columns: 2.1em 1fr auto; align-items: center; column-gap: .85em; height: 3.2em; padding: 0 .75em 0 .6em;\n  border-radius: calc(var(--vs-search-suggest-radius) - .35em); cursor: pointer; user-select: none; -webkit-user-select: none;\n}\n.vs-search-suggest-opt.is-recent { height: 2.7em; grid-template-columns: 2.1em 1fr auto; }\n.vs-search-suggest-dot {\n  justify-self: center; width: 2.1em; height: 2.1em; border-radius: 50%;\n  background: radial-gradient(circle at 34% 30%, color-mix(in srgb, var(--c, #ccc), #fff 45%), var(--c, #ccc) 55%, color-mix(in srgb, var(--c, #ccc), #000 22%));\n  box-shadow: inset 0 0 0 .5px rgba(0,0,0,.12), 0 0 0 .22em #fff, 0 0 0 calc(.22em + .5px) rgba(27,27,25,.08), 0 2px 5px -1px rgba(27,27,25,.2);\n}\n.vs-search-suggest-ico { justify-self: center; width: 1.05em; height: 1.05em; color: color-mix(in srgb, var(--vs-search-suggest-dim) 80%, transparent); }\n.vs-search-suggest-t { min-width: 0; display: grid; gap: .14em; }\n.vs-search-suggest-title { font-size: .98em; font-weight: 400; letter-spacing: -.01em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: color-mix(in srgb, var(--vs-search-suggest-ink) 82%, transparent); }\n.vs-search-suggest-sub { font-size: .78em; color: var(--vs-search-suggest-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n.vs-search-suggest mark { background: none; color: var(--vs-search-suggest-ink); font-weight: 500; text-decoration: underline; text-decoration-color: var(--vs-search-suggest-accent); text-decoration-thickness: 2px; text-underline-offset: .22em; text-decoration-skip-ink: none; }\n.vs-search-suggest-end { display: flex; align-items: center; gap: .55em; }\n.vs-search-suggest-chiptag { font-size: .74em; font-weight: 500; padding: .32em .65em; border-radius: 99px; background: var(--vs-search-suggest-chip); color: var(--vs-search-suggest-dim); box-shadow: inset 0 0 0 .5px rgba(27,27,25,.06); white-space: nowrap; }\n.vs-search-suggest-go { width: 0; overflow: hidden; opacity: 0; transition: width .3s cubic-bezier(.34,1.3,.64,1), opacity .2s; display: flex; justify-content: flex-end; }\n.vs-search-suggest-go kbd { background: var(--vs-search-suggest-card); }\n.vs-search-suggest-opt.is-active .vs-search-suggest-go { width: 1.9em; opacity: 1; }\n.vs-search-suggest-opt.is-active .vs-search-suggest-title { color: var(--vs-search-suggest-ink); }\n.vs-search-suggest-rm {\n  display: grid; place-items: center; width: 1.75em; height: 1.75em; padding: 0; border: 0; border-radius: 50%; background: none; color: var(--vs-search-suggest-dim);\n  cursor: pointer; opacity: 0; transition: opacity .15s, background .15s, color .15s, transform .25s cubic-bezier(.34,1.56,.64,1);\n}\n.vs-search-suggest-rm svg { width: .68em; height: .68em; }\n.vs-search-suggest-opt.is-active .vs-search-suggest-rm, .vs-search-suggest-opt:hover .vs-search-suggest-rm { opacity: 1; }\n.vs-search-suggest-rm:hover { background: color-mix(in srgb, var(--vs-search-suggest-card), var(--vs-search-suggest-ink) 8%); color: var(--vs-search-suggest-ink); }\n.vs-search-suggest-rm:active { transform: scale(.85); }\n@media (hover: none) { .vs-search-suggest-rm { opacity: .7; } .vs-search-suggest-go { display: none; } }\n\n/* shimmer */\n.vs-search-suggest-skel { display: grid; grid-template-columns: 2.1em 1fr; align-items: center; column-gap: .85em; height: 3.2em; padding: 0 .75em 0 .6em; }\n.vs-search-suggest-skel i, .vs-search-suggest-skel b { display: block; background: linear-gradient(90deg, #efeee9 0%, #f7f6f3 45%, #efeee9 90%) 0 0 / 300% 100%; animation: vs-search-suggest-sh 1.1s linear infinite; }\n.vs-search-suggest-skel i { width: 2.1em; height: 2.1em; border-radius: 50%; }\n.vs-search-suggest-skel span { display: grid; gap: .45em; }\n.vs-search-suggest-skel b { height: .7em; border-radius: .35em; }\n.vs-search-suggest-skel b + b { height: .55em; }\n@keyframes vs-search-suggest-sh { from { background-position: 100% 0; } to { background-position: -50% 0; } }\n.vs-search-suggest.is-stale .vs-search-suggest-list { opacity: .5; transition: opacity .2s .08s; }\n\n/* empty */\n.vs-search-suggest-empty { display: none; padding: 1.6em 1em 1.2em; text-align: center; }\n.vs-search-suggest.is-empty .vs-search-suggest-empty { display: grid; gap: .4em; justify-items: center; }\n.vs-search-suggest-empty strong { font-weight: 500; font-size: 1em; }\n.vs-search-suggest-empty span { font-size: .84em; color: var(--vs-search-suggest-dim); }\n.vs-search-suggest-empty button { font: inherit; font-weight: 500; color: var(--vs-search-suggest-ink); background: var(--vs-search-suggest-chip); border: 0; border-radius: 99px; padding: .25em .7em; margin: 0 .15em; cursor: pointer; transition: background .15s; }\n.vs-search-suggest-empty button:hover { background: color-mix(in srgb, var(--vs-search-suggest-chip), var(--vs-search-suggest-ink) 7%); }\n.vs-search-suggest-empty button:focus-visible { outline: 1.5px solid var(--vs-search-suggest-accent); outline-offset: 2px; }\n.vs-search-suggest-sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }\n@media (prefers-reduced-motion: reduce) {\n  .vs-search-suggest-skel i, .vs-search-suggest-skel b, .vs-search-suggest-prog::before, .vs-search-suggest-spin circle:last-child { animation: none; }\n}\n@media (forced-colors: active) { .vs-search-suggest-box { border: 1px solid CanvasText; } .vs-search-suggest-pill { outline: 2px solid Highlight; } }\n</style>\n\n<div class=\"vs-search-suggest\" data-label=\"Search recipes\" data-placeholder=\"Search recipes, ingredients…\" data-noun=\"recipes\" data-hotkey>\n  <script type=\"application/json\" class=\"vs-search-suggest-data\">{\n    \"items\": [\n      {\"id\": \"orzo\", \"title\": \"Charred leek & brown butter orzo\", \"tag\": \"Main\", \"meta\": \"35 min\", \"color\": \"#9aa86a\", \"terms\": \"leek butter parmesan pasta weeknight\"},\n      {\"id\": \"leek-soup\", \"title\": \"Leek, potato & smoked cheddar soup\", \"tag\": \"Soup\", \"meta\": \"50 min\", \"color\": \"#d6c28e\", \"terms\": \"leek potato cheese\"},\n      {\"id\": \"fritters\", \"title\": \"Crispy leek fritters, lemon yoghurt\", \"tag\": \"Side\", \"meta\": \"25 min\", \"color\": \"#b3bd72\", \"terms\": \"leek lemon yogurt\"},\n      {\"id\": \"vinaigrette\", \"title\": \"Braised leeks with mustard vinaigrette\", \"tag\": \"Side\", \"meta\": \"30 min\", \"color\": \"#c4c68a\", \"terms\": \"leek mustard\"},\n      {\"id\": \"pie\", \"title\": \"Chicken, leek & tarragon pie\", \"tag\": \"Main\", \"meta\": \"1 h 20\", \"color\": \"#d9a462\", \"terms\": \"leek chicken pastry\"},\n      {\"id\": \"miso-apples\", \"title\": \"Miso caramel baked apples\", \"tag\": \"Dessert\", \"meta\": \"45 min\", \"color\": \"#c9853f\", \"terms\": \"miso apple caramel\"},\n      {\"id\": \"aubergine\", \"title\": \"Miso-glazed aubergine\", \"tag\": \"Main\", \"meta\": \"30 min\", \"color\": \"#6a4a4f\", \"terms\": \"miso eggplant weeknight\"},\n      {\"id\": \"galette\", \"title\": \"Rhubarb & cardamom galette\", \"tag\": \"Baking\", \"meta\": \"1 h\", \"color\": \"#d97285\", \"terms\": \"rhubarb pastry\"},\n      {\"id\": \"scallion\", \"title\": \"Ginger scallion noodles\", \"tag\": \"Noodles\", \"meta\": \"15 min\", \"color\": \"#e0c067\", \"terms\": \"ginger spring onion weeknight\"},\n      {\"id\": \"soba\", \"title\": \"Cold sesame soba\", \"tag\": \"Noodles\", \"meta\": \"20 min\", \"color\": \"#a38b69\", \"terms\": \"sesame buckwheat\"},\n      {\"id\": \"stew\", \"title\": \"Saffron fennel stew with clams\", \"tag\": \"Soup\", \"meta\": \"55 min\", \"color\": \"#e0a23a\", \"terms\": \"saffron fennel seafood\"},\n      {\"id\": \"sumac\", \"title\": \"Sumac roast chicken & flatbreads\", \"tag\": \"Main\", \"meta\": \"1 h 10\", \"color\": \"#b5502f\", \"terms\": \"sumac chicken bread\"},\n      {\"id\": \"panna\", \"title\": \"Burnt honey panna cotta\", \"tag\": \"Dessert\", \"meta\": \"20 min\", \"color\": \"#e6be78\", \"terms\": \"honey cream\"},\n      {\"id\": \"cake\", \"title\": \"Pistachio olive oil cake\", \"tag\": \"Baking\", \"meta\": \"1 h\", \"color\": \"#a4b168\", \"terms\": \"pistachio olive oil\"},\n      {\"id\": \"orange\", \"title\": \"Blood orange & fennel salad\", \"tag\": \"Side\", \"meta\": \"10 min\", \"color\": \"#de6247\", \"terms\": \"orange fennel citrus\"},\n      {\"id\": \"chickpeas\", \"title\": \"Smoky chickpeas with greens\", \"tag\": \"Main\", \"meta\": \"25 min\", \"color\": \"#c07d3c\", \"terms\": \"chickpea paprika weeknight\"},\n      {\"id\": \"rye\", \"title\": \"No-knead seeded rye\", \"tag\": \"Bread\", \"meta\": \"14 h\", \"color\": \"#8a6848\", \"terms\": \"bread rye seeds\"},\n      {\"id\": \"gnocchi\", \"title\": \"Brown butter sage gnocchi\", \"tag\": \"Main\", \"meta\": \"40 min\", \"color\": \"#d6b266\", \"terms\": \"gnocchi butter sage\"},\n      {\"id\": \"dal\", \"title\": \"Coconut lime dal\", \"tag\": \"Main\", \"meta\": \"35 min\", \"color\": \"#e6c255\", \"terms\": \"lentil coconut lime weeknight\"},\n      {\"id\": \"tart\", \"title\": \"Tomato & anchovy tart\", \"tag\": \"Baking\", \"meta\": \"50 min\", \"color\": \"#d1523a\", \"terms\": \"tomato anchovy pastry\"},\n      {\"id\": \"ramen\", \"title\": \"Black garlic mushroom ramen\", \"tag\": \"Noodles\", \"meta\": \"45 min\", \"color\": \"#5e4a3a\", \"terms\": \"garlic mushroom miso\"},\n      {\"id\": \"pancakes\", \"title\": \"Lemon ricotta pancakes\", \"tag\": \"Breakfast\", \"meta\": \"20 min\", \"color\": \"#efd078\", \"terms\": \"lemon ricotta\"},\n      {\"id\": \"corn\", \"title\": \"Charred corn & lime salad\", \"tag\": \"Side\", \"meta\": \"15 min\", \"color\": \"#e7bf48\", \"terms\": \"corn lime\"},\n      {\"id\": \"tatin\", \"title\": \"Pear & brown butter tarte tatin\", \"tag\": \"Dessert\", \"meta\": \"1 h 15\", \"color\": \"#c9954d\", \"terms\": \"pear butter pastry\"}\n    ],\n    \"recent\": [\"Charred leek & brown butter orzo\", \"miso\", \"rhubarb galette\", \"weeknight noodles\"],\n    \"popular\": [\"sumac\", \"panna\", \"scallion\"],\n    \"tryInstead\": [\"leek\", \"miso\", \"noodles\"]\n  }</script>\n</div>\n\n<script>\n(() => {\n  if (window.__vsSearchSuggest) return; window.__vsSearchSuggest = true;\n  const reduce = matchMedia('(prefers-reduced-motion: reduce)');\n  const svg = (d, sw = 1.8) => `<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"${sw}\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">${d}</svg>`;\n  const I = {\n    glass: svg('<circle cx=\"11\" cy=\"11\" r=\"6.8\"/><path d=\"m16.2 16.2 4.3 4.3\"/>', 1.9),\n    spin: '<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke-width=\"2\" aria-hidden=\"true\"><circle cx=\"12\" cy=\"12\" r=\"8.5\" stroke=\"currentColor\" opacity=\".18\"/><circle cx=\"12\" cy=\"12\" r=\"8.5\" stroke-linecap=\"round\"/></svg>',\n    x: svg('<path d=\"M6 6l12 12M18 6 6 18\"/>', 2.6),\n    clock: svg('<path d=\"M3.5 12a8.5 8.5 0 1 0 2.5-6\"/><path d=\"M3.5 4.5V8H7\"/><path d=\"M12 7.5V12l3 2\"/>', 1.7),\n    trend: svg('<path d=\"m3.5 16.5 6-6 4 4 7-7\"/><path d=\"M15 7.5h5.5V13\"/>', 1.7),\n    search: svg('<circle cx=\"11\" cy=\"11\" r=\"6.8\"/><path d=\"m16.2 16.2 4.3 4.3\"/>', 1.7)\n  };\n  const esc = (s) => String(s).replace(/[&<>\"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;' }[c]));\n  const fold = (s) => { let o = ''; for (const ch of s) { const f = ch.normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase(); o += f.length === ch.length ? f : ch.toLowerCase(); } return o; };\n  // every word of the query must hit; ranges come back in original-string indices\n  function hit(text, q) {\n    const t = fold(text), words = q.split(/\\s+/).filter(Boolean); let s = 0; const r = [];\n    for (const w of words) {\n      let i = -1, best = -1;\n      for (let p = t.indexOf(w); p >= 0; p = t.indexOf(w, p + 1)) { if (p === 0 || /[\\s\\-&,(]/.test(t[p - 1])) { best = p; break; } if (i < 0) i = p; }\n      const at = best >= 0 ? best : i; if (at < 0) return null;\n      s += at === 0 ? 100 : best >= 0 ? 80 - at * .1 : 50 - at * .1; r.push([at, at + w.length]);\n    }\n    r.sort((a, b) => a[0] - b[0]); return { s: s / words.length, r };\n  }\n  const mark = (text, ranges) => { if (!ranges || !ranges.length) return esc(text); let o = '', a = 0; for (const [s, e] of ranges) { if (s < a) continue; o += esc(text.slice(a, s)) + '<mark>' + esc(text.slice(s, e)) + '</mark>'; a = e; } return o + esc(text.slice(a)); };\n  const spring = (x, k, c) => ({ x, v: 0, g: x, k, c });\n  const step = (s, dt) => { const n = Math.max(1, Math.round(dt * 240)), h = dt / n; for (let i = 0; i < n; i++) { s.v += (-s.k * (s.x - s.g) - s.c * s.v) * h; s.x += s.v * h; } return Math.abs(s.v) > .01 || Math.abs(s.x - s.g) > .01; };\n  let uid = 0, hotkeyBound = false;\n\n  function mount(el) {\n    if (el.__vsSS) return; el.__vsSS = true;\n    const id = 'vs-ss-' + (++uid);\n    let data = { items: [], recent: [], popular: [], tryInstead: [] };\n    const dn = el.querySelector('.vs-search-suggest-data');\n    if (dn) { try { data = Object.assign(data, JSON.parse(dn.textContent)); } catch (e) {} dn.remove(); }\n    const byId = new Map(data.items.map((d) => [d.id, d]));\n    const noun = el.dataset.noun || 'results', key = el.dataset.storageKey;\n    const latency = () => { const v = parseFloat(el.dataset.latency); return v >= 0 ? v : 260; }; // read live, so it can change\n    let recent = data.recent.slice();\n    if (key) { try { const v = JSON.parse(localStorage.getItem(key) || 'null'); if (Array.isArray(v)) recent = v.slice(0, 6); } catch (e) {} }\n    const save = () => { if (key) { try { localStorage.setItem(key, JSON.stringify(recent)); } catch (e) {} } };\n\n    el.innerHTML = `<div class=\"vs-search-suggest-box\"><div class=\"vs-search-suggest-field\"><span class=\"vs-search-suggest-icon\"><span class=\"vs-search-suggest-glass\">${I.glass}</span><span class=\"vs-search-suggest-spin\">${I.spin}</span></span><input class=\"vs-search-suggest-input\" id=\"${id}-in\" type=\"search\" role=\"combobox\" aria-autocomplete=\"list\" aria-expanded=\"false\" aria-controls=\"${id}-lb\" autocomplete=\"off\" autocapitalize=\"off\" spellcheck=\"false\" enterkeyhint=\"search\"><span class=\"vs-search-suggest-keys\" aria-hidden=\"true\">${el.hasAttribute('data-hotkey') ? `<kbd>${/Mac|iP(hone|ad)/.test(navigator.platform) ? '⌘' : 'Ctrl'}</kbd><kbd>K</kbd>` : ''}</span><button type=\"button\" class=\"vs-search-suggest-clear\" aria-label=\"Clear search\" tabindex=\"-1\">${I.x}</button><span class=\"vs-search-suggest-prog\" aria-hidden=\"true\"></span></div><div class=\"vs-search-suggest-panel\"><div class=\"vs-search-suggest-list\" role=\"listbox\" id=\"${id}-lb\"></div><div class=\"vs-search-suggest-empty\"><strong></strong><span></span></div></div></div><span class=\"vs-search-suggest-sr\" aria-live=\"polite\"></span>`;\n    const box = el.firstChild, input = el.querySelector('input'), panel = el.querySelector('.vs-search-suggest-panel'), list = el.querySelector('.vs-search-suggest-list');\n    const emptyEl = el.querySelector('.vs-search-suggest-empty'), live = el.querySelector('.vs-search-suggest-sr'), clearB = el.querySelector('.vs-search-suggest-clear'), field = el.querySelector('.vs-search-suggest-field');\n    input.setAttribute('aria-label', el.dataset.label || 'Search'); list.setAttribute('aria-label', 'Suggestions');\n    input.placeholder = el.dataset.placeholder || el.dataset.label || 'Search';\n    const pill = document.createElement('span'); pill.className = 'vs-search-suggest-pill'; pill.setAttribute('aria-hidden', 'true');\n\n    let open = false, query = '', rows = [], active = -1, token = 0, debT = 0, liveT = 0, results = null, mode = 'home';\n    const S = { h: spring(0, 520, 40), y: spring(0, 560, 40), po: spring(0, 420, 38) };\n    let raf = 0, last = 0;\n    const fieldH = () => field.offsetHeight;\n    function frame(now) {\n      const dt = Math.min(.05, (now - last) / 1000 || .016); last = now; let busy = false;\n      for (const k in S) { const s = S[k]; if (reduce.matches) { s.x = s.g; s.v = 0; continue; } if (step(s, dt)) busy = true; else { s.x = s.g; s.v = 0; } }\n      box.style.height = Math.max(fieldH(), S.h.x).toFixed(2) + 'px';\n      const st = Math.min(.2, Math.abs(S.y.v) / 8000);\n      pill.style.transform = `translate3d(0,${S.y.x.toFixed(2)}px,0) scale(${(1 - st * .2).toFixed(4)},${(1 + st).toFixed(4)})`;\n      pill.style.opacity = Math.max(0, Math.min(1, S.po.x)).toFixed(3);\n      raf = busy ? requestAnimationFrame(frame) : 0;\n    }\n    const wake = () => { if (!raf) { last = performance.now(); raf = requestAnimationFrame(frame); } };\n    const measure = (snap) => { S.h.g = open ? fieldH() + panel.offsetHeight : fieldH(); if (snap) { S.h.x = S.h.g; S.h.v = 0; } wake(); };\n\n    // ── rows: descriptors → keyed DOM, surviving rows FLIP, new rows stagger in\n    const rowEls = new Map();\n    function descriptors() {\n      const out = [];\n      if (mode === 'home') {\n        if (recent.length) { out.push({ k: 'h:recent', head: 'Recent', clear: true }); recent.forEach((r) => out.push({ k: 'r:' + r, type: 'recent', text: r })); }\n        const pop = data.popular.map((i) => byId.get(i)).filter(Boolean);\n        if (pop.length) { out.push({ k: 'h:pop', head: 'Popular this week' }); pop.forEach((d) => out.push({ k: 'i:' + d.id, type: 'item', d })); }\n      } else if (mode === 'loading') {\n        out.push({ k: 'h:sug', head: 'Suggestions' }); for (let i = 0; i < 3; i++) out.push({ k: 'sk:' + i, skel: true });\n      } else if (results && results.length) {\n        out.push({ k: 'h:sug', head: 'Suggestions', count: `${results.length} ${results.length === 1 ? noun.replace(/s$/, '') : noun}` });\n        results.forEach((x) => out.push({ k: 'i:' + x.d.id, type: 'item', d: x.d, r: x.r, sub: x.sub }));\n      }\n      return out;\n    }\n    function rowHTML(o, oid) {\n      if (o.head) return `<span>${esc(o.head)}</span>${o.clear ? '<button type=\"button\" tabindex=\"-1\" data-clear>Clear</button>' : o.count ? `<span>${esc(o.count)}</span>` : ''}`;\n      if (o.skel) return '<i></i><span><b style=\"width:' + (52 + (o.k.slice(-1) * 17) % 30) + '%\"></b><b style=\"width:' + (26 + (o.k.slice(-1) * 11) % 16) + '%\"></b></span>';\n      if (o.type === 'recent') return `<span class=\"vs-search-suggest-ico\">${I.clock}</span><span class=\"vs-search-suggest-t\"><span class=\"vs-search-suggest-title\">${esc(o.text)}</span></span><span class=\"vs-search-suggest-end\"><button type=\"button\" class=\"vs-search-suggest-rm\" tabindex=\"-1\" aria-hidden=\"true\" data-rm title=\"Remove\">${I.x}</button></span>`;\n      const d = o.d;\n      return `<span class=\"vs-search-suggest-dot\" style=\"--c:${esc(d.color || '#ccc')}\"></span><span class=\"vs-search-suggest-t\"><span class=\"vs-search-suggest-title\">${mark(d.title, o.r)}</span><span class=\"vs-search-suggest-sub\">${[d.meta, o.sub].filter(Boolean).join(' · ')}</span></span><span class=\"vs-search-suggest-end\">${d.tag ? `<span class=\"vs-search-suggest-chiptag\">${esc(d.tag)}</span>` : ''}<span class=\"vs-search-suggest-go\" aria-hidden=\"true\"><kbd>↵</kbd></span></span>`;\n    }\n    function render(opts = {}) {\n      const desc = descriptors();\n      const before = new Map(); for (const [k, n] of rowEls) if (n.isConnected) before.set(k, n.offsetTop);\n      const prevKey = rows[active] && rows[active].k;\n      const keep = new Set(), frag = []; rows = []; let gi = 0, cur = null, n = 0;\n      list.textContent = ''; list.appendChild(pill);\n      for (const o of desc) {\n        let node = rowEls.get(o.k);\n        if (o.head) {\n          cur = document.createElement('div'); cur.className = 'vs-search-suggest-group'; cur.setAttribute('role', 'group');\n          if (!node) { node = document.createElement('div'); node.className = 'vs-search-suggest-gh'; node.setAttribute('role', 'presentation'); node.id = `${id}-h${++gi}${Math.random().toString(36).slice(2, 6)}`; }\n          node.innerHTML = rowHTML(o); cur.setAttribute('aria-labelledby', node.id); cur.appendChild(node); list.appendChild(cur);\n        } else if (o.skel) {\n          if (!node) { node = document.createElement('div'); node.className = 'vs-search-suggest-skel'; node.setAttribute('aria-hidden', 'true'); node.innerHTML = rowHTML(o); }\n          cur.appendChild(node);\n        } else {\n          if (!node) { node = document.createElement('div'); node.setAttribute('role', 'option'); node.id = `${id}-o${Math.random().toString(36).slice(2, 9)}`; }\n          node.className = 'vs-search-suggest-opt' + (o.type === 'recent' ? ' is-recent' : '');\n          node.innerHTML = rowHTML(o); node.dataset.i = n; node.setAttribute('aria-selected', 'false');\n          cur.appendChild(node); rows.push({ ...o, el: node }); n++;\n        }\n        rowEls.set(o.k, node); keep.add(o.k); frag.push([o.k, node]);\n      }\n      for (const k of [...rowEls.keys()]) if (!keep.has(k)) rowEls.delete(k);\n      el.classList.toggle('is-empty', mode === 'results' && !rows.length);\n      // motion: survivors glide from where they were, newcomers rise in a stagger\n      if (!reduce.matches && open) {\n        let j = 0;\n        for (const [k, node] of frag) {\n          if (before.has(k)) { const dy = before.get(k) - node.offsetTop; if (Math.abs(dy) > 1) node.animate([{ transform: `translateY(${dy}px)` }, { transform: 'none' }], { duration: 460, easing: 'cubic-bezier(.3,1.25,.5,1)' }); }\n          else if (!opts.quiet) node.animate([{ opacity: 0, transform: 'translateY(8px)' }, { opacity: 1, transform: 'none' }], { duration: 340, delay: Math.min(8, j++) * 34, easing: 'cubic-bezier(.2,.8,.2,1)', fill: 'backwards' });\n        }\n      }\n      let ai = prevKey ? rows.findIndex((r) => r.k === prevKey) : -1;\n      if (ai < 0 && mode === 'results' && rows.length) ai = 0;\n      setActive(ai, { snap: ai < 0 || !prevKey || opts.snap });\n      measure(!open);\n    }\n    function setActive(i, o = {}) {\n      rows.forEach((r, k) => { r.el.classList.toggle('is-active', k === i); r.el.setAttribute('aria-selected', String(k === i)); });\n      active = i;\n      if (i < 0 || !rows[i]) { input.removeAttribute('aria-activedescendant'); S.po.g = 0; wake(); return; }\n      const r = rows[i].el; if (open) input.setAttribute('aria-activedescendant', r.id);\n      pill.style.height = r.offsetHeight + 'px';\n      const k = parseFloat(getComputedStyle(el).getPropertyValue('--vs-search-suggest-stiffness')) || 560; S.y.k = k; S.y.c = 2 * Math.sqrt(k) * .84;\n      S.y.g = r.offsetTop;\n      if (o.snap || S.po.x < .05) { S.y.x = S.y.g; S.y.v = 0; }\n      S.po.g = 1;\n      if (o.scroll !== false) { const top = S.y.g, bot = top + r.offsetHeight, pad = 8; if (top < panel.scrollTop + pad) panel.scrollTop = Math.max(0, top - 30); else if (bot > panel.scrollTop + panel.clientHeight - pad) panel.scrollTop = bot - panel.clientHeight + pad + 6; }\n      wake();\n    }\n    function move(d, abs) { if (!rows.length) return; let i = abs != null ? abs : active < 0 ? (d > 0 ? 0 : rows.length - 1) : active + d; setActive(Math.max(0, Math.min(rows.length - 1, i))); }\n    function setOpen(v) {\n      if (v === open) return; open = v; el.classList.toggle('is-open', v); input.setAttribute('aria-expanded', v);\n      if (v) { render(); measure(false); } else { input.removeAttribute('aria-activedescendant'); S.po.g = 0; measure(false); }\n      wake();\n    }\n    // ── search: debounce, stale guard, shimmer\n    const local = (q) => new Promise((res) => setTimeout(() => {\n      const fq = fold(q.trim()), out = [];\n      for (const d of data.items) {\n        const a = hit(d.title, fq); if (a) { out.push({ d, s: a.s + 20, r: a.r }); continue; }\n        const b = hit(d.terms || '', fq); if (b) { const w = (d.terms || '').split(' ').find((t) => fold(t).includes(fq.split(/\\s+/)[0])) || ''; out.push({ d, s: b.s * .6, r: null, sub: w ? `has <mark>${esc(w)}</mark>` : '' }); }\n        else { const c = hit(d.tag || '', fq); if (c) out.push({ d, s: c.s * .5, r: null }); }\n      }\n      res(out.sort((x, y) => y.s - x.s).slice(0, 7));\n    }, latency() * (.85 + Math.random() * .35)));\n    function search() {\n      const q = query.trim(), t = ++token;\n      clearTimeout(debT);\n      el.dispatchEvent(new CustomEvent('vs-search-suggest-query', { bubbles: true, detail: { query: q } }));\n      if (!q) { el.classList.remove('is-loading', 'is-stale'); mode = 'home'; results = null; render(); return; }\n      el.classList.add('is-loading');\n      if (mode !== 'results') { mode = 'loading'; render({ snap: true }); } else el.classList.add('is-stale');\n      debT = setTimeout(async () => {\n        let res; try { res = el.vsSource ? await el.vsSource(q) : await local(q); } catch (e) { res = []; }\n        if (t !== token) return; // a newer keystroke won\n        if (el.vsSource) res = res.map((d) => ({ d, r: (hit(d.title, fold(q)) || {}).r, sub: d.sub ? esc(d.sub) : '' }));\n        results = res; mode = 'results'; el.classList.remove('is-loading', 'is-stale');\n        emptyEl.firstChild.textContent = `No ${noun} for “${q}”`;\n        const tries = (data.tryInstead || []).filter((x) => fold(x) !== fold(q)).slice(0, 2);\n        emptyEl.lastChild.innerHTML = tries.length ? 'Try ' + tries.map((x) => `<button type=\"button\" data-try=\"${esc(x)}\">${esc(x)}</button>`).join(' or ') : 'Check the spelling, or try fewer words';\n        render();\n        clearTimeout(liveT); liveT = setTimeout(() => { live.textContent = res.length ? `${res.length} suggestions` : `No ${noun} found`; }, 300);\n      }, 90);\n    }\n    function remember(text) { recent = [text, ...recent.filter((r) => fold(r) !== fold(text))].slice(0, 5); save(); }\n    function commit(i) {\n      const r = rows[i]; if (!r) return;\n      if (r.type === 'recent') { query = r.text; input.value = r.text; el.classList.add('has-q'); search(); return; }\n      remember(r.d.title);\n      input.value = r.d.title; query = ''; el.classList.add('has-q');\n      el.dispatchEvent(new CustomEvent('vs-search-suggest-commit', { bubbles: true, detail: { item: r.d, query: query } }));\n      live.textContent = `Opened ${r.d.title}`;\n      mode = 'home'; results = null; setOpen(false); input.select();\n    }\n    function removeRecent(text) {\n      recent = recent.filter((r) => r !== text); save(); live.textContent = `Removed ${text} from recent searches`;\n      // the active bar stays on the same slot, so repeated Shift+Delete walks down the list\n      const cur = rows[active], at = active; render({ quiet: true });\n      if (cur && cur.type === 'recent') { const n = rows.filter((r) => r.type === 'recent').length; setActive(n ? Math.min(at, n - 1) : -1); }\n    }\n    function clearQuery() { input.value = ''; query = ''; el.classList.remove('has-q'); token++; search(); }\n\n    input.addEventListener('input', () => { query = input.value; el.classList.toggle('has-q', !!query); if (!open) setOpen(true); search(); });\n    input.addEventListener('keydown', (e) => {\n      if (e.isComposing) return; const k = e.key;\n      if (k === 'ArrowDown' || k === 'ArrowUp') { e.preventDefault(); if (!open) { setOpen(true); return; } move(k === 'ArrowDown' ? 1 : -1); }\n      else if ((k === 'Home' || k === 'End') && open && active >= 0) { e.preventDefault(); move(0, k === 'Home' ? 0 : rows.length - 1); }\n      else if (k === 'Enter') { if (open && active >= 0) { e.preventDefault(); commit(active); } else if (query.trim()) { remember(query.trim()); setOpen(false); } }\n      else if (k === 'Escape') { e.preventDefault(); if (input.value) { clearQuery(); if (!open) setOpen(false); } else if (open) setOpen(false); else input.blur(); }\n      else if (k === 'Delete' && e.shiftKey && rows[active] && rows[active].type === 'recent') { e.preventDefault(); removeRecent(rows[active].text); }\n      else if (k === 'Tab' && open) setOpen(false);\n    });\n    input.addEventListener('focus', () => { el.classList.add('is-focus'); });\n    input.addEventListener('blur', () => { el.classList.remove('is-focus'); });\n    input.addEventListener('click', () => { if (!open) setOpen(true); });\n    el.addEventListener('focusout', (e) => { if (!el.contains(e.relatedTarget)) setOpen(false); });\n    field.addEventListener('mousedown', (e) => { if (e.target !== input) { e.preventDefault(); input.focus(); if (!e.target.closest('button') && !open) setOpen(true); } });\n    clearB.addEventListener('mousedown', (e) => e.preventDefault());\n    clearB.addEventListener('click', () => { clearQuery(); input.focus(); if (!open) setOpen(true); });\n    panel.addEventListener('mousedown', (e) => { if (!e.target.closest('[data-try]')) e.preventDefault(); });\n    let lx = -1, ly = -1;\n    list.addEventListener('pointermove', (e) => {\n      if (e.pointerType !== 'mouse' || (e.clientX === lx && e.clientY === ly)) return; lx = e.clientX; ly = e.clientY;\n      const o = e.target.closest('.vs-search-suggest-opt'); if (o && +o.dataset.i !== active) setActive(+o.dataset.i, { scroll: false });\n    });\n    list.addEventListener('click', (e) => {\n      if (e.target.closest('[data-clear]')) { recent = []; save(); live.textContent = 'Recent searches cleared'; render(); input.focus(); return; }\n      const o = e.target.closest('.vs-search-suggest-opt'); if (!o) return; const i = +o.dataset.i;\n      if (e.target.closest('[data-rm]')) { removeRecent(rows[i].text); input.focus(); return; }\n      setActive(i, { scroll: false }); commit(i);\n    });\n    emptyEl.addEventListener('click', (e) => { const b = e.target.closest('[data-try]'); if (!b) return; input.value = query = b.dataset.try; el.classList.add('has-q'); input.focus(); search(); });\n    new ResizeObserver(() => measure(true)).observe(field);\n\n    if (el.hasAttribute('data-hotkey') && !hotkeyBound) {\n      hotkeyBound = true;\n      document.addEventListener('keydown', (e) => {\n        const t = e.target, typing = t && (t.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName));\n        if ((e.key === 'k' && (e.metaKey || e.ctrlKey)) || (e.key === '/' && !typing)) { e.preventDefault(); input.focus(); input.select(); setOpen(true); }\n      });\n    }\n    el.vsOpen = () => { input.focus(); setOpen(true); };\n    el.vsClose = () => setOpen(false);\n    el.vsSetQuery = (q) => { input.value = query = q || ''; el.classList.toggle('has-q', !!query); if (!open) setOpen(true); search(); };\n    el.vsRecent = () => recent.slice();\n    render(); measure(true);\n  }\n  const init = () => document.querySelectorAll('.vs-search-suggest').forEach(mount);\n  document.readyState === 'loading' ? document.addEventListener('DOMContentLoaded', init) : init();\n  window.vsSearchSuggest = { mount };\n})();\n</script>\n"
    }
  ],
  "meta": {
    "category": "ui",
    "group": "Forms",
    "tags": [
      "search",
      "autocomplete",
      "suggestions",
      "typeahead",
      "combobox",
      "recent searches",
      "keyboard",
      "form",
      "input"
    ],
    "tech": [
      "CSS",
      "JS"
    ],
    "deps": [
      "https://fonts.googleapis.com/css2?family=Onest:wght@400;500&display=swap"
    ],
    "source": null,
    "free": true,
    "accent": "#3f7d3a",
    "added": "2026-10-01",
    "demo": "https://vividsites.app/api/component/search-suggest/index.html",
    "thumbnail": "https://vividsites.app/api/component/search-suggest/thumb.jpg",
    "video": "https://vividsites.app/api/component/search-suggest/thumb.mp4",
    "page": "https://vividsites.app/c/search-suggest"
  }
}