{
  "name": "empty-state",
  "type": "registry:component",
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "files": [
    {
      "type": "registry:component",
      "content": "\"use client\";\n\n/**\n * @author: @kokonutui\n * @description: Empty State\n * @version: 1.0.0\n * @date: 2026-11-08\n * @license: MIT\n * @website: https://kokonutui.com\n * @github: https://github.com/kokonut-labs/kokonutui\n */\n\nimport { FolderGit2, Plus, Trash2 } from \"lucide-react\";\nimport {\n  AnimatePresence,\n  LayoutGroup,\n  MotionConfig,\n  motion,\n} from \"motion/react\";\nimport { useEffect, useId, useRef, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface EmptyProject {\n  id: number;\n  name: string;\n  from: \"new\" | \"github\";\n}\n\nexport interface EmptyStateProps {\n  title?: string;\n  description?: string;\n  onCreate?: (project: EmptyProject) => void;\n  className?: string;\n}\n\nconst EASE_OUT = [0.23, 1, 0.32, 1] as const;\nconst MORPH = { type: \"spring\", stiffness: 380, damping: 34 } as const;\nconst TILE = \"linear-gradient(135deg, #8b5cf6, #ec4899)\";\nconst IMPORT_MS = 1600;\n\nexport default function EmptyState({\n  title = \"No projects yet\",\n  description = \"A project keeps your pages, assets and deploys in one place.\",\n  onCreate,\n  className,\n}: EmptyStateProps) {\n  const uid = useId();\n  const [projects, setProjects] = useState<EmptyProject[]>([]);\n  const [editing, setEditing] = useState<number | null>(null);\n  const [importing, setImporting] = useState<number | null>(null);\n  // Hovering the main button lifts the card that's about to be made.\n  const [primed, setPrimed] = useState(false);\n  const nextId = useRef(1);\n  const timer = useRef<number>(undefined);\n\n  useEffect(() => () => window.clearTimeout(timer.current), []);\n\n  const add = (from: EmptyProject[\"from\"]) => {\n    const project: EmptyProject = {\n      id: nextId.current++,\n      name: from === \"github\" ? \"kokonutui\" : \"Untitled project\",\n      from,\n    };\n    setPrimed(false);\n    setProjects((all) => [project, ...all]);\n    if (from === \"new\") {\n      setEditing(project.id);\n      onCreate?.(project);\n    } else {\n      setImporting(project.id);\n      timer.current = window.setTimeout(() => {\n        setImporting(null);\n        onCreate?.(project);\n      }, IMPORT_MS);\n    }\n  };\n\n  const rename = (id: number, name: string) =>\n    setProjects((all) =>\n      all.map((p) => (p.id === id ? { ...p, name: name.trim() || p.name } : p))\n    );\n\n  const empty = projects.length === 0;\n  // The card in the drawing turns into the oldest row, and back again.\n  const morphId = `${uid}-card`;\n\n  return (\n    <MotionConfig reducedMotion=\"user\">\n      <LayoutGroup id={uid}>\n        <div\n          className={cn(\n            \"flex min-h-[380px] w-full max-w-md flex-col overflow-hidden rounded-[24px] border border-zinc-200 bg-white text-zinc-900 dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-100\",\n            className\n          )}\n        >\n          <div className=\"flex h-14 items-center justify-between border-zinc-200 border-b px-4 dark:border-zinc-800\">\n            <span className=\"flex items-center gap-1.5 font-medium text-[14px]\">\n              Projects\n              <span className=\"text-zinc-400 tabular-nums dark:text-zinc-500\">\n                <Roll value={projects.length} />\n              </span>\n            </span>\n            <AnimatePresence initial={false}>\n              {!empty && (\n                <motion.button\n                  animate={{ opacity: 1, filter: \"blur(0px)\" }}\n                  className=\"flex h-8 items-center gap-1.5 rounded-full bg-zinc-900 px-3 font-medium text-[12.5px] text-white outline-hidden transition-colors duration-150 hover:bg-zinc-800 focus-visible:ring-2 focus-visible:ring-zinc-400/50 active:bg-zinc-700 dark:bg-white dark:text-zinc-900 dark:active:bg-zinc-300 dark:hover:bg-zinc-200\"\n                  exit={{\n                    opacity: 0,\n                    filter: \"blur(2px)\",\n                    transition: { duration: 0.12 },\n                  }}\n                  initial={{ opacity: 0, filter: \"blur(2px)\" }}\n                  onClick={() => add(\"new\")}\n                  transition={{ duration: 0.2, delay: 0.15 }}\n                  type=\"button\"\n                >\n                  <Plus aria-hidden=\"true\" className=\"size-3.5\" />\n                  New\n                </motion.button>\n              )}\n            </AnimatePresence>\n          </div>\n\n          <div className=\"relative flex flex-1 flex-col\">\n            {empty ? (\n              <div className=\"flex flex-1 flex-col items-center justify-center px-8 py-8 text-center\">\n                <Drawing morphId={morphId} primed={primed} />\n                <motion.div\n                  animate=\"shown\"\n                  initial=\"hidden\"\n                  transition={{ staggerChildren: 0.05, delayChildren: 0.1 }}\n                >\n                  <motion.p\n                    className=\"mt-6 font-semibold text-[17px] tracking-tight\"\n                    variants={REVEAL}\n                  >\n                    {title}\n                  </motion.p>\n                  <motion.p\n                    className=\"mx-auto mt-1 max-w-[260px] text-[13.5px] text-zinc-500 leading-snug dark:text-zinc-400\"\n                    variants={REVEAL}\n                  >\n                    {description}\n                  </motion.p>\n                  <motion.div\n                    className=\"mt-5 flex items-center justify-center gap-2\"\n                    variants={REVEAL}\n                  >\n                    <button\n                      className=\"flex h-9 items-center gap-1.5 rounded-full bg-zinc-900 px-4 font-medium text-[13px] text-white outline-hidden transition-colors duration-150 hover:bg-zinc-800 focus-visible:ring-2 focus-visible:ring-zinc-400/50 active:bg-zinc-700 dark:bg-white dark:text-zinc-900 dark:active:bg-zinc-300 dark:hover:bg-zinc-200\"\n                      onBlur={() => setPrimed(false)}\n                      onClick={() => add(\"new\")}\n                      onFocus={() => setPrimed(true)}\n                      onPointerEnter={(event) => {\n                        if (event.pointerType === \"mouse\") {\n                          setPrimed(true);\n                        }\n                      }}\n                      onPointerLeave={() => setPrimed(false)}\n                      type=\"button\"\n                    >\n                      <Plus aria-hidden=\"true\" className=\"size-4\" />\n                      New project\n                    </button>\n                    <button\n                      className=\"flex h-9 items-center gap-1.5 rounded-full bg-zinc-100 px-4 font-medium text-[13px] outline-hidden transition-colors duration-150 hover:bg-zinc-200 focus-visible:ring-2 focus-visible:ring-zinc-400/50 active:bg-zinc-300 dark:bg-zinc-900 dark:active:bg-zinc-700 dark:hover:bg-zinc-800\"\n                      onClick={() => add(\"github\")}\n                      type=\"button\"\n                    >\n                      <FolderGit2 aria-hidden=\"true\" className=\"size-4\" />\n                      Import\n                    </button>\n                  </motion.div>\n                </motion.div>\n              </div>\n            ) : (\n              <ul className=\"flex flex-col gap-1 p-2\">\n                <AnimatePresence initial={false} mode=\"popLayout\">\n                  {projects.map((project, index) => (\n                    <Row\n                      editing={editing === project.id}\n                      importing={importing === project.id}\n                      key={project.id}\n                      layoutId={\n                        index === projects.length - 1 ? morphId : undefined\n                      }\n                      onDelete={() =>\n                        setProjects((all) =>\n                          all.filter((p) => p.id !== project.id)\n                        )\n                      }\n                      onRename={(name) => {\n                        rename(project.id, name);\n                        setEditing(null);\n                      }}\n                      project={project}\n                    />\n                  ))}\n                </AnimatePresence>\n              </ul>\n            )}\n          </div>\n        </div>\n      </LayoutGroup>\n    </MotionConfig>\n  );\n}\n\nconst REVEAL = {\n  hidden: { opacity: 0, y: 6, filter: \"blur(2px)\" },\n  shown: {\n    opacity: 1,\n    y: 0,\n    filter: \"blur(0px)\",\n    transition: { duration: 0.3, ease: EASE_OUT },\n  },\n};\n\n/** Two blank cards fanned behind the one you're about to make. */\nfunction Drawing({ morphId, primed }: { morphId: string; primed: boolean }) {\n  const spread = primed ? 1.35 : 1;\n  return (\n    <div className=\"relative h-[92px] w-[150px]\">\n      {[-1, 1].map((side) => (\n        <motion.div\n          animate={{\n            rotate: side * 7.5 * spread,\n            x: side * 22 * spread,\n            y: 6,\n          }}\n          className=\"absolute inset-x-4 top-2 h-[76px] rounded-2xl border border-zinc-200 bg-zinc-50 dark:border-zinc-800 dark:bg-zinc-900\"\n          initial={{ rotate: 0, x: 0, y: 0 }}\n          key={side}\n          transition={MORPH}\n        />\n      ))}\n      <motion.div\n        animate={{ y: primed ? -5 : 0 }}\n        className=\"absolute inset-x-4 top-0 flex h-[80px] flex-col justify-between border border-zinc-300 border-dashed bg-white p-3 shadow-[0_8px_24px_-12px_rgba(0,0,0,0.25)] dark:border-zinc-700 dark:bg-zinc-950\"\n        layoutId={morphId}\n        style={{ borderRadius: 16 }}\n        transition={MORPH}\n      >\n        <span\n          className=\"flex size-7 items-center justify-center rounded-lg text-white\"\n          style={{ background: TILE }}\n        >\n          <Plus aria-hidden=\"true\" className=\"size-4\" />\n        </span>\n        <span className=\"flex flex-col gap-1.5\">\n          <span className=\"h-1.5 w-14 rounded-full bg-zinc-200 dark:bg-zinc-800\" />\n          <span className=\"h-1.5 w-9 rounded-full bg-zinc-100 dark:bg-zinc-900\" />\n        </span>\n      </motion.div>\n    </div>\n  );\n}\n\nfunction Row({\n  project,\n  editing,\n  importing,\n  layoutId,\n  onRename,\n  onDelete,\n}: {\n  project: EmptyProject;\n  editing: boolean;\n  importing: boolean;\n  layoutId?: string;\n  onRename: (name: string) => void;\n  onDelete: () => void;\n}) {\n  const inputRef = useRef<HTMLInputElement>(null);\n\n  useEffect(() => {\n    if (editing) {\n      inputRef.current?.focus();\n      inputRef.current?.select();\n    }\n  }, [editing]);\n\n  let meta = \"Created just now\";\n  if (importing) {\n    meta = \"Copying 142 files from GitHub\";\n  } else if (project.from === \"github\") {\n    meta = \"Imported from GitHub · 142 files\";\n  }\n\n  return (\n    <motion.li\n      animate={{ opacity: 1, scale: 1 }}\n      className=\"group relative flex h-14 items-center gap-3 overflow-hidden px-2 hover:bg-zinc-50 active:bg-zinc-100 dark:hover:bg-zinc-900/60\"\n      exit={{ opacity: 0, scale: 0.97, transition: { duration: 0.15 } }}\n      initial={layoutId ? false : { opacity: 0, scale: 0.97 }}\n      layout\n      layoutId={layoutId}\n      style={{ borderRadius: 14 }}\n      transition={MORPH}\n    >\n      {/* The row's contents arrive just after the card lands. */}\n      <motion.span\n        animate=\"shown\"\n        className=\"flex min-w-0 flex-1 items-center gap-3\"\n        initial=\"hidden\"\n        layout=\"position\"\n        transition={{ staggerChildren: 0.04, delayChildren: 0.08 }}\n      >\n        <motion.span\n          className=\"flex size-9 shrink-0 items-center justify-center rounded-xl text-white\"\n          style={{ background: TILE }}\n          variants={REVEAL}\n        >\n          {project.from === \"github\" ? (\n            <FolderGit2 aria-hidden=\"true\" className=\"size-4\" />\n          ) : (\n            <span className=\"font-semibold text-[14px]\">\n              {project.name.charAt(0).toUpperCase()}\n            </span>\n          )}\n        </motion.span>\n        <motion.span className=\"min-w-0 flex-1\" variants={REVEAL}>\n          {editing ? (\n            <input\n              aria-label=\"Project name\"\n              className=\"w-full rounded-md bg-transparent font-medium text-[16px] outline-hidden ring-1 ring-zinc-300 ring-offset-2 ring-offset-white sm:text-[14px] dark:ring-zinc-700 dark:ring-offset-zinc-950\"\n              defaultValue={project.name}\n              onBlur={(event) => onRename(event.currentTarget.value)}\n              onKeyDown={(event) => {\n                if (event.key === \"Enter\" || event.key === \"Escape\") {\n                  event.currentTarget.blur();\n                }\n              }}\n              ref={inputRef}\n            />\n          ) : (\n            <span className=\"block truncate font-medium text-[14px]\">\n              {project.name}\n            </span>\n          )}\n          <span className=\"relative block h-[18px] text-[12px] text-zinc-500 dark:text-zinc-400\">\n            <AnimatePresence initial={false} mode=\"popLayout\">\n              <motion.span\n                animate={{ opacity: 1, y: 0, filter: \"blur(0px)\" }}\n                className=\"block truncate\"\n                exit={{ opacity: 0, y: -4, filter: \"blur(2px)\" }}\n                initial={{ opacity: 0, y: 4, filter: \"blur(2px)\" }}\n                key={meta}\n                transition={{ duration: 0.2, ease: EASE_OUT }}\n              >\n                {meta}\n              </motion.span>\n            </AnimatePresence>\n          </span>\n        </motion.span>\n      </motion.span>\n      <button\n        aria-label={`Delete ${project.name}`}\n        className=\"flex size-8 shrink-0 items-center justify-center rounded-full text-zinc-400 outline-hidden transition-[opacity,color] duration-150 hover:text-rose-500 focus-visible:opacity-100 focus-visible:ring-2 focus-visible:ring-zinc-400/50 [@media(hover:hover)]:opacity-0 [@media(hover:hover)]:group-hover:opacity-100\"\n        disabled={importing}\n        onClick={onDelete}\n        type=\"button\"\n      >\n        <Trash2 aria-hidden=\"true\" className=\"size-4\" />\n      </button>\n\n      {/* Import progress runs along the bottom of the row. */}\n      <AnimatePresence>\n        {importing && (\n          <motion.span\n            animate={{ scaleX: 1 }}\n            aria-hidden=\"true\"\n            className=\"absolute inset-x-2 bottom-0 h-[2px] origin-left rounded-full\"\n            exit={{ opacity: 0, transition: { duration: 0.3 } }}\n            initial={{ scaleX: 0 }}\n            style={{ background: TILE }}\n            transition={{\n              duration: IMPORT_MS / 1000,\n              ease: [0.33, 0.6, 0.4, 1],\n            }}\n          />\n        )}\n      </AnimatePresence>\n    </motion.li>\n  );\n}\n\n/** A count that rolls up when it grows and down when it shrinks. */\nfunction Roll({ value }: { value: number }) {\n  const previous = useRef(value);\n  const up = value >= previous.current;\n  useEffect(() => {\n    previous.current = value;\n  }, [value]);\n  return (\n    <span className=\"relative inline-flex overflow-hidden\">\n      <AnimatePresence initial={false} mode=\"popLayout\">\n        <motion.span\n          animate={{ y: \"0%\", opacity: 1 }}\n          exit={{ y: up ? \"-70%\" : \"70%\", opacity: 0 }}\n          initial={{ y: up ? \"70%\" : \"-70%\", opacity: 0 }}\n          key={value}\n          transition={{ duration: 0.2, ease: EASE_OUT }}\n        >\n          {value}\n        </motion.span>\n      </AnimatePresence>\n    </span>\n  );\n}\n",
      "path": "/components/kokonutui/empty-state.tsx",
      "target": "components/kokonutui/empty-state.tsx"
    }
  ]
}