{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "morphing-search",
  "type": "registry:ui",
  "title": "Morphing Search",
  "description": "A frosted pill search bar that turns terms into filter chips. Focus it and a panel of recent searches and suggestions blooms out of the bar like a drop of goo; press Enter and the word you typed lifts out of the field, flashes lime and lands in the bar as a teal chip. Remove a chip and the word flies back into the recent list.",
  "dependencies": [
    "gsap"
  ],
  "registryDependencies": [],
  "cssVars": {
    "theme": {}
  },
  "files": [
    {
      "path": "components/tweenui/morphing-search.tsx",
      "type": "registry:component",
      "target": "components/tweenui/morphing-search.tsx",
      "content": "'use client';\n\nimport {\n  useEffect,\n  useId,\n  useLayoutEffect,\n  useMemo,\n  useRef,\n  useState,\n  type FocusEvent,\n  type KeyboardEvent,\n  type PointerEvent as ReactPointerEvent,\n} from 'react';\nimport gsap from 'gsap';\nimport { CustomEase } from 'gsap/CustomEase';\nimport { cn } from '@/lib/utils';\n\ngsap.registerPlugin(CustomEase);\n\nconst TEAL = '#045f64';\nconst LIME = '#c6f56f';\nconst INK = '#071d1b';\nconst CHIP_HEIGHT = 36;\nconst CHIP_INSET = 12;\nconst FLIGHT = 0.62;\nconst LIFT = 12;\nconst BAR_HEIGHT = 56;\nconst PANEL_GAP = 12;\nconst SEED = { width: 120, height: 40, bottom: 8, borderRadius: 20 };\nconst SEED_STYLE = { ...SEED };\n\ntype Kind = 'typed' | 'recent' | 'suggestion';\n\ninterface Option {\n  key: string;\n  term: string;\n  kind: Kind;\n}\n\ninterface Flight {\n  term: string;\n  to: 'bar' | 'list';\n  left: number;\n  top: number;\n  width: number;\n  height: number;\n  fontSize: string;\n}\n\nexport interface MorphingSearchProps {\n  suggestions?: readonly string[];\n  chips?: string[];\n  defaultChips?: string[];\n  onChipsChange?: (chips: string[]) => void;\n  defaultRecents?: string[];\n  onRecentsChange?: (recents: string[]) => void;\n  maxRecents?: number;\n  maxSuggestions?: number;\n  maxChips?: number;\n  placeholder?: string;\n  label?: string;\n  name?: string;\n  className?: string;\n}\n\nconst prefersReducedMotion = () =>\n  typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n\nconst sameTerm = (a: string, b: string) => a.toLowerCase() === b.toLowerCase();\n\nconst createEases = () => {\n  CustomEase.create('search-glide', '0.32, 0.72, 0, 1');\n};\n\nconst SearchIcon = ({ className }: { className?: string }) => (\n  <svg viewBox=\"0 0 20 20\" fill=\"none\" aria-hidden=\"true\" className={className}>\n    <circle cx=\"9\" cy=\"9\" r=\"5.75\" stroke=\"currentColor\" strokeWidth={1.6} />\n    <path d=\"m13.25 13.25 3.5 3.5\" stroke=\"currentColor\" strokeWidth={1.6} strokeLinecap=\"round\" />\n  </svg>\n);\n\nconst ClockIcon = () => (\n  <svg viewBox=\"0 0 20 20\" fill=\"none\" aria-hidden=\"true\" className=\"size-4 shrink-0\">\n    <circle cx=\"10\" cy=\"10\" r=\"6.75\" stroke=\"currentColor\" strokeWidth={1.5} />\n    <path\n      d=\"M10 6.5V10l2.25 1.5\"\n      stroke=\"currentColor\"\n      strokeWidth={1.5}\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n    />\n  </svg>\n);\n\nconst PlusIcon = () => (\n  <svg viewBox=\"0 0 20 20\" fill=\"none\" aria-hidden=\"true\" className=\"size-4 shrink-0\">\n    <path d=\"M10 4.5v11M4.5 10h11\" stroke=\"currentColor\" strokeWidth={1.6} strokeLinecap=\"round\" />\n  </svg>\n);\n\nconst CloseIcon = () => (\n  <svg viewBox=\"0 0 20 20\" fill=\"none\" aria-hidden=\"true\" className=\"size-3.5\">\n    <path d=\"m6 6 8 8M14 6l-8 8\" stroke=\"currentColor\" strokeWidth={1.8} strokeLinecap=\"round\" />\n  </svg>\n);\n\nconst Match = ({ term, query }: { term: string; query: string }) => {\n  const at = query ? term.toLowerCase().indexOf(query.toLowerCase()) : -1;\n  if (at < 0) return term;\n  return (\n    <>\n      {term.slice(0, at)}\n      <strong className=\"font-semibold text-[#045f64]\">{term.slice(at, at + query.length)}</strong>\n      {term.slice(at + query.length)}\n    </>\n  );\n};\n\nconst MorphingSearch = ({\n  suggestions = [],\n  chips: chipsProp,\n  defaultChips = [],\n  onChipsChange,\n  defaultRecents = [],\n  onRecentsChange,\n  maxRecents = 5,\n  maxSuggestions = 5,\n  maxChips,\n  placeholder = 'Search',\n  label = 'Search',\n  name,\n  className,\n}: MorphingSearchProps) => {\n  const rootRef = useRef<HTMLDivElement>(null);\n  const chipRowRef = useRef<HTMLDivElement>(null);\n  const inputRef = useRef<HTMLInputElement>(null);\n  const panelRef = useRef<HTMLDivElement>(null);\n  const blobRef = useRef<HTMLSpanElement>(null);\n  const markerRef = useRef<HTMLSpanElement>(null);\n  const ghostRef = useRef<HTMLSpanElement>(null);\n  const revealed = useRef<boolean | null>(null);\n  const markerShown = useRef(false);\n  const latestChips = useRef<string[]>(defaultChips);\n  const id = useId();\n  const gooId = `search-goo-${id.replace(/[^a-zA-Z0-9_-]/g, '')}`;\n  const [ownChips, setOwnChips] = useState(defaultChips);\n  const [recents, setRecents] = useState(defaultRecents);\n  const [query, setQuery] = useState('');\n  const [open, setOpen] = useState(false);\n  const [active, setActive] = useState(0);\n  const [marked, setMarked] = useState<string | null>(null);\n  const [flight, setFlight] = useState<Flight | null>(null);\n  const [landing, setLanding] = useState<string | null>(null);\n  const [leaving, setLeaving] = useState<string | null>(null);\n  const [note, setNote] = useState({ text: '', count: 0 });\n\n  const chips = chipsProp ?? ownChips;\n  const typed = query.trim();\n  const listId = `${id}-list`;\n  const hintId = `${id}-hint`;\n  const optionId = (index: number) => `${id}-option-${index}`;\n\n  useLayoutEffect(() => {\n    latestChips.current = chips;\n  });\n\n  const options = useMemo<Option[]>(() => {\n    const held = chips.filter((chip) => chip !== leaving);\n    if (!typed) {\n      return recents\n        .filter((term) => !held.some((chip) => sameTerm(chip, term)))\n        .map((term) => ({ key: `recent-${term}`, term, kind: 'recent' }));\n    }\n    const needle = typed.toLowerCase();\n    const taken = new Set([needle, ...held.map((chip) => chip.toLowerCase())]);\n    const found: Option[] = [];\n    const consider = (term: string, kind: Kind) => {\n      const key = term.toLowerCase();\n      if (taken.has(key) || !key.includes(needle)) return;\n      taken.add(key);\n      found.push({ key: `${kind}-${term}`, term, kind });\n    };\n    recents.forEach((term) => consider(term, 'recent'));\n    suggestions.forEach((term) => consider(term, 'suggestion'));\n    return [\n      { key: `typed-${typed}`, term: typed, kind: 'typed' },\n      ...found.slice(0, maxSuggestions),\n    ];\n  }, [typed, recents, suggestions, chips, leaving, maxSuggestions]);\n\n  const current = Math.min(active, Math.max(options.length - 1, 0));\n  const showPanel = open && options.length > 0;\n  const activeId = showPanel ? optionId(current) : undefined;\n\n  const announce = (text: string) => setNote((last) => ({ text, count: last.count + 1 }));\n\n  const commitChips = (next: string[]) => {\n    if (chipsProp === undefined) setOwnChips(next);\n    onChipsChange?.(next);\n  };\n\n  const commitRecents = (next: string[]) => {\n    setRecents(next);\n    onRecentsChange?.(next);\n  };\n\n  const chipElement = (term: string) =>\n    Array.from(rootRef.current?.querySelectorAll<HTMLElement>('[data-chip]') ?? []).find(\n      (element) => element.dataset.chip === term\n    );\n\n  const rowLabel = (term: string) =>\n    Array.from(panelRef.current?.querySelectorAll<HTMLElement>('[data-value]') ?? [])\n      .find((element) => element.dataset.value === term)\n      ?.querySelector<HTMLElement>('[data-term]');\n\n  const add = (term: string, source: HTMLElement | null) => {\n    const value = term.trim();\n    if (!value) return;\n    const exists = chips.some((chip) => sameTerm(chip, value));\n    if (!exists && maxChips !== undefined && chips.length >= maxChips) {\n      announce(`You can add up to ${maxChips} filters`);\n      return;\n    }\n    if (!exists) {\n      if (source && !prefersReducedMotion()) {\n        const box = source.getBoundingClientRect();\n        setFlight({\n          term: value,\n          to: 'bar',\n          left: box.left - CHIP_INSET,\n          top: box.top + (box.height - CHIP_HEIGHT) / 2,\n          width: 0,\n          height: CHIP_HEIGHT,\n          fontSize: getComputedStyle(source).fontSize,\n        });\n        setLanding(value);\n      }\n      commitChips([...chips, value]);\n      announce(`Added filter ${value}`);\n    }\n    commitRecents(\n      [value, ...recents.filter((recent) => !sameTerm(recent, value))].slice(0, maxRecents)\n    );\n    setQuery('');\n    setActive(0);\n  };\n\n  const remove = (term: string) => {\n    const finish = () => {\n      commitChips(latestChips.current.filter((chip) => chip !== term));\n      setLeaving((current) => (current === term ? null : current));\n      announce(`Removed filter ${term}`);\n    };\n    commitRecents(\n      [term, ...recents.filter((recent) => !sameTerm(recent, term))].slice(0, maxRecents)\n    );\n    const element = chipElement(term);\n    if (!element || prefersReducedMotion()) finish();\n    else {\n      const box = element.getBoundingClientRect();\n      setLeaving(term);\n      setLanding(term);\n      setFlight({\n        term,\n        to: 'list',\n        left: box.left,\n        top: box.top,\n        width: box.width,\n        height: box.height,\n        fontSize: getComputedStyle(element).fontSize,\n      });\n      gsap.to(element, {\n        width: 0,\n        paddingLeft: 0,\n        paddingRight: 0,\n        marginRight: -6,\n        opacity: 0,\n        duration: 0.3,\n        ease: 'power2.inOut',\n        onComplete: finish,\n      });\n    }\n    setMarked(null);\n    inputRef.current?.focus();\n  };\n\n  const forgetRecent = (term: string) => {\n    commitRecents(recents.filter((recent) => recent !== term));\n    announce(`Removed ${term} from recent searches`);\n  };\n\n  const moveTo = (index: number) => {\n    setActive(index);\n    document.getElementById(optionId(index))?.scrollIntoView?.({ block: 'nearest' });\n  };\n\n  const sourceFor = (index: number) => {\n    const option = options[index];\n    if (!option || option.kind === 'typed') return inputRef.current;\n    return (\n      document.getElementById(optionId(index))?.querySelector<HTMLElement>('[data-term]') ?? null\n    );\n  };\n\n  const stepMark = (direction: -1 | 1) => {\n    const at = marked === null ? chips.length : chips.indexOf(marked);\n    const next = at + direction;\n    setMarked(next >= chips.length ? null : (chips[Math.max(next, 0)] ?? null));\n  };\n\n  const onKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {\n    if (event.nativeEvent.isComposing) return;\n    const field = event.currentTarget;\n    const atStart = field.selectionStart === 0 && field.selectionEnd === 0;\n\n    if (marked !== null) {\n      if (event.key === 'Backspace' || event.key === 'Delete') {\n        event.preventDefault();\n        remove(marked);\n        return;\n      }\n      if (event.key === 'ArrowLeft' || event.key === 'ArrowRight') {\n        event.preventDefault();\n        stepMark(event.key === 'ArrowLeft' ? -1 : 1);\n        return;\n      }\n      if (event.key === 'Escape') {\n        event.preventDefault();\n        setMarked(null);\n        return;\n      }\n      setMarked(null);\n    }\n\n    if (event.key === 'Escape') {\n      event.preventDefault();\n      if (query) setQuery('');\n      else setOpen(false);\n    } else if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {\n      event.preventDefault();\n      if (!open) setOpen(true);\n      else if (options.length) {\n        const step = event.key === 'ArrowDown' ? 1 : options.length - 1;\n        moveTo((current + step) % options.length);\n      }\n    } else if (event.key === 'Enter') {\n      const option = options[current];\n      if (!showPanel || !option) return;\n      event.preventDefault();\n      add(option.term, sourceFor(current));\n    } else if (event.key === 'Delete' && showPanel && options[current]?.kind === 'recent') {\n      event.preventDefault();\n      const option = options[current];\n      if (option) forgetRecent(option.term);\n    } else if (\n      (event.key === 'Backspace' || event.key === 'ArrowLeft') &&\n      atStart &&\n      chips.length\n    ) {\n      event.preventDefault();\n      const last = chips[chips.length - 1] ?? null;\n      setMarked(last);\n      if (last) announce(`${last} selected. Press Backspace again to remove it.`);\n    }\n  };\n\n  const onBarPointerDown = (event: ReactPointerEvent<HTMLDivElement>) => {\n    if (event.target instanceof Element && event.target.closest('button, input')) return;\n    event.preventDefault();\n    inputRef.current?.focus();\n    setOpen(true);\n  };\n\n  const onBlur = (event: FocusEvent<HTMLDivElement>) => {\n    const next = event.relatedTarget;\n    if (!(next instanceof Node) || rootRef.current?.contains(next)) return;\n    setOpen(false);\n    setMarked(null);\n  };\n\n  useEffect(() => {\n    if (!open) return;\n    const onPointerDown = (event: PointerEvent) => {\n      if (event.target instanceof Node && rootRef.current?.contains(event.target)) return;\n      setOpen(false);\n      setMarked(null);\n    };\n    document.addEventListener('pointerdown', onPointerDown, true);\n    return () => document.removeEventListener('pointerdown', onPointerDown, true);\n  }, [open]);\n\n  useLayoutEffect(() => {\n    if (!flight) return;\n    const root = rootRef.current;\n    const ghost = ghostRef.current;\n    const toBar = flight.to === 'bar';\n    const target = toBar ? chipElement(flight.term) : rowLabel(flight.term);\n    if (!root || !ghost || !target) {\n      setFlight(null);\n      setLanding(null);\n      return;\n    }\n    createEases();\n    const row = chipRowRef.current;\n    if (toBar && row) row.scrollLeft = row.scrollWidth;\n\n    const rootBox = root.getBoundingClientRect();\n    const goal = target.getBoundingClientRect();\n    const fromX = flight.left - rootBox.left;\n    const fromY = flight.top - rootBox.top;\n    const toX = goal.left - rootBox.left - (toBar ? 0 : CHIP_INSET);\n    const toY = goal.top - rootBox.top + (toBar ? 0 : (goal.height - CHIP_HEIGHT) / 2);\n\n    gsap.set(ghost, { x: fromX, y: fromY, fontSize: flight.fontSize, opacity: 1 });\n    const natural = ghost.offsetWidth;\n    gsap.set(ghost, {\n      width: toBar ? natural : flight.width,\n      color: toBar ? INK : LIME,\n      backgroundColor: toBar ? 'rgba(198, 245, 111, 0)' : TEAL,\n    });\n\n    const timeline = gsap.timeline({\n      onComplete: () => {\n        setLanding(null);\n        setFlight(null);\n      },\n    });\n    timeline\n      .to(\n        ghost,\n        {\n          x: toX,\n          width: toBar ? goal.width : natural,\n          fontSize: 14,\n          duration: FLIGHT,\n          ease: 'search-glide',\n        },\n        0\n      )\n      .to(ghost, { y: Math.min(fromY, toY) - LIFT, duration: FLIGHT * 0.4, ease: 'power2.out' }, 0)\n      .to(ghost, { y: toY, duration: FLIGHT * 0.6, ease: 'power2.inOut' }, FLIGHT * 0.4)\n      .to(\n        ghost,\n        { backgroundColor: LIME, color: INK, duration: FLIGHT * 0.35, ease: 'power1.out' },\n        0\n      )\n      .to(\n        ghost,\n        toBar\n          ? { backgroundColor: TEAL, color: LIME, duration: FLIGHT * 0.5, ease: 'power1.inOut' }\n          : {\n              backgroundColor: 'rgba(198, 245, 111, 0)',\n              duration: FLIGHT * 0.5,\n              ease: 'power1.inOut',\n            },\n        FLIGHT * 0.45\n      );\n    return () => {\n      timeline.kill();\n    };\n  }, [flight]);\n\n  useLayoutEffect(() => {\n    const panel = panelRef.current;\n    const blob = blobRef.current;\n    if (!panel || !blob) return;\n    createEases();\n    const first = revealed.current === null;\n    const changed = revealed.current !== showPanel;\n    revealed.current = showPanel;\n\n    const settled = () => ({\n      width: panel.offsetWidth,\n      height: panel.offsetHeight,\n      bottom: BAR_HEIGHT + PANEL_GAP,\n      borderRadius: 28,\n    });\n\n    if (!changed && gsap.isTweening(blob)) return;\n    if (!changed || first || prefersReducedMotion()) {\n      gsap.killTweensOf([blob, panel]);\n      gsap.set(blob, showPanel ? settled() : { ...SEED });\n      gsap.set(panel, { opacity: showPanel ? 1 : 0, y: 0 });\n      return;\n    }\n\n    if (showPanel) {\n      gsap.to(blob, {\n        ...settled(),\n        duration: 0.64,\n        ease: 'search-glide',\n        overwrite: true,\n        onComplete: () => {\n          gsap.to(blob, { ...settled(), duration: 0.2, ease: 'power2.out' });\n        },\n      });\n      gsap.fromTo(\n        panel,\n        { opacity: 0, y: 10 },\n        { opacity: 1, y: 0, duration: 0.3, delay: 0.24, ease: 'power2.out', overwrite: true }\n      );\n    } else {\n      gsap.to(panel, { opacity: 0, duration: 0.12, ease: 'power1.out', overwrite: true });\n      gsap.to(blob, { ...SEED, duration: 0.44, ease: 'power3.inOut', overwrite: true });\n    }\n  }, [showPanel]);\n\n  useEffect(() => {\n    const panel = panelRef.current;\n    const blob = blobRef.current;\n    if (!showPanel || !panel || !blob) return;\n    const observer = new ResizeObserver(() => {\n      if (gsap.isTweening(blob)) return;\n      gsap.to(blob, {\n        width: panel.offsetWidth,\n        height: panel.offsetHeight,\n        bottom: BAR_HEIGHT + PANEL_GAP,\n        borderRadius: 28,\n        duration: prefersReducedMotion() ? 0 : 0.3,\n        ease: 'search-glide',\n        overwrite: true,\n      });\n    });\n    observer.observe(panel);\n    return () => observer.disconnect();\n  }, [showPanel]);\n\n  useLayoutEffect(() => {\n    const marker = markerRef.current;\n    if (!marker) return;\n    const row = activeId ? document.getElementById(activeId) : null;\n    if (!row) {\n      gsap.killTweensOf(marker);\n      gsap.set(marker, { opacity: 0 });\n      markerShown.current = false;\n      return;\n    }\n    gsap.set(marker, { height: row.offsetHeight, opacity: 1 });\n    if (markerShown.current && !prefersReducedMotion()) {\n      gsap.to(marker, { y: row.offsetTop, duration: 0.3, ease: 'power3.out', overwrite: true });\n    } else {\n      gsap.killTweensOf(marker);\n      gsap.set(marker, { y: row.offsetTop });\n    }\n    markerShown.current = true;\n  }, [activeId, options]);\n\n  return (\n    <div ref={rootRef} onBlur={onBlur} className={cn('relative w-full max-w-xl', className)}>\n      <svg aria-hidden=\"true\" className=\"pointer-events-none absolute size-0\">\n        <defs>\n          <filter\n            id={gooId}\n            x=\"-20%\"\n            y=\"-20%\"\n            width=\"140%\"\n            height=\"140%\"\n            colorInterpolationFilters=\"sRGB\"\n          >\n            <feGaussianBlur in=\"SourceGraphic\" stdDeviation=\"7\" result=\"blur\" />\n            <feColorMatrix\n              in=\"blur\"\n              mode=\"matrix\"\n              values=\"1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 22 -9\"\n              result=\"goo\"\n            />\n            <feComposite in=\"SourceGraphic\" in2=\"goo\" operator=\"atop\" />\n          </filter>\n        </defs>\n      </svg>\n      <div\n        aria-hidden=\"true\"\n        data-goo\n        className=\"pointer-events-none absolute inset-x-0 bottom-0 h-[460px]\"\n        style={{\n          filter: `url(#${gooId}) drop-shadow(0 6px 12px rgba(4, 95, 100, 0.08))`,\n        }}\n      >\n        <span className=\"absolute inset-x-0 bottom-0 h-14 rounded-full bg-white\" />\n        <span ref={blobRef} style={SEED_STYLE} className=\"absolute inset-x-0 mx-auto bg-white\" />\n      </div>\n      <div\n        ref={panelRef}\n        inert={!showPanel}\n        aria-hidden={!showPanel}\n        className=\"absolute inset-x-0 bottom-full z-10 mb-3 p-2 opacity-0\"\n      >\n        <div className=\"flex h-9 items-center justify-between px-3\">\n          <span className=\"text-[11px] font-medium tracking-[0.08em] text-[#045f64]/60 uppercase\">\n            {typed ? 'Suggestions' : 'Recent'}\n          </span>\n          {!typed && options.length > 0 && (\n            <button\n              type=\"button\"\n              onClick={() => {\n                commitRecents([]);\n                announce('Cleared recent searches');\n                inputRef.current?.focus();\n              }}\n              className=\"cursor-pointer rounded-md border-0 bg-transparent px-1.5 py-0.5 text-xs text-[#045f64]/60 transition-colors outline-none hover:text-[#045f64] focus-visible:ring-2 focus-visible:ring-[#045f64] motion-reduce:transition-none\"\n            >\n              Clear\n            </button>\n          )}\n        </div>\n        <div className=\"relative max-h-72 [scrollbar-width:thin] overflow-y-auto overscroll-contain\">\n          <span\n            ref={markerRef}\n            aria-hidden=\"true\"\n            // Panel radius (28) minus its padding (8), so the corners stay concentric.\n            className=\"pointer-events-none absolute inset-x-0 top-0 rounded-[20px] bg-[#045f64]/[0.08] opacity-0\"\n          />\n          <div id={listId} role=\"listbox\" aria-label={typed ? 'Suggestions' : 'Recent searches'}>\n            {options.map((option, index) => (\n              <div\n                key={option.key}\n                id={optionId(index)}\n                data-value={option.term}\n                role=\"option\"\n                aria-selected={index === current}\n                onPointerDown={(event) => event.preventDefault()}\n                onPointerMove={() => index !== current && setActive(index)}\n                onClick={() => add(option.term, sourceFor(index))}\n                className=\"relative flex h-11 cursor-pointer items-center gap-3 rounded-[20px] px-3 text-sm text-[#071d1b]\"\n              >\n                <span className=\"flex text-[#045f64]/60\">\n                  {option.kind === 'recent' ? (\n                    <ClockIcon />\n                  ) : option.kind === 'typed' ? (\n                    <PlusIcon />\n                  ) : (\n                    <SearchIcon className=\"size-4 shrink-0\" />\n                  )}\n                </span>\n                <span\n                  data-term\n                  className={cn(\n                    'min-w-0 flex-1 truncate',\n                    flight?.to === 'list' && landing === option.term && 'opacity-0'\n                  )}\n                >\n                  {option.kind === 'typed' ? (\n                    option.term\n                  ) : (\n                    <Match term={option.term} query={typed} />\n                  )}\n                </span>\n              </div>\n            ))}\n          </div>\n        </div>\n      </div>\n\n      <div\n        onPointerDown={onBarPointerDown}\n        className=\"relative z-20 flex h-14 cursor-text items-center gap-2 rounded-full border border-[#045f64]/15 bg-white/80 ps-4 pe-4 backdrop-blur-xl transition-[border-color,box-shadow] duration-200 ease-out focus-within:border-[#045f64]/35 focus-within:shadow-[0_0_0_4px_rgba(4,95,100,0.1)] motion-reduce:transition-none\"\n      >\n        <span className=\"flex shrink-0 text-[#045f64]\">\n          <SearchIcon className=\"size-5\" />\n        </span>\n        <div\n          ref={chipRowRef}\n          className=\"flex min-w-0 shrink [scrollbar-width:none] items-center gap-1.5 overflow-x-auto\"\n        >\n          {chips.map((chip) => (\n            <span\n              key={chip}\n              data-chip={chip}\n              className={cn(\n                'flex h-9 shrink-0 items-center gap-1 overflow-hidden rounded-full ps-3 pe-1 text-sm whitespace-nowrap text-[#c6f56f] transition-[background-color,box-shadow] duration-150 motion-reduce:transition-none',\n                marked === chip ? 'bg-[#0f3f42] ring-2 ring-[#c6f56f] ring-inset' : 'bg-[#045f64]',\n                flight?.to === 'bar' && landing === chip && 'opacity-0'\n              )}\n            >\n              {chip}\n              <button\n                type=\"button\"\n                aria-label={`Remove filter ${chip}`}\n                onClick={() => remove(chip)}\n                className=\"grid size-7 shrink-0 cursor-pointer place-items-center rounded-full border-0 bg-transparent text-[#c6f56f]/70 transition-colors outline-none hover:bg-[#c6f56f]/15 hover:text-[#c6f56f] focus-visible:ring-2 focus-visible:ring-[#c6f56f] motion-reduce:transition-none\"\n              >\n                <CloseIcon />\n              </button>\n            </span>\n          ))}\n        </div>\n        <input\n          ref={inputRef}\n          type=\"text\"\n          role=\"combobox\"\n          aria-label={label}\n          aria-expanded={showPanel}\n          aria-controls={showPanel ? listId : undefined}\n          aria-autocomplete=\"list\"\n          aria-activedescendant={activeId}\n          aria-describedby={hintId}\n          autoComplete=\"off\"\n          spellCheck={false}\n          enterKeyHint=\"search\"\n          value={query}\n          placeholder={placeholder}\n          onChange={(event) => {\n            setQuery(event.target.value);\n            setActive(0);\n            setOpen(true);\n          }}\n          onFocus={() => setOpen(true)}\n          onClick={() => setOpen(true)}\n          onKeyDown={onKeyDown}\n          className=\"h-9 min-w-24 flex-1 border-0 bg-transparent p-0 text-base text-[#071d1b] outline-none placeholder:text-[#045f64]/50\"\n        />\n      </div>\n\n      {flight && (\n        <span\n          ref={ghostRef}\n          aria-hidden=\"true\"\n          className=\"pointer-events-none absolute top-0 left-0 z-30 flex h-9 w-max items-center rounded-full ps-3 pe-3 whitespace-nowrap opacity-0\"\n        >\n          {flight.term}\n        </span>\n      )}\n\n      {name && chips.map((chip) => <input key={chip} type=\"hidden\" name={name} value={chip} />)}\n      <span id={hintId} className=\"sr-only\">\n        Press Enter to add a filter. Backspace at the start of the field selects the last filter;\n        press it again to remove it. Delete removes the highlighted recent search.\n      </span>\n      <span aria-live=\"polite\" className=\"sr-only\">\n        <span key={note.count}>{note.text}</span>\n      </span>\n    </div>\n  );\n};\n\nexport default MorphingSearch;\n"
    }
  ],
  "docs": "Docs, props and examples: https://tween-ui.vercel.app/component/morphing-search\nGSAP plugins (CustomEase) are registered inside the component, so there is nothing to set up."
}