{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "empty-state",
  "type": "registry:ui",
  "title": "Empty State",
  "description": "One line, one reason, one action, with an icon that draws itself in.",
  "categories": [
    "async"
  ],
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://www.stealth.pm/r/tokens.json",
    "https://www.stealth.pm/r/cn.json",
    "https://www.stealth.pm/r/motion.json"
  ],
  "files": [
    {
      "path": "components/ui/empty-state.tsx",
      "content": "\"use client\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport { cn } from \"@/lib/cn\";\nimport { ease } from \"@/lib/motion\";\n\nexport type EmptyStateVariant = \"default\" | \"first-use\" | \"no-results\" | \"inbox-zero\";\n\nexport type EmptyStateProps = Omit<React.ComponentProps<\"div\">, \"title\"> & {\n  /**\n   * default: something could be here. first-use: nothing has been made yet (dashed edge, a place to fill).\n   * no-results: a search or filter came back empty. inbox-zero: everything is done, and that is good news.\n   */\n  variant?: EmptyStateVariant;\n  /** One line saying what would be here: “No projects yet”. */\n  heading: React.ReactNode;\n  /** One short line on why it matters or how it fills. */\n  description?: React.ReactNode;\n  /** Replaces the variant's drawn icon. 20px, currentColor. Pass null for none. */\n  icon?: React.ReactNode;\n  /** The action that fills it, usually one primary button, optionally a quieter second. */\n  children?: React.ReactNode;\n  /** sm sits inside a table, menu or popover; md fills a region. */\n  size?: \"sm\" | \"md\";\n  headingLevel?: 2 | 3 | 4;\n};\n\nexport function EmptyState({\n  variant = \"default\",\n  heading,\n  description,\n  icon,\n  children,\n  size = \"md\",\n  headingLevel = 3,\n  className,\n  ...rest\n}: EmptyStateProps) {\n  const reduce = useReducedMotion();\n  const H = `h${headingLevel}` as const;\n  const sm = size === \"sm\";\n\n  // A quiet arrival: the tile settles first, then the words, then the action.\n  // Only on mount; it never replays on re-render or filter change. The starting\n  // pose is the same with reduced motion (so server and client HTML match); only\n  // the opacity is animated then, and the travel and blur resolve at once.\n  const still = { y: { duration: 0 }, scale: { duration: 0 }, filter: { duration: 0 } };\n  const rise = (i: number) => ({\n    initial: { opacity: 0, y: 6, filter: \"blur(2px)\" },\n    animate: { opacity: 1, y: 0, filter: \"blur(0px)\" },\n    transition: reduce ? { duration: 0.2, delay: i * 0.03, ...still } : { duration: 0.42, ease: ease.out, delay: 0.06 + i * 0.05 },\n  });\n\n  return (\n    <div\n      role=\"status\"\n      data-variant={variant}\n      data-size={size}\n      className={cn(\n        \"flex w-full flex-col items-center justify-center text-center\",\n        sm ? \"gap-3 px-4 py-6\" : \"gap-4 px-6 py-10\",\n        variant === \"first-use\" && \"rounded-xl border border-dashed border-line-2\",\n        className,\n      )}\n      {...rest}\n    >\n      {icon !== null && (\n        <motion.div\n          aria-hidden\n          initial={{ opacity: 0, y: 6, scale: 0.94 }}\n          animate={{ opacity: 1, y: 0, scale: 1 }}\n          transition={reduce ? { duration: 0.2, ...still } : { duration: 0.46, ease: ease.out }}\n          className={cn(\n            \"relative grid shrink-0 place-items-center border bg-raised shadow-[var(--shadow)]\",\n            sm ? \"size-8 rounded-lg [&_svg]:size-4\" : \"size-10 rounded-xl [&_svg]:size-5\",\n            variant === \"inbox-zero\" ? \"border-success/25 text-success\" : \"border-line-2 text-fg-2\",\n          )}\n        >\n          {icon ?? <Glyph variant={variant} reduce={!!reduce} />}\n        </motion.div>\n      )}\n\n      <div className={cn(\"flex max-w-[34ch] flex-col\", sm ? \"gap-0.5\" : \"gap-1\")}>\n        <motion.div {...rise(1)}>\n          <H className={cn(\"text-balance font-medium text-fg\", sm ? \"text-[13px] tracking-[-0.006em]\" : \"text-[14.5px] tracking-[-0.015em]\")}>{heading}</H>\n        </motion.div>\n        {description != null && (\n          <motion.p {...rise(2)} className={cn(\"text-pretty text-fg-3\", sm ? \"text-[12px] leading-[17px]\" : \"text-[12.5px] leading-[19px]\")}>\n            {description}\n          </motion.p>\n        )}\n      </div>\n\n      {children != null && (\n        <motion.div {...rise(3)} className={cn(\"flex flex-wrap items-center justify-center gap-2\", !sm && \"pt-1\")}>\n          {children}\n        </motion.div>\n      )}\n    </div>\n  );\n}\n\n// Each variant's icon draws itself in as the tile lands: stroke first, detail last.\nfunction Glyph({ variant, reduce }: { variant: EmptyStateVariant; reduce: boolean }) {\n  // Same starting pose either way so the server HTML matches; reduced motion just skips to the end.\n  const draw = (delay: number, duration = 0.4) => ({\n    initial: { pathLength: 0 },\n    animate: { pathLength: 1 },\n    transition: reduce ? { duration: 0 } : { duration, ease: ease.out, delay },\n  });\n  const props = { width: 20, height: 20, viewBox: \"0 0 16 16\", fill: \"none\", stroke: \"currentColor\", strokeWidth: 1.25, strokeLinecap: \"round\" as const, strokeLinejoin: \"round\" as const };\n\n  if (variant === \"first-use\")\n    return (\n      <svg {...props}>\n        <motion.rect x=\"2.75\" y=\"2.75\" width=\"10.5\" height=\"10.5\" rx=\"2.25\" {...draw(0.12, 0.5)} />\n        <motion.path d=\"M8 5.5v5M5.5 8h5\" {...draw(0.4, 0.26)} />\n      </svg>\n    );\n  if (variant === \"no-results\")\n    return (\n      <svg {...props}>\n        <motion.circle cx=\"7\" cy=\"7\" r=\"4.25\" {...draw(0.12, 0.5)} />\n        <motion.path d=\"m10.25 10.25 3 3\" {...draw(0.46, 0.2)} />\n        <motion.path d=\"M5.6 7h2.8\" {...draw(0.58, 0.18)} />\n      </svg>\n    );\n  if (variant === \"inbox-zero\")\n    return (\n      <svg {...props}>\n        <motion.circle cx=\"8\" cy=\"8\" r=\"5.75\" {...draw(0.1, 0.5)} />\n        <motion.path d=\"m5.5 8.25 1.75 1.75 3.25-3.75\" {...draw(0.42, 0.32)} />\n      </svg>\n    );\n  return (\n    <svg {...props}>\n      <motion.path d=\"M2.5 9.5 4 3.75h8L13.5 9.5v3a.75.75 0 0 1-.75.75H3.25a.75.75 0 0 1-.75-.75z\" {...draw(0.12, 0.55)} />\n      <motion.path d=\"M2.5 9.5h3l.75 1.5h3.5l.75-1.5h3\" {...draw(0.42, 0.3)} />\n    </svg>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "docs": "https://www.stealth.pm/ui/empty-state"
}
