{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "search-cell",
  "type": "registry:ui",
  "title": "Search Cell",
  "description": "An animated search input illustration that cycles through queries with a typewriter effect and blur-in result rows.",
  "author": "Ahdeetai <https://aditya.is-cool.dev>",
  "registryDependencies": [],
  "dependencies": ["motion", "lucide-react", "clsx", "tailwind-merge"],
  "files": [
    {
      "type": "registry:ui",
      "path": "components/ui/search-cell.tsx",
      "content": "'use client';\n\nimport * as React from 'react';\nimport { motion, AnimatePresence } from 'motion/react';\nimport { Search } from 'lucide-react';\nimport { cn } from '@/lib/utils';\n\nexport interface SearchCellResult {\n  path: string;\n  title: string;\n  highlight: string;\n}\n\nexport interface SearchCellQuery {\n  query: string;\n  results: SearchCellResult[];\n}\n\nconst DEFAULT_QUERIES: SearchCellQuery[] = [\n  {\n    query: 'Espresso machine',\n    results: [\n      { path: 'Manuals / Bar stand', title: 'Espresso machine manual', highlight: 'Espresso' },\n      { path: 'Equipment / Inventory', title: 'Bar equipment checklist', highlight: 'machine' },\n    ],\n  },\n  {\n    query: 'How do I reset my password',\n    results: [\n      { path: 'Help / Account', title: 'How to reset your password', highlight: 'reset' },\n      { path: 'Help / Security', title: 'Password and security settings', highlight: 'password' },\n    ],\n  },\n  {\n    query: 'Q3 sales report',\n    results: [\n      { path: 'Reports / Finance', title: 'Q3 2024 sales overview', highlight: 'Q3' },\n      { path: 'Reports / Archive', title: 'Quarterly sales history', highlight: 'sales' },\n    ],\n  },\n];\n\nconst SearchCellHighlight = ({ title, highlight }: { title: string; highlight: string }) => {\n  const idx = title.toLowerCase().indexOf(highlight.toLowerCase());\n  if (idx === -1) return <span>{title}</span>;\n  return (\n    <>\n      {title.slice(0, idx)}\n      <mark className=\"rounded bg-emerald-100 px-0.5 text-emerald-700 dark:bg-emerald-900/50 dark:text-emerald-400\">\n        {title.slice(idx, idx + highlight.length)}\n      </mark>\n      {title.slice(idx + highlight.length)}\n    </>\n  );\n};\nSearchCellHighlight.displayName = 'SearchCellHighlight';\n\nexport interface SearchCellProps extends React.HTMLAttributes<HTMLDivElement> {\n  queries?: SearchCellQuery[];\n  typeSpeed?: number;\n  eraseSpeed?: number;\n  pauseDuration?: number;\n  label?: string;\n  showBadges?: boolean;\n}\n\nconst SearchCell = React.forwardRef<HTMLDivElement, SearchCellProps>(\n  (\n    {\n      className,\n      queries = DEFAULT_QUERIES,\n      typeSpeed = 75,\n      eraseSpeed = 40,\n      pauseDuration = 2400,\n      label = 'Live search',\n      showBadges = true,\n      ...props\n    },\n    ref,\n  ) => {\n    const [queryIndex, setQueryIndex] = React.useState(0);\n    const [text, setText] = React.useState('');\n    const [showResults, setShowResults] = React.useState(false);\n    const timers = React.useRef<ReturnType<typeof setTimeout>[]>([]);\n    const queriesRef = React.useRef(queries);\n    const typeSpeedRef = React.useRef(typeSpeed);\n    const eraseSpeedRef = React.useRef(eraseSpeed);\n    const pauseDurationRef = React.useRef(pauseDuration);\n    React.useEffect(() => { queriesRef.current = queries; }, [queries]);\n    React.useEffect(() => { typeSpeedRef.current = typeSpeed; }, [typeSpeed]);\n    React.useEffect(() => { eraseSpeedRef.current = eraseSpeed; }, [eraseSpeed]);\n    React.useEffect(() => { pauseDurationRef.current = pauseDuration; }, [pauseDuration]);\n\n    const clear = () => { timers.current.forEach(clearTimeout); timers.current = []; };\n    const after = (fn: () => void, ms: number) => { const id = setTimeout(fn, ms); timers.current.push(id); };\n\n    React.useEffect(() => {\n      clear();\n      setShowResults(false);\n      setText('');\n\n      const full = queriesRef.current[queryIndex].query;\n      let i = 0;\n      let ticker: ReturnType<typeof setInterval>;\n\n      const erase = () => {\n        setShowResults(false);\n        let j = full.length;\n        ticker = setInterval(() => {\n          j--;\n          setText(full.slice(0, j));\n          if (j === 0) {\n            clearInterval(ticker);\n            after(() => setQueryIndex((p) => (p + 1) % queriesRef.current.length), 300);\n          }\n        }, eraseSpeedRef.current);\n      };\n\n      ticker = setInterval(() => {\n        i++;\n        setText(full.slice(0, i));\n        if (i === full.length) {\n          clearInterval(ticker);\n          after(() => setShowResults(true), 400);\n          after(() => erase(), pauseDurationRef.current + 400);\n        }\n      }, typeSpeedRef.current);\n\n      return () => { clear(); clearInterval(ticker); };\n    }, [queryIndex]);\n\n    const currentResults = queries[queryIndex].results;\n\n    return (\n      <div ref={ref} className={cn('flex flex-col gap-3 p-4', className)} {...props}>\n        <div className=\"flex items-center gap-2 rounded-lg border border-zinc-200 bg-zinc-50 px-3 py-2 dark:border-zinc-800 dark:bg-zinc-900\">\n          <Search className=\"h-3.5 w-3.5 shrink-0 text-zinc-400\" />\n          <span className=\"min-w-0 flex-1 truncate text-sm text-zinc-800 dark:text-zinc-200\">\n            {text}\n            <motion.span\n              animate={{ opacity: [1, 0, 1] }}\n              transition={{ duration: 0.9, repeat: Infinity }}\n              className=\"ml-0.5 inline-block h-3.5 w-px bg-zinc-800 align-middle dark:bg-zinc-200\"\n            />\n          </span>\n        </div>\n\n        <AnimatePresence mode=\"wait\">\n          {showResults && (\n            <motion.div\n              key={queryIndex}\n              initial={{ opacity: 0, y: 6 }}\n              animate={{ opacity: 1, y: 0 }}\n              exit={{ opacity: 0, y: 6 }}\n              transition={{ duration: 0.25 }}\n              className=\"rounded-lg border border-zinc-200 bg-white p-3 dark:border-zinc-800 dark:bg-zinc-900\"\n            >\n              <p className=\"mb-2 text-[10px] uppercase tracking-widest text-zinc-400\">\n                Found {currentResults.length} {currentResults.length === 1 ? 'result' : 'results'}\n              </p>\n              <div className=\"flex flex-col gap-2\">\n                {currentResults.map((r, i) => (\n                  <motion.div\n                    key={i}\n                    initial={{ opacity: 0, filter: 'blur(4px)', y: 10 }}\n                    animate={{ opacity: 1, filter: 'blur(0px)', y: 0 }}\n                    transition={{ duration: 0.3, delay: i * 0.1, ease: 'easeInOut' }}\n                    className={cn('flex flex-col gap-0.5', i > 0 && 'border-t border-zinc-100 pt-2 dark:border-zinc-800')}\n                  >\n                    <p className=\"text-[10px] text-zinc-400\">{r.path}</p>\n                    <p className=\"text-sm text-zinc-800 dark:text-zinc-200\">\n                      <SearchCellHighlight title={r.title} highlight={r.highlight} />\n                    </p>\n                  </motion.div>\n                ))}\n              </div>\n            </motion.div>\n          )}\n        </AnimatePresence>\n\n        {showBadges && (\n          <AnimatePresence>\n            {showResults && (\n              <motion.div\n                initial={{ opacity: 0 }}\n                animate={{ opacity: 1 }}\n                exit={{ opacity: 0 }}\n                className=\"flex gap-2\"\n              >\n                <span className=\"rounded-full border border-zinc-200 px-2 py-0.5 text-[10px] text-zinc-500 dark:border-zinc-700\">\n                  {currentResults.length} {currentResults.length === 1 ? 'result' : 'results'}\n                </span>\n                <span className=\"rounded-full border border-emerald-200 bg-emerald-50 px-2 py-0.5 text-[10px] text-emerald-600 dark:border-emerald-800 dark:bg-emerald-900/30 dark:text-emerald-400\">\n                  {label}\n                </span>\n              </motion.div>\n            )}\n          </AnimatePresence>\n        )}\n      </div>\n    );\n  },\n);\nSearchCell.displayName = 'SearchCell';\n\nexport { SearchCell, SearchCellHighlight, DEFAULT_QUERIES };\nexport default SearchCell;"
    },
    {
      "type": "registry:lib",
      "path": "lib/utils.ts",
      "content": "import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}"
    }
  ]
}
