{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"empty-state","type":"registry:component","title":"Empty State","description":"Empty-state block with an icon or illustration slot, title, description, primary and secondary actions, three sizes and an optional dashed border; the default SVG art (a floating document stack or a search lens) draws in and drifts gently, and the whole block staggers in when scrolled into view.","author":"kara <ztzyrant@gmail.com>","dependencies":["clsx","motion","tailwind-merge"],"registryDependencies":[],"files":[{"path":"components/motion/empty-state.tsx","type":"registry:component","target":"@components/motion/empty-state.tsx","content":"\"use client\";\n// karaui.kamisama-v0.my.id/components/motion/empty-state\n\nimport { motion, useReducedMotion, type Variants } from \"motion/react\";\nimport type { ReactNode } from \"react\";\nimport { EASE_IN_OUT, EASE_OUT } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\n\nexport type EmptyStateSize = \"sm\" | \"md\" | \"lg\";\nexport type EmptyStateIllustrationVariant = \"documents\" | \"search\";\n\n/* -------------------------------------------------------------------------- */\n/*                                Illustration                                */\n/* -------------------------------------------------------------------------- */\n\nexport interface EmptyStateIllustrationProps {\n  variant?: EmptyStateIllustrationVariant;\n  /** Width in px; height follows the 5:4 viewBox. */\n  size?: number;\n  className?: string;\n}\n\nconst draw = (delay: number, reduce: boolean) => ({\n  initial: reduce ? { pathLength: 1, opacity: 0 } : { pathLength: 0, opacity: 0 },\n  whileInView: { pathLength: 1, opacity: 1 },\n  viewport: { once: true },\n  transition: reduce\n    ? { duration: 0.2 }\n    : {\n        pathLength: { duration: 0.5, ease: EASE_OUT, delay },\n        opacity: { duration: 0.1, delay },\n      },\n});\n\n// Idle float: translate only (no rotate/scale), so no SVG transform-origin is\n// involved. Rotations in the art are static `transform` attributes on an\n// inner <g> that motion never touches.\nconst float = (reduce: boolean, amplitude: number, duration: number, delay: number) =>\n  reduce\n    ? {}\n    : {\n        animate: { y: [0, -amplitude, 0] },\n        transition: { duration, ease: EASE_IN_OUT, repeat: Infinity, delay },\n      };\n\n/** Small default empty-state art: a floating document stack or a search lens. */\nexport function EmptyStateIllustration({\n  variant = \"documents\",\n  size = 120,\n  className,\n}: EmptyStateIllustrationProps) {\n  const reduce = useReducedMotion() ?? false;\n  const lineClass = \"stroke-border-strong\";\n\n  return (\n    <svg\n      width={size}\n      height={(size * 4) / 5}\n      viewBox=\"0 0 120 96\"\n      fill=\"none\"\n      aria-hidden=\"true\"\n      focusable=\"false\"\n      className={cn(\"overflow-visible\", className)}\n    >\n      <ellipse cx=\"60\" cy=\"88\" rx=\"30\" ry=\"3.5\" className=\"fill-muted\" />\n      {variant === \"documents\" ? (\n        <>\n          <motion.g {...float(reduce, 2, 3.4, 0.4)}>\n            <g transform=\"rotate(-9 46 48)\">\n              <rect x=\"24\" y=\"20\" width=\"44\" height=\"56\" rx=\"7\" strokeWidth=\"1.5\" className=\"fill-muted stroke-border-strong\" />\n            </g>\n          </motion.g>\n          <motion.g {...float(reduce, 2.5, 3.8, 0.9)}>\n            <g transform=\"rotate(8 74 48)\">\n              <rect x=\"52\" y=\"20\" width=\"44\" height=\"56\" rx=\"7\" strokeWidth=\"1.5\" className=\"fill-muted stroke-border-strong\" />\n            </g>\n          </motion.g>\n          <motion.g {...float(reduce, 3.5, 3, 0)}>\n            <rect x=\"37\" y=\"16\" width=\"46\" height=\"60\" rx=\"7\" strokeWidth=\"1.5\" className=\"fill-card stroke-border-strong\" />\n            <motion.path d=\"M46 30h20\" strokeWidth=\"3\" strokeLinecap=\"round\" className=\"stroke-foreground/70\" {...draw(0.25, reduce)} />\n            <motion.path d=\"M46 40h28\" strokeWidth=\"3\" strokeLinecap=\"round\" className={lineClass} {...draw(0.35, reduce)} />\n            <motion.path d=\"M46 49h24\" strokeWidth=\"3\" strokeLinecap=\"round\" className={lineClass} {...draw(0.45, reduce)} />\n            <motion.path d=\"M46 58h16\" strokeWidth=\"3\" strokeLinecap=\"round\" className={lineClass} {...draw(0.55, reduce)} />\n            <motion.g\n              initial={{ opacity: 0, y: reduce ? 0 : 4 }}\n              whileInView={{ opacity: 1, y: 0 }}\n              viewport={{ once: true }}\n              transition={{ duration: 0.3, ease: EASE_OUT, delay: 0.6 }}\n            >\n              <circle cx=\"84\" cy=\"17\" r=\"9\" className=\"fill-primary\" />\n              <path d=\"M84 13v8M80 17h8\" strokeWidth=\"2\" strokeLinecap=\"round\" className=\"stroke-primary-foreground\" />\n            </motion.g>\n          </motion.g>\n        </>\n      ) : (\n        <>\n          <motion.g {...float(reduce, 1.5, 3.6, 0.5)}>\n            <rect x=\"22\" y=\"22\" width=\"64\" height=\"50\" rx=\"8\" strokeWidth=\"1.5\" className=\"fill-muted stroke-border-strong\" />\n            <motion.path d=\"M32 36h30\" strokeWidth=\"3\" strokeLinecap=\"round\" className={lineClass} {...draw(0.2, reduce)} />\n            <motion.path d=\"M32 46h22\" strokeWidth=\"3\" strokeLinecap=\"round\" className={lineClass} {...draw(0.3, reduce)} />\n            <motion.path d=\"M32 56h26\" strokeWidth=\"3\" strokeLinecap=\"round\" className={lineClass} {...draw(0.4, reduce)} />\n          </motion.g>\n          <motion.g {...float(reduce, 3.5, 3, 0)}>\n            <circle cx=\"76\" cy=\"48\" r=\"17\" strokeWidth=\"1.5\" className=\"fill-card stroke-border-strong\" />\n            <motion.circle cx=\"76\" cy=\"48\" r=\"11\" strokeWidth=\"3\" className=\"stroke-foreground/70\" {...draw(0.35, reduce)} />\n            <motion.path d=\"M84.5 56.5 96 68\" strokeWidth=\"5\" strokeLinecap=\"round\" className=\"stroke-foreground/70\" {...draw(0.6, reduce)} />\n            <motion.path d=\"M72.5 44.5l7 7M79.5 44.5l-7 7\" strokeWidth=\"2\" strokeLinecap=\"round\" className=\"stroke-muted-foreground\" {...draw(0.75, reduce)} />\n          </motion.g>\n        </>\n      )}\n    </svg>\n  );\n}\n\n/* -------------------------------------------------------------------------- */\n/*                                 EmptyState                                 */\n/* -------------------------------------------------------------------------- */\n\nexport interface EmptyStateProps {\n  title: ReactNode;\n  description?: ReactNode;\n  /**\n   * Icon or illustration slot. Defaults to `<EmptyStateIllustration />`;\n   * pass `null` for none.\n   */\n  icon?: ReactNode;\n  /** Primary call to action (e.g. a library `Button`). */\n  action?: ReactNode;\n  /** Secondary call to action, rendered after the primary. */\n  secondaryAction?: ReactNode;\n  size?: EmptyStateSize;\n  /** Dashed border container, for empty regions inside a page. */\n  bordered?: boolean;\n  /** Heading element for the title. Default `h3`. */\n  titleAs?: \"h2\" | \"h3\" | \"h4\" | \"p\";\n  /** Extra content under the actions (links, hints). */\n  children?: ReactNode;\n  className?: string;\n}\n\nconst SIZE: Record<\n  EmptyStateSize,\n  { root: string; title: string; description: string; art: number }\n> = {\n  sm: { root: \"gap-3 px-4 py-6\", title: \"text-sm\", description: \"text-xs max-w-[32ch]\", art: 88 },\n  md: { root: \"gap-4 px-6 py-10\", title: \"text-base\", description: \"text-sm max-w-[38ch]\", art: 120 },\n  lg: { root: \"gap-5 px-8 py-14\", title: \"text-lg\", description: \"text-base max-w-[44ch]\", art: 152 },\n};\n\n/**\n * Empty-state block: illustration, title, description and actions that\n * stagger in when scrolled into view (opacity + small rise; opacity only under\n * reduced motion).\n */\nexport function EmptyState({\n  title,\n  description,\n  icon,\n  action,\n  secondaryAction,\n  size = \"md\",\n  bordered = false,\n  titleAs: Title = \"h3\",\n  children,\n  className,\n}: EmptyStateProps) {\n  const reduce = useReducedMotion();\n  const s = SIZE[size];\n\n  const container: Variants = {\n    hidden: {},\n    show: { transition: { staggerChildren: 0.07, delayChildren: 0.05 } },\n  };\n  const item: Variants = {\n    hidden: { opacity: 0, y: reduce ? 0 : 8 },\n    show: { opacity: 1, y: 0, transition: { duration: 0.32, ease: EASE_OUT } },\n  };\n\n  const art = icon === undefined ? <EmptyStateIllustration size={s.art} /> : icon;\n\n  return (\n    <motion.div\n      variants={container}\n      initial=\"hidden\"\n      whileInView=\"show\"\n      viewport={{ once: true, amount: 0.4 }}\n      className={cn(\n        \"flex w-full flex-col items-center text-center\",\n        s.root,\n        bordered && \"rounded-2xl border border-dashed border-border-strong\",\n        className,\n      )}\n    >\n      {art != null ? (\n        <motion.div variants={item} className=\"flex items-center justify-center text-muted-foreground\">\n          {art}\n        </motion.div>\n      ) : null}\n      <div className=\"flex flex-col items-center gap-1.5\">\n        <motion.div variants={item}>\n          <Title className={cn(\"font-semibold tracking-tight text-foreground\", s.title)}>\n            {title}\n          </Title>\n        </motion.div>\n        {description != null ? (\n          <motion.p variants={item} className={cn(\"text-muted-foreground text-balance\", s.description)}>\n            {description}\n          </motion.p>\n        ) : null}\n      </div>\n      {action != null || secondaryAction != null ? (\n        <motion.div variants={item} className=\"flex flex-wrap items-center justify-center gap-2\">\n          {action}\n          {secondaryAction}\n        </motion.div>\n      ) : null}\n      {children != null ? <motion.div variants={item}>{children}</motion.div> : null}\n    </motion.div>\n  );\n}\n"},{"path":"lib/ease.ts","type":"registry:lib","target":"@lib/ease.ts","content":"// Shared motion tokens. Easing curves mirror the CSS custom properties in\n// globals.css; springs are the canonical physics used across components.\n// Strong custom variants — defaults like `ease-in`/`ease-out` feel weak.\n\nexport const EASE_OUT = [0.16, 1, 0.3, 1] as const;\nexport const EASE_IN_OUT = [0.77, 0, 0.175, 1] as const;\nexport const EASE_DRAWER = [0.32, 0.72, 0, 1] as const;\n\n/** CSS string form of EASE_OUT for inline style transitions. */\nexport const EASE_OUT_CSS = \"cubic-bezier(0.16, 1, 0.3, 1)\";\n\n/** Press feedback on buttons and other tappable surfaces. */\nexport const SPRING_PRESS = {\n  type: \"spring\",\n  stiffness: 500,\n  damping: 30,\n  mass: 0.6,\n} as const;\n\n/** Content swaps — label/icon slots trading places inside a control. */\nexport const SPRING_SWAP = {\n  type: \"spring\",\n  stiffness: 460,\n  damping: 30,\n  mass: 0.55,\n} as const;\n\n/** Overlay panel entrances — modals and sheets summoned by pointer. */\nexport const SPRING_PANEL = {\n  type: \"spring\",\n  stiffness: 420,\n  damping: 40,\n  mass: 0.5,\n} as const;\n\n/** Shared-layout glides — pills, indicators and panels morphing between positions. */\nexport const SPRING_LAYOUT = {\n  type: \"spring\",\n  stiffness: 360,\n  damping: 32,\n  mass: 0.6,\n} as const;\n\n/** Cursor-follow physics for decorative mouse tracking (magnetic, tilt, dock). */\nexport const SPRING_MOUSE = {\n  stiffness: 200,\n  damping: 15,\n  mass: 0.3,\n} as const;\n\n/** Dragged handles and fills (sliders) — critically damped `useSpring` config,\n * so the value follows the pointer butterily and never rebounds off an end. */\nexport const SPRING_GLIDE = {\n  stiffness: 700,\n  damping: 50,\n  mass: 0.5,\n} as const;\n"},{"path":"lib/utils.ts","type":"registry:lib","target":"@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}\n"}]}